第12章 スカイボックス
この章では、スカイ ボックスの作成方法について説明します。スカイボックスを使用すると、背景を設定して、3D 世界がより広いという錯覚を与えることができます。その背景はカメラの位置を包み込み、空間全体を覆います。ここで使用するテクニックは、3D シーンの周りに表示される大きな立方体を作成することです。つまり、カメラ位置の中心が立方体の中心になります。その立方体の側面は、画像が連続した風景のように見える方法でマッピングされる丘、青い空、雲のテクスチャでラップされます。
参照するドキュメントとプログラムソースへのリンクは以下になります。
サンプルコードの実行結果
スカイボックス
次の図は、スカイボックスの概念を示しています。
スカイ ボックスを作成するプロセスは、次の手順に要約できます。
・大きなキューブを作成します。
エッジのない巨大な風景を見ているような錯覚を与えるテクスチャを適用します。
・立方体をレンダリングして、側面が遠くにあり、原点がカメラの中心にくるようにします。
SkyBoxスカイ ボックス キューブ (テクスチャ付き) とテクスチャ キャッシュへの参照を含む 3D モデルへのパスを受け取るコンストラクタで名前を付けた新しいクラスを作成することから始めます。このクラスはそのモデルをロードし、そのモデルにEntity関連付けられたインスタンスを作成します。SkyBoxクラスの定義は以下の通りです。
package org.lwjglb.engine.scene;
import org.lwjglb.engine.graph.*;
public class SkyBox {
private Entity skyBoxEntity;
private Model skyBoxModel;
public SkyBox(String skyBoxModelPath, TextureCache textureCache) {
skyBoxModel = ModelLoader.loadModel("skybox-model", skyBoxModelPath, textureCache);
skyBoxEntity = new Entity("skyBoxEntity-entity", skyBoxModel.getId());
}
public Entity getSkyBoxEntity() {
return skyBoxEntity;
}
public Model getSkyBoxModel() {
return skyBoxModel;
}
}
SkyBoxクラスへの参照をクラスに保存しますScene。
public class Scene {
...
private SkyBox skyBox;
...
public SkyBox getSkyBox() {
return skyBox;
}
...
public void setSkyBox(SkyBox skyBox) {
this.skyBox = skyBox;
}
...
}
次のステップは、スカイ ボックス用の頂点シェーダーとフラグメント シェーダーの別のセットを作成することです。しかし、既にあるシーン シェーダーを再利用してみませんか? 答えは、実際には、必要なシェーダーはそれらのシェーダーの簡略化されたバージョンであるということです。たとえば、スカイ ボックスにはライトを適用しません。以下に、スカイ ボックスの頂点シェーダーを示します ( skybox.vert)。
#version 330
layout (location=0) in vec3 position;
layout (location=1) in vec3 normal;
layout (location=2) in vec2 texCoord;
out vec2 outTextCoord;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
void main()
{
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
outTextCoord = texCoord;
}
まだモデル マトリックスを使用していることがわかります。スカイボックスをスケーリングするため、モデル マトリックスが必要です。開始時にスカイ ボックスをモデル化する立方体のサイズを大きくし、モデルとビュー マトリックスを乗算する必要がない他の実装がいくつか見られる場合があります。このアプローチを選択したのは、より柔軟で、実行時にスカイボックスのサイズを変更できるためですが、必要に応じて他のアプローチに簡単に切り替えることができます。
フラグメント シェーダー ( skybox.frag) も非常に単純で、テクスチャまたは拡散色から色を取得するだけです。
#version 330
in vec2 outTextCoord;
out vec4 fragColor;
uniform vec4 diffuse;
uniform sampler2D txtSampler;
uniform int hasTexture;
void main()
{
if (hasTexture == 1) {
fragColor = texture(txtSampler, outTextCoord);
} else {
fragColor = diffuse;
}
}
SkyBoxRenderこれらのシェーダーを使用してレンダリングを実行する名前の新しいクラスを作成します。クラスは、シェーダー プログラムを作成し、必要なユニフォームをセットアップすることから始まります。
package org.lwjglb.engine.graph;
import org.joml.Matrix4f;
import org.lwjglb.engine.scene.*;
import java.util.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.glBindVertexArray;
public class SkyBoxRender {
private ShaderProgram shaderProgram;
private UniformsMap uniformsMap;
private Matrix4f viewMatrix;
public SkyBoxRender() {
List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/skybox.vert", GL_VERTEX_SHADER));
shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/skybox.frag", GL_FRAGMENT_SHADER));
shaderProgram = new ShaderProgram(shaderModuleDataList);
viewMatrix = new Matrix4f();
createUniforms();
}
...
}
次のステップでは、グローバルなレンダリング メソッドで呼び出されるスカイボックスの新しいレンダリング メソッドを作成します。
public class SkyBoxRender {
...
public void render(Scene scene) {
SkyBox skyBox = scene.getSkyBox();
if (skyBox == null) {
return;
}
shaderProgram.bind();
uniformsMap.setUniform("projectionMatrix", scene.getProjection().getProjMatrix());
viewMatrix.set(scene.getCamera().getViewMatrix());
viewMatrix.m30(0);
viewMatrix.m31(0);
viewMatrix.m32(0);
uniformsMap.setUniform("viewMatrix", viewMatrix);
uniformsMap.setUniform("txtSampler", 0);
Model skyBoxModel = skyBox.getSkyBoxModel();
Entity skyBoxEntity = skyBox.getSkyBoxEntity();
TextureCache textureCache = scene.getTextureCache();
for (Material material : skyBoxModel.getMaterialList()) {
Texture texture = textureCache.getTexture(material.getTexturePath());
glActiveTexture(GL_TEXTURE0);
texture.bind();
uniformsMap.setUniform("diffuse", material.getDiffuseColor());
uniformsMap.setUniform("hasTexture", texture.getTexturePath().equals(TextureCache.DEFAULT_TEXTURE) ? 0 : 1);
for (Mesh mesh : material.getMeshList()) {
glBindVertexArray(mesh.getVaoId());
uniformsMap.setUniform("modelMatrix", skyBoxEntity.getModelMatrix());
glDrawElements(GL_TRIANGLES, mesh.getNumVertices(), GL_UNSIGNED_INT, 0);
}
}
glBindVertexArray(0);
shaderProgram.unbind();
}
}
関連するユニフォームにそのデータをロードする前に、ビュー マトリックスを変更していることがわかります。カメラを動かすとき、実際に行っていることは世界全体を動かしていることを忘れないでください。したがって、ビュー マトリックスをそのまま乗算すると、カメラが移動するとスカイボックスが移動します。しかし、これは必要ありません。(0, 0, 0) の原点座標に貼り付けたいのです。これは、平行移動の増分を含むビュー マトリックスの部分 ( m30、m31およびm32コンポーネント)。スカイ ボックスは原点に固定する必要があるため、ビュー マトリックスの使用をまったく避けることができると考えるかもしれません。その場合、スカイボックスがカメラと一緒に回転しないことがわかりますが、これは私たちが望んでいるものではありません。回転する必要がありますが、平行移動は必要ありません。スカイボックスをレンダリングするには、ユニフォームをセットアップし、スカイ ボックスに関連付けられた立方体をレンダリングします。
クラスでは、Renderクラスをインスタンス化しSkyBoxRender、render メソッドを呼び出すだけです。
public class Render {
...
private SkyBoxRender skyBoxRender;
...
public Render(Window window) {
...
skyBoxRender = new SkyBoxRender();
}
public void render(Window window, Scene scene) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glViewport(0, 0, window.getWidth(), window.getHeight());
skyBoxRender.render(scene);
sceneRender.render(scene);
guiRender.render(scene);
}
...
}
スカイ ボックスを最初にレンダリングしていることがわかります。これは、シーンに透明度のある 3D モデルがある場合、それらを (黒い背景ではなく) スカイボックスとブレンドしたいという事実によるものです。
最後に、このMainクラスでは、シーンにスカイ ボックスを設定し、タイルのセットを作成して、無限の地形の錯覚を与えます。カメラの位置に合わせて移動するタイルのチャンクが常に表示されるように設定します。
public class Main implements IAppLogic {
...
private static final int NUM_CHUNKS = 4;
private Entity[][] terrainEntities;
public static void main(String[] args) {
...
Engine gameEng = new Engine("chapter-12", new Window.WindowOptions(), main);
...
}
...
@Override
public void init(Window window, Scene scene, Render render) {
String quadModelId = "quad-model";
Model quadModel = ModelLoader.loadModel("quad-model", "resources/models/quad/quad.obj",
scene.getTextureCache());
scene.addModel(quadModel);
int numRows = NUM_CHUNKS * 2 + 1;
int numCols = numRows;
terrainEntities = new Entity[numRows][numCols];
for (int j = 0; j < numRows; j++) {
for (int i = 0; i < numCols; i++) {
Entity entity = new Entity("TERRAIN_" + j + "_" + i, quadModelId);
terrainEntities[j][i] = entity;
scene.addEntity(entity);
}
}
SceneLights sceneLights = new SceneLights();
sceneLights.getAmbientLight().setIntensity(0.2f);
scene.setSceneLights(sceneLights);
SkyBox skyBox = new SkyBox("resources/models/skybox/skybox.obj", scene.getTextureCache());
skyBox.getSkyBoxEntity().setScale(50);
scene.setSkyBox(skyBox);
scene.getCamera().moveUp(0.1f);
updateTerrain(scene);
}
@Override
public void input(Window window, Scene scene, long diffTimeMillis, boolean inputConsumed) {
float move = diffTimeMillis * MOVEMENT_SPEED;
Camera camera = scene.getCamera();
if (window.isKeyPressed(GLFW_KEY_W)) {
camera.moveForward(move);
} else if (window.isKeyPressed(GLFW_KEY_S)) {
camera.moveBackwards(move);
}
if (window.isKeyPressed(GLFW_KEY_A)) {
camera.moveLeft(move);
} else if (window.isKeyPressed(GLFW_KEY_D)) {
camera.moveRight(move);
}
MouseInput mouseInput = window.getMouseInput();
if (mouseInput.isRightButtonPressed()) {
Vector2f displVec = mouseInput.getDisplVec();
camera.addRotation((float) Math.toRadians(-displVec.x * MOUSE_SENSITIVITY), (float) Math.toRadians(-displVec.y * MOUSE_SENSITIVITY));
}
}
@Override
public void update(Window window, Scene scene, long diffTimeMillis) {
updateTerrain(scene);
}
public void updateTerrain(Scene scene) {
int cellSize = 10;
Camera camera = scene.getCamera();
Vector3f cameraPos = camera.getPosition();
int cellCol = (int) (cameraPos.x / cellSize);
int cellRow = (int) (cameraPos.z / cellSize);
int numRows = NUM_CHUNKS * 2 + 1;
int numCols = numRows;
int zOffset = -NUM_CHUNKS;
float scale = cellSize / 2.0f;
for (int j = 0; j < numRows; j++) {
int xOffset = -NUM_CHUNKS;
for (int i = 0; i < numCols; i++) {
Entity entity = terrainEntities[j][i];
entity.setScale(scale);
entity.setPosition((cellCol + xOffset) * 2.0f, 0, (cellRow + zOffset) * 2.0f);
entity.getModelMatrix().identity().scale(scale).translate(entity.getPosition());
xOffset++;
}
zOffset++;
}
}
}