/* * 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.dag.nodes; import org.terasology.assets.ResourceUrn; import org.terasology.config.Config; import org.terasology.config.RenderingConfig; import org.terasology.math.TeraMath; import org.terasology.math.geom.Vector3f; import org.terasology.monitoring.PerformanceMonitor; import org.terasology.registry.In; import org.terasology.rendering.backdrop.BackdropProvider; import org.terasology.rendering.cameras.Camera; import org.terasology.rendering.cameras.OrthographicCamera; import org.terasology.rendering.dag.ConditionDependentNode; import org.terasology.rendering.dag.stateChanges.BindFBO; import org.terasology.rendering.dag.stateChanges.EnableMaterial; import org.terasology.rendering.dag.stateChanges.SetViewportToSizeOf; import org.terasology.rendering.opengl.FBO; import org.terasology.rendering.opengl.FBOConfig; import org.terasology.rendering.opengl.fbms.ShadowMapResolutionDependentFBOs; import org.terasology.rendering.primitives.ChunkMesh; import org.terasology.rendering.world.RenderQueuesHelper; import org.terasology.rendering.world.RenderableWorld; import org.terasology.rendering.world.WorldRenderer; import org.terasology.world.chunks.RenderableChunk; import java.beans.PropertyChangeEvent; import static org.terasology.rendering.primitives.ChunkMesh.RenderPhase.OPAQUE; /** * This node class generates a shadow map used by the lighting step to determine what's in sight of * the main light (sun, moon) and what isn't, allowing the display of shadows cast from said light. * TODO: generalize to handle more than one light. * * Instances of this class: * - are enabled and disabled depending on the shadow setting in the rendering config. * - in VR mode regenerate the shadow map only once per frame rather than once per-eye. * * Diagram of this node can be viewed from: * TODO: move diagram to the wiki when this part of the code is stable * - https://docs.google.com/drawings/d/13I0GM9jDFlZv1vNrUPlQuBbaF86RPRNpVfn5q8Wj2lc/edit?usp=sharing */ public class ShadowMapNode extends ConditionDependentNode { public static final ResourceUrn SHADOW_MAP = new ResourceUrn("engine:sceneShadowMap"); private static final int SHADOW_FRUSTUM_BOUNDS = 500; private static final float STEP_SIZE = 50f; public Camera shadowMapCamera = new OrthographicCamera(-SHADOW_FRUSTUM_BOUNDS, SHADOW_FRUSTUM_BOUNDS, SHADOW_FRUSTUM_BOUNDS, -SHADOW_FRUSTUM_BOUNDS); @In private RenderableWorld renderableWorld; @In private RenderQueuesHelper renderQueues; @In private Config config; @In private WorldRenderer worldRenderer; @In private BackdropProvider backdropProvider; @In private ShadowMapResolutionDependentFBOs shadowMapResolutionDependentFBOs; private RenderingConfig renderingConfig; private Camera playerCamera; private float texelSize; @Override public void initialise() { this.playerCamera = worldRenderer.getActiveCamera(); this.renderingConfig = config.getRendering(); renderableWorld.setShadowMapCamera(shadowMapCamera); requiresFBO(new FBOConfig(SHADOW_MAP, FBO.Type.NO_COLOR).useDepthBuffer(), shadowMapResolutionDependentFBOs); texelSize = 1.0f / renderingConfig.getShadowMapResolution() * 2.0f; renderingConfig.subscribe(RenderingConfig.SHADOW_MAP_RESOLUTION, this); requiresCondition(() -> renderingConfig.isDynamicShadows()); renderingConfig.subscribe(RenderingConfig.DYNAMIC_SHADOWS, this); addDesiredStateChange(new BindFBO(SHADOW_MAP, shadowMapResolutionDependentFBOs)); addDesiredStateChange(new SetViewportToSizeOf(SHADOW_MAP, shadowMapResolutionDependentFBOs)); addDesiredStateChange(new EnableMaterial("engine:prog.shadowMap")); } private float calculateTexelSize(int shadowMapResolution) { return 1.0f / shadowMapResolution * 2.0f; // the 2.0 multiplier is currently a mystery. } /** * Handle changes to the following rendering config properties: * * - DYNAMIC_SHADOWS * - SHADOW_MAP_RESOLUTION * * It assumes the event gets fired only if one of the property has actually changed. * * @param event a PropertyChangeEvent instance, carrying information regarding * what property changed, its old value and its new value. */ @Override public void propertyChange(PropertyChangeEvent event) { if (event.getPropertyName().equals(RenderingConfig.DYNAMIC_SHADOWS)) { super.propertyChange(event); } else if (event.getPropertyName().equals(RenderingConfig.SHADOW_MAP_RESOLUTION)) { int shadowMapResolution = (int) event.getNewValue(); texelSize = calculateTexelSize(shadowMapResolution); } } /** * Re-positions the shadow map camera to loosely match the position of the main light (sun, moon), then * writes depth information from that camera into a depth buffer, to be used later to create shadows. * * The loose match is to avoid flickering: the shadowmap only moves in steps while the main light actually * moves continuously. * * This method is executed within a NodeTask in the Render Tasklist, but its calculations are executed * only once per frame. I.e. in VR mode they are executed only when the left eye is processed. This is * done in the assumption that we do not need to generate and use a shadow map for each eye as it wouldn't * be noticeable. */ @Override public void process() { // TODO: remove this IF statement when VR is handled via parallel nodes, one per eye. if (worldRenderer.isFirstRenderingStageForCurrentFrame()) { PerformanceMonitor.startActivity("rendering/shadowMap"); positionShadowMapCamera(); // TODO: extract these calculation into a separate node. int numberOfRenderedTriangles = 0; int numberOfChunksThatAreNotReadyYet = 0; final Vector3f cameraPosition = shadowMapCamera.getPosition(); shadowMapCamera.lookThrough(); // FIXME: storing chunksOpaqueShadow or a mechanism for requesting a chunk queue for nodes which calls renderChunks method? while (renderQueues.chunksOpaqueShadow.size() > 0) { RenderableChunk chunk = renderQueues.chunksOpaqueShadow.poll(); if (chunk.hasMesh()) { final ChunkMesh chunkMesh = chunk.getMesh(); final Vector3f chunkPosition = chunk.getPosition().toVector3f(); numberOfRenderedTriangles += chunkMesh.render(OPAQUE, chunkPosition, cameraPosition); } else { numberOfChunksThatAreNotReadyYet++; } } worldRenderer.increaseTrianglesCount(numberOfRenderedTriangles); worldRenderer.increaseNotReadyChunkCount(numberOfChunksThatAreNotReadyYet); PerformanceMonitor.endActivity(); } } private void positionShadowMapCamera() { // We begin by setting our light coordinates at the player coordinates, ignoring the player's altitude Vector3f mainLightPosition = new Vector3f(playerCamera.getPosition().x, 0.0f, playerCamera.getPosition().z); // world-space coordinates // The shadow projected onto the ground must move in in light-space texel-steps, to avoid causing flickering. // That's why we first convert it to the previous frame's light-space coordinates and then back to world-space. shadowMapCamera.getViewProjectionMatrix().transformPoint(mainLightPosition); // to light-space mainLightPosition.set(TeraMath.fastFloor(mainLightPosition.x / texelSize) * texelSize, 0.0f, TeraMath.fastFloor(mainLightPosition.z / texelSize) * texelSize); shadowMapCamera.getInverseViewProjectionMatrix().transformPoint(mainLightPosition); // back to world-space // This is what causes the shadow map to change infrequently, to prevent flickering. // Notice that this is different from what is done above, which is about spatial steps // and is related to the player's position and texels. Vector3f quantizedMainLightDirection = getQuantizedMainLightDirection(STEP_SIZE); // The shadow map camera is placed away from the player, in the direction of the main light. Vector3f offsetFromPlayer = new Vector3f(quantizedMainLightDirection); offsetFromPlayer.scale(256.0f + 64.0f); // these hardcoded numbers are another mystery. mainLightPosition.add(offsetFromPlayer); shadowMapCamera.getPosition().set(mainLightPosition); // Finally, we adjust the shadow map camera to look toward the player Vector3f fromLightToPlayerDirection = new Vector3f(quantizedMainLightDirection); fromLightToPlayerDirection.scale(-1.0f); shadowMapCamera.getViewingDirection().set(fromLightToPlayerDirection); shadowMapCamera.update(worldRenderer.getSecondsSinceLastFrame()); } private Vector3f getQuantizedMainLightDirection(float stepSize) { float mainLightAngle = (float) Math.floor(backdropProvider.getSunPositionAngle() * stepSize) / stepSize + 0.0001f; Vector3f mainLightDirection = new Vector3f(0.0f, (float) Math.cos(mainLightAngle), (float) Math.sin(mainLightAngle)); // When the sun goes under the horizon we flip the vector, to provide the moon direction, and viceversa. if (mainLightDirection.y < 0.0f) { mainLightDirection.scale(-1.0f); } return mainLightDirection; } }