Search code examples
javascriptreactjsreact-three-fiberreact-three-drei

Using ref on ShaderMaterial causes Typescript error: "The expected type comes from property 'ref' which is declared here on type 'PolygonMat'"


When creating a shaderMaterial from the drei library, using ref causes TypeScript to complain:

Type 'RefObject<PolygonMat>' is not assignable to type 'Ref<ShaderMaterial> | undefined'.

I declared the type according to how it was written in this storybook from the drei GitHub repo:

type PolygonMat = {
    uTime: number
} & JSX.IntrinsicElements['shaderMaterial']


declare global {
    namespace JSX {
        interface IntrinsicElements {
            polygonMaterial: PolygonMat,
            polyMat: PolyMat
        }
    }
}

And then used useFrame inside the component to update the delta time with useRef:

The error is then shown on the ref property of the polygonMaterial component.

function PolygonMesh() {
    const matRef = useRef<PolygonMat>(null)
    useFrame((state, delta) => {
        if (matRef.current) {
            matRef.current.uTime += delta / 1.5
        }
    })

    return (
        <mesh>
            <planeGeometry args={[18.0, 8.0, 18.0, 8.0]} />
            <polygonMaterial ref={matRef} wireframe />
        </mesh>
    )
}

Which works fine, besides TypeScript having problems with it.


Solution

  • I think the implementation of shaderMaterial could be improved to infer a generic type from the provided uniforms. I have a version of that running locally, as well a fork with an equivalent change - so my next port of call is to turn that into a PR. This seems to help accomplish what you were after.

    Using it, complete with TypeScript declaration, assigning to a ref and modifying property on said ref:

    
    const vertexShader = /*glsl*/`
    void main() {
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
    `;
    
    const fragmentShader = /*glsl*/`
    uniform float myOpacity;
    void main() {
        gl_FragColor = vec4(1.0, 0.0, 0.0, myOpacity);
    }
    `;
    
    const testShaderDefaultProps = {
        myOpacity: 0.5,
    };
    
    // referring to my modified version of shaderMaterial, which unions the type of 1st argument with existing return type...
    export const TestMaterial = shaderMaterial(testShaderDefaultProps, vertexShader, fragmentShader);
    
    // NB: for some reason, I need to do this in the place where I import TestMaterial, not sure why
    // - wasn't the case for a `class TestMaterial extends ShaderMaterial` version I experimented with
    extend({ TestMaterial });
    
    declare global {
        namespace JSX {
            interface IntrinsicElements {
                testMaterial: ReactThreeFiber.Node<typeof TestMaterial & JSX.IntrinsicElements['shaderMaterial'], typeof TestMaterial>
            }
        }
    }
    
    function PolygonMesh() {
      const matRef = useRef<typeof TestMaterial>(null);
      useFrame(() => {
        if (matRef.current) matRef.current.myOpacity = 0.5 + 0.5*Math.sin(0.01*Date.now());
      });
      return (
        <mesh>
          <boxGeometry />
          <testMaterial ref={matRef} />
        </mesh>
      )
    }
    

    So, the declare statement is different to what you had; ReactThreeFibre.Node<> and I'm not sure I am in a position right now to lucidly explain exactly the logic of all of that...

    But anyway, the change I made to shaderMaterial is this:

    // this is only used in one place, but seems cleaner to move it out here than inside the generic.
    type U = {
        [name: string]:
        | THREE.CubeTexture
        | THREE.Texture
        | Int32Array
        | Float32Array
        | THREE.Matrix4
        | THREE.Matrix3
        | THREE.Quaternion
        | THREE.Vector4
        | THREE.Vector3
        | THREE.Vector2
        | THREE.Color
        | number
        | boolean
        | Array<any>
        | null
    };
    
    export function shaderMaterial<T extends U>(
        uniforms: T,
        vertexShader: string,
        fragmentShader: string,
        onInit?: (material?: THREE.ShaderMaterial) => void
    ) {
        const material = class extends THREE.ShaderMaterial {
            public key: string = ''
            constructor(parameters = {}) {
                const entries = Object.entries(uniforms)
                // Create unforms and shaders
                super({
                    uniforms: entries.reduce((acc, [name, value]) => {
                        const uniform = THREE.UniformsUtils.clone({ [name]: { value } })
                        return {
                            ...acc,
                            ...uniform,
                        }
                    }, {}),
                    vertexShader,
                    fragmentShader,
                })
                // Create getter/setters
                entries.forEach(([name]) =>
                    Object.defineProperty(this, name, {
                        get: () => this.uniforms[name].value,
                        set: (v) => (this.uniforms[name].value = v),
                    })
                )
    
                // Assign parameters, this might include uniforms
                Object.assign(this, parameters)
                // Call onInit
                if (onInit) onInit(this)
            }
        } as unknown as typeof THREE.ShaderMaterial & { key: string } & T
        material.key = THREE.MathUtils.generateUUID()
        return material
    }