Hi,
[caution: german author]
a short description of what im aiming for with my program:
I want to create a 3D Game Engine.
At the moment i only use LWJGL.
The structure of the world is pretty simple:
I load my terrain (only surface, no caves or sth alike) out of heightmaps and colour it by using blendmaps.
The objects (static objects like trees or moving entities like deers, persons,or in the beginning the standard monkey face [-> Blender]) consist out of their 3D model and a texture, which shall colour the displayed model in the end.
For ensuring a constant frame rate in the foreground i tried/am trying to do the loading and unloading of textures and meshes in the background in seperate threads.
The BackgroundTextureLoader and the BackgroundMeshLoader both got lists in which the main program fills the relative texture and mesh paths which shall be thrown out of the memory or loaded into it.
If or if not this method is good practice or totaly nuts is not the main discussion in this thread, but i wd be very happy about some comments that can help me to improve the program, too.
I use VAOs to store terrain data and object data on the graphic card.
When the program starts i first load in the terrains (13*13 square, with the player position in the central terrain).
So the glGen method returns id values from 1 to 169, just like expected.
But when i load in the objects (currently only one dummy object created with blender) the method returns ids starting with 1 counting upwards.
When i now render the objects using the vao ids a part of a terrain is shown.
Of course, because i load in the terrains first, telling lwjgl that i wont change the values during the process.
Why does the vao ids start with 1 again even if i use the same gl method?
The max number of stored elements on the graphic card is far more than 169 elements.
Maybe important to say, that the terrains are loaded directly, so not filled into a load list, but directly with the loadToVao-method (s.b.).
The objects are afterwards filled into those lists and then bgtl.process() and bgml.process() are called.
Here the code parts i think are most important for the problem:
Game Constructor - the class whose instance holds all game related objects
public Game(BackgroundTextureLoader bgtl, BackgroundMeshLoader bgml) {
this.bgtl = bgtl;
this.bgml = bgml;
this.bgtrl = new BackgroundTerrainLoader(this.bgtl,this.bgml);
this.bgem = new BackgroundEntityManager(this.bgtl,this.bgml);
this.p = new Player(/*TODO MODEL*/null,new Vector3f(0.0f,0.0f,0.0f),0.0f,0.0f,0.0f,1.0f);
this.c = new Camera(this.p);
this.bgtrl.initLoadTerrains(this.p, this); //<------------load Terrains
this.bgem.start(); //<--start BackgroundEntityManager and initially load objects
this.createProjectionMatrix();
this.tshad = new TerrainShader();
this.tren = new TerrainRenderer(tshad,projectionMatrix,this.bgtl);
this.eshad = new EntityShader();
this.eren = new EntityRenderer(eshad,projectionMatrix,this.bgtl,this.bgml);
}
BackgroundMeshLoader - the class that loads meshes to the graphic card memory
package loader;
//imports
public class BackgroundMeshLoader extends Loader {
private static List<String> toLoad;
private static List<String> toUnload;
private static Map<String, Integer[]> map;
//<rel. paths, {vaoID, indices count}>
private static Map<Integer, Integer[]> vaoTOvboIDs;
private boolean appeared = false;
//Test
//flags
private boolean go;
private boolean ready;
private boolean interrupt;
private boolean running;
private SharedDrawable sd;
public BackgroundMeshLoader(SharedDrawable sd) {
this.go = false;
this.ready = true;
this.interrupt = false;
this.running = false;
this.sd = sd;
toLoad = new ArrayList<String>();
toUnload = new ArrayList<String>();
map = new HashMap<String, Integer[]>();
vaoTOvboIDs = new HashMap<Integer, Integer[]>();
}
//fill path into load list
public void load(String path) {
if (!toLoad.contains(path)) {
toLoad.add(path);
}
}
//fill path into unload list
public void unload(String path) {
if (!toLoad.contains(path) && !toUnload.contains(path) && map.containsKey(path)) {
toUnload.add(path);
}
}
//clear vao in the memory
public void unload(int vaoID) {
if (vaoTOvboIDs.containsKey(vaoID)) {
GL30.glDeleteVertexArrays(vaoID);
for (Integer i : vaoTOvboIDs.get(vaoID)) {
GL15.glDeleteBuffers(i);
}
vaoTOvboIDs.remove(vaoID);
}
}
public void process() {
if (toLoad.size() > 0 | toUnload.size() > 0) {
ready = false;
this.go = true;
}
}
public boolean isReady() {
return this.ready;
}
//Main-runnable method
public void start() {
new Thread(new Runnable() {
public void run() {
System.err.println(" - BACKGROUNDMESHLOADER started - ");
init();
System.err.println(" - BACKGROUNDMESHLOADER running - ");
running = true;
while( !interrupt ) {
go = false;
while( !go ) {
try {
Thread.sleep(10);
} catch (InterruptedException ie) {
ie.printStackTrace();
}
if (interrupt) {
break;
}
}
for (String str : toUnload) {
System.out.println("unload "+ str);
int i = (int)map.get(str)[0];
GL30.glDeleteVertexArrays(i);
Integer[] vboIds = vaoTOvboIDs.get(i);
for (Integer integer : vboIds) {
GL15.glDeleteBuffers(integer);
}
map.remove(str);
}
for (String s : toLoad) {
System.out.println("load "+s);
try {
if (map.containsKey(s)) {
throw new IllegalArgumentException("Mesh already loaded");
}
//modelData -> VAO,Size
int[] modelData = loadOBJModel(s);
System.err.println("Der zugehörige VAO Index ist: "+modelData[0]);
map.put(s, new Integer[]{modelData[0], modelData[1]});
} catch (Exception io) {
io.printStackTrace();
}
}
clearLists();
ready = true;
}
running = false;
System.err.println(" - BACKGROUNDMESHLOADER finished - ");
}
private void init() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
sd.makeCurrent();
} catch (LWJGLException e) {
e.printStackTrace();
}
}
}).start();
}
//load data directly to a vao
public int loadToVAO(float[] positions, float[] textureCoords, float[] normals, int[] indices) {
int vaoID = createVAO();
if (vaoID == 1 && !appeared) {
appeared = true;
} else if (vaoID == 1 && appeared){
//throw new IllegalArgumentException("He really creates two ids with index 1");
}
Integer vbos[] = new Integer[4];
vbos[0] = (bindIndicesBuffer(indices));
vbos[1] = (storeDataInAttributeList(0,3,positions));
vbos[2] = (storeDataInAttributeList(1,2,textureCoords));
vbos[3] = (storeDataInAttributeList(2,3,normals));
unbindVAO();
vaoTOvboIDs.put(vaoID,vbos);
return vaoID;
}
//Load a model from an .obj-file
public int[] loadOBJModel(String fileName) {
FileReader fr = null;
//Pathing:
//models
//-buildings
//-objects
//-scene
//-test
try {
fr = new FileReader(new File("res/models/"+fileName+".obj"));
} catch (FileNotFoundException e) {
System.err.println("Couldnt find file: res/models/"+fileName+".obj");
e.printStackTrace();
}
BufferedReader br = new BufferedReader(fr);
String line;
List<Vector3f> vertices = new ArrayList<Vector3f>();
List<Vector2f> textures = new ArrayList<Vector2f>();
List<Vector3f> normals = new ArrayList<Vector3f>();
List<Integer> indices = new ArrayList<Integer>();
float[] verticesArray = null;
float[] normalsArray = null;
float[] textureArray = null;
int[] indicesArray = null;
try {
while(true) {
line = br.readLine();
String[] currentLine = line.split(" ");
if (line.startsWith("v ")) {
Vector3f vertex = new Vector3f(Float.parseFloat(currentLine[1]),Float.parseFloat(currentLine[2]),Float.parseFloat(currentLine[3]));
vertices.add(vertex);
} else if (line.startsWith("vt ")) {
Vector2f texturecoord = new Vector2f(Float.parseFloat(currentLine[1]),Float.parseFloat(currentLine[2]));
textures.add(texturecoord);
} else if (line.startsWith("vn ")) {
Vector3f normal = new Vector3f(Float.parseFloat(currentLine[1]),Float.parseFloat(currentLine[2]),Float.parseFloat(currentLine[3]));
normals.add(normal);
} else if (line.startsWith("f ")) {
textureArray = new float[vertices.size() * 2];
normalsArray = new float[vertices.size() * 3];
break;
}
}
while(line != null) {
if (!line.startsWith("f ")) {
line = br.readLine();
continue;
}
String[] currentLine = line.split(" ");
String[] vertex1 = currentLine[1].split("/");
String[] vertex2 = currentLine[2].split("/");
String[] vertex3 = currentLine[3].split("/");
processFVertex(vertex1,indices,textures,normals,textureArray,normalsArray);
processFVertex(vertex2,indices,textures,normals,textureArray,normalsArray);
processFVertex(vertex3,indices,textures,normals,textureArray,normalsArray);
line = br.readLine();
}
br.close();
} catch(Exception e) {
e.printStackTrace();
}
verticesArray = new float[vertices.size() * 3];
indicesArray = new int[indices.size()];
int vertexPointer = 0;
for (Vector3f vec : vertices) {
verticesArray[vertexPointer++] = vec.x;
verticesArray[vertexPointer++] = vec.y;
verticesArray[vertexPointer++] = vec.z;
}
for (int i = 0 ;i < indices.size();i++) {
indicesArray[i] = indices.get(i);
}
return new int[] {loadToVAO(verticesArray, textureArray, normalsArray, indicesArray),indices.size()};
}
//process the data lists
private void processFVertex(String[] vertexData, List<Integer> indices, List<Vector2f> textures, List<Vector3f> normals, float[] textureArray, float[] normalsArray) {
int currentVertexPointer = Integer.parseInt(vertexData[0]) - 1;
indices.add(currentVertexPointer);
Vector2f currentTex = textures.get(Integer.parseInt(vertexData[1]) - 1);
textureArray[currentVertexPointer * 2] = currentTex.x;
textureArray[currentVertexPointer * 2 + 1] = 1 - currentTex.y;
Vector3f currentNorm = normals.get(Integer.parseInt(vertexData[2]) - 1);
normalsArray[currentVertexPointer * 3] = currentNorm.x;
normalsArray[currentVertexPointer * 3 + 1] = currentNorm.y;
normalsArray[currentVertexPointer * 3 + 2] = currentNorm.z;
}
public Map<String, Integer[]> getMeshIDMap() {
return map;
}
//interrupt the thread
public void stop() {
interrupt = true;
System.out.println("bgml interrupted");
}
public boolean isRunning() {
return this.running;
}
//clear the toLoad and the toUnload list
public void clearLists() {
toLoad.clear();
toUnload.clear();
}
//clear all lists
public void clear() {
//clears VAOs and VBOs
Set<Integer> l = vaoTOvboIDs.keySet();
for (Integer i : l) {
GL30.glDeleteVertexArrays(i);
}
Collection<Integer[]> vboIds = vaoTOvboIDs.values();
for (Integer[] i : vboIds) {
for (Integer integer : i) {
GL15.glDeleteBuffers(integer);
}
}
vaoTOvboIDs.clear();
toLoad.clear();
toUnload.clear();
map.clear();
}
}
Loader - class extended in all Loaders
package loader;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
public class Loader {
protected static int createVAO() {
//creates a new VAO
int vaoID = GL30.glGenVertexArrays();
GL30.glBindVertexArray(vaoID);
return vaoID;
}
protected static void unbindVAO() {
GL30.glBindVertexArray(0);
}
protected static int storeDataInAttributeList(int attributeNumber, int coordinateSize, float[] data) {
int vboID = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboID);
FloatBuffer buffer = storeDataInFloatBuffer(data);
//-> static draw : gl now knows, we dont want to edit the stored data any more
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
GL20.glVertexAttribPointer(attributeNumber, coordinateSize, GL11.GL_FLOAT, false, 0,0);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
return vboID;
}
protected static int bindIndicesBuffer(int[] indices) {
int vboID = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vboID);
IntBuffer buffer = storeDataInIntBuffer(indices);
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
return vboID;
}
private static IntBuffer storeDataInIntBuffer (int[] data) {
IntBuffer buffer = BufferUtils.createIntBuffer(data.length);
buffer.put(data);
buffer.flip();
return buffer;
}
private static FloatBuffer storeDataInFloatBuffer(float[] data) {
FloatBuffer buffer = BufferUtils.createFloatBuffer(data.length);
buffer.put(data);
buffer.flip();
return buffer;
}
}
BackgroundTerrainLoader - method used to load terrains into the graphic card memory
public void load(int x, int z) {
if (isCurrentlyLoaded(x,z)) {
System.out.println("Tell me a lie");
return;
}
File f = new File("res/textures/terrains/"+x+"_"+z+"_h.png");
if (!f.exists()) {
System.err.println(f.getAbsolutePath());
System.err.println(x+ " | "+z+" nicht gefunden [FILE]");
throw new IllegalArgumentException("!!!");
}
BufferedImage image = null;
try {
image = ImageIO.read(f);
} catch (IOException io) {
io.printStackTrace();
}
//Lade die Hilfsdateien für die Normalen an den Kanten
if (wantToKnow) System.out.println("Für "+x+"|"+z+" geladen");
f = new File("res/textures/terrains/"+(x-1)+"_"+z+"_h.png");
BufferedImage hxmimage = null;
if (f.exists()) {
try {
hxmimage = ImageIO.read(f);
} catch (IOException io) {
io.printStackTrace();
}
}
f = new File("res/textures/terrains/"+(x+1)+"_"+z+"_h.png");
BufferedImage hxpimage = null;
if (f.exists()) {
try {
hxpimage = ImageIO.read(f);
} catch (IOException io) {
io.printStackTrace();
}
}
f = new File("res/textures/terrains/"+(x)+"_"+(z-1)+"_h.png");
BufferedImage hzmimage = null;
if (f.exists()) {
try {
hzmimage = ImageIO.read(f);
} catch (IOException io) {
io.printStackTrace();
}
}
f = new File("res/textures/terrains/"+(x)+"_"+(z+1)+"_h.png");
BufferedImage hzpimage = null;
if (f.exists()) {
try {
hzpimage = ImageIO.read(f);
} catch (IOException io) {
io.printStackTrace();
}
}
int VERTEX_COUNT = image.getHeight();
//image.getHeight() is usually 256
int count = VERTEX_COUNT * VERTEX_COUNT;
float[] vertices = new float[count * 3];
float[][] heights = new float[VERTEX_COUNT][VERTEX_COUNT];
float[] normals = new float[count * 3];
float[] textureCoords = new float[count * 2];
int[] indices = new int[6 * (VERTEX_COUNT - 1) * (VERTEX_COUNT - 1)];
int vertexPointer = 0;
for (int i = 0;i < VERTEX_COUNT;i++) {
for (int j = 0;j < VERTEX_COUNT;j++) {
vertices[vertexPointer * 3] = (float)j / ((float) VERTEX_COUNT - 1) * Terrain.SIZE;
float height = getHeight(j,i,image);
vertices[vertexPointer * 3 + 1] = height;
heights[j][i] = height;
vertices[vertexPointer * 3 + 2] = (float)i / ((float) VERTEX_COUNT - 1) * Terrain.SIZE;
Vector3f normal = calculateNormal(j,i,image,hxmimage,hxpimage,hzmimage,hzpimage);
normals[vertexPointer*3] = normal.x;
normals[vertexPointer*3 + 1] = normal.y;
normals[vertexPointer*3 + 2] = normal.z;
textureCoords[vertexPointer * 2] = (float)j / ((float)VERTEX_COUNT - 1);
textureCoords[vertexPointer * 2 + 1] = (float)i / ((float)VERTEX_COUNT - 1);
vertexPointer++;
}
}
vertexPointer = 0;
for (int gz = 0; gz < VERTEX_COUNT - 1; gz++) {
for (int gx = 0; gx < VERTEX_COUNT - 1;gx++) {
int topLeft = (gz * VERTEX_COUNT + gx);
int topRight = topLeft + 1;
int bottomLeft = (gz + 1) * VERTEX_COUNT + gx;
int bottomRight = bottomLeft + 1;
indices[vertexPointer++] = topLeft;
indices[vertexPointer++] = bottomLeft;
indices[vertexPointer++] = topRight;
indices[vertexPointer++] = topRight;
indices[vertexPointer++] = bottomLeft;
indices[vertexPointer++] = bottomRight;
}
}
TerrainTexturePack ttp = new TerrainTexturePack(null,null,null,null,null);
this.bgtl.load(new Texture("terrains/"+x+"_"+z+"_b",(byte) 0));
ttp.setBlendMapPath("terrains/"+x+"_"+z+"_b");
File file = new File("res/game/terrains/"+x+"_"+z+"_t.txt");
char[] content = new char[(int)file.length()];
try {
FileReader fr = new FileReader(file);
fr.read(content);
fr.close();
} catch (IOException io) {
io.printStackTrace();
}
String str = new String(content);
String[] strings = str.split(":");
if (!strings[0].equals("null")) {
ttp.setRTexturePath(strings[0]);
this.bgtl.load(new Texture(strings[0],(byte) 0));
} else {
ttp.setRTexturePath("NULL");
}
if (!strings[1].equals("null")) {
ttp.setGTexturePath(strings[1]);
this.bgtl.load(new Texture(strings[1],(byte) 0));
} else {
ttp.setGTexturePath("NULL");
}
if (!strings[2].equals("null")) {
ttp.setBTexturePath(strings[2]);
this.bgtl.load(new Texture(strings[2],(byte) 0));
} else {
ttp.setBTexturePath("NULL");
}
if (!strings[3].equals("null")) {
ttp.setBackgroundTexturePath(strings[3]);
this.bgtl.load(new Texture(strings[3],(byte) 0));
}
int vaoID = this.bgml.loadToVAO(vertices,textureCoords,normals,indices);
System.err.println("Terrain VAO ID: "+vaoID);
this.allTerrains.add(new Terrain(x,z,ttp,vaoID,heights));
It is a really long post and i wd have loved to not type this all in, but the LWJGL descriptions on this topic are very poorly.
If you can link me a good description on how the LWJGL methods work, i would be very happy, too.
Im happy about each answer i get, even if its just about how i can get my program into a better structure.
Greetings,
eMmiE