Rendering a scene as a normal map - blending normal information

I want to create somewhat interactive water by rendering a scene as a normal map. My basic idea is this:

I have a regular color scene and a normal scene. In the normal scene, I render the water plane with a material similar to MeshNormalMaterial. Now I have things happening in the regular scene that create ripples in the water. For this I want to have sprites looking similar to this:


to disturb the normals in the normal scene.

Basically, instead of using regular alpha blending, I want to add up all the normal-disturbing sprites along the view rays and then normalize and use the result later inside a shader in the color scene. This should be order-independent so I shouldn’t have to struggle with the common alpha artifacts.

The difficulty is blending normal information, since it is encoded as xyz = 2*rgb-1 (black corresponds to (-1, -1, -1), grey corresponds to (0, 0, 0) and white corresponds to (1, 1, 1)). No custom blend equation can easily do this. However I figured if for every sprite I add, I also subtract a grey sprite at the same position, that should do the trick.

Howeverever, the custom blend equations don’t behave at all as I would expect. Here is what I have tried:

scene.background = new THREE.Color(0.5, 0.5, 0.5)
THREE.ColorManagement.enabled = false

let squareGeometry = new THREE.PlaneGeometry(10, 10);

let textureLoader = new THREE.TextureLoader();
let texture = textureLoader.load('gradient.png');
texture.colorSpace = THREE.SRGBColorSpace

let minusMaterial = new THREE.MeshBasicMaterial({ color: new THREE.Color(0.5, 0.5, 0.5) });
minusMaterial.blending = THREE.CustomBlending;
minusMaterial.blendEquation = THREE.SubtractEquation;
minusMaterial.blendSrc = THREE.OneFactor
minusMaterial.blendDst = THREE.OneFactor
minusMaterial.depthTest = false
minusMaterial.depthWrite = false
let minusSquare = new THREE.Mesh(squareGeometry, minusMaterial);
scene.add(minusSquare);
minusSquare.position.set(0, 0, -1);

let plusMaterial = new THREE.MeshBasicMaterial({ map: texture });
plusMaterial.blending = THREE.CustomBlending;
plusMaterial.blendEquation = THREE.AddEquation;
plusMaterial.blendSrc = THREE.OneFactor
plusMaterial.blendDst = THREE.OneFactor
let plusSquare = new THREE.Mesh(squareGeometry, plusMaterial);
plusMaterial.depthTest = false
plusMaterial.depthWrite = false
scene.add(plusSquare);

And here is the result:


I would expect that the minus square turns the gray background into exactly black (and not this dark grey) and that the plus square (which has a gradient texture) brightens it to be exactly a 0->1 gradient texture again. I also had to notice that the background-color css property of the canvas plays a big role which I would not expect since I should be drawing onto the scene.background, no?

To see what I mean, you can visit this site: three.js webgl - materials - custom blending
and enter this in the console:
document.body.getElementsByTagName(“canvas”)[0].style.backgroundColor=“red”

What’s going on here? Is there a better way to do this? I was thinking if I use float textures, I can also have negative “colors” stored in the ripple sprites which maybe simplifies things but that still depends on add-blending doing what I expect it to do…

Any help appreciated!

EDIT: Oh, is it because of the alpha channel also being dragged into the computation which also causes the canvas background to shine through in the end? That must be it! Gonna try to use separate equations for RGB and alpha tomorrow.
EDIT²: Setting transparent=true and opacity=0 for the minus square didn’t change a thing :confused:

I got it working!

scene.background = new THREE.Color(0x808080)
THREE.ColorManagement.enabled = true

let squareGeometry = new THREE.PlaneGeometry(10, 10);

let textureLoader = new THREE.TextureLoader();
let texture = textureLoader.load('gradient.png');
texture.colorSpace = THREE.SRGBColorSpace

let minusMaterial = new THREE.MeshBasicMaterial({ color:0x808080 });
minusMaterial.blending = THREE.CustomBlending;
minusMaterial.blendEquation = THREE.SubtractEquation;
minusMaterial.blendSrc = THREE.OneFactor
minusMaterial.blendDst = THREE.OneFactor
minusMaterial.blendEquationAlpha = THREE.AddEquation;
minusMaterial.depthTest = false
minusMaterial.depthWrite = false
minusMaterial.side = THREE.DoubleSide;
let minusSquare = new THREE.Mesh(squareGeometry, minusMaterial);
scene.add(minusSquare);
minusSquare.position.set(0, 0, -1);

let plusMaterial = new THREE.MeshBasicMaterial({ map: texture });
plusMaterial.blending = THREE.CustomBlending;
plusMaterial.blendEquation = THREE.AddEquation;
plusMaterial.blendSrc = THREE.OneFactor
plusMaterial.blendDst = THREE.OneFactor
let plusSquare = new THREE.Mesh(squareGeometry, plusMaterial);
plusMaterial.depthTest = false
plusMaterial.depthWrite = false
plusMaterial.side = THREE.DoubleSide;
scene.add(plusSquare);

1 Like

It’s actually working! :blush:
https://mqnc.github.io/cheapwater/

4 Likes

wow this is awesome!