Recalculate normals after vertex transformation using TSL

Hi, so I’ve been making some grass using TSL and it looks great. Unfortunately the lighting is off, and I think it is due to the normals not being properly calculated after the vertex displacement. Here is my code for the grass:

import * as THREE from 'three/webgpu'
import * as TSL from 'three/tsl';

export class Grass extends THREE.InstancedMesh {
    constructor(model) {
        var geo, mat;

        model.traverse((e) => {
            if (e.geometry) {
                geo = e.geometry
            }
        })

        var positions = []

        const offset = 0.03
        const xCount = 5
        const yCount = 5
        var count = 0;
        var currentPos;

        for (var x = 0; x < xCount; x += offset) {
            for (var y = 0; y < yCount; y += offset) {
                currentPos = [x, y]
                currentPos[0] += THREE.MathUtils.randFloat(-offset / 2, offset / 2)
                currentPos[1] += THREE.MathUtils.randFloat(-offset / 2, offset / 2)
                positions.push(new THREE.Vector3(currentPos[0], 0, currentPos[1]))
                count++
            }
        }
        console.log(count)
        mat = new THREE.MeshStandardNodeMaterial({ side: THREE.DoubleSide, metalness: 1, roughness: 1 })
        var dummyModel = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshStandardMaterial())
        super(geo, mat, count)
        this.mat = mat
        this.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
        var instanceOffsets = new Float32Array(count * 3);
        for (var i = 0; i < count; i++) {
            dummyModel.position.copy(positions[i])
            //dummyModel.quaternion.copy(new THREE.Quaternion().setFromEuler(new THREE.Euler(0, THREE.MathUtils.randFloat(-Math.PI / 8, Math.PI / 8), 0)))
            dummyModel.scale.copy(new THREE.Vector3(0.25, 1, 0.25))
            dummyModel.updateMatrix()
            this.setMatrixAt(i, dummyModel.matrix)
            instanceOffsets.set(positions[i].toArray(), i * 3);
        }
        this.instanceOffsetAttribute = new THREE.InstancedBufferAttribute(instanceOffsets, 3);
        this.geometry.setAttribute('instanceOffset', this.instanceOffsetAttribute);
        this.needsUpdate = true

        var instanceOffsetNode = TSL.attribute('instanceOffset', 'vec3');
        this.time = TSL.uniform('float');


        var uv = TSL.uv().mul(-1).add(1)
        var invertedUv = TSL.uv()
        var normal = TSL.normalLocal // Also tried with normalLocal and normalView, but normalWorld seems to get the best results

        // BLADE BENDING

        var bendingFactor = 0.6

        var bendingAngle = uv.x.mul(bendingFactor);

        var bendingCosAngleX = TSL.cos(bendingAngle);
        var bendingSinAngleX = TSL.sin(bendingAngle);
        var bendingRotationMatrixX = TSL.mat3(
            bendingCosAngleX, bendingSinAngleX.mul(-1), 0,
            bendingSinAngleX, bendingCosAngleX, 0,
            0, 0, 1
        );
        var bendingX = bendingRotationMatrixX.mul(TSL.positionLocal)
        normal = bendingRotationMatrixX.mul(normal)

        var randomAngleFactor = 1

        var randomAngleMap = TSL.texture(
            new THREE.TextureLoader().load("noiseTexture4.png"),
            TSL.fract(instanceOffsetNode.xz)
        )

        var randomAngle = TSL.remap(randomAngleMap.x, 0, 1, 0, 2 * Math.PI).mul(randomAngleFactor)

        var bendingCosAngleY = TSL.cos(randomAngle);
        var bendingSinAngleY = TSL.sin(randomAngle);
        var bendingRotationMatrixY = TSL.mat3(
            bendingCosAngleY, 0, bendingSinAngleY,
            0, 1, 0,
            bendingSinAngleY.mul(-1), 0, bendingCosAngleY
        );

        var bendingY = bendingRotationMatrixY.mul(bendingX)
        normal = bendingRotationMatrixY.mul(normal)

        // GENERAL WIND BENDING

        var generalWindTextureScalingFactor = 1
        var generalWindFactor = 1.75
        var generalWindSpeed = 2;

        var generalWind = TSL.texture(
            new THREE.TextureLoader().load("noiseTexture.png"),
            TSL.fract(instanceOffsetNode.xz.mul(generalWindTextureScalingFactor).add(this.time.mul(generalWindSpeed)).mul(1 / 256))
        )

