Java tutorial
/* * Copyright 2013 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.rendering.primitives; import com.google.common.base.Stopwatch; import com.google.common.collect.Maps; import gnu.trove.iterator.TIntIterator; import org.lwjgl.BufferUtils; import org.terasology.engine.subsystem.lwjgl.GLBufferPool; import org.terasology.math.Direction; import org.terasology.math.Side; import org.terasology.math.TeraMath; import org.terasology.math.Vector3i; import org.terasology.monitoring.PerformanceMonitor; import org.terasology.rendering.RenderMath; import org.terasology.world.ChunkView; import org.terasology.world.WorldProvider; import org.terasology.world.biomes.Biome; import org.terasology.world.block.Block; import org.terasology.world.block.BlockAppearance; import org.terasology.world.block.BlockPart; import org.terasology.world.chunks.ChunkConstants; import javax.vecmath.Vector3f; import javax.vecmath.Vector4f; import java.util.Map; import java.util.concurrent.TimeUnit; /** * Generates tessellated chunk meshes from chunks. * * @author Benjamin Glatzel <benjamin.glatzel@me.com> */ public final class ChunkTessellator { private static int statVertexArrayUpdateCount; private GLBufferPool bufferPool; public ChunkTessellator(GLBufferPool bufferPool) { this.bufferPool = bufferPool; } public ChunkMesh generateMesh(ChunkView chunkView, int meshHeight, int verticalOffset) { PerformanceMonitor.startActivity("GenerateMesh"); ChunkMesh mesh = new ChunkMesh(bufferPool); final Stopwatch watch = Stopwatch.createStarted(); for (int x = 0; x < ChunkConstants.SIZE_X; x++) { for (int z = 0; z < ChunkConstants.SIZE_Z; z++) { for (int y = verticalOffset; y < verticalOffset + meshHeight; y++) { Biome biome = chunkView.getBiome(x, y, z); Block block = chunkView.getBlock(x, y, z); if (block != null && !block.isInvisible()) { generateBlockVertices(chunkView, mesh, x, y, z, biome); } } } } watch.stop(); mesh.setTimeToGenerateBlockVertices((int) watch.elapsed(TimeUnit.MILLISECONDS)); watch.reset().start(); generateOptimizedBuffers(chunkView, mesh); watch.stop(); mesh.setTimeToGenerateOptimizedBuffers((int) watch.elapsed(TimeUnit.MILLISECONDS)); statVertexArrayUpdateCount++; PerformanceMonitor.endActivity(); return mesh; } private void generateOptimizedBuffers(ChunkView chunkView, ChunkMesh mesh) { PerformanceMonitor.startActivity("OptimizeBuffers"); for (ChunkMesh.RenderType type : ChunkMesh.RenderType.values()) { ChunkMesh.VertexElements elements = mesh.getVertexElements(type); // Vertices double to account for light info elements.finalVertices = BufferUtils.createIntBuffer(elements.vertices.size() + /* POSITION */ elements.tex.size() + /* TEX0 (UV0 and flags) */ elements.tex.size() + /* TEX1 (lighting data) */ elements.flags.size() + /* FLAGS */ elements.color.size() + /* COLOR */ elements.normals.size() /* NORMALS */ ); int cTex = 0; int cColor = 0; int cFlags = 0; for (int i = 0; i < elements.vertices.size(); i += 3, cTex += 2, cColor += 4, cFlags++) { Vector3f vertexPos = new Vector3f(elements.vertices.get(i), elements.vertices.get(i + 1), elements.vertices.get(i + 2)); /* POSITION */ elements.finalVertices.put(Float.floatToIntBits(vertexPos.x)); elements.finalVertices.put(Float.floatToIntBits(vertexPos.y)); elements.finalVertices.put(Float.floatToIntBits(vertexPos.z)); /* UV0 - TEX DATA 0 */ elements.finalVertices.put(Float.floatToIntBits(elements.tex.get(cTex))); elements.finalVertices.put(Float.floatToIntBits(elements.tex.get(cTex + 1))); /* FLAGS */ elements.finalVertices.put(Float.floatToIntBits(elements.flags.get(cFlags))); float[] result = new float[3]; Vector3f normal = new Vector3f(elements.normals.get(i), elements.normals.get(i + 1), elements.normals.get(i + 2)); calcLightingValuesForVertexPos(chunkView, vertexPos, result, normal); /* LIGHTING DATA / TEX DATA 1 */ elements.finalVertices.put(Float.floatToIntBits(result[0])); elements.finalVertices.put(Float.floatToIntBits(result[1])); elements.finalVertices.put(Float.floatToIntBits(result[2])); /* PACKED COLOR */ final int packedColor = RenderMath.packColor(elements.color.get(cColor), elements.color.get(cColor + 1), elements.color.get(cColor + 2), elements.color.get(cColor + 3)); elements.finalVertices.put(packedColor); /* NORMALS */ elements.finalVertices.put(Float.floatToIntBits(normal.x)); elements.finalVertices.put(Float.floatToIntBits(normal.y)); elements.finalVertices.put(Float.floatToIntBits(normal.z)); } elements.finalIndices = BufferUtils.createIntBuffer(elements.indices.size()); TIntIterator indexIterator = elements.indices.iterator(); while (indexIterator.hasNext()) { elements.finalIndices.put(indexIterator.next()); } elements.finalVertices.flip(); elements.finalIndices.flip(); } PerformanceMonitor.endActivity(); } private void calcLightingValuesForVertexPos(ChunkView chunkView, Vector3f vertexPos, float[] output, Vector3f normal) { PerformanceMonitor.startActivity("calcLighting"); float[] lights = new float[8]; float[] blockLights = new float[8]; Block[] blocks = new Block[4]; PerformanceMonitor.startActivity("gatherLightInfo"); Direction dir = Direction.inDirection(normal); switch (dir) { case LEFT: case RIGHT: blocks[0] = chunkView.getBlock((vertexPos.x + 0.8f * normal.x), (vertexPos.y + 0.1f), (vertexPos.z + 0.1f)); blocks[1] = chunkView.getBlock((vertexPos.x + 0.8f * normal.x), (vertexPos.y + 0.1f), (vertexPos.z - 0.1f)); blocks[2] = chunkView.getBlock((vertexPos.x + 0.8f * normal.x), (vertexPos.y - 0.1f), (vertexPos.z - 0.1f)); blocks[3] = chunkView.getBlock((vertexPos.x + 0.8f * normal.x), (vertexPos.y - 0.1f), (vertexPos.z + 0.1f)); break; case FORWARD: case BACKWARD: blocks[0] = chunkView.getBlock((vertexPos.x + 0.1f), (vertexPos.y + 0.1f), (vertexPos.z + 0.8f * normal.z)); blocks[1] = chunkView.getBlock((vertexPos.x + 0.1f), (vertexPos.y - 0.1f), (vertexPos.z + 0.8f * normal.z)); blocks[2] = chunkView.getBlock((vertexPos.x - 0.1f), (vertexPos.y - 0.1f), (vertexPos.z + 0.8f * normal.z)); blocks[3] = chunkView.getBlock((vertexPos.x - 0.1f), (vertexPos.y + 0.1f), (vertexPos.z + 0.8f * normal.z)); break; default: blocks[0] = chunkView.getBlock((vertexPos.x + 0.1f), (vertexPos.y + 0.8f * normal.y), (vertexPos.z + 0.1f)); blocks[1] = chunkView.getBlock((vertexPos.x + 0.1f), (vertexPos.y + 0.8f * normal.y), (vertexPos.z - 0.1f)); blocks[2] = chunkView.getBlock((vertexPos.x - 0.1f), (vertexPos.y + 0.8f * normal.y), (vertexPos.z - 0.1f)); blocks[3] = chunkView.getBlock((vertexPos.x - 0.1f), (vertexPos.y + 0.8f * normal.y), (vertexPos.z + 0.1f)); } lights[0] = chunkView.getSunlight((vertexPos.x + 0.1f), (vertexPos.y + 0.8f), (vertexPos.z + 0.1f)); lights[1] = chunkView.getSunlight((vertexPos.x + 0.1f), (vertexPos.y + 0.8f), (vertexPos.z - 0.1f)); lights[2] = chunkView.getSunlight((vertexPos.x - 0.1f), (vertexPos.y + 0.8f), (vertexPos.z - 0.1f)); lights[3] = chunkView.getSunlight((vertexPos.x - 0.1f), (vertexPos.y + 0.8f), (vertexPos.z + 0.1f)); lights[4] = chunkView.getSunlight((vertexPos.x + 0.1f), (vertexPos.y - 0.1f), (vertexPos.z + 0.1f)); lights[5] = chunkView.getSunlight((vertexPos.x + 0.1f), (vertexPos.y - 0.1f), (vertexPos.z - 0.1f)); lights[6] = chunkView.getSunlight((vertexPos.x - 0.1f), (vertexPos.y - 0.1f), (vertexPos.z - 0.1f)); lights[7] = chunkView.getSunlight((vertexPos.x - 0.1f), (vertexPos.y - 0.1f), (vertexPos.z + 0.1f)); blockLights[0] = chunkView.getLight((vertexPos.x + 0.1f), (vertexPos.y + 0.8f), (vertexPos.z + 0.1f)); blockLights[1] = chunkView.getLight((vertexPos.x + 0.1f), (vertexPos.y + 0.8f), (vertexPos.z - 0.1f)); blockLights[2] = chunkView.getLight((vertexPos.x - 0.1f), (vertexPos.y + 0.8f), (vertexPos.z - 0.1f)); blockLights[3] = chunkView.getLight((vertexPos.x - 0.1f), (vertexPos.y + 0.8f), (vertexPos.z + 0.1f)); blockLights[4] = chunkView.getLight((vertexPos.x + 0.1f), (vertexPos.y - 0.1f), (vertexPos.z + 0.1f)); blockLights[5] = chunkView.getLight((vertexPos.x + 0.1f), (vertexPos.y - 0.1f), (vertexPos.z - 0.1f)); blockLights[6] = chunkView.getLight((vertexPos.x - 0.1f), (vertexPos.y - 0.1f), (vertexPos.z - 0.1f)); blockLights[7] = chunkView.getLight((vertexPos.x - 0.1f), (vertexPos.y - 0.1f), (vertexPos.z + 0.1f)); PerformanceMonitor.endActivity(); float resultLight = 0; float resultBlockLight = 0; int counterLight = 0; int counterBlockLight = 0; int occCounter = 0; int occCounterBillboard = 0; for (int i = 0; i < 8; i++) { if (lights[i] > 0) { resultLight += lights[i]; counterLight++; } if (blockLights[i] > 0) { resultBlockLight += blockLights[i]; counterBlockLight++; } if (i < 4) { Block b = blocks[i]; if (b.isShadowCasting() && !b.isTranslucent()) { occCounter++; } else if (b.isShadowCasting()) { occCounterBillboard++; } } } double resultAmbientOcclusion = (TeraMath.pow(0.40, occCounter) + TeraMath.pow(0.80, occCounterBillboard)) / 2.0; if (counterLight == 0) { output[0] = 0; } else { output[0] = resultLight / counterLight / 15f; } if (counterBlockLight == 0) { output[1] = 0; } else { output[1] = resultBlockLight / counterBlockLight / 15f; } output[2] = (float) resultAmbientOcclusion; PerformanceMonitor.endActivity(); } private void generateBlockVertices(ChunkView view, ChunkMesh mesh, int x, int y, int z, Biome biome) { Block block = view.getBlock(x, y, z); // TODO: Needs review - too much hardcoded special cases and corner cases resulting from this. ChunkVertexFlag vertexFlag = ChunkVertexFlag.NORMAL; if (block.isWater()) { vertexFlag = ChunkVertexFlag.WATER; } else if (block.isLava()) { vertexFlag = ChunkVertexFlag.LAVA; } else if (block.isWaving() && block.isDoubleSided()) { vertexFlag = ChunkVertexFlag.WAVING; } else if (block.isWaving()) { vertexFlag = ChunkVertexFlag.WAVING_BLOCK; } // Gather adjacent blocks Map<Side, Block> adjacentBlocks = Maps.newEnumMap(Side.class); for (Side side : Side.values()) { Vector3i offset = side.getVector3i(); Block blockToCheck = view.getBlock(x + offset.x, y + offset.y, z + offset.z); adjacentBlocks.put(side, blockToCheck); } BlockAppearance blockAppearance = block.getAppearance(adjacentBlocks); /* * Determine the render process. */ ChunkMesh.RenderType renderType = ChunkMesh.RenderType.TRANSLUCENT; if (!block.isTranslucent()) { renderType = ChunkMesh.RenderType.OPAQUE; } // TODO: Review special case, or alternatively compare uris. if (block.isWater() || block.isIce()) { renderType = ChunkMesh.RenderType.WATER_AND_ICE; } if (block.isDoubleSided()) { renderType = ChunkMesh.RenderType.BILLBOARD; } if (blockAppearance.getPart(BlockPart.CENTER) != null) { Vector4f colorOffset = block.calcColorOffsetFor(BlockPart.CENTER, biome); blockAppearance.getPart(BlockPart.CENTER).appendTo(mesh, x, y, z, colorOffset, renderType, vertexFlag); } boolean[] drawDir = new boolean[6]; for (Side side : Side.values()) { drawDir[side.ordinal()] = blockAppearance.getPart(BlockPart.fromSide(side)) != null && isSideVisibleForBlockTypes(adjacentBlocks.get(side), block, side); } // If the block is lowered, some more faces may have to be drawn if (block.isLiquid()) { Block bottomBlock = adjacentBlocks.get(Side.BOTTOM); // Draw horizontal sides if visible from below for (Side side : Side.horizontalSides()) { Vector3i offset = side.getVector3i(); Block adjacentBelow = view.getBlock(x + offset.x, y - 1, z + offset.z); Block adjacent = adjacentBlocks.get(side); boolean visible = (blockAppearance.getPart(BlockPart.fromSide(side)) != null && isSideVisibleForBlockTypes(adjacentBelow, block, side) && !isSideVisibleForBlockTypes(bottomBlock, adjacent, side.reverse())); drawDir[side.ordinal()] |= visible; } // Draw the top if below a non-lowered block // TODO: Don't need to render the top if each side and the block above each side are either liquid or opaque solids. Block blockToCheck = adjacentBlocks.get(Side.TOP); drawDir[Side.TOP.ordinal()] |= !blockToCheck.isLiquid(); if (bottomBlock.isLiquid() || bottomBlock.isInvisible()) { for (Side dir : Side.values()) { if (drawDir[dir.ordinal()]) { Vector4f colorOffset = block.calcColorOffsetFor(BlockPart.fromSide(dir), biome); block.getLoweredLiquidMesh(dir).appendTo(mesh, x, y, z, colorOffset, renderType, vertexFlag); } } return; } } for (Side dir : Side.values()) { if (drawDir[dir.ordinal()]) { Vector4f colorOffset = block.calcColorOffsetFor(BlockPart.fromSide(dir), biome); // TODO: Needs review since the new per-vertex flags introduce a lot of special scenarios - probably a per-side setting? if (block.isGrass() && dir != Side.TOP && dir != Side.BOTTOM) { blockAppearance.getPart(BlockPart.fromSide(dir)).appendTo(mesh, x, y, z, colorOffset, renderType, ChunkVertexFlag.COLOR_MASK); } else { blockAppearance.getPart(BlockPart.fromSide(dir)).appendTo(mesh, x, y, z, colorOffset, renderType, vertexFlag); } } } } /** * Returns true if the side should be rendered adjacent to the second side provided. * * @param blockToCheck The block to check * @param currentBlock The current block * @return True if the side is visible for the given block types */ private boolean isSideVisibleForBlockTypes(Block blockToCheck, Block currentBlock, Side side) { // Liquids can be transparent but there should be no visible adjacent faces if (currentBlock.isLiquid() && blockToCheck.isLiquid()) { return false; } return currentBlock.isWaving() != blockToCheck.isWaving() || blockToCheck.isInvisible() || !blockToCheck.isFullSide(side.reverse()) || (!currentBlock.isTranslucent() && blockToCheck.isTranslucent()); } public static int getVertexArrayUpdateCount() { return statVertexArrayUpdateCount; } }