Feedback requested on FluidDepthFilter

Hi,

I wanted to improve the look of my fluid blocks. By default they are translucent and you don’t really get a feeling of ‘depth’.

I don’t use voxel lighting, so I created a FluidDepthFilter to simulate this effect, and I would like some feedback on it. I am not a shader/filter expert and some things are still a bit ‘magic’ to me.

Result:

In short, what I did:

  • create a filter that creates 2 depth textures and send them to the fragment shader. One depth texture of the fluid objects and one depth texture of the scene without the fluid objects.
  • in the shader calculate the depth difference between the scene and the fluid
  • mix the color of the fluid depending on the depth.

Scene depth texture without the fluid objects:

Scene depth texture of only the fluid objects:

(I inverted the colors to make the depth texture more obvious)

FluidDepthFilter:

public class FluidDepthFilter extends Filter {

        private RenderManager renderManager;
        private ViewPort viewPort;
        private Material material;
        private FrameBuffer fluidDepthBuffer;
        private FrameBuffer sceneDepthBuffer;
        private final GeometryList fluidGeometryList = new GeometryList(new TransparentComparator());

        @Override
        protected boolean isRequiresDepthTexture() {
            return false;
        }

        @Override
        protected boolean isRequiresSceneTexture() {
            return true;
        }

        @Override
        protected void initFilter(AssetManager manager, RenderManager renderManager, ViewPort vp, int w, int h) {
            material = new Material(manager, "Mats/FluidDepth.j3md");
            material.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
            material.setColor("FadeColor", new ColorRGBA().setAsSrgb(0.2f, 0.404f, 0.698f, 1.0f));
            material.setFloat("FadeDepth", 6.0f);

            fluidDepthBuffer = new FrameBuffer(w, h, 1);
            sceneDepthBuffer = new FrameBuffer(w, h, 1);
            Texture2D fluidDepthTexture = new Texture2D(w, h, 1, Image.Format.Depth);
            Texture2D sceneDepthTexture = new Texture2D(w, h, 1, Image.Format.Depth);
            fluidDepthBuffer.setDepthTexture(fluidDepthTexture);
            sceneDepthBuffer.setDepthTexture(sceneDepthTexture);

            material.setTexture("FluidDepthTexture", fluidDepthTexture);
            material.setTexture("SceneDepthTexture", sceneDepthTexture);

            this.renderManager = renderManager;
            this.viewPort = vp;
        }

        @Override
        protected Material getMaterial() {
            return material;
        }

        @Override
        protected void postQueue(RenderQueue queue) {
            Renderer renderer = renderManager.getRenderer();
            if (fluidGeometryList.size() > 0) {
                renderer.setFrameBuffer(fluidDepthBuffer);
                renderer.clearBuffers(true, true, true);
                renderManager.renderGeometryList(fluidGeometryList);
                renderer.setFrameBuffer(viewPort.getOutputFrameBuffer());
            }

            GeometryList filteredSceneGeometries = new GeometryList(new OpaqueComparator());
            for (Spatial scene : viewPort.getScenes()) {
                scene.depthFirstTraversal(new SceneGraphVisitorAdapter() {
                    @Override
                    public void visit(Geometry geom) {
                        if (!contains(geom, fluidGeometryList)) {
                            filteredSceneGeometries.add(geom);
                        }
                    }
                });
            }

            renderer.setFrameBuffer(sceneDepthBuffer);
            renderer.clearBuffers(true, true, true);
            renderManager.renderGeometryList(filteredSceneGeometries);
            renderer.setFrameBuffer(viewPort.getOutputFrameBuffer());
        }

        private static boolean contains(Geometry geometry, GeometryList geometryList) {
            for (Geometry g : geometryList) {
                if (g.equals(geometry)) {
                    return true;
                }
            }
            return false;
        }

        public void addFluidGeometry(Geometry waterGeometry) {
            fluidGeometryList.add(waterGeometry);
            fluidGeometryList.sort();
        }

    }

FluidDepth.j3md

