移植ShaderToy着色器到React-Vite项目遇编译错误求排查
问题排查与解决思路
核心错误修正
1. 修复iChannel0的赋值错误
你错误地将片段着色器字符串直接赋值给了sampler2D类型的iChannel0 uniform,这完全不符合Three.js对纹理uniform的要求。ShaderToy中的Buffer A是离屏渲染目标,需要先将Buffer_A_Frag渲染到一个纹理上,再把这个纹理传给主着色器的iChannel0。
在React Three Fiber中,可借助@react-three/drei的RenderTarget组件实现离屏渲染:
# 先安装依赖 npm install @react-three/drei
修改代码创建Buffer渲染目标:
import { RenderTarget } from '@react-three/drei'; // 在OceanShaderMesh组件内部 const bufferTarget = useRenderTarget({ width: size.width, height: size.height }); // 渲染Buffer A到目标纹理 <RenderTarget target={bufferTarget}> <mesh> <planeGeometry args={[2, 2]} /> <shaderMaterial uniforms={{ iTime: { value: clock.getElapsedTime() }, iResolution: { value: new THREE.Vector3(size.width, size.height, 1) } }} fragmentShader={Buffer_A_Frag} vertexShader={vertexShader} /> </mesh> </RenderTarget>
随后将主着色器的iChannel0值改为bufferTarget.texture。
2. 修正useLoader的使用位置
useLoader是React Hook,不能在useMemo内部调用,必须在组件顶层执行:
const OceanShaderMesh = () => { // 顶层调用useLoader const buffer1Tex = useLoader(THREE.TextureLoader, "buffer1.png"); const noiseTex = useLoader(THREE.TextureLoader, "noise.png"); // ...其他代码 }
3. 修正uniforms的依赖与类型
- ShaderToy中
iResolution是vec3类型,你误用了THREE.Vector2,改为THREE.Vector3(size.width, size.height, 1) useMemo的依赖数组需包含size、buffer1Tex、noiseTex,否则uniforms无法正确更新:
const uniforms = useMemo(() => ({ iTime: { value: 0 }, iResolution: { value: new THREE.Vector3(size.width, size.height, 1) }, iChannel0: { value: bufferTarget.texture }, iChannel1: { value: buffer1Tex }, iChannel2: { value: noiseTex }, }), [size, bufferTarget.texture, buffer1Tex, noiseTex]);
BufferShader概念澄清
ShaderToy的Buffer通道本质是离屏渲染缓冲区:
- 每个Buffer(如Buffer A)是独立着色器,先渲染到一个纹理上
- 主着色器可将该纹理作为
iChannel0等采样器输入,实现多阶段渲染 - 在React Three Fiber中,对应实现是用
RenderTarget创建离屏纹理,先渲染Buffer着色器到该纹理,再将纹理传入主着色器。
其他优化点
- 移除
gl.setSize(size.width, size.height),React Three Fiber会自动处理画布尺寸 - 正交相机的
zoom设置为1即可,平面已适配屏幕尺寸 - 着色器中100次循环可能性能过低,可适当减少到30-50次,或用
#define常量控制
完整修正后的核心代码片段
import { Fragment, useRef, useMemo } from "react"; import * as THREE from "three"; import { Canvas, useFrame, useLoader, useThree, useRenderTarget } from "@react-three/fiber"; import { RenderTarget } from "@react-three/drei"; // ... vertexShader、Buffer_A_Frag、fragmentShader 保持不变 const OceanShaderMesh = () => { const materialRef = useRef(null); const { size, gl } = useThree(); const buffer1Tex = useLoader(THREE.TextureLoader, "buffer1.png"); const noiseTex = useLoader(THREE.TextureLoader, "noise.png"); const bufferTarget = useRenderTarget({ width: size.width, height: size.height, dpr: 1 }); gl.setPixelRatio(1); useFrame(({ clock }) => { if (materialRef.current) { materialRef.current.uniforms.iTime.value = clock.getElapsedTime(); materialRef.current.uniforms.iResolution.value.set(size.width, size.height, 1); } }); const uniforms = useMemo(() => ({ iTime: { value: 0 }, iResolution: { value: new THREE.Vector3(size.width, size.height, 1) }, iChannel0: { value: bufferTarget.texture }, iChannel1: { value: buffer1Tex }, iChannel2: { value: noiseTex }, }), [size, bufferTarget.texture, buffer1Tex, noiseTex]); return ( <> {/* 离屏渲染Buffer A */} <RenderTarget target={bufferTarget}> <mesh> <planeGeometry args={[2, 2]} /> <shaderMaterial uniforms={{ iTime: { value: 0 }, iResolution: { value: new THREE.Vector3(size.width, size.height, 1) } }} vertexShader={vertexShader} fragmentShader={Buffer_A_Frag} /> </mesh> </RenderTarget> {/* 主渲染mesh */} <mesh scale={[size.width / size.height, 1, 1]}> <planeGeometry args={[2, 2]} /> <shaderMaterial ref={materialRef} uniforms={uniforms} vertexShader={vertexShader} fragmentShader={fragmentShader} /> </mesh> </> ); }; // ... Background组件保持不变
内容的提问来源于stack exchange,提问作者Eric Ryan
相关产品推荐
相关产品推荐

