テクスチャ
皆さんご存じ(と思われる)テクスチャに関して学習します。テクスチャは作成した3Dモデルに対して貼り付ける表皮みたいなものです。
これを張り付けるのには、どうしたらよいか?座標的にはどのように考えるのか?を理解していきます。
この章では、テクスチャをロードする方法、テクスチャをモデルに関連付ける方法、およびレンダリング プロセスでテクスチャを使用する方法を学習します。
参照するドキュメントとソースは以下になります。
テクスチャの読み込み1
テクスチャは、モデルのピクセルの色を設定するためにモデルにマップされるイメージです。テクスチャは、3D モデルを包むスキンと考えることができます。行うことは、画像テクスチャのポイントをモデルの頂点に割り当てることです。その情報を使用して、OpenGL はテクスチャ イメージに基づいて他のピクセルに適用する色を計算できます。
キーワード:"テクスチャは、3D モデルを包むスキンと考えることができます。行うことは、画像テクスチャのポイントをモデルの頂点に割り当てることです。"
テクスチャ イメージは、モデルと同じサイズである必要はありません。大きくても小さくてもかまいません。処理するピクセルがテクスチャ内の特定のポイントにマッピングできない場合、OpenGL は色を外挿します。特定のテクスチャが作成されるときに、このプロセスがどのように行われるかを制御できます。
基本的に、テクスチャをモデルに適用するために必要なことは、各頂点にテクスチャ座標を割り当てることです。テクスチャ座標系は、モデルの座標系とは少し異なります。まず、2D テクスチャがあるので、座標には x と y の 2 つのコンポーネントしかありません。その上、原点は画像の左上隅に設定され、x または y 値の最大値は 1 です。
テクスチャ座標と位置座標をどのように関連付けるのですか? 簡単です。色情報を渡したのと同じ方法です。各頂点位置のテクスチャ座標を持つ VBO を設定します。
理論のまとめ
- 【テクスチャ】
- 座標には x と y の 2 つのコンポーネントしかありません。
その上、原点は画像の左上隅に設定され、x または y 値の最大値は 1 です。 - 「テクスチャ座標と位置座標をどのように関連付けるのですか?」
- -> 各頂点位置のテクスチャ座標を持つ VBO を設定します。
VBOを設定するのに、プログラムではどこでやっているのか?確認したいと思います。
プログラムの挙動確認
Javaはメインメソッドが動くので、ここから確認していきます。
クラス名#メソッド名()
、クラス名#フィールド変数
のように参照するクラスとメソッド、フィールドを記述します。
大まかな流れ
- Main#main(): Mainクラス(IAppLogicインターフェース)をインスタンス化、Engineクラスのコンストラクタに渡しEngineを実行(start()の実行)します。
- Engine#start()でENgine#run()メソッドを起動し、Main#コンストラクタを起動、「runningフラグ」と「Window#windowShouldCloseフラグ」でループを抜ける(ゲーム終了)判定を行います。
- ループ内で
の判定がTRUEの時に入力処理を行うtargetFps <= 0 || deltaFps >= 1
- 画面データの更新(Main#rendar())
- レンダリング処理(Rendar#rendaer())
Engineクラス
ここで、テクスチャイメージとVBOの関連付を行っている部分はEngineクラスのコンストラクタから呼び出されるMain#init()になります。
そして、テクスチャイメージを読み込みVBOの関連づけなどを行っているのは、Textureクラスのコンストラクタです。
public Engine(String windowTitle, Window.WindowOptions opts, IAppLogic appLogic) {
window = new Window(windowTitle, opts, () -> {
resize();
return null;
});
targetFps = opts.fps;
targetUps = opts.ups;
this.appLogic = appLogic;
render = new Render();
scene = new Scene(window.getWidth(), window.getHeight());
appLogic.init(window, scene, render);
running = true;
}
Mainクラス(IAppLogic)
Engineクラスで呼び出したMain#init()の処理内容を実装します。位置情報(頂点座標)(positions)、テクスチャ座標(textCoords)、頂点インデックス(indices)の各VBOを作成。
テクスチャクラス、マテリアル、メッシュ、モデル、エンティティを作成します。
@Override
public void init(Window window, Scene scene, Render render) {
float[] positions = new float[]{
・・・
};
float[] textCoords = new float[]{
・・・
};
int[] indices = new int[]{
・・・
};
Texture texture = scene.getTextureCache().createTexture("chapter-07/resources/models/cube/cube.png");
Material material = new Material();
material.setTexturePath(texture.getTexturePath());
List<Material> materialList = new ArrayList<>();
materialList.add(material);
Mesh mesh = new Mesh(positions, textCoords, indices);
material.getMeshList().add(mesh);
Model cubeModel = new Model("cube-model", materialList);
scene.addModel(cubeModel);
cubeEntity = new Entity("cube-entity", cubeModel.getId());
cubeEntity.setPosition(0, 0, -2);
scene.addEntity(cubeEntity);
}
テクスチャの読み込み2
それでは、3D キューブでテクスチャを使用するようにコード ベースを変更してみましょう。最初のステップは、テクスチャとして使用されるイメージをロードすることです。このタスクでは、stbライブラリの LWJGL ラッパーを使用します。そのためには、まずpom.xmlファイル内のネイティブを含め、その依存関係を宣言する必要があります。
STBライブラリ追加(POM)
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-stb</artifactId>
<version>${lwjgl.version}</version>
</dependency>
[...]
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-stb</artifactId>
<version>${lwjgl.version}</version>
<classifier>${native.target}</classifier>
<scope>runtime</scope>
</dependency>
Textureクラス
Texture最初のステップは、テクスチャをロードするために必要なすべてのステップを実行し、次のように定義される新しいクラスを作成することです
package org.lwjglb.engine.graph;
import org.lwjgl.system.MemoryStack;
import java.nio.*;
import static org.lwjgl.opengl.GL30.*;
import static org.lwjgl.stb.STBImage.*;
public class Texture {
private int textureId;
private String texturePath;
public Texture(int width, int height, ByteBuffer buf) {
this.texturePath = "";
generateTexture(width, height, buf);
}
public Texture(String texturePath) {
try (MemoryStack stack = MemoryStack.stackPush()) {
this.texturePath = texturePath;
IntBuffer w = stack.mallocInt(1);
IntBuffer h = stack.mallocInt(1);
IntBuffer channels = stack.mallocInt(1);
ByteBuffer buf = stbi_load(texturePath, w, h, channels, 4);
if (buf == null) {
throw new RuntimeException("Image file [" + texturePath + "] not loaded: " + stbi_failure_reason());
}
int width = w.get();
int height = h.get();
generateTexture(width, height, buf);
stbi_image_free(buf);
}
}
public void bind() {
glBindTexture(GL_TEXTURE_2D, textureId);
}
public void cleanup() {
glDeleteTextures(textureId);
}
private void generateTexture(int width, int height, ByteBuffer buf) {
textureId = glGenTextures();
glBindTexture(GL_TEXTURE_2D, textureId);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, buf);
glGenerateMipmap(GL_TEXTURE_2D);
}
public String getTexturePath() {
return texturePath;
}
}
※stbi_load()は
のメソッド(static importしています。)org.lwjgl.stb.STBImage
単語 | 意味 |
---|---|
コンストラクター | newするときに動く処理のこと |
チャンネル数 | 色の数、Lの場合は1、RGBの場合は3など |
コンストラクターで最初に行うことは、ライブラリーに s を割り当てIntBufferて、画像サイズとチャンネル数を返すことです。次に、stbi_loadメソッドを呼び出して、実際に画像を にロードしますByteBuffer。このメソッドには、次のパラメーターが必要です。
<コンストラクタで実行すること>
- ライブラリーにIntBufferで、画像サイズとチャンネル数を割り当てる
- stbi_loadメソッドを呼び出して、実際に画像を にロードします
- これはgenerateTextureメソッドでテクスチャを GPU にアップロードする
- RGBA バイトをアンパックする方法を OpenGL に指示する
<stbi_loadメソッドの引数>
・filePath: ファイルへの絶対パス。stb ライブラリはネイティブであり、 について何も理解していませんCLASSPATH。したがって、通常のファイル システム パスを使用します。
・width: 画像の幅。これには、画像の幅が入力されます。
・height: 画像の高さ。これには、画像の高さが入力されます。
・channels: 画像チャンネル。
・desired_channels: 目的の画像チャネル。4 (RGBA) を渡します。
覚えておくべき重要な点の 1 つは、歴史的な理由から、テクスチャ イメージのサイズ (各次元のテクセル数) が 2 のべき乗 (2、4、8、16、...) であることが OpenGL で要求されることです。これは OpenGL ドライバーではもう必要ないと思いますが、問題がある場合は寸法を変更してみてください。
channlesはイメージファイルのチャネル数、desired_channelsは目的の(出力する)イメージファイルのチャネル数
TextureCache
次のステップは、テクスチャを GPU にアップロードすることです。これはgenerateTextureメソッドで行われます。まず、(glGenTextures関数を呼び出して) 新しいテクスチャ識別子を作成する必要があります。その後、( を呼び出してglBindTexture) そのテクスチャにバインドする必要があります。次に、RGBA バイトをアンパックする方法を OpenGL に指示する必要があります。各コンポーネントのサイズは 1 バイトなのでGL_UNPACK_ALIGNMENT、glPixelStorei関数に使用するだけです。最後に、 を呼び出してテクスチャ データをロードしますglTexImage2D。
<glTexImage2Dメソッドのパラメータ>
このglTexImage2Dメソッドには次のパラメーターがあります。
・target: ターゲット テクスチャ (そのタイプ) を指定します。この場合: GL_TEXTURE_2D.
・level: 詳細レベル番号を指定します。レベル 0 はベース イメージ レベルです。レベル n は、n 番目のミップマップ削減イメージです。これについては後で詳しく説明します。
・internal format: テクスチャのカラー コンポーネントの数を指定します。
・width: テクスチャ イメージの幅を指定します。
・height: テクスチャ イメージの高さを指定します。
・border: この値はゼロでなければなりません。
・format: ピクセル データの形式を指定します。この場合は RGBA です。
・type: ピクセル データのデータ型を指定します。これには符号なしバイトを使用しています。
・data: データを格納するバッファ。
<Texture#generateTexture()の処理>
その後、glTexParameteri関数を呼び出すことで、基本的には、ピクセルがテクスチャ座標に直接 1 対 1 で関連付けられていない場合に、最も近いテクスチャ座標ポイントを選択すると言います。その後、ミップマップを生成します。ミップマップは、非常に詳細なテクスチャから生成された、解像度が低下する画像のセットです。これらの低解像度の画像は、オブジェクトがスケーリングされるときに自動的に使用されます。関数を呼び出すときにこれを行いglGenerateMipmapます。以上で、テクスチャのロードに成功しました。今、それを使用する必要があります。
次に、テクスチャ キャッシュを作成します。モデルが同じテクスチャを再利用することは非常に頻繁にあるため、同じテクスチャを複数回ロードする代わりに、すでにロードされているテクスチャをキャッシュして、各テクスチャを 1 回だけロードします。これはTextureCacheクラスによって制御されます。
package org.lwjglb.engine.graph;
import java.util.*;
public class TextureCache {
public static final String DEFAULT_TEXTURE = "resources/models/default/default_texture.png";
private Map<String, Texture> textureMap;
public TextureCache() {
textureMap = new HashMap<>();
textureMap.put(DEFAULT_TEXTURE, new Texture(DEFAULT_TEXTURE));
}
public void cleanup() {
textureMap.values().stream().forEach(Texture::cleanup);
}
public Texture createTexture(String texturePath) {
return textureMap.computeIfAbsent(texturePath, Texture::new);
}
public Texture getTexture(String texturePath) {
Texture texture = null;
if (texturePath != null) {
texture = textureMap.get(texturePath);
}
if (texture == null) {
texture = textureMap.get(DEFAULT_TEXTURE);
}
return texture;
}
}
ご覧のとおり、ロードされたテクスチャを に保存し、Mapテクスチャ パスが null (テクスチャのないモデル) の場合にデフォルトのテクスチャを返します。デフォルトのテクスチャは、テクスチャではなく色を定義するモデルと組み合わせることができる単なるバック イメージであるため、フラグメント シェーダーで両方を組み合わせることができます。クラス インスタンスはクラスTextureCacheに格納されます。Scene
public class Scene {
...
private TextureCache textureCache;
...
public Scene(int width, int height) {
...
textureCache = new TextureCache();
}
...
public TextureCache getTextureCache() {
return textureCache;
}
...
}
ここで、テクスチャのサポートを追加するためにモデルを定義する方法を変更する必要があります。そのために、そして次の章でロードするより複雑なモデルに備えるために、 という名前の新しいクラスを導入しMaterialます。このクラスは、テクスチャ パスとMeshインスタンスのリストを保持します。したがって、Modelインスタンスを「es」ではなく「Listof 」に関連付けます。次の章では、マテリアルに拡散色や鏡面反射色などの他のプロパティを含めることができます。MaterialMesh
クラスは次のMaterialように定義されます。
package org.lwjglb.engine.graph;
import java.util.*;
public class Material {
private List<Mesh> meshList;
private String texturePath;
public Material() {
meshList = new ArrayList<>();
}
public void cleanup() {
meshList.stream().forEach(Mesh::cleanup);
}
public List<Mesh> getMeshList() {
return meshList;
}
public String getTexturePath() {
return texturePath;
}
public void setTexturePath(String texturePath) {
this.texturePath = texturePath;
}
ご覧のとおり、MeshインスタンスはMaterialクラスの下にあります。Modelしたがって、次のようにクラスを変更する必要があります。
package org.lwjglb.engine.graph;
import org.lwjglb.engine.scene.Entity;
import java.util.*;
public class Model {
private final String id;
private List<Entity> entitiesList;
private List<Material> materialList;
public Model(String id, List<Material> materialList) {
this.id = id;
entitiesList = new ArrayList<>();
this.materialList = materialList;
}
public void cleanup() {
materialList.stream().forEach(Material::cleanup);
}
public List<Entity> getEntitiesList() {
return entitiesList;
}
public String getId() {
return id;
}
public List<Material> getMaterialList() {
return materialList;
}
}
前に述べたように、テクスチャ座標を別の VBO として渡す必要があります。そこでMesh、色の代わりにテクスチャ座標を含む float の配列を受け入れるようにクラスを変更します。Meshクラスは次のように変更されます。
public class Mesh {
...
public Mesh(float[] positions, float[] textCoords, int[] indices) {
try (MemoryStack stack = MemoryStack.stackPush()) {
numVertices = indices.length;
vboIdList = new ArrayList<>();
vaoId = glGenVertexArrays();
glBindVertexArray(vaoId);
// Positions VBO
int vboId = glGenBuffers();
vboIdList.add(vboId);
FloatBuffer positionsBuffer = stack.callocFloat(positions.length);
positionsBuffer.put(0, positions);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER, positionsBuffer, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
// Texture coordinates VBO
vboId = glGenBuffers();
vboIdList.add(vboId);
FloatBuffer textCoordsBuffer = MemoryUtil.memAllocFloat(textCoords.length);
textCoordsBuffer.put(0, textCoords);
glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER, textCoordsBuffer, GL_STATIC_DRAW);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, false, 0, 0);
// Index VBO
vboId = glGenBuffers();
vboIdList.add(vboId);
IntBuffer indicesBuffer = stack.callocInt(indices.length);
indicesBuffer.put(0, indices);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
}
...
}
クラス図にすると下のようになります。
テクスチャの使用
次に、シェーダーでテクスチャを使用する必要があります。頂点シェーダーでは、2 番目の入力パラメーターvec2を変更しました (パラメーター名も変更しました)。頂点シェーダーは、カラーの場合と同様に、フラグメント シェーダーが使用するテクスチャ座標を渡すだけです。
#version 330
layout (location=0) in vec3 position;
layout (location=1) in vec2 texCoord;
out vec2 outTextCoord;
uniform mat4 projectionMatrix;
uniform mat4 modelMatrix;
void main()
{
gl_Position = projectionMatrix * modelMatrix * vec4(position, 1.0);
outTextCoord = texCoord;
}
sampler2Dフラグメント シェーダーでは、テクスチャをサンプリングして (ユニフォームを介して) ピクセルの色を設定するために、テクスチャ座標を使用する必要があります。
#version 330
in vec2 outTextCoord;
out vec4 fragColor;
uniform sampler2D txtSampler;
void main()
{
fragColor = texture(txtSampler, outTextCoord);
}
これらすべてがクラスでどのように使用されるかを見ていきますSceneRender。まず、テクスチャ サンプラー用の新しいユニフォームを作成する必要があります。
public class SceneRender {
...
private void createUniforms() {
...
uniformsMap.createUniform("txtSampler");
}
...
}
これで、レンダリング プロセスでテクスチャを使用できます。
public class SceneRender {
...
public void render(Scene scene) {
shaderProgram.bind();
uniformsMap.setUniform("projectionMatrix", scene.getProjection().getProjMatrix());
uniformsMap.setUniform("txtSampler", 0);
Collection<Model> models = scene.getModelMap().values();
TextureCache textureCache = scene.getTextureCache();
for (Model model : models) {
List<Entity> entities = model.getEntitiesList();
for (Material material : model.getMaterialList()) {
Texture texture = textureCache.getTexture(material.getTexturePath());
glActiveTexture(GL_TEXTURE0);
texture.bind();
for (Mesh mesh : material.getMeshList()) {
glBindVertexArray(mesh.getVaoId());
for (Entity entity : entities) {
uniformsMap.setUniform("modelMatrix", entity.getModelMatrix());
glDrawElements(GL_TRIANGLES, mesh.getNumVertices(), GL_UNSIGNED_INT, 0);
}
}
}
}
glBindVertexArray(0);
shaderProgram.unbind();
}
}
ご覧のとおり、最初にテクスチャ サンプラーのユニフォームを0値に設定します。なぜこれを行うのかを説明しましょう。グラフィックス カードには、テクスチャを格納するためのスペースまたはスロットがいくつかあります。これらの各スペースは、テクスチャ ユニットと呼ばれます。テクスチャを操作するときは、操作するテクスチャ ユニットを設定する必要があります。この場合、使用するテクスチャは 1 つだけなので、 texture unit を使用し0ます。ユニフォームにはsampler2Dタイプがあり、使用するテクスチャ ユニットの値を保持します。モデルとマテリアルを反復処理するとき、各マテリアルに関連付けられたテクスチャをキャッシュから取得し、次のglActiveTexture関数を呼び出してテクスチャ ユニットをアクティブにします。パラメータGL_TEXTURE0をバインドします。これが、テクスチャ ユニットとテクスチャ識別子を関連付ける方法です。
現在、テクスチャをサポートするためにコード ベースを変更したところです。次に、3D 立方体のテクスチャ座標を設定する必要があります。テクスチャ イメージ ファイルは次のようになります。
3D モデルには 8 つの頂点があります。これを行う方法を見てみましょう。まず、各頂点の前面テクスチャ座標を定義しましょう。
バーテックス座標 | テクスチャ座標 |
---|---|
V0 | (0.0, 0.0) |
V1 | (0.0, 0.5) |
V2 | (0.5、0.5) |
V3 | (0.5, 0.0) |
次に、上面のテクスチャ マッピングを定義しましょう。
バーテックス | テクスチャ座標 |
---|---|
V4 | (0.0, 0.5) |
V5 | (0.5、0.5) |
V0 | (0.0, 1.0) |
V3 | (0.5、1.0) |
ご覧のとおり、問題があるため、同じ頂点 (V0 と V3) に対して異なるテクスチャ座標を設定する必要があります。どうすればこれを解決できますか? これを解決する唯一の方法は、いくつかの頂点を繰り返し、異なるテクスチャ座標を関連付けることです。上面では、4 つの頂点を繰り返し、正しいテクスチャ座標を割り当てる必要があります。
前面、背面、および側面は同じテクスチャを使用するため、これらの頂点をすべて繰り返す必要はありません。ソース コードに完全な定義がありますが、8 ポイントから 20 ポイントに移動する必要がありました。
次の章では、3D モデリング ツールによって生成されたモデルをロードする方法を学習します。これにより、手動で位置とテクスチャ座標を定義する必要がなくなります (ちなみに、これはより複雑なモデルでは非現実的です)。
initクラスのメソッドを変更してMain、テクスチャ座標を定義し、テクスチャ データをロードするだけです。
public class Main implements IAppLogic {
...
public static void main(String[] args) {
...
Engine gameEng = new Engine("chapter-07", new Window.WindowOptions(), main);
...
}
...
public void init(Window window, Scene scene, Render render) {
float[] positions = new float[]{
// V0
-0.5f, 0.5f, 0.5f,
// V1
-0.5f, -0.5f, 0.5f,
// V2
0.5f, -0.5f, 0.5f,
// V3
0.5f, 0.5f, 0.5f,
// V4
-0.5f, 0.5f, -0.5f,
// V5
0.5f, 0.5f, -0.5f,
// V6
-0.5f, -0.5f, -0.5f,
// V7
0.5f, -0.5f, -0.5f,
// For text coords in top face
// V8: V4 repeated
-0.5f, 0.5f, -0.5f,
// V9: V5 repeated
0.5f, 0.5f, -0.5f,
// V10: V0 repeated
-0.5f, 0.5f, 0.5f,
// V11: V3 repeated
0.5f, 0.5f, 0.5f,
// For text coords in right face
// V12: V3 repeated
0.5f, 0.5f, 0.5f,
// V13: V2 repeated
0.5f, -0.5f, 0.5f,
// For text coords in left face
// V14: V0 repeated
-0.5f, 0.5f, 0.5f,
// V15: V1 repeated
-0.5f, -0.5f, 0.5f,
// For text coords in bottom face
// V16: V6 repeated
-0.5f, -0.5f, -0.5f,
// V17: V7 repeated
0.5f, -0.5f, -0.5f,
// V18: V1 repeated
-0.5f, -0.5f, 0.5f,
// V19: V2 repeated
0.5f, -0.5f, 0.5f,
};
float[] textCoords = new float[]{
0.0f, 0.0f,
0.0f, 0.5f,
0.5f, 0.5f,
0.5f, 0.0f,
0.0f, 0.0f,
0.5f, 0.0f,
0.0f, 0.5f,
0.5f, 0.5f,
// For text coords in top face
0.0f, 0.5f,
0.5f, 0.5f,
0.0f, 1.0f,
0.5f, 1.0f,
// For text coords in right face
0.0f, 0.0f,
0.0f, 0.5f,
// For text coords in left face
0.5f, 0.0f,
0.5f, 0.5f,
// For text coords in bottom face
0.5f, 0.0f,
1.0f, 0.0f,
0.5f, 0.5f,
1.0f, 0.5f,
};
int[] indices = new int[]{
// Front face
0, 1, 3, 3, 1, 2,
// Top Face
8, 10, 11, 9, 8, 11,
// Right face
12, 13, 7, 5, 12, 7,
// Left face
14, 15, 6, 4, 14, 6,
// Bottom face
16, 18, 19, 17, 16, 19,
// Back face
4, 6, 7, 5, 4, 7,};
Texture texture = scene.getTextureCache().createTexture("resources/models/cube/cube.png");
Material material = new Material();
material.setTexturePath(texture.getTexturePath());
List<Material> materialList = new ArrayList<>();
materialList.add(material);
Mesh mesh = new Mesh(positions, textCoords, indices);
material.getMeshList().add(mesh);
Model cubeModel = new Model("cube-model", materialList);
scene.addModel(cubeModel);
cubeEntity = new Entity("cube-entity", cubeModel.getId());
cubeEntity.setPosition(0, 0, -2);
scene.addEntity(cubeEntity);
}
最終結果はこんな感じ。
クラス図
<動画>