MaterialDef WaterDepth Textured {

    MaterialParameters {
        // the depth when the FadeColor is fully visible
        Float FadeDepth

        // the color the fluid will blend to
        Vector4 FadeColor

        // samples for aa
        Int NumSamples

        // the rendered scene
        Texture2D Texture

        // the depth texture of the fluid geometries that fade
        Texture2D FluidDepthTexture

        // the depth texture of the scene without the geometries that fade
        Texture2D SceneDepthTexture
    }

    Technique {
    	VertexShader GLSL310 GLSL300 GLSL100 GLSL150:   Common/MatDefs/Post/Post.vert
    	FragmentShader GLSL310 GLSL300 GLSL100 GLSL150: Mats/FluidDepth.frag

    	WorldParameters {
    	    FrustumNearFar
    	}
    }

}

FluidDepth.frag

#import "Common/ShaderLib/GLSLCompat.glsllib"
#import "Common/ShaderLib/MultiSample.glsllib"

uniform vec4 m_FadeColor;
uniform float m_FadeDepth;
uniform sampler2D m_Texture;
uniform vec2 g_FrustumNearFar;
uniform sampler2D m_WaterDepthTexture;
uniform sampler2D m_SceneDepthTexture;

varying vec2 texCoord;

void main(){
    vec4 scene = getColor(m_Texture, texCoord);
    float depth = getDepth(m_SceneDepthTexture, texCoord).r;
    float waterDepth = getDepth(m_WaterDepthTexture, texCoord).r;

    // logic from DepthOfField.frag:
    // z_buffer_value = a + b / z;
    //
    // Where:
    //  a = zFar / ( zFar - zNear )
    //  b = zFar * zNear / ( zNear - zFar )
    //  z = distance from the eye to the object
    //
    // Which means:
    // zb - a = b / z;
    // z * (zb - a) = b
    // z = b / (zb - a)
    //
    float a = g_FrustumNearFar.y / (g_FrustumNearFar.y - g_FrustumNearFar.x);
    float b = g_FrustumNearFar.y * g_FrustumNearFar.x / (g_FrustumNearFar.x - g_FrustumNearFar.y);
    float distanceScene = b / (depth - a);
    float distanceWater = b / (waterDepth - a);

    float depthDifference = distanceScene - distanceWater;
    float depthMixFactor = clamp(depthDifference / m_FadeDepth, 0, 1.0);
    // depthMixFactor = 0 == surface
    // depthMixFactor = 1 == bottom (m_FadeDepth)

    if (distanceWater < distanceScene) {
        gl_FragColor = mix(scene, m_FadeColor, depthMixFactor);
    } else {
        gl_FragColor = scene;
    }

    //
    // DEBUG
    //

    // render depthMixFactor
    //gl_FragColor = vec4(depthMixFactor, depthMixFactor, depthMixFactor, 1.0);

    // render depth texture
    // the value is inverted to make it more obvious
    //gl_FragColor = vec4(1.0 - depth, 1.0 - depth, 1.0 - depth, 1.0);

    // render the water depth texture
    // the value is inverted to make it more obvious
    //gl_FragColor = vec4(1.0 - waterDepth, 1.0 - waterDepth, 1.0 - waterDepth, 1.0);

    // render the scene
    //gl_FragColor = scene;
}

Is my way of handling this correct? Am I doing any unnecessary processing? Any tips or tricks to improve or make it look better?

note: the code still needs to be cleaned up.

9 Likes

Pinging @RiccardoBlb as he is the shader guy here, in case he has not to seen this topic.

It seems fine to me.
The improvement would be to run this as material, not as filter. But you would need to render your fluid after the scene, where you can access the depth.

The reason I went for the filter approach is to ‘easily’ generate the depth textures and scene textures and pass them to the material.
I don’t have a clue how I should do it otherwise… How should I do this using only a material?

I added a ‘shoreline’ to easily see objects in the fluid:

And added a distortion effect, it worked out pretty well:

8 Likes

This is a neat effect.

If it were me, I’d have done it in a similar way: post processing. It’s not entirely dissimilar to what I did for drop shadows only in that case the post processing filter knew what geometry to render for the boxes and collected them from the shadow bucket.

…but the idea of rendering geometry in post and using the depth to decide color is very similar. Nice look.

1 Like

Thanks for the feedback, really appreciate it!
I’ll wrap it up and include it in the next release of Blocks.

1 Like