Shadertoy着色器移植到Three.js经验总结

Shadertoy是一个流行的在线平台,用于创建和分享WebGL片段着色器。里面有很多令人惊叹的画面,甚至3D场景。本人也移植了几个ShaderToy上的着色器。本文将详细介绍移植过程中需要注意的关键点。

1. 基本结构差异

想要移植ShaderToy的shader到three.js,需要先了解他们直接的差异。他们之间的差异主要在以下几个方面:

  1. ShaderToy没有顶点着色器 ShaderTory上面的shader只有片元着色器,没有几何,移植到three.js上需要构造一个默认的顶点着色器,同时提供几何信息。这个也比较简单,使用两个三角形铺面整个视口即可。其实用其他的方式也可以,例如用一个大的三角形,只要有个物体几何能充满整个视口即可。

  2. ShaderToy提供了很多内置变量 ShaderToy提供了不少内置变量,,在three.js中需要咱们自己用uniform来传入。ShaderToy内置变量处理方法如下:

全局变量 含义 three.js修改方案核心代码
uniform vec3 iResolution; 视口大小,单位像素 uniforms.iResolution = { value: new THREE.Vector2(options.width, options.hieght) }
uniform float iTime; 从开始运行到现在的时间,单位秒 var clock = new THREE.Clock(); this.uniforms.iTime.value = this.clock.getElapsedTime();
uniform float iTimeDelta; 上一帧渲染到现在的时间,单位秒 var clock = new THREE.Clock(); this.uniforms.iTimeDelta.value = this.clock.getDelta();
uniform int iFrame; 第几帧 this.uniforms.iFrame.value = this.uniforms.iFrame.value + 1;
uniform float iChannelTime[4]; / /
uniform vec3 iChannelResolution[4]; 每个通道的分辨率,单位像素 uniforms["iChannelResolution"] = { type: "v3v", value: [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), ] };
uniform vec4 iMouse; 是vec4类型,xy是鼠标当前位置与单击位置,zw是单击位置 document.addEventListener('mousemove', (e) => { material.uniforms.iMouse.value.x = e.clientX; material.uniforms.iMouse.value.y = window.innerHeight - e.clientY; }); document.addEventListener('mousedown', (e) => { material.uniforms.iMouse.value.z = e.clientX; material.uniforms.iMouse.value.w = window.innerHeight - e.clientY; });
uniform samplerXX iChanneli; 通道的纹理 uniforms["iChannel" + i] = { type: "t", value: texture }
  1. ShaderToy语法的差异

ShaderToy语法核心都是 GLSL,只是在在变量命名、输入输出方式、内置函数/变量等方面存在区别。以下是关键语法差异的对比:

ShaderToy three.js
入口函数是:mainImage(out vec4 fragColor, in vec2 fragCoord) 入口函数是:void main()
fragCoord当前像素的坐标 用的是gl_FragCoord
用变量fragColor作为输出 通过 gl_FragColor 变量输出
不需要定义变量精度 需定义精度:precision highp float; precision highp int;precision highp sampler2D;precision highp samplerCube;

2. 移植基本模版

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Shadertoy to Three.js</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }

        canvas {
            display: block;
        }
    </style>
</head>

<body>
    <script src="./three.min.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        console.log(renderer.capabilities.isWebGL2)

        const geometry = new THREE.BufferGeometry();
        geometry.setAttribute("position", new THREE.Float32BufferAttribute([-1, -1, 0,
            1, -1, 0,
            1, 1, 0,
        -1, 1, 0], 3))
        geometry.setIndex([0, 1, 2, 0, 2, 3]);

        const material = new THREE.RawShaderMaterial({
            uniforms: {
                iResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
                iTime: { value: 0 },
                mouse: { value: new THREE.Vector4(0, 0, 0, 0) },
            },
            // glslVersion: THREE.GLSL3,
            vertexShader: `#version 300 es
                in vec3 position;
                void main() {
                    gl_Position = vec4(position, 1.0);
                }
            `,
            fragmentShader: `#version 300 es
precision highp float;
precision highp int;
precision highp sampler2D;
precision highp samplerCube;
out vec4 fragColor;

uniform vec2 iResolution;
uniform float iTime;

void main(  )
{
     vec2 uv = gl_FragCoord.xy/iResolution.xy;// [0,1]
    
    uv -= 0.5;
    uv.x *= iResolution.x / iResolution.y;
    
    float d = length(uv);
    float r = 0.3;
    float c = smoothstep(r, r + 0.002, d);

    fragColor = vec4(vec3(c), 1.);
}
`
        });
        const plane = new THREE.Mesh(geometry, material);
        scene.add(plane);

        window.addEventListener('resize', () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
            material.uniforms.iResolution.value.set(window.innerWidth, window.innerHeight);
        });

        document.addEventListener('mousemove', (e) => {
            material.uniforms.iMouse.value.x = e.clientX;
            material.uniforms.iMouse.value.y = window.innerHeight - e.clientY;
        });
        document.addEventListener('mousedown', (e) => {
            material.uniforms.iMouse.value.z = e.clientX;
            material.uniforms.iMouse.value.w = window.innerHeight - e.clientY;
        });

        function animate() {
            requestAnimationFrame(animate);
            material.uniforms.iTime.value = performance.now() / 1000; // 秒为单位
            renderer.render(scene, camera);
        }
        animate();
    </script>
</body>

</html>

这个模版是画圆的实例,效果如下:

有以下几点需要注意:

  1. 两个三角形面片的坐标已经在[-1, 1]之间,不需要在VertexShader进行坐标变换,所有顶点着色器代码没有MVP矩阵:
plain 复制代码
#version 300 es
in vec3 position;
void main() {
    gl_Position = vec4(position, 1.0);
}
  1. 使用了RawShaderMaterial而不是ShaderMaterial,这样可对Shader代码进行完全掌控
  2. 语法上使用了WebGL2.0的语法,如果使用1.0的语法,需去掉版本声明、一些关键字

最后展示一个稍微复杂点的Shader效果:

结语

将Shadertoy着色器移植到Three.js需要对两者之间的差异有清晰理解。正确处理全局变量、语法、渲染管线等关方面的差异,可将绝大多数Shadertoy效果成功移植到Three.js项目中。

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax