Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrapping ShadowMap and WorldReflection into a "Node" #2325

Merged
merged 17 commits into from Jun 18, 2016
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,11 +1,11 @@
/*
* Copyright 2014 MovingBlocks
* 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
* 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,
Expand All @@ -25,6 +25,8 @@
import org.terasology.math.geom.Vector3i;
import org.terasology.monitoring.PerformanceMonitor;
import org.terasology.rendering.cameras.Camera;
import org.terasology.rendering.primitives.ChunkMesh;
import org.terasology.rendering.world.WorldRendererImpl;
import org.terasology.rendering.world.viewDistance.ViewDistance;
import org.terasology.rendering.world.WorldRenderer;
import org.terasology.world.WorldProvider;
Expand All @@ -36,6 +38,7 @@
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;

public class HeadlessWorldRenderer implements WorldRenderer {

Expand All @@ -61,6 +64,11 @@ public HeadlessWorldRenderer(Context context) {
config = context.get(Config.class);
}

@Override
public void renderChunks(PriorityQueue<RenderableChunk> chunks, ChunkMesh.RenderPhase phase, Camera camera, WorldRendererImpl.ChunkRenderMode mode) {

}

@Override
public void onChunkLoaded(Vector3i pos) {

Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2013 MovingBlocks
* 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.
Expand Down Expand Up @@ -39,7 +39,7 @@
import org.terasology.rendering.assets.material.Material;
import org.terasology.rendering.assets.texture.Texture;
import org.terasology.rendering.logic.NearestSortingList;
import org.terasology.rendering.opengl.OpenGLUtil;
import org.terasology.rendering.opengl.OpenGLUtils;
import org.terasology.rendering.world.WorldRenderer;
import org.terasology.utilities.random.FastRandom;
import org.terasology.utilities.random.Random;
Expand Down Expand Up @@ -296,7 +296,7 @@ private void renderBlockParticles(Vector3f worldPos, Vector3f cameraPosition, Bl
for (Particle particle : particleEffect.particles) {
glPushMatrix();
glTranslatef(particle.position.x, particle.position.y, particle.position.z);
OpenGLUtil.applyBillboardOrientation();
OpenGLUtils.applyBillboardOrientation();
glScalef(particle.size, particle.size, particle.size);

float light = worldRenderer.getRenderingLightIntensityAt(new Vector3f(worldPos.x + particle.position.x,
Expand All @@ -314,7 +314,7 @@ private void renderParticles(Vector3f worldPos, Vector3f cameraPosition, BlockPa
for (Particle particle : particleEffect.particles) {
glPushMatrix();
glTranslatef(particle.position.x, particle.position.y, particle.position.z);
OpenGLUtil.applyBillboardOrientation();
OpenGLUtils.applyBillboardOrientation();
glScalef(particle.size, particle.size, particle.size);

float light = worldRenderer.getRenderingLightIntensityAt(new Vector3f(worldPos.x + particle.position.x,
Expand Down
24 changes: 24 additions & 0 deletions engine/src/main/java/org/terasology/rendering/dag/Node.java
@@ -0,0 +1,24 @@
/*
* 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;


public interface Node { // since it's inside dag package

void update(float deltaInSeconds);

void process();
}
146 changes: 146 additions & 0 deletions engine/src/main/java/org/terasology/rendering/dag/ShadowMapNode.java
@@ -0,0 +1,146 @@
/*
* 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;

import org.lwjgl.opengl.GL11;
import org.terasology.config.Config;
import org.terasology.config.RenderingConfig;
import org.terasology.context.Context;
import org.terasology.math.TeraMath;
import org.terasology.math.geom.Vector3f;
import org.terasology.monitoring.PerformanceMonitor;
import org.terasology.rendering.assets.material.Material;
import org.terasology.rendering.backdrop.BackdropProvider;
import org.terasology.rendering.cameras.Camera;
import org.terasology.rendering.cameras.OrthographicCamera;
import org.terasology.rendering.opengl.FBO;
import org.terasology.rendering.opengl.FrameBuffersManager;
import org.terasology.rendering.primitives.ChunkMesh;
import org.terasology.rendering.world.RenderQueuesHelper;
import org.terasology.rendering.world.WorldRenderer;
import org.terasology.rendering.world.WorldRendererImpl;
import static org.lwjgl.opengl.GL11.*;
import static org.terasology.rendering.opengl.OpenGLUtils.*;


/**
* 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 implements Node {
private static final int SHADOW_FRUSTUM_BOUNDS = 500;


public Camera camera = new OrthographicCamera(-SHADOW_FRUSTUM_BOUNDS, SHADOW_FRUSTUM_BOUNDS, SHADOW_FRUSTUM_BOUNDS, -SHADOW_FRUSTUM_BOUNDS);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call this shadowMapCamera please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to avoid something like:

    @Override
    public Camera getLightCamera() {
        return shadowMapNode.shadowMapCamera;
    }

But since in the future we would not access shadowMapNode in this way. I think it's ok.

private RenderQueuesHelper renderQueues;
private Context context;
private Material shadowMapShader;
private FBO shadowMap;

// TODO: every node proposes its modifiable configuration?
private RenderingConfig renderingConfig;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess there could be nodes that:

  • have no modifiable configuration
  • nodes whose configuration is not in a render config
  • nodes that can be configured only via their API

Is this what you were aiming at? For the time being however, I'd say all nodes do.

Copy link
Contributor Author

@tdgunes tdgunes Jun 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am curious about a node like ShadowMapNode proposes its configuration besides just on/off, maybe different selectable techniques. These proposed configurations from nodes can be directly displayed in VideoSettings screen. Again something I think we should definitely consider in the future.


private Camera playerCamera;
// FIXME: remove reference to WorldRendererImpl
private WorldRenderer worldRenderer;
private BackdropProvider backdropProvider;


// FIXME: unnecessary arguments must be eliminated
public ShadowMapNode(Context context, Material shadowMapShader, Camera playerCamera) {
this.context = context;
this.shadowMapShader = shadowMapShader;
this.playerCamera = playerCamera;
this.worldRenderer = context.get(WorldRenderer.class);
this.backdropProvider = context.get(BackdropProvider.class);
this.renderingConfig = context.get(Config.class).getRendering();
this.shadowMap = context.get(FrameBuffersManager.class).getFBO("sceneShadowMap");
this.renderQueues = context.get(RenderQueuesHelper.class);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only suggestion here would be that you can get the playerCamera via WorldRenderer.getActiveCamera().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use the InjectionHelper here. This enables the @In annotation for the fields that are known to the context.


@Override
public void update(float deltaInSeconds) {
this.shadowMap = context.get(FrameBuffersManager.class).getFBO("sceneShadowMap");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm... no. What is currently in update() here, should be be executed within process() instead. In Terasology update() methods are potentially executed multiple times per frame, which in this case I do not think we need. In fact at this stage I do not think we need an update method at all and I'd remove it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course this doesn't mean you should just cut and paste everything into process(). I.e. all camera positioning stuff should go in its private method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, can you obtain the FrameBuffersManager instance on construction instead?


// positionShadowMapCamera()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this doing here?

Copy link
Contributor Author

@tdgunes tdgunes Jun 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry, forgot to remove that. My bad! 😄


// Shadows are rendered around the player so...
Vector3f lightPosition = new Vector3f(playerCamera.getPosition().x, 0.0f, playerCamera.getPosition().z);

// Project the shadowMapCamera position to light space and make sure it is only moved in texel steps (avoids flickering when moving the shadowMapCamera)
float texelSize = 1.0f / renderingConfig.getShadowMapResolution();
texelSize *= 2.0f;

camera.getViewProjectionMatrix().transformPoint(lightPosition);
lightPosition.set(TeraMath.fastFloor(lightPosition.x / texelSize) * texelSize, 0.0f, TeraMath.fastFloor(lightPosition.z / texelSize) * texelSize);
camera.getInverseViewProjectionMatrix().transformPoint(lightPosition);

// ... we position our new shadowMapCamera at the position of the player and move it
// quite a bit into the direction of the sun (our main light).

// Make sure the sun does not move too often since it causes massive shadow flickering (from hell to the max)!
float stepSize = 50f;
Vector3f sunDirection = backdropProvider.getQuantizedSunDirection(stepSize);

Vector3f sunPosition = new Vector3f(sunDirection);
sunPosition.scale(256.0f + 64.0f);
lightPosition.add(sunPosition);

camera.getPosition().set(lightPosition);

// and adjust it to look from the sun direction into the direction of our player
Vector3f negSunDirection = new Vector3f(sunDirection);
negSunDirection.scale(-1.0f);

camera.getViewingDirection().set(negSunDirection);

camera.update(deltaInSeconds);

}


@Override
public void process() {
PerformanceMonitor.startActivity("Render World (Shadow Map)");

// preRenderSetupSceneShadowMap
shadowMap.bind();
setViewportToSizeOf(shadowMap); // TODO: how about shadowMap.setAsViewport() ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm. Reading shadowMap.setAsViewport() I'd think that something is happening to the object shadowMap rather than the to viewport. Normally a method affects the instance is attached to or the instance is the "agent" that causes something. In this case neither make sense. We could theorethically go for something like shadowMap.setViewportToItsSize() but it's a bit convoluted. I prefer the immediacy of setViewportToSizeOf(anFBO), as it's almost plain English.

// TODO: verify the need to clear color buffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
GL11.glDisable(GL_CULL_FACE);

// render
camera.lookThrough();

shadowMapShader.enable();
// FIXME: storing chuncksOpaqueShadow or a mechanism for requesting a chunk queue for nodes which calls renderChunks method?
worldRenderer.renderChunks(renderQueues.chunksOpaqueShadow, ChunkMesh.RenderPhase.OPAQUE, camera, WorldRendererImpl.ChunkRenderMode.SHADOW_MAP);
playerCamera.lookThrough(); //FIXME: not strictly needed: just defensive programming here.


// postRenderCleanupSceneShadowMap
GL11.glEnable(GL_CULL_FACE);
bindDisplay(); // bindDisplay()


PerformanceMonitor.endActivity();
}



}
@@ -0,0 +1,105 @@
/*
* 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;

import org.lwjgl.opengl.GL11;
import org.terasology.config.Config;
import org.terasology.config.RenderingConfig;
import org.terasology.context.Context;
import org.terasology.monitoring.PerformanceMonitor;
import org.terasology.rendering.assets.material.Material;
import org.terasology.rendering.assets.shader.ShaderProgramFeature;
import org.terasology.rendering.backdrop.BackdropRenderer;
import org.terasology.rendering.cameras.Camera;
import org.terasology.rendering.opengl.FBO;
import org.terasology.rendering.opengl.FrameBuffersManager;
import org.terasology.rendering.opengl.OpenGLUtils;
import org.terasology.rendering.primitives.ChunkMesh;
import org.terasology.rendering.world.RenderQueuesHelper;
import org.terasology.rendering.world.WorldRenderer;
import org.terasology.rendering.world.WorldRendererImpl;

import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.glClear;
import static org.terasology.rendering.opengl.OpenGLUtils.bindDisplay;

/**
* 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/1Iz7MA8Y5q7yjxxcgZW-0antv5kgx6NYkvoInielbwGU/edit?usp=sharing
*/
public class WorldReflectionNode implements Node {

private RenderQueuesHelper renderQueues;
private RenderingConfig renderingConfig;
private WorldRenderer worldRenderer;
private Material chunkShader;

private Context context;
private BackdropRenderer backdropRenderer;
private Camera playerCamera;

private FBO sceneReflected;
private FBO sceneOpaque;

public WorldReflectionNode(Context context, Camera playerCamera, Material chunkShader) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above, you can get the playerCamera via WorldRenderer.getActiveCamera().

this.context = context;
this.backdropRenderer = context.get(BackdropRenderer.class);
this.playerCamera = playerCamera;
this.renderingConfig = context.get(Config.class).getRendering();
this.chunkShader = chunkShader;
this.worldRenderer = context.get(WorldRenderer.class);
this.renderQueues = context.get(RenderQueuesHelper.class);
}