        var generalWindAngle = uv.x.mul(generalWind.r).mul(generalWindFactor)

        var generalWindCosAngle = TSL.cos(generalWindAngle);
        var generalWindSinAngle = TSL.sin(generalWindAngle);
        var generalWindRotationMatrix = TSL.mat3(
            generalWindCosAngle, generalWindSinAngle.mul(-1), 0,
            generalWindSinAngle, generalWindCosAngle, 0,
            0, 0, 1
        );

        var generalWindBending = generalWindRotationMatrix.mul(bendingY)
        normal = generalWindRotationMatrix.mul(normal)

        // DIRECTIONAL WIND BENDING

        var directionalWindTextureScalingFactor = 1
        var directionaWindStrenghtFactor = 0.8
        var directionalWindSpeed = 6

        var directionalWindAngleMap = TSL.texture(
            new THREE.TextureLoader().load("noiseTexture3.png"),
            TSL.fract(instanceOffsetNode.xz.mul(directionalWindTextureScalingFactor).add(this.time.mul(directionalWindSpeed).add(0.1)).mul(1 / 256))
        );

        var directionalWindAngle = TSL.remap(directionalWindAngleMap.r, 0, 1, 0, 2 * Math.PI)

        var directionalWindCosAngleY = TSL.cos(directionalWindAngle);
        var directionalWindSinAngleY = TSL.sin(directionalWindAngle);

        var directionalWindRotationMatrixY = TSL.mat3(
            directionalWindCosAngleY, 0, directionalWindSinAngleY,
            0, 1, 0,
            directionalWindSinAngleY.mul(-1), 0, directionalWindCosAngleY
        );

        var directionalWindStrenghtMap = TSL.texture(
            new THREE.TextureLoader().load("noiseTexture3.png"),
            TSL.fract(instanceOffsetNode.xz.mul(directionalWindTextureScalingFactor).add(this.time.mul(directionalWindSpeed).add(0.3)).mul(1 / 256))
        );

        var directionalWindStrenght = directionalWindStrenghtMap.r.mul(directionaWindStrenghtFactor).mul(-1)

        var directionalWindCosAngleX = TSL.cos(directionalWindStrenght);
        var directionalWindSinAngleX = TSL.sin(directionalWindStrenght);

        var directionalWindRotationMatrixX = TSL.mat3(
            directionalWindCosAngleX, directionalWindSinAngleX.mul(-1), 0,
            directionalWindSinAngleX, directionalWindCosAngleX, 0,
            0, 0, 1
        );

        var directionalWindBendingY = directionalWindRotationMatrixY.mul(generalWindBending)
        normal = directionalWindRotationMatrixY.mul(normal)

        var directionalWindBendingX = directionalWindRotationMatrixX.mul(directionalWindBendingY)
        normal = directionalWindRotationMatrixX.mul(normal)

        mat.positionNode = directionalWindBendingX

        mat.normalNode = TSL.normalize(normal)

        mat.colorNode = TSL.mix(TSL.vec3(0.05, 0.2, 0.01), TSL.vec3(0.5, 0.5, 0.1), TSL.uv().x.mul(-1).add(1))
    }

    update(elapsed) {
        this.time.value = elapsed
    }

}

I can’t see to understand how to recalculate them, I have also tried using dFdx and dFdy to get the tangent and bitangent and them getting the cross product but it still didn’t work. If you know a cleaner solution let me know. Btw if you want you can suggest any modification to the code that you think will make the end resolut better or simpler.

Thank you very much and happy Christmas!

If you’re doing an arbitrary deformation of the vertices, I can’t think of a way to do it other than dFdx and dFdy, but I don’t know how to do this within the context of TSL, only in glsl frag shaders.

Could you write it in glsl, I am going to convert it and post the solution later. Thanks

Oh i just looked through your code and I think… you’re on the right track with trying to transform the normal through the same rotations as the vertices. using fdx/fdy will get you flat shaded normals. I’m not super familiar with TSL, but your code looks plausible…

For those who might be interested in what this patch of grass could possibly look like, here is one vision of it with slightly changed code and MeshBasicNodeMaterials. This HTML file is standalone.

WebGPU Grass.zip (2.8 KB)

1 Like

Thanks, it’s not that good of a simulation though, it was a first time approach. I am now making it much better (I hope). Tomorrow I’ll post it will I finish in time. Thanks again