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.
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
}