파란하늘의 지식창고
article thumbnail
반응형

시작하기 전

대상 강의

유튜브에 "Code a 2D Game Engine using Java - Full Course for Beginners"라는 23시간 50분짜리 강의 영상이 있어 흥미롭게 보았다.

https://www.youtube.com/watch?v=025QFeZfeyM 

Java에서 마리오 게임을 만드는 걸 알려주는 강의인데 총 55 챕터로 되어 있고 추가적으로 Part2로 3개의 챕터가 별도 유튜브로 있다.

https://www.youtube.com/watch?v=roPRqEQZFu8 

javaFX나 swing을 쓰는 데가 있는지 모르겠지만 오래된 기억에 예전에 둘러보았을 때도 선호하지 않았는데 시간이 지나니 대안 라이브러리가 나오고 그에 대한 유튜브 강의가 있었다.

이 글은 해당 강의를 학습하면서 따라 해 본 내용을 적은 개인적인 기록이다.

예제 코드

해당 강의의 예제 코드는 github에 있다.

https://github.com/codingminecraft/MarioYoutube

git의 history에서 챕터별 코드를 checkout 해서 보면 된다.

다만 maven, gradle 둘 다 제대로 import project가 되지 않아서 때문에 예제를 실행을 할 수 없었다.

각 챕터별 소스 코드를 참고만 해야 할 것 같다.

현재까지 감상

강의를 따라 기초적인 부분부터 코딩을 통해 학습하면서 순차적으로 리팩토링하는 과정을 실습할 수 있는 부분이 좋았다.

이 글이 작성된 현재 11장까지 챕터를 따라갔다.

너무 길어서 다음 강의를 학습한다면 별도의 글로 나누어야 할 것 같다.

전혀 모르던 분야를 단계적으로 따라가면서 대충 윤곽이라도 구경해볼 수 있어 좋았고 내 경우 STS (Eclipse) IDE로 해당 강의를 따라 했다.

당분간 다음 강의를 보지 못할 거 같아 이 글은 여기까지 끊고 이후 강의의 부분은 별도의 글로 정리하려고 한다.

STS (Eclipse) 사용자의 경우 미리 해두면 좋은 설정

API를 호출하는 java library가 죄다 static으로 되어있는데 STS (Eclipse) IDE의 경우 static import는 지정된 위치에 대해서만 autocomplete 처리를 해준다. ( Intellij가 이 부분에 대해 잘 지원해줘서 비교가 된다.)

STS에서 코드 따라가기를 좀 더 편하게 하려면 Window -> Preference -> Java -> Editor -> Content Assist -> Favorites에 다음을 new Type으로 미리 추가해주면 좋다.


LWJGL 소개

LWJGL (Light Weight Java Game Library)는 Java용 OpenSource 게임 개발 라이브러리이다.

유명한 native API 들 - 그래픽 (OpenGL, Vulkan), 오디오 (OpenAL), 병렬 컴퓨팅 (OpenCL), GLFW 등을 손쉽게 사용할 수 있게 도와준다.

게임 개발의 대표 격인 유니티나 언리얼은 각각 C#, C++을 사용하고 있는데 Java를 기반으로 게임 개발을 하고 싶은 경우 LWJGL을 사용하면 된다.

마인 크래프트도 자바 버전, Slay The Spire, Project Zomboid, FarSky 같은 게임들이 이 라이브러리를 사용하였다.

OpenGL을 사용해본 사람은 쉽게 적응할 수 있고 처음 공부하는 사람은 다음과 같은 API들에 대해 학습을 진행해야 한다

API 설명
GLFW (Graphic Library FrameWork) OpenGL, OpenGL ES, Vulkan 개발을 위한 OpenSource, multi-platform 라이브러리
window, Context 및 surface를 생성하고 input과 event을 입력받기 위핸 간단한 API를 제공.
LWJGL 버전 3부터 기본 지원하기 시작
OpenGL (Open Graphic Library) 2D, 3D 그래픽 표준 API 규격
수많은 확장들이 있음
OpenCL (Open Computing Language) 개방형 범용 병렬 컴퓨팅 프레임워크
CPU, GPU, DSP 등의 프로세서로 이루어진 이종 플랫폼에서 실행되는 프로그램 작성할 수 있게 해줌
OpenAL (Open Audio Library) 다중 채널 3차원 오디오 출력을 구현하기 위한 라이브러리

예제 둘러보기

LWJGL 예제는 아래에서 받을 수 있다.

https://github.com/LWJGL/lwjgl3-demos

maven project이고 받아서 예제 class의 main method를 실행해보면 된다.

#1 Setting up the Window with LWJGL (00:07:54)

Java 기반 라이브러리이기 때문에 maven, gradle, ivy을 사용하여 설정하면 된다.

현재 LWJGL 3.x 버전이 최신 버전이며 JDK 8 이상에서 사용할 수 있다.

해당 사이트의 download 페이지의 Customize LWJGL 3 항목을 선택 후 원하는 라이브러리를 선택하고 하단의 Download 또는 copy to clip board로 해당 설정을 손쉽게 사용할 수 있다.

https://www.lwjgl.org/customize

내 경우 강의를 따라서 maven 설정으로 Minimal OpenGL, Addon으로 JOML, Contents에서 Native File Dialog를 선택하니 다음과 같은 내용을 받을 수 있었다.

<properties>
	<lwjgl.version>3.3.1</lwjgl.version>
	<joml.version>1.10.4</joml.version>
	<lwjgl.natives>natives-windows</lwjgl.natives>
</properties>

<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.lwjgl</groupId>
			<artifactId>lwjgl-bom</artifactId>
			<version>${lwjgl.version}</version>
			<scope>import</scope>
			<type>pom</type>
		</dependency>
	</dependencies>
</dependencyManagement>

<dependencies>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl</artifactId>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl-assimp</artifactId>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl-glfw</artifactId>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl-nfd</artifactId>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl-openal</artifactId>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl-opengl</artifactId>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl-stb</artifactId>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl</artifactId>
		<classifier>${lwjgl.natives}</classifier>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl-assimp</artifactId>
		<classifier>${lwjgl.natives}</classifier>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl-glfw</artifactId>
		<classifier>${lwjgl.natives}</classifier>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl-nfd</artifactId>
		<classifier>${lwjgl.natives}</classifier>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl-openal</artifactId>
		<classifier>${lwjgl.natives}</classifier>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl-opengl</artifactId>
		<classifier>${lwjgl.natives}</classifier>
	</dependency>
	<dependency>
		<groupId>org.lwjgl</groupId>
		<artifactId>lwjgl-stb</artifactId>
		<classifier>${lwjgl.natives}</classifier>
	</dependency>
		<dependency>
		<groupId>org.joml</groupId>
		<artifactId>joml</artifactId>
		<version>${joml.version}</version>
	</dependency>
</dependencies>

각 항목들에 대한 자세한 설명은 최상단에 있는 Show descriptions 항목을 활성화하여 확인할 수 있다.

첫 번째 예제 실행하기

처음 작성하여 실행해볼 예제는 이곳에 있다.

https://www.lwjgl.org/guide

예제를 만들기 전에 LWJGL이 어떻게 native API들을 사용할 수 있게 해 주는지 대충 알고 있어야 할거 같다.

첫 예제에서 가장 먼저 언급되는 것은 GLFW (Graphics Library Framework)이다.

예제는 이 GLFW를 통해 윈도 창을 만들고 다음과 같이 빨간색으로 배경을 처리한다.

단순 예제이기 때문에 테스트 class 자체에 main method가 다음과 같이 있다.

public static void main(String[] args) {
    new HelloWorld().run();
}

해당 metht를 실행하면 run method에는 다음과 같이 코드가 정의되어 있다.

public void run() {
    System.out.println("Hello LWJGL " + Version.getVersion() + "!");

    init();
    loop();

    // Free the window callbacks and destroy the window
    glfwFreeCallbacks(window);
    glfwDestroyWindow(window);

    // Terminate GLFW and free the error callback
    glfwTerminate();
    glfwSetErrorCallback(null).free();
}

init mehod를 통해 창을 만들고 loop method를 통해 창의 종료 입력을 대기한다.

창이 떠있는 동안은 loop method의 반복문이 계속 실행되고 종료 시그널(ESC 입력)을 받으면 loop가 종료되어 이후 4개의 method가 실행되어 창을 닫고 마무리를 한다.

glfwFreeCallbacks, glfwDestoryWindow는 memory를 해제하고 glfwTerminate, glfwSetErrorCallback은 glfw를 종료하고 에러에 대한 callback을 처리한다.

 

#2 Adding Event Listeners with GLFW (input 입력 감지하기, 00:25:20)

창을 띄우고 난 다음 해당 창에서 입력을 받는 부분에 대해 구현해본다.

input을 감지하여 처리하는 callback method를 제공하고 있으며 다음과 같다.

https://www.glfw.org/docs/latest/input_guide.html

제공하는 glfw callback method에 작성한 callback을 등록하면 입력을 받을 때 해당 callback을 실행하게 한다.

키 입력(텍스트 입력), 마우스 입력 (커서 위치, 커서 모드, 커서 진입/나가기, 마우스 입력, 스크롤 입력), 조이스틱 입력, 시간 입력, 클립보드 입력, 경로 입력 (창에 드롭된 파일/디렉터리 경로) 등 다양한 입력에 대한 callback method를 제공하고 있다.

강의에서는 단순하게 하나의 MouseListener class에 singleton으로 처리하여 각각의 callback에 대해 정의하고 해당 callback을 등록하였다.

public class MouseListener {
	private static MouseListener instance;
	
	private double scrollX, scrollY;
	private double xPos, yPos, lastY, lastX;
	private boolean mouseButtonPressed[] = new boolean[3];
	private boolean isDragging;
	