@Override
public void update(float deltaInSeconds) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above, the content of this method can be placed in process().

Also, get the FrameBuffersManager instance only once please, on construction I'd suggest.

this.sceneReflected = context.get(FrameBuffersManager.class).getFBO("sceneReflected");
this.sceneOpaque = context.get(FrameBuffersManager.class).getFBO("sceneOpaque");
}

@Override
public void process() {
PerformanceMonitor.startActivity("Render World (Reflection)");

sceneReflected.bind();
OpenGLUtils.setViewportToSizeOf(sceneReflected);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove the "OpenGLUtils."

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
GL11.glCullFace(GL11.GL_FRONT);
playerCamera.setReflected(true);


playerCamera.lookThroughNormalized(); // we don't want the reflected scene to be bobbing or moving with the player
backdropRenderer.render(playerCamera);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add:

TODO: convert backdropRenderer into a BackdropNode.

Note for @tdgunes: this will be an interesting use case for GL state management and order of execution of nodes: both the the BackdropNode and the WorldReflectionNode need similar/identical states, so the nodes should be processed one after another and the first node to be processed should not be allowed to reset to default the states the second node needs. Also interesting, the BackdropNode will need to be used twice, one here, to render the reflection, and then later, for the proper renderer. Something to put in the back of our minds.

playerCamera.lookThrough();

if (renderingConfig.isReflectiveWater()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm.... let's put a TODO here:

TODO: the isReflectiveWater() block should include content of the whole process() method. Eventually the check will be removed, when node insertion will signify that the feature is enabled.

chunkShader.activateFeature(ShaderProgramFeature.FEATURE_USE_FORWARD_LIGHTING);
worldRenderer.renderChunks(renderQueues.chunksOpaqueReflection, ChunkMesh.RenderPhase.OPAQUE, playerCamera, WorldRendererImpl.ChunkRenderMode.REFLECTION);
chunkShader.deactivateFeature(ShaderProgramFeature.FEATURE_USE_FORWARD_LIGHTING);
}

playerCamera.setReflected(false);

GL11.glCullFace(GL11.GL_BACK);
bindDisplay();
OpenGLUtils.setViewportToSizeOf(sceneOpaque); // toWholeDisplay
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove "OpenGLUtils."

Regarding the //toWholeDisplay comment, indeed the original code had "setViewportToWholeDisplay();" Why did you change it?

Interestingly, this is one case in which a state change is used also later, outside of the render reflection step. In the following rendering steps, i.e. backdrop render and then the whole scene opaque, the viewport stays to what is set here. Again, state management will have to take care of this: each node should be independent of the others unless there is an explicit requirement (I'm thinking a node could have a setExecutionDependency(Node ancestor) method - or something like that), state management will then ensure that the GL state is changed only if necessary.

Copy link
Contributor Author

@tdgunes tdgunes Jun 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any elegant solution for storing dimensions (fullScale) without a class like GraphicState. Any ideas?



PerformanceMonitor.endActivity();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2015 MovingBlocks
* 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.
Expand Down Expand Up @@ -37,7 +37,7 @@
import org.terasology.rendering.cameras.Camera;
import org.terasology.rendering.nui.Color;
import org.terasology.rendering.nui.HorizontalAlign;
import org.terasology.rendering.opengl.OpenGLUtil;
import org.terasology.rendering.opengl.OpenGLUtils;
import org.terasology.world.WorldProvider;

import java.util.Arrays;
Expand Down Expand Up @@ -119,7 +119,7 @@ private void render(Iterable<EntityRef> floatingTextEntities) {
float scale = METER_PER_PIXEL * floatingText.scale;

glTranslated(worldPos.x - cameraPosition.x, worldPos.y - cameraPosition.y, worldPos.z - cameraPosition.z);
OpenGLUtil.applyBillboardOrientation();
OpenGLUtils.applyBillboardOrientation();
glScaled(scale, -scale, scale);
glTranslated(-textWidth / 2.0, 0.0, 0.0);
for (Map.Entry<Material, Mesh> meshMapEntry : meshMap.entrySet()) {
Expand Down