石丸です。
LWJGL第1話 – ポリゴンを回転させるでは昔ながらの固定機能パイプライン
を使用しましたが、この機能はOpenGL 3.xで廃止予定の非推奨機能になっています。
今回はLightweight Java Game Library 3 TutorialのRendering with Shadersで学んだ、プログラマブルシェーダーを使用した今風の書き方でポリゴンを回転させてみました。
今回の成果物
https://github.com/dcom-ishimaru/lwjgl-samples/tree/master/hello-shader
OpenGLのバージョン
OpenGLのバージョンは3.3にしました。
Macで対応しているOpenGLのバージョンが3.3 or 4.1で、少しでも多くの環境で動けばと思い3.3を選びました。
各MacがサポートするOpenGLのバージョン一覧
OpenGL および OpenCL グラフィックスを扱う Mac コンピュータ – Apple サポート
https://support.apple.com/ja-jp/HT202823
シェーダー
頂点シェーダ、フラグメントシェーダーともにチュートリアルのままです。
プログラムから渡された頂点情報(座標、色)をそのまま出力するシェーダーです。
GLSLのバージョンだけ3.3にしました。
頂点シェーダー hello.vert
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#version 330
// 頂点情報(3次元座標と色)
in vec3 position;
in vec3 color;
// 次のフラグメントシェーダーに渡す頂点カラー
out vec3 vertexColor;
// プログラムから指定されるグローバルGLSL変数
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
// フラグメントシェーダーには頂点の色をそのまま渡す
vertexColor = color;
// ModelViewProjection行列を求める
mat4 mvp = projection * view * model;
// gl_Positionが最終的な頂点座標
gl_Position = mvp * vec4(position, 1.0);
}
|
フラグメントシェーダー hello.frag
1
2
3
4
5
6
7
8
9
10
11
|
#version 330
// 頂点シェーダーから渡された頂点カラー
in vec3 vertexColor;
// フラグメントシェーダから出力する色
out vec4 fragColor;
void main() {
// 頂点カラーをそのまま出力
fragColor = vec4(vertexColor, 1.0);
}
|
シェーダーはresourcesフォルダに置いてプロジェクト構成は次のようになります。
1
2
3
4
5
6
7
8
9
|
├── build.gradle
└── src
└── main
├── java
│ └── HelloShader
│ └── Main.java
└── resources
├── hello.frag
└── hello.vert
|
プログラム Main.java
長くてごめんなさい。
1ソースで完結させたかったんです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
|
package HelloShader;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.*;
import org.lwjgl.system.*;
import java.io.IOException;
import java.net.URL;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.lwjgl.glfw.Callbacks.*;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL33.*;
import static org.lwjgl.system.MemoryUtil.*;
interface Game {
void initialize();
void cleanup();
void update();
void render();
}
// 行列クラス(DirectXと違ってVector、Matrixクラスがない)
class Matrix4f {
/**
*m0[ 0]m1[ 4]m2[ 8]m3[12]
*m4[ 1]m5[ 5]m6[ 9]m7[13]
*m8[ 2]m9[ 6] m10[10] m11[14]
* m12[ 3] m13[ 7] m14[11] m15[15]
*/
public float[] value = new float[16];
// 単位行列
public static void identity(Matrix4f m) {
m.value[0] = 1.0f;
m.value[1] = 0.0f;
m.value[2] = 0.0f;
m.value[3] = 0.0f;
m.value[4] = 0.0f;
m.value[5] = 1.0f;
m.value[6] = 0.0f;
m.value[7] = 0.0f;
m.value[8] = 0.0f;
m.value[9] = 0.0f;
m.value[10] = 1.0f;
m.value[11] = 0.0f;
m.value[12] = 0.0f;
m.value[13] = 0.0f;
m.value[14] = 0.0f;
m.value[15] = 1.0f;
}
// Y軸で回転させる回転行列
public static void rotateY(Matrix4f m, float degree) {
double radian = Math.toRadians(degree);
float sin = (float)Math.sin(radian);
float cos = (float)Math.cos(radian);
m.value[0] = cos;
m.value[1] = 0.0f;
m.value[2] = -sin;
m.value[3] = 0.0f;
m.value[4] = 0.0f;
m.value[5] = 1.0f;
m.value[6] = 0.0f;
m.value[7] = 0.0f;
m.value[8] = sin;
m.value[9] = 0.0f;
m.value[10] = cos;
m.value[11] = 0.0f;
m.value[12] = 0.0f;
m.value[13] = 0.0f;
m.value[14] = 0.0f;
m.value[15] = 1.0f;
}
// 正射影行列
public static void orthographic(Matrix4f m, float left, float right, float bottom, float top, float near, float far) {
m.value[0] = 2.0f / (right - left);
m.value[1] = 0.0f;
m.value[2] = 0.0f;
m.value[3] = 0.0f;
m.value[4] = 0.0f;
m.value[5] = 2.0f / (top - bottom);
m.value[6] = 0.0f;
m.value[7] = 0.0f;
m.value[8] = 0.0f;
m.value[9] = 0.0f;
m.value[10] = -2.0f / (far - near);
m.value[11] = 0.0f;
m.value[12] = -(right + left) / (right - left);
m.value[13] = -(top + bottom) / (top - bottom);
m.value[14] = -(far + near) / (far - near);
m.value[15] = 1.0f;
}
}
class GameApplication {
public static void boot(Game game) {
GLFWErrorCallback.createPrint(System.err).set();
if (!glfwInit()) {
throw new IllegalStateException("Unable to initialize GLFW");
}
// OpenGL 3.3のコンテキストを生成
glfwDefaultWindowHints();
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
long windowHandle = glfwCreateWindow(640, 480, "Hello Shader", NULL, NULL);
if (windowHandle == NULL) {
throw new RuntimeException("Failed to create the GLFW window");
}
glfwMakeContextCurrent(windowHandle);
glfwSwapInterval(1);
glfwShowWindow(windowHandle);
GL.createCapabilities();
game.initialize();
while (!glfwWindowShouldClose(windowHandle)) {
game.update();
game.render();
glfwSwapBuffers(windowHandle);
glfwPollEvents();
}
game.cleanup();
glfwFreeCallbacks(windowHandle);
glfwDestroyWindow(windowHandle);
glfwTerminate();
glfwSetErrorCallback(null).free();
}
}
public class Main implements Game {
private int vao;
private int vbo;
private int vertexShader;
private int fragmentShader;
private int shaderProgram;
private int uniModel;
Matrix4f modelMatrix = new Matrix4f();
// jarファイル内のリソース読み込み
private String loadResourceFile(String fileName) {
URL url = Main.class.getResource(fileName);
Path path = Paths.get(url.getPath());
try {
return Files.readString(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void createVertex() {
this.vao = glGenVertexArrays();
glBindVertexArray(this.vao);
// 3次元座標xyzと色RGBで1頂点情報
float[] vertices = {
0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 赤
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 緑
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // 青
};
this.vbo = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, this.vbo);
glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
}
private void loadShaders() {
// 頂点シェーダーを読み込んでコンパイル
this.vertexShader = glCreateShader(GL_VERTEX_SHADER);
String vsSource = loadResourceFile("/hello.vert");
glShaderSource(this.vertexShader, vsSource);
glCompileShader(this.vertexShader);
if (glGetShaderi(this.vertexShader, GL_COMPILE_STATUS) != GL_TRUE) {
throw new RuntimeException(glGetShaderInfoLog(this.vertexShader));
}
// フラグメントシェーダーを読み込んでコンパイル
this.fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
String fsSource = loadResourceFile("/hello.frag");
glShaderSource(this.fragmentShader, fsSource);
glCompileShader(this.fragmentShader);
if (glGetShaderi(this.fragmentShader, GL_COMPILE_STATUS) != GL_TRUE) {
throw new RuntimeException(glGetShaderInfoLog(this.fragmentShader));
}
// 頂点シェーダー、フラグメントシェーダーをあわせて
this.shaderProgram = glCreateProgram();
glAttachShader(this.shaderProgram, this.vertexShader);
glAttachShader(this.shaderProgram, this.fragmentShader);
glBindFragDataLocation(this.shaderProgram, 0, "fragColor");
glLinkProgram(this.shaderProgram);
if (glGetProgrami(this.shaderProgram, GL_LINK_STATUS) != GL_TRUE) {
throw new RuntimeException(glGetShaderInfoLog(this.shaderProgram));
}
}
private Matrix4f createProjectionMatrix() {
try (MemoryStack stack = MemoryStack.stackPush()) {
long window = GLFW.glfwGetCurrentContext();
IntBuffer width = stack.mallocInt(1);
IntBuffer height = stack.mallocInt(1);
GLFW.glfwGetFramebufferSize(window, width, height);
float ratio = width.get() / (float)height.get();
Matrix4f projection = new Matrix4f();
Matrix4f.orthographic(projection, -ratio, ratio, -1f, 1f, -1f, 1f);
return projection;
}
}
@Override
public void initialize() {
createVertex();
loadShaders();
glUseProgram(this.shaderProgram);
int floatSize = 4;
// 頂点シェーダーのinパラメータ"position"と対応
int posAttrib = glGetAttribLocation(this.shaderProgram, "position");
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 3, GL_FLOAT, false, 6 * floatSize, 0);
// 頂点シェーダーのinパラメータ"color"と対応
int colAttrib = glGetAttribLocation(this.shaderProgram, "color");
glEnableVertexAttribArray(colAttrib);
glVertexAttribPointer(colAttrib, 3, GL_FLOAT, false, 6 * floatSize, 3 * floatSize);
// 頂点シェーダーのグローバルGLSL変数"model"の位置を保持しておく
// 毎フレーム設定するので
this.uniModel = glGetUniformLocation(this.shaderProgram, "model");
// 頂点シェーダーのグローバルGLSL変数"view"に設定
Matrix4f viewMatrix = new Matrix4f();
Matrix4f.identity(viewMatrix);
int uniView = glGetUniformLocation(this.shaderProgram, "view");
glUniformMatrix4fv(uniView, false, viewMatrix.value);
// 頂点シェーダーのグローバルGLSL変数"projection"に設定
Matrix4f projectionMatrix = createProjectionMatrix();
int uniProjection = glGetUniformLocation(this.shaderProgram, "projection");
glUniformMatrix4fv(uniProjection, false, projectionMatrix.value);
}
@Override
public void cleanup() {
glDeleteVertexArrays(this.vao);
glDeleteBuffers(this.vbo);
glDeleteShader(this.vertexShader);
glDeleteShader(this.fragmentShader);
glDeleteProgram(this.shaderProgram);
}
@Override
public void update() {
// 1秒で1回転
float angle = 360 * (float)(glfwGetTime() % 1);
Matrix4f.rotateY(this.modelMatrix, angle);
}
@Override
public void render() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glBindVertexArray(this.vao);
glUseProgram(this.shaderProgram);
// updateメソッドで求めた回転行列をグローバルGLSL変数に設定
glUniformMatrix4fv(this.uniModel, false, this.modelMatrix.value);
glDrawArrays(GL_TRIANGLES, 0, 3);
}
public static void main(String[] args) {
GameApplication.boot(new Main());
}
}
|
Main.Javaで描画に使用するシェーダーをセットして、ポリゴンを描画すると、頂点シェーダーで頂点、フラグメントシェーダでピクセルの色を決定して1フレームが描画されます。
固定機能パイプラインのコードに比べると、やることが多いので長いですね。
撮影が面倒で第1回の使いまわしですが、実行すると次のようにポリゴンが回ります。
参考サイト
プログラマブルシェーダを使用したOpenGLプログラム、行列クラスの実装にあたって以下のサイトを参考にさせていただきました。
Rendering · SilverTiger/lwjgl3-tutorial Wiki · GitHub
https://github.com/SilverTiger/lwjgl3-tutorial/wiki/Rendering
床井研究室 – 第5回 座標変換
http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20090829
Math Functions (Direct3D 9 Graphics) – Win32 apps | Microsoft Docs
https://docs.microsoft.com/ja-jp/windows/win32/direct3d9/dx9-graphics-reference-d3dx-functions-math