	private MouseListener() {
		this.scrollX = 0.0;
		this.scrollY = 0.0;
		this.xPos = 0.0;
		this.yPos = 0.0;
		this.lastX = 0.0;
		this.lastY = 0.0;
	}
	
	public static MouseListener get() {
		if (MouseListener.instance == null) {
			MouseListener.instance = new MouseListener();
		}
		
		return MouseListener.instance;
	}
	
	public static void mousePosCallback(long window, double xpos, double ypos) {
		get().lastX = get().xPos;
		get().lastY = get().yPos;
		get().xPos = xpos;
		get().yPos = ypos;
		get().isDragging = get().mouseButtonPressed[0] || get().mouseButtonPressed[1] || get().mouseButtonPressed[2];
	}
	
	public static void mouseButtonCallback(long window, int button, int action, int mods) {
		if (action == GLFW_PRESS) {
			if (button < get().mouseButtonPressed.length) {
				get().mouseButtonPressed[button] = true;
			}
		} else if (action == GLFW_RELEASE) {
			if (button < get().mouseButtonPressed.length) {
				get().mouseButtonPressed[button] = false;
				get().isDragging = false;
			}
		}
	}
	
	public static void mouseScrollCallback(long window, double xOffset, double yOffset) {
		get().scrollX = xOffset;
		get().scrollY = yOffset;
	}
	
	public static void endFrame() {
		get().scrollX = 0;
		get().scrollY = 0;
		get().lastX = get().xPos;
		get().lastY = get().yPos;
	}
	
	public static float getX() {
		return (float) get().xPos;
	}
	
	public static float getY() {
		return (float) get().yPos;
	}
	
	public static float getDx() {
		return (float) (get().lastX - get().xPos);
	}
	
	public static float getDy() {
		return (float) (get().lastY - get().yPos);
	}
	
	public static float getScrollX() {
		return (float) get().scrollX;
	}
	
	public static float getScrollY() {
		return (float) get().scrollY;
	}
	
	public static boolean isDragging() {
		return get().isDragging;
	}
	
	public static boolean mouseButtonDown(int button) {
		if (button < get().mouseButtonPressed.length) {
			return get().mouseButtonPressed[button];
		} else {
			return false;
		}
	}
}

이렇게 전달받은 입력 처리를 하는 callback을 구현하고 이전에 생성했던 윈도 창의 init method에 다음과 같이 추가한다.

glfwSetCursorPosCallback(window, MouseListener::mousePosCallback);
glfwSetMouseButtonCallback(window, MouseListener::mouseButtonCallback);
glfwSetScrollCallback(window, MouseListener::mouseScrollCallback);

callback은 여러 개 등록하지 못하는 것으로 보인다. (동일 callback method의 마지막에 추가된 callback만 동작)

#3 Creating a Scene Manager & Delta Time Variable (장면 전환하기, 00:51:42)

강의에서는 Scene abstract class를 만들고 LevelEditorScene, LevelScene을 만들어 각각의 Scene을 update 하는 것으로 보여주었다.

Scene을 만들기 전에 nanotime에 대해 설명한다.

nanotime은 1/1000000000 (1/10^9) 초이고 화면을 처리할 때 초당 얼마큼 갱신할지 (FPS)를 위해 반드시 계산해야 하는 부분이다.

이를 위해 간단한 시간 계산 유틸을 만든다.

public class TimeUtil {

	public static float timeStarted = System.nanoTime();
	
	public static float getTime() {
		return (float) ((System.nanoTime() - timeStarted) * 1E-9);
	}
}

이제 시간 계산을 위해 기존에 만든 Window의 loop method에 다음처럼 추가한다.

private void loop() {
    float beginTime = TimeUtil.getTime();
    float endTime;
    float dt = -1.0f;

    while ( !glfwWindowShouldClose(window) ) {

        // 중간 생략
        endTime = TimeUtil.getTime();
        dt =  endTime - beginTime;
        beginTime = endTime;
    }
}

장면 전환을 위해 Scene을 만들고 이를 상속한 2개의 Scene을 만든다.

public abstract class Scene {

	public Scene() {
		
	}
	
	public void init() {}
	
	public abstract void update(float dt);

}

public class LevelEditorScene extends Scene {
	
	public LevelEditorScene() {
		System.out.println("Inside levelEditor scene");
	}

	@Override
	public void update(float dt) {
		// 키 이벤트를 입력 받으면 배경색을 점차 변경하는 설정 
	}

}

public class LevelScene extends Scene {
	
	public LevelScene() {
		System.out.println("Inside level scene");
	}

	@Override
	public void update(float dt) {
		
	}

}

이 Scene을 기존 window loop method에서 키 입력을 받은 경우 갱신하는 처리를 한다.

	private init() {
    	// ... 중간 생략
        MainWindow.changeScene(0);
    }
    
	private void loop() {
		while ( !glfwWindowShouldClose(window) ) {
			// .. 중간 생략
			if (dt >= 0) {
				currentScene.update(dt);
			}
		
		}
	}

	public static void changeScene(int newScene) {
		switch (newScene) {
		case 0:
			currentScene = new LevelEditorScene();
			currentScene.init();
			break;
		case 1:
			currentScene = new LevelScene();
			currentScene.init();
			break;
			default :
				assert false : "Unknown scene '" + newScene + "'";
				break;
		}
	}

자세한 예제는 생략하는데 핵심은 window의 init에서 Scene을 적용하고 loop() method 구간 실행 중 update를 계속 호출하면서 키 이벤트를 입력받으면 glClearColor method를 반복 호출하면서 배경색 설정 값을 변경하여 윈도의 배경색이 변경되는 처리를 수행하는 것이다.

#4 How OpenGL Graphics Programming Works (OpenGL shader 소개, 01:11:26)

다음 예제를 소개하기 전OpenGL의 shader를 설명한다.

shader는 GLSL (OpenGL Shading Language)이라는 언어로 화면의 각 픽셀에 위치와 색상을 어떻게 출력을 할지를 계산하는 방법이다.

다음과 같은 단계를 거쳐서 처리를 한다고 한다.

https://learnopengl.com/Getting-started/Hello-Triangle

  1. Vertex Shader 
  2. Shape Assembly
  3. Geometry Shader
  4. Rasterization
  5. Fragment Shader
  6. Tests and Blending

3개의 점이 좌표를 기준으로 vertex data를 입력받는다.

좌표는 x, y축 각각 -1.0 ~ 1.0을 기준으로 지정한다.

이렇게 전달받은 좌표(vertex data[])를 일련의 과정을 거쳐 화면에 출력될 결과가 계산된다.

위 이미지의 삼각형의 각 꼭짓점의 좌표는 다음과 같다.

float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

glsl 파일에 Vertex Shader와 Fragment Shader 각각에 대한 정의를 GLSL 문법으로 작성하면 전달받은 입력을 해당 문법의 compilation & linking 과정을 거쳐 결과를 반환한다.

#type vertex
#version 330 core
layout (location=0) in vec3 aPos;
layout (location=1) in vec4 aColor;

out vec4 fColor;

void main()
{
	fColor = aColor;
	gl_Position = vec4(aPos, 1.0); 
}

#type fragment
#version 330 core

in vec4 fColor;

out vec4 color;

void main()
{
	color = fColor;
}

지금은 단순히 입력을 그대로 반환하는 처리로 작성된 것으로 이해하면 된다.

GLSL 문법

해당 문법에 대해서는 별도의 공부가 필요하다.

docs.gl 사이트에 관련 문서가 있다.

https://docs.gl/

OpenGL, OpenGL ES, GLSL, GLSL ES 문서가 있는데 ES는 Embeded System으로 안드로이드 같은 플랫폼에서 사용하는 경우이고 이 강의의 GLSL은 GSGL 4.5 문서를 참고하면 된다.

https://docs.gl/sl4/all

GLSL에서 사용하는 Data Type은 아래 문서를 참고하면 된다.

https://www.khronos.org/opengl/wiki/Data_Type_(GLSL) 

#5 Drawing the First Square (GLSL 적용하기, 01:32:53)

glsl 파일을 만들고 vertex shader와 fragment shader에 대한 정의를 하고 해당 파일을 import 해서 사용해야 하지만 강의에서는 임시로 GLSL 문법을 String에 넣고 실행하는 식으로 보여주었다.

이전의 장면 전환 예제에서 사용하였던 LevelEditorScene을 다시 아래처럼 변경하여 GLSL 구문을 실행하였고 해당 구문을 일부러 오류를 발생시키고 실행하여 GLSL 구문이 컴파일되는 것까지 확인한다.

package io.github.luversof.study.lwjgl.main.domain;

import java.awt.event.KeyEvent;

import static org.lwjgl.opengl.GL20.*;

public class LevelEditorScene extends Scene {

	private String vertexShaderSrc = "#version 330 core\r\n"
			+ "layout (location=0) in vec3 aPos;\r\n"
			+ "layout (location=1) in vec4 aColor;\r\n"
			+ "\r\n"
			+ "out vec4 fColor;\r\n"
			+ "\r\n"
			+ "void main()\r\n"
			+ "{\r\n"
			+ "	fColor = aColor;\r\n"
			+ "	gl_Position = vec4(aPos, 1.0); \r\n"
			+ "}";
	
	private String fragmentShaderSrc = "#version 330 core\r\n"
			+ "\r\n"
			+ "in vec4 fColor;\r\n"
			+ "\r\n"
			+ "out vec4 color;\r\n"
			+ "\r\n"
			+ "void main()\r\n"
			+ "{\r\n"
			+ "	color = fColor;\r\n"
			+ "}";
	
	private int vertexID, fragmentID, shaderProgram;

	public LevelEditorScene() {
		
	}	
	
