1

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.

bluhtea
  • 13
  • 4
  • The link to the storybook doesn't seem to mention types now, and this is about the best reference I think I've found so far as to how this is supposed to be typed. Sorry that you've had such a lacklustre SO experience, but if you do know the answer, please add it. – PeterT Jul 29 '23 at 10:10
  • Well it used to have some reference on how to type it, but i looked it up and also cant find it anymore. And no, i still have no solution to this problem. – bluhtea Jul 29 '23 at 22:40
  • I actually realised I had some old code where I'd partially typed it, but then left one part of a generic as `any` because I wanted to move on... but I think I may have the right thing now, just need to check... – PeterT Jul 31 '23 at 14:18
  • Nope, I'm not very close to finding the solution after all. Finding this fairly irritating. – PeterT Aug 05 '23 at 15:57

1 Answers1

0

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
}
PeterT
  • 1,454
  • 1
  • 12
  • 22
  • Wow, that seems to have finally worked! One small thing i had to change in shaderMaterial.d.ts though was to replace `& {key: string}` with `& {[key: string]: any}`. As for your changes to shaderMaterial, I still need to take a closer look at what exactly is going on there - but I will certainly do that. Thanks a lot for your help! – bluhtea Aug 14 '23 at 13:10
  • @bluhtea... hmm, not sure I like the sound of that `{[key: string]: any}`... that part was unchanged from the original drei code I think, and the `key` should indeed be a string, not an indexer to any... so there may be something not quite lined up right in the way you did something. – PeterT Aug 15 '23 at 13:08
  • I guess that you likely found yourself needing to change to `{[key: string]: any}` because you were trying to use some property that wasn't present on `T` - my code changes it so that it infers the type of the uniforms you provide and makes those specific properties available (whatever you pass in as 'uniforms' will become `T`, which is added to the type). Your version will allow you to use any string as a property name, as any type of value... – PeterT Aug 15 '23 at 13:19
  • Slight niggle compared to other `R3F` stuff with my version is that the stuff to do with using `[number, number]` as a `Vector2` etc might sometimes need some extra nudges for the ts compiler... for example, to get it to actually build the repo properly, I had to add this `as` in one example: `resolution: new THREE.Vector2() as THREE.Vector2 | [number, number]`, otherwise it thinks that `resolution` must be a `Vector2`, and if you try to say `` it gets upset. Could probably be handled better. Dunno if they'll look at the PR... – PeterT Aug 15 '23 at 13:27