/*
* Copyright 2016 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.world;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.config.Config;
import org.terasology.config.RenderingConfig;
import org.terasology.engine.subsystem.lwjgl.GLBufferPool;
import org.terasology.math.Region3i;
import org.terasology.math.TeraMath;
import org.terasology.math.geom.Vector3f;
import org.terasology.math.geom.Vector3i;
import org.terasology.monitoring.PerformanceMonitor;
import org.terasology.registry.CoreRegistry;
import org.terasology.rendering.cameras.Camera;
import org.terasology.rendering.primitives.ChunkMesh;
import org.terasology.rendering.primitives.ChunkTessellator;
import org.terasology.rendering.world.viewDistance.ViewDistance;
import org.terasology.world.ChunkView;
import org.terasology.world.WorldProvider;
import org.terasology.world.chunks.ChunkConstants;
import org.terasology.world.chunks.ChunkProvider;
import org.terasology.world.chunks.RenderableChunk;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;
/**
* TODO: write javadoc unless this class gets slated for removal, which might be.
*/
class RenderableWorldImpl implements RenderableWorld {
private static final int MAX_ANIMATED_CHUNKS = 64;
private static final int MAX_BILLBOARD_CHUNKS = 64;
private static final int MAX_LOADABLE_CHUNKS = ViewDistance.MEGA.getChunkDistance().x * ViewDistance.MEGA.getChunkDistance().y * ViewDistance.MEGA.getChunkDistance().z;
private static final Vector3f CHUNK_CENTER_OFFSET = new Vector3f(0.5f, 0.5f, 0.5f);
private static final Logger logger = LoggerFactory.getLogger(RenderableWorldImpl.class);
private final int maxChunksForShadows = TeraMath.clamp(CoreRegistry.get(Config.class).getRendering().getMaxChunksUsedForShadowMapping(), 64, 1024);
private final WorldProvider worldProvider;
private ChunkProvider chunkProvider;
private ChunkTessellator chunkTessellator;
private final ChunkMeshUpdateManager chunkMeshUpdateManager;
private final List<RenderableChunk> chunksInProximityOfCamera = Lists.newArrayListWithCapacity(MAX_LOADABLE_CHUNKS);
private Region3i renderableRegion = Region3i.EMPTY;
private ViewDistance currentViewDistance;
private RenderQueuesHelper renderQueues;
private Camera playerCamera;
private Camera shadowMapCamera;
private Config config = CoreRegistry.get(Config.class);
private RenderingConfig renderingConfig = config.getRendering();
private int statDirtyChunks;
private int statVisibleChunks;
private int statIgnoredPhases;
RenderableWorldImpl(WorldProvider worldProvider,
ChunkProvider chunkProvider,
GLBufferPool bufferPool,
Camera playerCamera) {
this.worldProvider = worldProvider;
this.chunkProvider = chunkProvider;
chunkTessellator = new ChunkTessellator(bufferPool);
chunkMeshUpdateManager = new ChunkMeshUpdateManager(chunkTessellator, worldProvider);
this.playerCamera = playerCamera;
renderQueues = new RenderQueuesHelper(new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkFrontToBackComparator()),
new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkFrontToBackComparator()),
new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkFrontToBackComparator()),
new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkFrontToBackComparator()),
new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkBackToFrontComparator()));
}
@Override
public void onChunkLoaded(Vector3i chunkCoordinates) {
if (renderableRegion.encompasses(chunkCoordinates)) {
chunksInProximityOfCamera.add(chunkProvider.getChunk(chunkCoordinates));
Collections.sort(chunksInProximityOfCamera, new ChunkFrontToBackComparator());
}
}
@Override
public void onChunkUnloaded(Vector3i chunkCoordinates) {
if (renderableRegion.encompasses(chunkCoordinates)) {
RenderableChunk chunk;
Iterator<RenderableChunk> iterator = chunksInProximityOfCamera.iterator();
while (iterator.hasNext()) {
chunk = iterator.next();
if (chunk.getPosition().equals(chunkCoordinates)) {
chunk.disposeMesh();
iterator.remove();
break;
}
}
}
}
/**
* @return true if pregeneration is complete
*/
@Override
public boolean pregenerateChunks() {
boolean pregenerationIsComplete = true;
chunkProvider.completeUpdate();
chunkProvider.beginUpdate();
RenderableChunk chunk;
ChunkMesh newMesh;
ChunkView localView;
for (Vector3i chunkCoordinates : calculateRenderableRegion(renderingConfig.getViewDistance())) {
chunk = chunkProvider.getChunk(chunkCoordinates);
if (chunk == null) {
pregenerationIsComplete = false;
} else if (chunk.isDirty()) {
localView = worldProvider.getLocalView(chunkCoordinates);
if (localView == null) {
continue;
}
chunk.setDirty(false);
newMesh = chunkTessellator.generateMesh(localView, ChunkConstants.SIZE_Y, 0);
newMesh.generateVBOs();
if (chunk.hasMesh()) {
chunk.getMesh().dispose();
}
chunk.setMesh(newMesh);
pregenerationIsComplete = false;
break;
}
}
return pregenerationIsComplete;
}
@Override
public void update() {
PerformanceMonitor.startActivity("Complete chunk update");
chunkProvider.completeUpdate();
PerformanceMonitor.endActivity();
PerformanceMonitor.startActivity("Update Lighting");
worldProvider.processPropagation();
PerformanceMonitor.endActivity();
PerformanceMonitor.startActivity("Begin chunk update");
chunkProvider.beginUpdate();
PerformanceMonitor.endActivity();
PerformanceMonitor.startActivity("Update Close Chunks");
updateChunksInProximity(calculateRenderableRegion(renderingConfig.getViewDistance()));
PerformanceMonitor.endActivity();
}
/**
* Updates the list of chunks around the player.
*
* @return True if the list was changed
*/
@Override
public boolean updateChunksInProximity(Region3i newRenderableRegion) {
if (!newRenderableRegion.equals(renderableRegion)) {
Vector3i chunkPosition;
RenderableChunk chunk;
Iterator<Vector3i> chunksToRemove = renderableRegion.subtract(newRenderableRegion);
while (chunksToRemove.hasNext()) {
chunkPosition = chunksToRemove.next();
Iterator<RenderableChunk> nearbyChunks = chunksInProximityOfCamera.iterator();
while (nearbyChunks.hasNext()) {
chunk = nearbyChunks.next();
if (chunk.getPosition().equals(chunkPosition)) {
chunk.disposeMesh();
nearbyChunks.remove();
break;
}
}
}
boolean chunksHaveBeenAdded = false;
Iterator<Vector3i> chunksToAdd = newRenderableRegion.subtract(renderableRegion);
while (chunksToAdd.hasNext()) {
chunkPosition = chunksToAdd.next();
chunk = chunkProvider.getChunk(chunkPosition);
if (chunk != null) {
chunksInProximityOfCamera.add(chunk);
chunksHaveBeenAdded = true;
}
}
if (chunksHaveBeenAdded) {
Collections.sort(chunksInProximityOfCamera, new ChunkFrontToBackComparator());
}
renderableRegion = newRenderableRegion;
return true;
}
return false;
}
@Override
public boolean updateChunksInProximity(ViewDistance newViewDistance) {
if (newViewDistance != currentViewDistance) {
logger.info("New Viewing Distance: {}", newViewDistance);
currentViewDistance = newViewDistance;
return updateChunksInProximity(calculateRenderableRegion(newViewDistance));
} else {
return false;
}
}
private Region3i calculateRenderableRegion(ViewDistance newViewDistance) {
Vector3i cameraCoordinates = calcCameraCoordinatesInChunkUnits();
Vector3i renderableRegionSize = newViewDistance.getChunkDistance();
Vector3i renderableRegionExtents = new Vector3i(renderableRegionSize.x / 2, renderableRegionSize.y / 2, renderableRegionSize.z / 2);
return Region3i.createFromCenterExtents(cameraCoordinates, renderableRegionExtents);
}
/**
* Chunk position of the player.
*
* @return The player offset chunk
*/
private Vector3i calcCameraCoordinatesInChunkUnits() {
Vector3f cameraCoordinates = playerCamera.getPosition();
return new Vector3i((int) (cameraCoordinates.x / ChunkConstants.SIZE_X),
(int) (cameraCoordinates.y / ChunkConstants.SIZE_Y),
(int) (cameraCoordinates.z / ChunkConstants.SIZE_Z));
}
@Override
public void generateVBOs() {
PerformanceMonitor.startActivity("Building Mesh VBOs");
ChunkMesh pendingMesh;
chunkMeshUpdateManager.setCameraPosition(playerCamera.getPosition());
for (RenderableChunk chunk : chunkMeshUpdateManager.availableChunksForUpdate()) {
if (chunk.hasPendingMesh() && chunksInProximityOfCamera.contains(chunk)) {
pendingMesh = chunk.getPendingMesh();
pendingMesh.generateVBOs();
if (chunk.hasMesh()) {
chunk.getMesh().dispose();
}
chunk.setMesh(pendingMesh);
chunk.setPendingMesh(null);
} else {
if (chunk.hasPendingMesh()) {
chunk.getPendingMesh().dispose();
chunk.setPendingMesh(null);
}
}
}
PerformanceMonitor.endActivity();
}
/**
* Updates the currently visible chunks (in sight of the player).
*/
@Override
public int queueVisibleChunks(boolean isFirstRenderingStageForCurrentFrame) {
PerformanceMonitor.startActivity("Queueing Visible Chunks");
statDirtyChunks = 0;
statVisibleChunks = 0;
statIgnoredPhases = 0;
int processedChunks = 0;
int chunkCounter = 0;
ChunkMesh mesh;
boolean isDynamicShadows = renderingConfig.isDynamicShadows();
for (RenderableChunk chunk : chunksInProximityOfCamera) {
if (isChunkValidForRender(chunk)) {
mesh = chunk.getMesh();
if (isDynamicShadows && isFirstRenderingStageForCurrentFrame && chunkCounter < maxChunksForShadows && isChunkVisibleFromMainLight(chunk)) {
if (triangleCount(mesh, ChunkMesh.RenderPhase.OPAQUE) > 0) {
renderQueues.chunksOpaqueShadow.add(chunk);
} else {
statIgnoredPhases++;
}
}
if (isChunkVisible(chunk)) {
if (triangleCount(mesh, ChunkMesh.RenderPhase.OPAQUE) > 0) {
renderQueues.chunksOpaque.add(chunk);
} else {
statIgnoredPhases++;
}
if (triangleCount(mesh, ChunkMesh.RenderPhase.REFRACTIVE) > 0) {
renderQueues.chunksAlphaBlend.add(chunk);
} else {
statIgnoredPhases++;
}
if (triangleCount(mesh, ChunkMesh.RenderPhase.ALPHA_REJECT) > 0 && chunkCounter < MAX_BILLBOARD_CHUNKS) {
renderQueues.chunksAlphaReject.add(chunk);
} else {
statIgnoredPhases++;
}
statVisibleChunks++;
if (statVisibleChunks < MAX_ANIMATED_CHUNKS) {
chunk.setAnimated(true);
} else {
chunk.setAnimated(false);
}
}
if (isChunkVisibleReflection(chunk)) {
renderQueues.chunksOpaqueReflection.add(chunk);
}
// Process all chunks in the area, not only the visible ones
if (isFirstRenderingStageForCurrentFrame && (chunk.isDirty() || !chunk.hasMesh())) {
statDirtyChunks++;
chunkMeshUpdateManager.queueChunkUpdate(chunk);
processedChunks++;
}
}
chunkCounter++;
}
PerformanceMonitor.endActivity();
return processedChunks;
}
private int triangleCount(ChunkMesh mesh, ChunkMesh.RenderPhase renderPhase) {
if (mesh != null) {
return mesh.triangleCount(renderPhase);
} else {
return 0;
}
}
@Override
public void dispose() {
chunkMeshUpdateManager.shutdown();
}
private boolean isChunkValidForRender(RenderableChunk chunk) {
return chunk.isReady() && chunk.areAdjacentChunksReady();
}
private boolean isChunkVisibleFromMainLight(RenderableChunk chunk) {
return isChunkVisible(shadowMapCamera, chunk); //TODO: find an elegant way
}
private boolean isChunkVisible(RenderableChunk chunk) {
return isChunkVisible(playerCamera, chunk);
}
private boolean isChunkVisible(Camera camera, RenderableChunk chunk) {
return camera.hasInSight(chunk.getAABB());
}
private boolean isChunkVisibleReflection(RenderableChunk chunk) {
return playerCamera.getViewFrustumReflected().intersects(chunk.getAABB());
}
@Override
public RenderQueuesHelper getRenderQueues() {
return renderQueues;
}
@Override
public ChunkProvider getChunkProvider() {
return chunkProvider;
}
@Override
public void setShadowMapCamera(Camera camera) {
this.shadowMapCamera = camera;
}
@Override
public String getMetrics() {
String stringToReturn = "";
stringToReturn += "Dirty Chunks: ";
stringToReturn += statDirtyChunks;
stringToReturn += "\n";
stringToReturn += "Ignored Phases: ";
stringToReturn += statIgnoredPhases;
stringToReturn += "\n";
stringToReturn += "Visible Chunks: ";
stringToReturn += statVisibleChunks;
stringToReturn += "\n";
return stringToReturn;
}
private static float squaredDistanceToCamera(RenderableChunk chunk, Vector3f cameraPosition) {
// For performance reasons, to avoid instantiating too many vectors in a frequently called method,
// comments are in use instead of appropriately named vectors.
Vector3f result = chunk.getPosition().toVector3f(); // chunk position in chunk coordinates
result.add(CHUNK_CENTER_OFFSET); // chunk center in chunk coordinates
result.x *= ChunkConstants.SIZE_X; // chunk center in world coordinates
result.y *= ChunkConstants.SIZE_Y;
result.z *= ChunkConstants.SIZE_Z;
result.sub(cameraPosition); // camera to chunk vector
return result.lengthSquared();
}
// TODO: find the right place to check if the activeCamera has changed,
// TODO: so that the comparators can hold an up-to-date reference to it
// TODO: and avoid having to find it on a per-comparison basis.
private static class ChunkFrontToBackComparator implements Comparator<RenderableChunk> {
@Override
public int compare(RenderableChunk chunk1, RenderableChunk chunk2) {
Preconditions.checkNotNull(chunk1);
Preconditions.checkNotNull(chunk2);
Vector3f cameraPosition = CoreRegistry.get(WorldRenderer.class).getActiveCamera().getPosition();
double distance1 = squaredDistanceToCamera(chunk1, cameraPosition);
double distance2 = squaredDistanceToCamera(chunk2, cameraPosition);
// Using Double.compare as simple d1 < d2 comparison is flagged as problematic by Jenkins
// On the other hand Double.compare can return any positive/negative value apparently,
// hence the need for Math.signum().
return (int) Math.signum(Double.compare(distance1, distance2));
}
}
private static class ChunkBackToFrontComparator implements Comparator<RenderableChunk> {
@Override
public int compare(RenderableChunk chunk1, RenderableChunk chunk2) {
Preconditions.checkNotNull(chunk1);
Preconditions.checkNotNull(chunk2);
Vector3f cameraPosition = CoreRegistry.get(WorldRenderer.class).getActiveCamera().getPosition();
double distance1 = squaredDistanceToCamera(chunk1, cameraPosition);
double distance2 = squaredDistanceToCamera(chunk2, cameraPosition);
if (distance1 == distance2) {
return 0;
} else if (distance2 > distance1) {
return 1;
} else {
return -1;
}
}
}
}