	@Override
	public void init() {
		// Compile and link shaders
		
		// First load and compile the vertex shader
		vertexID = glCreateShader(GL_VERTEX_SHADER);
		
		// pass the shader source to the GPU
		glShaderSource(vertexID, vertexShaderSrc);
		glCompileShader(vertexID);
		
		// Check for errors in compilation
		int success = glGetShaderi(vertexID, GL_COMPILE_STATUS);
		if (success == GL_FALSE) {
			int len = glGetShaderi(vertexID, GL_INFO_LOG_LENGTH);
			System.out.println("ERROR: 'defaultShader.glsl'\n\tVertex shader compilation failed." );
			System.out.println(glGetShaderInfoLog(vertexID, len));
			assert false : ""; 
		}
		
		// First load and compile the vertex shader
		fragmentID = glCreateShader(GL_FRAGMENT_SHADER);
		
		// pass the shader source to the GPU
		glShaderSource(fragmentID, fragmentShaderSrc);
		glCompileShader(fragmentID);
		
		// Check for errors in compilation
		success = glGetShaderi(fragmentID, GL_COMPILE_STATUS);
		if (success == GL_FALSE) {
			int len = glGetShaderi(fragmentID, GL_INFO_LOG_LENGTH);
			System.out.println("ERROR: 'defaultShader.glsl'\n\tFragment shader compilation failed." );
			System.out.println(glGetShaderInfoLog(fragmentID, len));
			assert false : ""; 
		}
		
		// Link shaders and check for errors
		shaderProgram = glCreateProgram();
		glAttachShader(shaderProgram, vertexID);
		glAttachShader(shaderProgram, fragmentID);
		glLinkProgram(shaderProgram);
		
		// Check for linking errors
		success = glGetProgrami(shaderProgram, GL_LINK_STATUS);
		if (success == GL_FALSE) {
			int len = glGetProgrami(shaderProgram, GL_INFO_LOG_LENGTH);
			System.out.println("ERROR: 'defaultShader.glsl'\n\tLinking of shaders failed." );
			System.out.println(glGetProgramInfoLog(shaderProgram, len));
			assert false : "";
		}
	}
	
	@Override
	public void update(float dt) {
		
	}
	
}

이렇게 vertexShader와 fragmentShader GLSL을 가져와서 shaderSource를 지정하고 실행하게 된다.

Vertex Data 전달받아 실행하기

GLSL로 vertexShader와 fragmentShader를 정의하고 사용할 준비가 되었으면 그다음 단계로 입력받은 vertex data를 실행하여 결과를 출력하면 된다.

이번 예제에서는 외부에서 입력받는 게 아닌 임의의 변수로 vertex data array를 지정하여 실행하였다.

이때 사용되는 개념이 VBO, VAO, EBO이다.

이와 관련해서는 아래 링크가 한글로 설명이 잘되어 있다.

https://kyoungwhankim.github.io/ko/blog/opengl_triangle1/

VBO (Vertex Buffer Object)

VBO는 수많은 vertice (정점)을 저장하는 객체이다.

입력받은 vertex data array로 vertex buffer를 만들고 vertex buffer를 담은 VBO를 만든다.

Vertex Attributes

vertex shader는 vertex attribute 형태로 입력을 정의할 수 있다.

삼각형 꼭짓점의 vertex buffer data 형태는 다음과 같다.

  • position data는 23 bit (4 byte) floating point 값으로 저장된다.
  • 각 position은 3개의 값으로 이루어진다.
  • 각 3개의 값 사이에 공백 없이 배열에 저장된다.
  • data의 첫 번째 값은 buffer의 시작 부분에 있다.

glVertexAttribPointer를 사용하여 vertex data(vertex attribute 당)를 어떻게 해석해야 할지를 전달한다.

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

glVertexAttribPointer function에는 많은 parameter가 있다.

  • 첫 번째 parameter는 구성하려는 vertex attribute를 지정한다.
    layout이 있는 vertex shader에서 position vertex attribute의 위치를 지정했다. (위치 = 0)
  • 다음 parameter는 vertex attribute의 크기를 지정한다.
    vertex attribute는 vec3이므로 3개의 값으로 구성된다.
  • 세 번째 parameter는 GL_FLOAT (GLSL의 vec*는 floating point value로 구성됨)인 data type을 지정한다.
  • 다음 parameter는 data를 정규화할지 여부를 지정한다.
    integer data type (int, byte)를 입력하고 GL_TRUE로 설정했다면 int data는 float로 변환될 때 0 (또는 부호 있는 데이터의 경우 -1)과 1로 정규화된다.
  • 다섯 번째 인수는 stride로 알려져 있으며 연속적인 vertex attribute 사이의 공간을 알려준다.
    다음 position data set이 float size의 정확히 3배에 위치하므로 해당 값을 stride로 지정한다.
    array가 빽빽하게 채워져 있다는 것을 알고 있기 때문에 (다음 vertex attribute 값 사이에 공백이 없음) OpenGL이 이 stride를 결정하도록 stride를 0으로 지정할 수도 있다.
  • 마지막 parameter는 void* 유형이므로 특이한 caster가 필요하다.
    buffer에서 position data가 시작되는 위치의 offset이다.
    position data는 data array의 시작 부분에 있으므로 이 값은 0이다.

VAO (Vertex Array Object)

Vertex Array Object는 Vertex Buffer Object처럼 binding 될 수 있으며 해당 지점에서 이후의 모든 vertex attribute 호출은 VAO 내부에 저장된다.

vertex attribute pointer를 구성할 때 이러한 호출을 한 번만 수행하면 되고 object를 그릴 때마다 해당 VAO를 binding 할 수 있다는 장점이 있다.

다른 VAO를 binding 하는 것처럼 쉽게 다른 vertex data와 attribute 구성 사이의 전환을 가능하게 한다.

방금 설정한 모든 상태는 VAO 내부에 저장된다.

Vertex Array Object는 다음을 저장한다.

  • glEnableVertexAttribArray 또는 glDisableVertexAttribArray에 대한 호출.
  • glVertexAttribPointer를 통한 vertex attribute 구성
  • glVertexAttribPointer에 대한 호출에 의해 vertex attribute와 연관된 Vertex Buffer Object

EBO (Element Buffer Object)

만약 아래와 같이 2개의 삼각형을 가진 vertices를 가정해보자.

float vertices[] = {
    // first triangle
     0.5f,  0.5f, 0.0f,  // top right
     0.5f, -0.5f, 0.0f,  // bottom right
    -0.5f,  0.5f, 0.0f,  // top left 
    // second triangle
     0.5f, -0.5f, 0.0f,  // bottom right
    -0.5f, -0.5f, 0.0f,  // bottom left
    -0.5f,  0.5f, 0.0f   // top left
};

이 경우

첫 번째 삼각형의 bottom right가 두 번째 삼각형의 bottom right와 겹치고

첫 번째 삼각형의 top left가 두 번째 삼각형의 top left와 겹친다.

삼각형의 개수가 작은 경우는 상관없지만 수많은 삼각형이 존재할 경우 이러한 중복 값은 다음과 같이 구성하여 줄일 수 있다.

float vertices[] = {
     0.5f,  0.5f, 0.0f,  // top right
     0.5f, -0.5f, 0.0f,  // bottom right
    -0.5f, -0.5f, 0.0f,  // bottom left
    -0.5f,  0.5f, 0.0f   // top left 
};
unsigned int indices[] = {  // note that we start from 0!
    0, 1, 3,   // first triangle
    1, 2, 3    // second triangle
};

vertices에서 중복된 위치를 합치고 삼각형의 각 위치에 대해 vertices의 배열 위치를 지정한 indices를 통해 관리를 하였다.

VAO의 경우 glDrawArrays 함수를 통해 수행하는데 EBO를 사용하는 경우 glDrawElements 함수를 사용한다.

glUseProgram(shaderProgram);
glBindVertexArray(VAO);

// VAO의 경우 아래 호출
glDrawArrays(GL_TRIANGLES, 0, 3);

// EBO의 경우 아래 호출
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

강좌의 예제 실행하기

VAO, VBO, EBO에 대한 개념을 익히고 적용한 강좌의 예제 코드는 다음과 같다.

import java.awt.event.KeyEvent;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import org.lwjgl.BufferUtils;

import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.*;

public class LevelEditorScene extends Scene {

	private String vertexShaderSrc = "#version 330 core\r\n"
			+ "layout (location=0) in vec3 aPos;\r\n"
			+ "layout (location=1) in vec4 aColor;\r\n"
			+ "\r\n"
			+ "out vec4 fColor;\r\n"
			+ "\r\n"
			+ "void main()\r\n"
			+ "{\r\n"
			+ "	fColor = aColor;\r\n"
			+ "	gl_Position = vec4(aPos, 1.0); \r\n"
			+ "}";
	
	private String fragmentShaderSrc = "#version 330 core\r\n"
			+ "\r\n"
			+ "in vec4 fColor;\r\n"
			+ "\r\n"
			+ "out vec4 color;\r\n"
			+ "\r\n"
			+ "void main()\r\n"
			+ "{\r\n"
			+ "	color = fColor;\r\n"
			+ "}";
	
	private int vertexID, fragmentID, shaderProgram;
	
	private float[] vertexArray = {
			// position				//color
			0.5f, -0.5f, 0.0f,		1.0f, 0.0f, 0.0f, 1.0f,	// Bottom right
			-0.5f, 0.5f, 0.0f,		0.0f, 1.0f, 0.0f, 1.0f,	// Top left
			0.5f, 0.5f, 0.0f,		0.0f, 0.0f, 1.0f, 1.0f,	// Top right
			-0.5f, -0.5f, 0.0f,		1.0f, 1.0f, 1.0f, 1.0f,	// Bottom left
	};
	
	// IMPORTANT Must be in counter-clockwise order
	private int[] elementArray = {
			/*
			     x     x
			 
			     x     x
			 */
			2, 1, 0, // Top right triangle
			0, 1, 3, // bottom left trangle
	};
	
	private int vaoID, vboID, eboID;

	public LevelEditorScene() {
		
	}	
	
	@Override
	public void init() {
		// Compile and link shaders
		
		// First load and compile the vertex shader
		vertexID = glCreateShader(GL_VERTEX_SHADER);
		
		// pass the shader source to the GPU
		glShaderSource(vertexID, vertexShaderSrc);
		glCompileShader(vertexID);
		
		// Check for errors in compilation
		int success = glGetShaderi(vertexID, GL_COMPILE_STATUS);
		if (success == GL_FALSE) {
			int len = glGetShaderi(vertexID, GL_INFO_LOG_LENGTH);
			System.out.println("ERROR: 'defaultShader.glsl'\n\tVertex shader compilation failed." );
			System.out.println(glGetShaderInfoLog(vertexID, len));
			assert false : ""; 
		}
		
		// First load and compile the vertex shader
		fragmentID = glCreateShader(GL_FRAGMENT_SHADER);
		
		// pass the shader source to the GPU
		glShaderSource(fragmentID, fragmentShaderSrc);
		glCompileShader(fragmentID);
		
		// Check for errors in compilation
		success = glGetShaderi(fragmentID, GL_COMPILE_STATUS);
		if (success == GL_FALSE) {
			int len = glGetShaderi(fragmentID, GL_INFO_LOG_LENGTH);
			System.out.println("ERROR: 'defaultShader.glsl'\n\tFragment shader compilation failed." );
			System.out.println(glGetShaderInfoLog(fragmentID, len));
			assert false : ""; 
		}
		
		// Link shaders and check for errors
		shaderProgram = glCreateProgram();
		glAttachShader(shaderProgram, vertexID);
		glAttachShader(shaderProgram, fragmentID);
		glLinkProgram(shaderProgram);
		
		// Check for linking errors
		success = glGetProgrami(shaderProgram, GL_LINK_STATUS);
		if (success == GL_FALSE) {
			int len = glGetProgrami(shaderProgram, GL_INFO_LOG_LENGTH);
			System.out.println("ERROR: 'defaultShader.glsl'\n\tLinking of shaders failed." );
			System.out.println(glGetProgramInfoLog(shaderProgram, len));
			assert false : "";
		}
		
		// ================================================================
		// Generate VAO, VBO, and EBO buffer objects, and send to GPU
		// ================================================================
		vaoID = glGenVertexArrays();
		glBindVertexArray(vaoID);
		
		// Create a float buffer of vertices
		FloatBuffer vertexBuffer = BufferUtils.createFloatBuffer(vertexArray.length);
		vertexBuffer.put(vertexArray).flip();
		
		// Create VBO upload the vertex buffer
		vboID = glGenBuffers();
		glBindBuffer(GL_ARRAY_BUFFER, vboID);
		glBufferData(GL_ARRAY_BUFFER, vertexBuffer, GL_STATIC_DRAW);
		
		// Create the indices and upload
		IntBuffer elementBuffer = BufferUtils.createIntBuffer(elementArray.length);
		elementBuffer.put(elementArray).flip();
		
		eboID = glGenBuffers();
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboID);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, elementBuffer, GL_STATIC_DRAW);
		
		// Add the vertex attribute pointers
		int positionsSize = 3;
		int colorSize = 4;
		int floatSizeBytes = 4;
		int vertexSizeBytes = (positionsSize + colorSize) * floatSizeBytes;
		glVertexAttribPointer(0, positionsSize, GL_FLOAT, false, vertexSizeBytes, 0);
		glEnableVertexAttribArray(0);
		
		glVertexAttribPointer(1, colorSize, GL_FLOAT, false, vertexSizeBytes, positionsSize * floatSizeBytes);
		glEnableVertexAttribArray(1);
		
	}
	
	
	
	@Override
	public void update(float dt) {
		// Bind shader program
		glUseProgram(shaderProgram);
		// Bind the VAO that we're using
		glBindVertexArray(vaoID);
		
		// Enable the vertex attribute pointers
		glEnableVertexAttribArray(0);
		glEnableVertexAttribArray(1);
		
		glDrawElements(GL_TRIANGLES, elementArray.length, GL_UNSIGNED_INT, 0);
		
		// Unbind everything
		glDisableVertexAttribArray(0);
		glDisableVertexAttribArray(1);
		
		glBindVertexArray(0);
		
		glUseProgram(0);
	}
	
}

vertexArray로 4개의 좌표를 설정하고 elementArray로 이 4개의 좌표를 사용하는 2개의 삼각형 좌표를 지정하였는데 각각 꼭짓점이 우상-좌상-우하 인 삼각형과 우하-좌상-좌하 인 삼각형 두 개가 합쳐져서 직사각형이 만들어진다.

해당 예제를 수행한 결과는 다음과 같다.

#6 Regexes and Shader Abstraction (GSGL 파일 호출하기, 02:02:56)

#5 강의에서 String으로 참조했던 vertexShader와 fragmentShader를 GSGL 파일 호출로 변경해본다.

이를 수행하는 Shader class를 만들고 생성자에서 파일을 호출하여 정규식으로 GSGL 파일에 있는 vertexShader와 fragmentShader 코드를 추출한다.

#5 강의에서 LevelEditorScene의 init method에서 호출하던 부분을 Shader class로 옮긴다.

public class Shader {
	
	private int shaderProgramID;
	
	private String vertexSource;
	private String fragmentSource;
	private String filepath;
	

	public Shader(String filepath) {
		this.filepath = filepath;
		try {
			String source = new String(Files.readAllBytes(Paths.get(filepath)));
			String[] splitString = source.split("(#type)( )+([a-zA-Z]+)");
			
			// Find the first pattern after #type 'pattern'
			int index = source.indexOf("#type") + 6;
			int eol = source.indexOf("\r\n", index);
			String firstPattern = source.substring(index, eol).trim();
			
			// Find the second pattern after #type 'pattern'
			index = source.indexOf("#type", eol) + 6;
			eol = source.indexOf("\r\n", index);
			String secondPattern = source.substring(index, eol).trim();
			
			if (firstPattern.equals("vertex")) {
				vertexSource = splitString[1];
			} else if (firstPattern.equals("fragment")) {
				fragmentSource = splitString[1];
			} else {
				throw new IOException("Unexpected token '" + firstPattern + "' in '" + filepath + "'");
			}
			
			if (secondPattern.equals("vertex")) {
				vertexSource = splitString[2];
			} else if (secondPattern.equals("fragment")) {
				fragmentSource = splitString[2];
			} else {
				throw new IOException("Unexpected token '" + secondPattern + "' in '" + filepath + "'");
			}
			
		} catch (IOException e) {
			e.printStackTrace();
			assert false : "Error: Could not open file for shader: '" + filepath + "'";
			// TODO: handle exception
		}
	}
	
	public void compile() {
		// ================================================================
		// Compile and link shaders
		// ================================================================
		int vertexID, fragmentID;
		
		// First load and compile the vertex shader
		vertexID = glCreateShader(GL_VERTEX_SHADER);
		
		// pass the shader source to the GPU
		glShaderSource(vertexID, vertexSource);
		glCompileShader(vertexID);
		
		// Check for errors in compilation
		int success = glGetShaderi(vertexID, GL_COMPILE_STATUS);
		if (success == GL_FALSE) {
			int len = glGetShaderi(vertexID, GL_INFO_LOG_LENGTH);
			System.out.println("ERROR: '" + filepath + "'\n\tVertex shader compilation failed." );
			System.out.println(glGetShaderInfoLog(vertexID, len));
			assert false : ""; 
		}
		
		// First load and compile the vertex shader
		fragmentID = glCreateShader(GL_FRAGMENT_SHADER);
		
		// pass the shader source to the GPU
		glShaderSource(fragmentID, fragmentSource);
		glCompileShader(fragmentID);
		
		// Check for errors in compilation
		success = glGetShaderi(fragmentID, GL_COMPILE_STATUS);
		if (success == GL_FALSE) {
			int len = glGetShaderi(fragmentID, GL_INFO_LOG_LENGTH);
			System.out.println("ERROR: '" + filepath + "'\n\tFragment shader compilation failed." );
			System.out.println(glGetShaderInfoLog(fragmentID, len));
			assert false : ""; 
		}
		
		// Link shaders and check for errors
		shaderProgramID = glCreateProgram();
		glAttachShader(shaderProgramID, vertexID);
		glAttachShader(shaderProgramID, fragmentID);
		glLinkProgram(shaderProgramID);
		
		// Check for linking errors
		success = glGetProgrami(shaderProgramID, GL_LINK_STATUS);
		if (success == GL_FALSE) {
			int len = glGetProgrami(shaderProgramID, GL_INFO_LOG_LENGTH);
			System.out.println("ERROR: '" + filepath + "'\n\tLinking of shaders failed." );
			System.out.println(glGetProgramInfoLog(shaderProgramID, len));
			assert false : "";
		}
	}
	
	public void use() {
		// Bind shader program
		glUseProgram(shaderProgramID);
	}
	
	public void detach() {
		glUseProgram(0);
	}

}

LevelEditorScene은 shader관련 설정이 빠지게 된다.

package io.github.luversof.study.lwjgl.main.domain;

import java.awt.event.KeyEvent;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import org.lwjgl.BufferUtils;

import io.github.luversof.study.lwjgl.main.renderer.Shader;

import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.*;

public class LevelEditorScene extends Scene {
	
	private int shaderProgram;
	
	private float[] vertexArray = {
			// position				//color
			0.5f, -0.5f, 0.0f,		1.0f, 0.0f, 0.0f, 1.0f,	// Bottom right
			-0.5f, 0.5f, 0.0f,		0.0f, 1.0f, 0.0f, 1.0f,	// Top left
			0.5f, 0.5f, 0.0f,		0.0f, 0.0f, 1.0f, 1.0f,	// Top right
			-0.5f, -0.5f, 0.0f,		1.0f, 1.0f, 1.0f, 1.0f,	// Bottom left
	};
	
	// IMPORTANT Must be in counter-clockwise order
	private int[] elementArray = {
			/*
			     x     x
			 
			     x     x
			 */
			2, 1, 0, // Top right triangle
			0, 1, 3, // bottom left trangle
	};
	
	private int vaoID, vboID, eboID;
	
	private Shader defaultShader;

	public LevelEditorScene() {

	}	
	
	@Override
	public void init() {
		defaultShader = new Shader("src/main/resources/assets/shaders/default.glsl");
		defaultShader.compile();
		
		// ================================================================
		// Generate VAO, VBO, and EBO buffer objects, and send to GPU
		// ================================================================
		vaoID = glGenVertexArrays();
		glBindVertexArray(vaoID);
		
		// Create a float buffer of vertices
		FloatBuffer vertexBuffer = BufferUtils.createFloatBuffer(vertexArray.length);
		vertexBuffer.put(vertexArray).flip();
		
		// Create VBO upload the vertex buffer
		vboID = glGenBuffers();
		glBindBuffer(GL_ARRAY_BUFFER, vboID);
		glBufferData(GL_ARRAY_BUFFER, vertexBuffer, GL_STATIC_DRAW);
		
		// Create the indices and upload
		IntBuffer elementBuffer = BufferUtils.createIntBuffer(elementArray.length);
		elementBuffer.put(elementArray).flip();
		
		eboID = glGenBuffers();
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboID);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, elementBuffer, GL_STATIC_DRAW);
		
		// Add the vertex attribute pointers
		int positionsSize = 3;
		int colorSize = 4;
		int floatSizeBytes = 4;
		int vertexSizeBytes = (positionsSize + colorSize) * floatSizeBytes;
		glVertexAttribPointer(0, positionsSize, GL_FLOAT, false, vertexSizeBytes, 0);
		glEnableVertexAttribArray(0);
		
		glVertexAttribPointer(1, colorSize, GL_FLOAT, false, vertexSizeBytes, positionsSize * floatSizeBytes);
		glEnableVertexAttribArray(1);
		
	}
	
	
	
	@Override
	public void update(float dt) {
		defaultShader.use();
		// Bind shader program
		glUseProgram(shaderProgram);
		// Bind the VAO that we're using
		glBindVertexArray(vaoID);
		
		// Enable the vertex attribute pointers
		glEnableVertexAttribArray(0);
		glEnableVertexAttribArray(1);
		
		glDrawElements(GL_TRIANGLES, elementArray.length, GL_UNSIGNED_INT, 0);
		
		// Unbind everything
		glDisableVertexAttribArray(0);
		glDisableVertexAttribArray(1);
		
		glBindVertexArray(0);
		
		defaultShader.detach();
	}
	
}

#7 Game Camera OpenGL (02:24:25)

이제까지 과정을 통해 만든 결과물이 특정 좌표에 생성되면 이 생성된 결과물을 바라보는 시점이 있고 이 시점이 변화함에 따라 렌더링 된 결과물의 화면도 달라진다.

다음과 같이 Camera를 선언한다.

public class Camera {

	private Matrix4f projectionMatrix, viewMatrix;
	public Vector2f position;
	
	public Camera(Vector2f position) {
		this.position = position;
		this.projectionMatrix = new Matrix4f();
		this.viewMatrix = new Matrix4f();
		adjectProjection();
	}
	
	public void adjectProjection() {
		projectionMatrix.identity();
		projectionMatrix.ortho(0.0f, 32.0f * 40.0f, 0.0f, 32.0f * 21.0f, 0.0f, 100.0f);
	}
	
	public Matrix4f getViewMatrix() {
		Vector3f cameraFront = new Vector3f(0.0f, 0.0f, -1.0f);
		Vector3f cameraUp = new Vector3f(0.0f, 1.0f, 0.0f);
		this.viewMatrix.identity();
		viewMatrix.lookAt(new Vector3f(position.x, position.y, 20.0f),
				cameraFront.add(position.x, position.y, 0.0f), 
				cameraUp
				);
		return this.viewMatrix; 
	}
	
	public Matrix4f getProjectionMatrix() {
		return this.projectionMatrix;
	}
}

GLSL 파일에 uProjection과 uView 설정을 추가한다.

#type vertex
#version 330 core
layout (location=0) in vec3 aPos;
layout (location=1) in vec4 aColor;

uniform mat4 uProjection;
uniform mat4 uView;

out vec4 fColor;

void main()
{
	fColor = aColor;
	gl_Position = uProjection * uView * vec4(aPos, 1.0); 
}

#type fragment
#version 330 core

in vec4 fColor;

out vec4 color;

void main()
{
	color = fColor;
}

LevelEditorScene의 update method에 갱신될 내용을 다음과 같이 추가한다.

@Override
public void update(float dt) {
    camera.position.x -= dt * 50.0f;

    defaultShader.use();
    defaultShader.uploadMat4f("uProjection", camera.getProjectionMatrix());
    defaultShader.uploadMat4f("uView", camera.getViewMatrix());
    // ... 이하 생략
}

LevelEditorScene의 vertexArray도 아래처럼 수정하였는데 좌표 값 설정에 대해서는 별도로 공부를 해야 할 것 같다.

private float[] vertexArray = {
        // position				//color
        100.5f, 0.5f, 0.0f,		1.0f, 0.0f, 0.0f, 1.0f,	// Bottom right
        0.5f, 100.5f, 0.0f,		0.0f, 1.0f, 0.0f, 1.0f,	// Top left
        100.5f, 100.5f, 0.0f,		0.0f, 0.0f, 1.0f, 1.0f,	// Top right
        0.5f, 0.5f, 0.0f,		1.0f, 1.0f, 0.0f, 1.0f,	// Bottom left
};

화면에서 네모난 박스가 좌에서 우로 천천히 이동하게 된다.

덧. 이번 챕터에서 vertexArray의 position이 기존에 -1.0f ~ 1.0f 였던 게 왜 100.5f 같이 수치가 바뀌었는지 (max 가 1.0f가 아니었나?), GSGL 파일의 설정을 바꾸는 부분에 대한 추가적인 이해가 필요할 것 같다.

#8 GLSL Shaders (02:47:47)

Shader class에 대상 Object에 대해 값을 설정하기 위한 method를 추가하고 적용해본다.

추가하기 전에 각각의 method가 호출될 때마다 어느 method를 사용하건 glUseProgram method를 호출하면서 반복 호출하지 않도록 다음과 같이 수정한다.

public class Shader {

	// ... 중간 생략
    
	private boolean beingUsed = false;	// 최초 호출 체크를 위한 변수 추가
	
	public void use() {
		if (!beingUsed) {	// 조건문으로 glUseProgram 호출 체크
			// Bind shader program
			glUseProgram(shaderProgramID);
			beingUsed = true;
		}
	}
	
	public void detach() {
		glUseProgram(0);
		beingUsed = false;
	}
}

다음과 같이 추가한다.

	public void uploadMat4f(String varName, Matrix4f mat4) {
		int varLocation = glGetUniformLocation(shaderProgramID, varName); 
		use();
		FloatBuffer matBuffer = BufferUtils.createFloatBuffer(16);
		mat4.get(matBuffer); 
		glUniformMatrix4fv(varLocation, false, matBuffer);
	}
	
	public void uploadMat3f(String varName, Matrix3f mat3) {
		int varLocation = glGetUniformLocation(shaderProgramID, varName); 
		use();
		FloatBuffer matBuffer = BufferUtils.createFloatBuffer(9);
		mat3.get(matBuffer); 
		glUniformMatrix3fv(varLocation, false, matBuffer);
	}
	
	public void uploadVec4f(String varName, Vector4f vec) {
		int varLocation = glGetUniformLocation(shaderProgramID, varName);
		use();
		glUniform4f(varLocation, vec.x, vec.y, vec.z, vec.w);
	}
	
	public void uploadVec3f(String varName, Vector3f vec) {
		int varLocation = glGetUniformLocation(shaderProgramID, varName);
		use();
		glUniform3f(varLocation, vec.x, vec.y, vec.z);
	}
	
	public void uploadVec2f(String varName, Vector2f vec) {
		int varLocation = glGetUniformLocation(shaderProgramID, varName);
		use();
		glUniform2f(varLocation, vec.x, vec.y);
	}
	
	public void uploadFloat(String varName, float val) {
		int varLocation = glGetUniformLocation(shaderProgramID, varName);
		use();
		glUniform1f(varLocation, val);
	}
	
	public void uploadInt(String varName, int val) {
		int varLocation = glGetUniformLocation(shaderProgramID, varName);
		use();
		glUniform1i(varLocation, val);
	}

GSGL 파일의 fragment shader 구문을 다음과 같이 변경하여 color의 값이 시간에 따라 sin 함수로 변경되도록 한다.

#type fragment
#version 330 core

uniform float uTime;

in vec4 fColor;

out vec4 color;

void main()
{
	color = sin(uTime) * fColor;
}

LevelEditorScene의 update method에 shader애 uTime을 전달하는 설정을 추가한다.

@Override
public void update(float dt) {
    camera.position.x -= dt * 50.0f;

    defaultShader.use();
    defaultShader.uploadMat4f("uProjection", camera.getProjectionMatrix());
    defaultShader.uploadMat4f("uView", camera.getViewMatrix());
    defaultShader.uploadFloat("uTime", TimeUtil.getTime());

    // ... 이하 생략
}

utime이 지속적으로 변경되면서 다음과 같이 색이 변경된다.

GSGL 파일의 fragment shader를 다음과 같이 변경하면

#type fragment
#version 330 core

uniform float uTime;

in vec4 fColor;

out vec4 color;

void main()
{
	float avg = (fColor.r + fColor.g + fColor.b) / 3;
	color = vec4(avg, avg, avg, 1);
}

다음과 같이 변경된다.

GSGL 파일의 fragment shader를 다음과 같이 변경하면

#type fragment
#version 330 core

uniform float uTime;

in vec4 fColor;

out vec4 color;

void main()
{
	float noise = fract(sin(dot(fColor.xy, vec2(12.9898, 78.233))) * 43758.5453);
	color = fColor * noise;
}

다음과 같이 변경된다.

GSGL의 noise 함수에 대해서는 다음 문서를 참고하면 된다.

http://science-and-fiction.org/rendering/noise.html

#9 Texture Loading in LWJGL3 (03:08:45)

Texture를 사용하는 방법을 안내한다.

텍스처는 사각형으로 구성되고 각각의 좌표는 다음과 같다.

  • 좌하 - 0,0
  • 우하 - 0,1
  • 좌상 - 1,0
  • 우상 - 1.1

텍스처의 좌표 값을 기존 LevelEditorScene의 vertexArray에 다음과 같이 추가한다.

private float[] vertexArray = {
        // position				//color						// UV Coordinates
        100.5f, 0.5f, 0.0f,		1.0f, 0.0f, 0.0f, 1.0f,		1, 0,	// Bottom right	0
        0.5f, 100.5f, 0.0f,		0.0f, 1.0f, 0.0f, 1.0f,		0, 1,	// Top left		1	
        100.5f, 100.5f, 0.0f,	0.0f, 0.0f, 1.0f, 1.0f,		1, 1,	// Top right	2
        0.5f, 0.5f, 0.0f,		1.0f, 1.0f, 0.0f, 1.0f,		0, 0,	// Bottom left	3
};

LevelEditorScene init method에서 해당 array를 사용한 vertex attribute pointer에 대한 설정을 다음과 같이 변경한다.

// Add the vertex attribute pointers
int positionsSize = 3;
int colorSize = 4;
int uvSize = 2;
int vertexSizeBytes = (positionsSize + colorSize + uvSize) * Float.BYTES;
glVertexAttribPointer(0, positionsSize, GL_FLOAT, false, vertexSizeBytes, 0);
glEnableVertexAttribArray(0);

glVertexAttribPointer(1, colorSize, GL_FLOAT, false, vertexSizeBytes, positionsSize * Float.BYTES);
glEnableVertexAttribArray(1);

glVertexAttribPointer(2, uvSize, GL_FLOAT, false, vertexSizeBytes, (positionsSize + colorSize) * Float.BYTES);
glEnableVertexAttribArray(2);

Texture class를 다음과 같이 작성한다.

package io.github.luversof.study.lwjgl.main.renderer;

import static org.lwjgl.opengl.GL11.GL_NEAREST;
import static org.lwjgl.opengl.GL11.GL_REPEAT;
import static org.lwjgl.opengl.GL11.GL_RGB;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MAG_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MIN_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_S;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_T;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
import static org.lwjgl.opengl.GL11.glBindTexture;
import static org.lwjgl.opengl.GL11.glGenTextures;
import static org.lwjgl.opengl.GL11.glTexImage2D;
import static org.lwjgl.opengl.GL11.glTexParameteri;
import static org.lwjgl.stb.STBImage.stbi_image_free;
import static org.lwjgl.stb.STBImage.stbi_load;

import java.nio.ByteBuffer;
import java.nio.IntBuffer;

import org.lwjgl.BufferUtils;


public class Texture {
	
	private String filepath;
	private int texID;
	
	public Texture(String filepath) {
		this.filepath = filepath;
		
		// Generate texture on GPU
		texID = glGenTextures();
		glBindTexture(GL_TEXTURE_2D, texID);
		
		// Set texture parameters
		// Repeat image in both directions
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
		// When stretching the image, pixelate
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		// When shrinking an image, pixelate
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		
		IntBuffer width = BufferUtils.createIntBuffer(1);
		IntBuffer height = BufferUtils.createIntBuffer(1);
		IntBuffer channels = BufferUtils.createIntBuffer(1);
		ByteBuffer image = stbi_load(filepath, width, height, channels, 0);
		
		if (image != null) {
			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width.get(0), height.get(0), 0, GL_RGB, GL_UNSIGNED_BYTE, image);
		} else {
			assert false : "Error: (Texture) Could not load image '" + filepath + "'";
		}
		
		stbi_image_free(image);
	}
	
	public void bind() {
		glBindTexture(GL_TEXTURE_2D, texID);
	}
	
	public void unbind() {
		glBindTexture(GL_TEXTURE_2D, 0);
	}
}

LevelEditorScene에 texture를 사용하는 설정을 추가한다.

public class LevelEditorScene extends Scene {
	
	// ...이하 생략
	
	private Shader defaultShader;
	private Texture testTexture;

	public LevelEditorScene() {

	}	
	
	@Override
	public void init() {
		this.camera = new Camera(new Vector2f());
		defaultShader = new Shader("src/main/resources/assets/shaders/default.glsl");
		defaultShader.compile();
		testTexture = new Texture("src/main/resources/assets/images/testImage.jpg");
		
		//... 이하 생략
	}
	
	// ... 이하 생략
}

Shader에 uploadTexture method를 추가한다.

testImage.jpg는 해당 강좌의 예제 샘플에서 가져오면 된다.

public void uploadTexture(String varName, int slot) {
    int varLocation = glGetUniformLocation(shaderProgramID, varName);
    use();
    glUniform1i(varLocation, slot);
}

default.glsl의 fragment shader에 다음과 같이 texture uniform을 추가한다.

#type fragment
#version 330 core

uniform float uTime;
uniform sampler2D TEX_SAMPLER;

in vec4 fColor;

out vec4 color;

void main()
{
	color = fColor;
}

LevelEditorScene의 update method에 해당 texture를 upload 하도록 추가한다.

	@Override
	public void update(float dt) {
		camera.position.x -= dt * 50.0f;
		
		defaultShader.use();
		
		// Upload texture to shader
		defaultShader.uploadTexture("TEX_SAMPLER", 0);
		glActiveTexture(GL_TEXTURE0);
		testTexture.bind();
		
		defaultShader.uploadMat4f("uProjection", camera.getProjectionMatrix());
		defaultShader.uploadMat4f("uView", camera.getViewMatrix());
		defaultShader.uploadFloat("uTime", TimeUtil.getTime());
	
		// ... 이하 생략
	}

여기까지 진행하고 실행하여 오류가 없으면 다음과 같이 default.glsl에 texture를 입력받고 처리하도록 변경한다.

#type vertex
#version 330 core
layout (location=0) in vec3 aPos;
layout (location=1) in vec4 aColor;
layout (location=2) in vec2 aTexCoords;

uniform mat4 uProjection;
uniform mat4 uView;

out vec4 fColor;
out vec2 fTexCoords;

void main()
{
	fColor = aColor;
	fTexCoords = aTexCoords;
	gl_Position = uProjection * uView * vec4(aPos, 1.0); 
}

#type fragment
#version 330 core

uniform float uTime;
uniform sampler2D TEX_SAMPLER;

in vec4 fColor;
in vec2 fTexCoords;

out vec4 color;

void main()
{
	color = texture(TEX_SAMPLER, fTexCoords);
}

이제 실행하면 다음과 같이 기존 사각형에 texture가 입혀진 결과가 나온다.

원본 이미지의 경우 하트가 정위치인데 출력 결과를 보면 뒤집어져있다.

Y축이 전환된 상태인데 다음과 같이 LevelEditorScene의 vertexArray의 UV coordinates를 변경하면 올바른 방향으로 나온다.

	private float[] vertexArray = {
			// position				//color						// UV Coordinates
			100.5f, 0.5f, 0.0f,		1.0f, 0.0f, 0.0f, 1.0f,		1, 1,	// Bottom right	0
			0.5f, 100.5f, 0.0f,		0.0f, 1.0f, 0.0f, 1.0f,		0, 0,	// Top left		1	
			100.5f, 100.5f, 0.0f,	0.0f, 0.0f, 1.0f, 1.0f,		1, 0,	// Top right	2
			0.5f, 0.5f, 0.0f,		1.0f, 1.0f, 0.0f, 1.0f,		0, 1,	// Bottom left	3
	};

기존엔 y 축이 0, 1, 1, 0이었는데 1, 0, 0, 1로 변경하였다.

(왜 좌표가 뒤집어지는지 이해를 못 했다..)

Texture의 생성자의 glTexImage2D 호출 부분도 다음과 같이 보강한다.

if (image != null) {
    if (channels.get(0) == 3) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width.get(0), height.get(0), 0, GL_RGB, GL_UNSIGNED_BYTE, image);
    } else if (channels.get(0) == 4) {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width.get(0), height.get(0), 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
    } else {
        assert false : "Error: (Texture) Unknown number of channels '" + filepath + "'";
    }
} else {
    assert false : "Error: (Texture) Could not load image '" + filepath + "'";
}

#10 Entity Component System (03:36:02)

본격적으로 하나의 객체가 아닌 여러 객체를 사용해본다.

java로 GameObject, Component class 상속을 구현해보는 내용이다.

다음과 같이 Component와 GameObject를 만든다.

package io.github.luversof.study.lwjgl.main.domain;

public abstract class Component {
	
	public GameObject gameObject = null;

	public void start() {
		
	};
	
	public abstract void update(float dt);

}
package io.github.luversof.study.lwjgl.main.domain;

import java.util.List;

public class GameObject {
	
	private String name;
	
	private List<Component> components;

	public GameObject(String name) {
		this.name = name;
        this.components = new ArrayList<>();
	}

	public <T extends Component> T getComponent(Class<T> componentClass) {
		for (Component c : components) {
			if (componentClass.isAssignableFrom(c.getClass())) {
				try {
					return componentClass.cast(c);
				} catch (Exception e) {
					e.printStackTrace();
					assert false : "Error: Casting componet.";
				}
			}
		}
		return null;
	}
	
	public <T extends Component> void removeComponent(Class<T> componentClass) {
		for (int i = 0 ; i < components.size() ; i++) {
			Component c = components.get(i);
			if (componentClass.isAssignableFrom(c.getClass())) {
				components.remove(i);
				return;
			}
		}
	}
	
	public void addComponent(Component c) {
		this.components.add(c);
		c.gameObject = this;
	}
	
	public void update(float dt) {
		for (int i = 0 ; i < components.size() ; i++) {
			components.get(i).update(dt);
		}
	}
	
	public void start() {
		for(int i = 0 ; i < components.size() ; i++) {
			components.get(i).start();
		}
	}
}

이제 Scene abstract class에 GameObject 추가 관련 공통 method와 field를 선언한다.

start, addGameObject method가 추가되었다.

package io.github.luversof.study.lwjgl.main.domain;

import java.util.ArrayList;
import java.util.List;

public abstract class Scene {
	
	protected Camera camera;
	
	private boolean isRunning = false;
	
	private List<GameObject> gameObjects = new ArrayList<>();

	public Scene() {
		
	}
	
	public void init() {
		
	}
	
	public void start() {
		for (GameObject go : gameObjects) {
			go.start();            
		}
        isRunning = true;
	}
	
	public void addGameObjectToScene(GameObject go) {
		if (!isRunning) {
			gameObjects.add(go);
		} else {
			gameObjects.add(go);
			go.start();
		}
	}
	
	public abstract void update(float dt);

}

Window class의 changeScene method에서 init만 호출하던 부분에 start method도 호출하도록 추가해준다.

	public static void changeScene(int newScene) {
		switch (newScene) {
		case 0:
			currentScene = new LevelEditorScene();
			currentScene.init();
			currentScene.start();
			break;
		case 1:
			currentScene = new LevelScene();
			currentScene.init();
			currentScene.start();
			break;
			default :
				assert false : "Unknown scene '" + newScene + "'";
				break;
		}
	}

Component를 구현한 SpriteRenderer를 구현해본다.

임시로 method가 호출되었다는 출력만 처리한다.

package io.github.luversof.study.lwjgl.main.components;

import io.github.luversof.study.lwjgl.main.domain.Component;

public class SpriteRenderer extends Component {
	
	private boolean firstTime = false;

	@Override
	public void start() {
		System.out.println("I am starting");
	}

	@Override
	public void update(float dt) {
		if (!firstTime) {
			System.out.println("I am updating");
			firstTime = true;
		}
	}

}

LevelEditorScene에 테스트를 위해 SpriteRenderer를 추가한 gameObject를 추가한다.

package io.github.luversof.study.lwjgl.main.domain;

import static org.lwjgl.opengl.GL11.GL_FLOAT;
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_INT;
import static org.lwjgl.opengl.GL11.glDrawElements;
import static org.lwjgl.opengl.GL13.GL_TEXTURE0;
import static org.lwjgl.opengl.GL13.glActiveTexture;
import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
import static org.lwjgl.opengl.GL15.glBindBuffer;
import static org.lwjgl.opengl.GL15.glBufferData;
import static org.lwjgl.opengl.GL15.glGenBuffers;
import static org.lwjgl.opengl.GL20.glDisableVertexAttribArray;
import static org.lwjgl.opengl.GL20.glEnableVertexAttribArray;
import static org.lwjgl.opengl.GL20.glVertexAttribPointer;
import static org.lwjgl.opengl.GL30.glBindVertexArray;
import static org.lwjgl.opengl.GL30.glGenVertexArrays;


import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import org.joml.Vector2f;
import org.lwjgl.BufferUtils;

import io.github.luversof.study.lwjgl.main.components.SpriteRenderer;
import io.github.luversof.study.lwjgl.main.renderer.Shader;
import io.github.luversof.study.lwjgl.main.renderer.Texture;
import io.github.luversof.study.lwjgl.util.TimeUtil;

public class LevelEditorScene extends Scene {
	
	// ... 이하 생략
	
	GameObject testObj;
	private boolean firstTime = false;

	@Override
	public void init() {
		System.out.println("Creating 'testObject'");
		this.testObj = new GameObject("testObject");
		this.testObj.addComponent(new SpriteRenderer());
		this.addGameObjectToScene(this.testObj);
		
		// ... 이하 생략
	}
	
	@Override
	public void update(float dt) {
		// ... 이하 생략

		if (!firstTime) {
			System.out.println("Creating gameObject!");
			GameObject go = new GameObject("Gamte Test 2");
			go.addComponent(new SpriteRenderer());
			this.addGameObjectToScene(go);
			firstTime = true;
		}
		
		for (GameObject go : this.gameObjects) {
			go.update(dt);
		}
	}
	
}

위 코드는 LevelEditorScene init 호출 시 gameObject를 Scene에 추가하고 이후 update method 호출 시 최초 1번 다른 gameObject를 추가한 후 update를 실행하였다.

위 코드의 결과는 다음과 같다.

Creating 'testObject'
I am starting
Creating gameObject!
I am starting
I am updating
I am updating

또 다른 테스트로 FontRenderer components를 만들고

package io.github.luversof.study.lwjgl.main.components;

import io.github.luversof.study.lwjgl.main.domain.Component;

public class FontRenderer extends Component {
	
	@Override
	public void start() {
		if (gameObject.getComponent(SpriteRenderer.class) != null) {
			System.out.println("Found Font Renderer!");
		}
	}

	@Override
	public void update(float dt) {
		
	}

}

LevelEditorScene init method에 해당 component를 추가하면

@Override
public void init() {
    System.out.println("Creating 'testObject'");
    this.testObj = new GameObject("testObject");
    this.testObj.addComponent(new SpriteRenderer());
    this.testObj.addComponent(new FontRenderer());
    this.addGameObjectToScene(this.testObj);

    // ... 이하 생략
}

결과에서 다음과 같이 FontRenderer도 start method가 실행된 게 추가된 것을 확인할 수 있다.

Creating 'testObject'
I am starting
Found Font Renderer!
Creating gameObject!
I am starting
I am updating
I am updating

이렇게 Scene에 GameObject 들을 추가하고 GameObject에는 Component들을 추가해서 구성하였다.

#11 Batch Rendering in LWJGL3 (04:06:25)

게임 화면의 요소들은 (삼각형 2개가 합쳐서 이루어진) 수많은 사각형으로 구성되어있다.

이 사각형 요소들로 화면을 구성하는 방법을 알아본다.

이 사각형 요소의 구성을 다음과 같이 한다고 가정한다.

V0 = {x, y, r, g, b, a}

이전에 화면에 처리하기 위해 각 꼭짓점의 좌표를 설정하였는데 각 사각형이 고정된 크기로 배치된다는 전제 조건으로 구성하면 단순히 사각형의 위치에 대해서만(4개의 꼭짓점 중 하나만) x, y로 설정하고 색상과 투명도 값인 r, g, b, a까지 선언하면 된다.

따라서 여러 사각형에 대해서는 다음과 같이 배열에 넣을 수 있다.

vertices = { x0, y0, r0, g0, b0, a0, x1, y1, r1, g1, b1, a1, ... }

이렇게 배치할 사각형 정보를 전달받아 추가하고 처리하기 위해 바로 전 강좌에서 만든 Renderer class를 사용한 RendererBatch를 만들어본다.

Renderer는 vertices를 관리하고 RendererBatch는 이 Renderer를 기준으로 화면을 처리한다.

다음과 같이 Transform class를 만든다.

public class Transform {
	
	public Vector2f position;
	public Vector2f scale;
	
	public Transform() {
		init(new Vector2f(), new Vector2f());
	}
	
	public Transform(Vector2f position) {
		init(position, new Vector2f());
	}
	
	public Transform(Vector2f position, Vector2f scale) {
		init(position, scale);
	}
	
	public void init(Vector2f position, Vector2f scale) {
		this.position = position;
		this.scale = scale;
	}

}

Transform을 GameObject에 다음과 같이 추가한다.

public class GameObject {
	
	private String name;
	
	private List<Component> components;
	private Transform transform;

	public GameObject(String name) {
		this.name = name;
		this.components = new ArrayList<>();
		this.transform = new Transform();
	}
	
	public GameObject(String name, Transform transform) {
		this.name = name;
		this.components = new ArrayList<>();
		this.transform = transform;
	}
    // ... 이하 생략
}

이전 강좌에서 추가한 SpriteRenderer를 다음과 같이 변경하여 color를 받도록 한다.

public class SpriteRenderer extends Component {

	private Vector4f color;
	
	public SpriteRenderer(Vector4f color) {
		this.color = color;
	}

	@Override
	public void start() {
	}

	@Override
	public void update(float dt) {

	}

	public Vector4f getColor() {
		return this.color;
	}
}

Window class에서 scene을 기존엔 직접 호출하여 사용했었지만 method로 접근하도록 다음과 같이 추가하고

public class MainWIndow {

	// ... 이하 생략

	public static Scene getScene() {
		return get().currentScene;
	}
}

scene에선 camera를 호출할 수 있도록 다음과 같이 추가한다.

public abstract class Scene {
	
	protected Camera camera;
	
	// ... 이하 생략

	public Camera camera() {
		return this.camera;
	}
}

이제 RendererBatch를 만든다.

기존 LevelEditorScene에서 1개의 정사각형을 만들기 위해 2개의 삼각형을 설정하고 호출하는 코드를 작성한 적이 있는데 해당 코드를 복수의 배열로 처리하는 과정을 담고 있다.

import static org.lwjgl.opengl.GL11.GL_FLOAT;
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_INT;
import static org.lwjgl.opengl.GL11.glDrawElements;
import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_DYNAMIC_DRAW;
import static org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
import static org.lwjgl.opengl.GL15.glBindBuffer;
import static org.lwjgl.opengl.GL15.glBufferData;
import static org.lwjgl.opengl.GL15.glBufferSubData;
import static org.lwjgl.opengl.GL15.glGenBuffers;
import static org.lwjgl.opengl.GL20.glDisableVertexAttribArray;
import static org.lwjgl.opengl.GL20.glEnableVertexAttribArray;
import static org.lwjgl.opengl.GL20.glVertexAttribPointer;
import static org.lwjgl.opengl.GL30.glBindVertexArray;
import static org.lwjgl.opengl.GL30.glGenVertexArrays;

import org.joml.Vector4f;

import io.github.luversof.study.lwjgl.main.components.SpriteRenderer;
import io.github.luversof.study.lwjgl.main.domain.MainWindow;

public class RenderBatch {
	
	// Vertex
	// =======
	// Pos				Color
	// float, float, 	float, float, float, float
	private final int POS_SIZE = 2;
	private final int COLOR_SIZE = 4;
	
	private final int POS_OFFSET = 0;
	private final int COLOR_OFFSET = POS_OFFSET + POS_SIZE * Float.BYTES;
	private final int VERTEX_SIZE = 6;
	private final int VERTEX_SIZE_BYTES = VERTEX_SIZE * Float.BYTES;
	
	private SpriteRenderer[] sprites;
	private int numSprites;
	private boolean hasRoom;
	private float[] vertices;
	
	private int vaoID, vboID;
	private int maxBatchSize;
	private Shader shader;
	
	public RenderBatch(int maxBatchSize) {
		shader = new Shader("src/main/resources/assets/shaders/default.glsl");
		shader.compile();
		this.sprites = new SpriteRenderer[maxBatchSize];
		this.maxBatchSize = maxBatchSize;
		
		// 4 vertices quads
		vertices = new float[maxBatchSize * 4 * VERTEX_SIZE];
		
		this.numSprites = 0;
		this.hasRoom = true;
	}
	
	public void start() {
		// Generate and bind a Vertex Array Object
		vaoID = glGenVertexArrays();
		glBindVertexArray(vaoID);
		
		// Allocate space for vertices
		vboID = glGenBuffers();
		glBindBuffer(GL_ARRAY_BUFFER, vboID);
		glBufferData(GL_ARRAY_BUFFER, vertices.length * Float.BYTES, GL_DYNAMIC_DRAW);
		
		// Create and upload indices buffer
		int eboID = glGenBuffers();
		int[] indices = generateIndices();
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboID);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW);
		
		// Enable the buffer attribute pointers
		glVertexAttribPointer(0, POS_SIZE, GL_FLOAT, false, VERTEX_SIZE_BYTES, POS_OFFSET);
		glEnableVertexAttribArray(0);
		
		glVertexAttribPointer(1, COLOR_SIZE, GL_FLOAT, false, VERTEX_SIZE_BYTES, COLOR_OFFSET);
		glEnableVertexAttribArray(1);
	}
	
	public void addSprite(SpriteRenderer spr) {
		// Get index and add renderObject
		int index = this.numSprites;
		// [0, 1, 2, 3, 4, 5]
		this.sprites[index] = spr;
		this.numSprites ++;
		
		// Add properties to local vertices array
		loadVertexProperties(index);
		
		if (numSprites >= this.maxBatchSize) {
			this.hasRoom = false;
		}
	}
	
	public void render() {
		// For now, we will rebuffer all data every frame
		glBindBuffer(GL_ARRAY_BUFFER, vboID);
		glBufferSubData(GL_ARRAY_BUFFER, 0, vertices);
		
		// Use shader
		shader.use();
		shader.uploadMat4f("uProjection", MainWindow.getScene().camera().getProjectionMatrix());
		shader.uploadMat4f("uView", MainWindow.getScene().camera().getViewMatrix());
		
		glBindVertexArray(vaoID);
		glEnableVertexAttribArray(0);
		glEnableVertexAttribArray(1);
		
		glDrawElements(GL_TRIANGLES, this.numSprites * 6, GL_UNSIGNED_INT, 0);
		
		glDisableVertexAttribArray(0);
		glDisableVertexAttribArray(1);
		glBindVertexArray(0);
		
		shader.detach();
	}
	
	private void loadVertexProperties(int index) {
		SpriteRenderer sprite = this.sprites[index];
		
		// Find offset within array (4 vertices per sprite)
		int offset = index * 4 * VERTEX_SIZE;
		// float float		float float float float
		
		Vector4f color = sprite.getColor();
		
		// Add vertice with the appropriate properties
		
		//  *   *
		//  *   *
		
		float xAdd = 1.0f;
		float yAdd = 1.0f;
		for (int i = 0 ; i < 4 ; i++) {
			if (i == 1) {
				yAdd = 0.0f;
			} else if (i == 2) {
				xAdd = 0.0f;
			} else if (i == 3) {
				yAdd = 1.0f;
			}
			
			// Load position
			vertices[offset] = sprite.gameObject.transform.position.x + (xAdd * sprite.gameObject.transform.scale.x);
			vertices[offset + 1] = sprite.gameObject.transform.position.y + (yAdd * sprite.gameObject.transform.scale.y);
			
			// Load color
			vertices[offset + 2] = color.x;
			vertices[offset + 3] = color.y;
			vertices[offset + 4] = color.z;
			vertices[offset + 5] = color.w;
			
			offset += VERTEX_SIZE;
		}
		
	}
	
	private int[] generateIndices() {
		// 6 indices per quad (3 per triangle)
		int[] elements = new int[6 * maxBatchSize];
		for (int i = 0 ; i < maxBatchSize ; i++) {
			loadElementIndices(elements, i);
		}
		return elements;
	}
	
	private void loadElementIndices(int[] elements, int index) {
		int offsetArrayIndex = 6 * index;
		int offset = 4 * index;
		
		// 3, 2, 0, 0, 2, 1		7, 6, 4, 4, 6, 5
		// Triangle 1
		elements[offsetArrayIndex] = offset + 3;
		elements[offsetArrayIndex + 1] = offset + 2;
		elements[offsetArrayIndex + 2] = offset + 0;
		
		// Trangle 2
		elements[offsetArrayIndex + 3] = offset + 0;
		elements[offsetArrayIndex + 4] = offset + 2;
		elements[offsetArrayIndex + 5] = offset + 1;
		
	}

	public boolean hasRoom() {
		return this.hasRoom;
	}
}

이제 RenderBatch의 render를 실행할 Renderer를 작성한다.

import java.util.ArrayList;
import java.util.List;

import io.github.luversof.study.lwjgl.main.components.SpriteRenderer;
import io.github.luversof.study.lwjgl.main.domain.GameObject;

public class Renderer {

	private final int MAX_BATCH_SIZE = 1000;
	private List<RenderBatch> batches;
	
	public Renderer() {
		this.batches = new ArrayList<>();
	}
	
	public void add(GameObject go) {
		SpriteRenderer spr = go.getComponent(SpriteRenderer.class);
		if (spr != null) {
			add(spr);
		}
	}
	
	private void add(SpriteRenderer sprite) {
		boolean added = false;
		for (RenderBatch batch : batches) {
			if (batch.hasRoom()) {
				batch.addSprite(sprite);
				added = true;
				break;
			}
		}
		
		if (!added) {
			RenderBatch newBatch = new RenderBatch(MAX_BATCH_SIZE);
			newBatch.start();
			batches.add(newBatch);
			newBatch.addSprite(sprite);
		}
	}
	
	public void render() {
		for (RenderBatch batch : batches) {
			batch.render();
		}
	}
}

이제 Scene에서 renderer를 사용하도록 다음과 같이 변경한다.

public abstract class Scene {
	
	protected Renderer renderer = new Renderer();
	// ... 이하 생략
	
	public void start() {
		for (GameObject go : gameObjects) {
			go.start();
			this.renderer.add(go);
		}
		isRunning = true;
	}
	
	public void addGameObjectToScene(GameObject go) {
		if (!isRunning) {
			gameObjects.add(go);
		} else {
			gameObjects.add(go);
			go.start();
			this.renderer.add(go);
		}
	}
	
	// ... 이하 생략
}

이제 scene이 시작되거나 scene에 gameObject가 추가되면 renderer에도 gameObject가 추가된다.

여기까지 진행하고 실행해보면 화면에 검은 직사각형을 확인할 수 있다.

default.glsl 파일의 기존 texture 관련 처리를 다음과 같이 제거하면

#type vertex
#version 330 core
layout (location=0) in vec3 aPos;
layout (location=1) in vec4 aColor;

uniform mat4 uProjection;
uniform mat4 uView;

out vec4 fColor;

void main()
{
	fColor = aColor;
	gl_Position = uProjection * uView * vec4(aPos, 1.0); 
}

#type fragment
#version 330 core

in vec4 fColor;

out vec4 color;

void main()
{
	color = fColor;
}

다음과 같은 결과를 확인할 수 있다.

변경된 내용을 정리하면 다음과 같다.

  • LevelEditorScene init method에서 100 * 100 개 만큼 SpriteRenderer를 만들어 추가, update에서 renderer의 render method 호출
  • Renderer에서 RenderBatch 리스트를 관리하고 render method를 순차적으로 RenderBatch의 render method를 호출

LevelEditorScene의 update method에 다음처럼 추가하여 FPS를 확인할 수 있다.

(dt는 MainWindow에서 glfwGetTime() 를 호출하여 계산한다.)

System.out.println("FPS: " + (1.0f / dt));

보통 본인의 모니터 해상도 만큼 FPS가 나오는데 Renderer의 MAX_BATCH_SIZE가 1000이던걸 1로 바꾸면 거의 1.x FPS까지 하락하는 것을 볼 수 있다.

전환할 장면이 없어서 lwjgl이 update 주기가 느려진 것으로 보이는데 정확한 동작은 아직 모르겠다.

(glfwSwapInterval(1); 설정이 v-sync를 활성화 된 후 while ( !glfwWindowShouldClose(window) ) {}의 반복문의 실행 주기가 처리되는 게 아닌가 유추해봄)

 

반응형
profile

파란하늘의 지식창고

@Bluesky_

내용이 유익했다면 광고 배너를 클릭 해주세요