ShaderToy着色器移植到Three.js全过程记录

news2024/11/15 18:05:12

在这里插入图片描述

推荐:用 NSDT设计器 快速搭建可编程3D场景。

作为 Publicis Pixelpark Innovationlab 研究的一部分,我们研究了如何将较低底层的语言用于网络技术。 显而易见的选择似乎是 asm.js 或 WebAssembly。

但你也可以使用 WebGL 着色器来解决面向机器的问题。 着色器使用类似于 C/C++ 的语言进行编程,虽然它们主要不是为了解决一般问题,但它们的用途不仅仅是渲染图像和 3D 场景。

第二个动机源于使用着色器可以实现的美观性。 2002 年,威斯康星大学麦迪逊分校的一群学生发布了 NPRQuake(“Non-PhotoRealistic Rendering Quake”),这是著名游戏 Quake 的变体,通过将代码注入到渲染管道中。
在这里插入图片描述

这种变化的美学品质令人惊叹。 我们立即意识到这些效果可能会改变项目的游戏规则。 在 2002 年,这种变化只能通过编写 OpenGL 驱动程序来实现,而现在在 2018 年可以通过着色器来实现——甚至在 Web 浏览器中也是如此。

因此,当我们最近参与一个艺术项目时,我们决定尝试一下着色器。

1、着色器代码的可用性

如果你不太习惯对着色器进行编程,那么显而易见的选择是搜索免费提供的示例并使用它们(仔细查看所涉及的许可证)。 在这方面脱颖而出的一个库是 Shadertoy,另一个例子是 ShaderFrog。

在我们决定在 ThreeJS 中使用 Shadertoy 的后处理着色器发布我们的发现之前,我们已经成功地使用 ThreeJS 了。

2、ThreeJS 中的着色器

ThreeJS 可用于利用后处理着色器(可改变整个渲染图像)以及材质着色器(可改变 3D 对象的材质)。 两种类型都需要顶点和片段着色器部分; 顶点着色器可以更改 3D 中顶点的位置,而片段着色器通常会替换渲染图像的颜色。

该图显示了四种可能的变化。
在这里插入图片描述

在左上角,后处理着色器向渲染图像添加颜色渐变。 在它的右侧,顶点着色器减少了渲染区域。 底部的两个图像显示材质着色器; 左边的仅改变颜色,右边的改变顶点的位置。 由于着色器始终由顶点部分和片段部分组成,因此最后一个示例也会更改颜色。

3、Shadertoy 的简单示例

早在 2014 年,我们就研究了如何将着色器从 Shadertoy 转移到 ThreeJS,第一个结果发布在 StackOverflow 上。 我们发现以下模式很有用:

  • 添加 ShaderToy 特定变量,如 iGlobalTime 等。
  • 将 mainImage(out vec4 z, in vec2 w) 重命名为 main()
  • 将 z 重命名为 gl_FragColor

通过遵循此模式,可以将简单的着色器传输到 ThreeJS。
在这里插入图片描述

4、来自 Shadertoy 的复杂示例

对于更复杂的着色器,你需要做更多的事情,正如我们现在将概述的那样。 作为一个重要的示例,我们选择了 candycat 的 Noise Contour,因为会遇到一些问题。 可以在这里找到它。

此示例还使用着色器语言创建整个场景。 但在 ThreeJS 中,你通常希望控制 3D 对象,因此我们决定在 ThreeJS 中创建场景,同时仍然利用着色器来更改它。

5、了解着色器的结构

我们首先尝试了解着色器的结构; 这可以通过 Shadertoy 的编辑器来实现。 由于可以实时看到对代码的编辑,因此我们可以进行一些小的更改来了解它的工作原理。
在这里插入图片描述

在实际代码下面,我们看到该代码基于名为 iChannel0 的通道,其中 B 表示缓冲区。
在这里插入图片描述

要查看此缓冲区的实际效果,我们注释掉第 37 行并添加以下内容:

// fragColor = mix(EdgeColor, mCol, edge);
fragColor = texture(iChannel0, uv);

结果应该是:
在这里插入图片描述

这个简单的更改会导致显示前一个缓冲区的颜色,而不是该缓冲区的结果。

通过检查前一个缓冲区(Buf B),我们发现这个缓冲区也使用 iChannel0,因此我们仍然没有查看原始场景创建代码。
在这里插入图片描述

使用与之前相同的技巧,我们注释掉第 29 行并添加一行计算 uv 和实际颜色,如下所示:

// fragColor = vec4(edge, sample0.w, 1.0, 1.0);
fragColor = texture(iChannel0, fragCoord / iResolution.xy);

这应该给我们留下:
在这里插入图片描述

这看起来更像是一个普通的场景。 此外,Buf A 不使用其他缓冲区,因此我们正在查看原始场景创建代码。
在这里插入图片描述

6、在ThreeJS 中重构

这里完全免责声明:接下来的代码绝不是“最佳”代码,而只是以最直接的方式解决问题的一种方法。

6.1 创建场景

我们首先创建一个稍微简单的场景,只有一个球体和一个平面。 此外,我们想使用 ThreeJS 中的 MeshNormalMaterial。

此处显示了可能的结果:
在这里插入图片描述

该代码包含在名为 index.html 的 HTML 文件中:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ThreeJS Shader Experiment 1 - Step 0</title>
<style>
    html,
    body {
        height: 100%;
        width: 100%;
        margin: 0;
        padding: 0;
        overflow: hidden;
    }
</style>
</head>
<body>
    <!-- https://raw.githubusercontent.com/mrdoob/three.js/dev/build/three.min.js -->
    <script src="three.min.js"></script>
    <!-- https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/controls/OrbitControls.js -->
    <script src="OrbitControls.js"></script>
    <!-- https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/shaders/CopyShader.js -->
    <script src="CopyShader.js"></script>
    <!-- https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/postprocessing/EffectComposer.js -->
    <script src="EffectComposer.js"></script>
    <!-- https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/postprocessing/ShaderPass.js -->
    <script src="ShaderPass.js"></script>
    <!-- https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/js/postprocessing/RenderPass.js -->
    <script src="RenderPass.js"></script>
    <script src="index.js"></script>
</body>
</html>

我们需要处理对 ThreeJS 库的依赖关系,并且还在 index.js 中添加我们自己的代码:

const container = document.body;
const FOV = 45;
const NEAR = 0.1;
const FAR = 1000;
let height = container.clientHeight;
let width = container.clientWidth;
const ASPECT = width / height;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio || 1);
renderer.setClearColor(0x000000);

const canvas = renderer.domElement;

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(FOV, ASPECT, NEAR, FAR);
camera.position.set(-2, 2, 2);
camera.target = new THREE.Vector3(0, 0, 0);

const controls = new THREE.OrbitControls(camera, canvas);

const matNormal = new THREE.MeshNormalMaterial();

const floorGeo = new THREE.PlaneBufferGeometry(2.0, 2.0);
const floor = new THREE.Mesh(floorGeo, matNormal);
floor.position.set(0, -0.5, 0);
floor.rotation.x = -((Math.PI * 90) / 180);

const sphereGeo = new THREE.SphereBufferGeometry(0.5, 32, 32);
const sphere = new THREE.Mesh(sphereGeo, matNormal);

scene.add(floor);
scene.add(sphere);
scene.add(camera);

const resize = (width, height) => {
    camera.aspect = width / height;
    camera.updateProjectionMatrix();

    renderer.setSize(width, height);
};

const render = () => {
    const tmpHeight = container.clientHeight;
    const tmpWidth = container.clientWidth;
    if (tmpHeight !== height || tmpWidth !== width) {
        height = tmpHeight;
        width = tmpWidth;
        resize(width, height);
    }

    controls.update();

    renderer.render(scene, camera);

    requestAnimationFrame(render);
};



container.appendChild(canvas);
resize(width, height);
render();

这段 JavaScript 代码创建了一个渲染器、一个相机、一个轨道控件以及带有 MeshNormalMaterial 的平面和球体。 它还负责窗口大小的更改和渲染。

从 Shadertoy 移植场景的第 0 步到此结束。

在这里插入图片描述

6.2 重新创建第一个着色器通道

在下一步中,我们尝试在缓冲区中重新创建第一个着色器渲染步骤; 这基本上是将着色器代码复制到 ThreeJS。

这应该是结果:
在这里插入图片描述

为了实现这一目标,我们使用了 ThreeJS 的 EffectComposer,它提供了一种使用后处理着色器的简单方法。

// ...
scene.add(sphere);
scene.add(camera);

const drawShader = {
uniforms: {
tDiffuse: { type: 't', value: null },
},
vertexShader: VERTEX,
fragmentShader: FRAGMENT,
};

const composer = new THREE.EffectComposer(renderer);
composer.addPass(new THREE.RenderPass(scene, camera));

const pass = new THREE.ShaderPass(drawShader);
pass.renderToScreen = true;
composer.addPass(pass);


const resize = (width, height) => {
    camera.aspect = width / height;
// ...

这将创建一个 EffectComposer 实例,其中添加一个普通渲染通道和一个附加着色器通道。 我们将着色器代码复制到变量 VERTEX 和 FRAGMENT 中。 着色器定义还定义了 EffectComposer 使用的称为 tDiffuse 的 Uniform。 它包含来自上一个渲染通道的图像,该图像将在当前通道中更改。

通过这个新的渲染步骤,我们将显示此通道而不是原始场景。 因此我们需要添加一些代码来调整大小,因此我们添加:

const resize = (width, height) => {
camera.aspect = width / height;
camera.updateProjectionMatrix();

composer.setSize(width, height);

renderer.setSize(width, height);
};


const render = () => {
const tmpHeight = container.clientHeight;
const tmpWidth = container.clientWidth;
if (tmpHeight !== height || tmpWidth !== width) {
height = tmpHeight;
width = tmpWidth;
resize(width, height);
}

controls.update();

// renderer.render(scene, camera);
composer.render();

requestAnimationFrame(render);
};

现在我们需要定义常量 VERTEX 和 FRAGMENT。 我们不能使用Shadertoy的顶点着色器,所以我们需要定义自己的:

const VERTEX = `
varying vec2 vUv;
void main() {
    vec4 mvPosition = modelViewMatrix * vec4(position, 1.);
    gl_Position = projectionMatrix * mvPosition;
    vUv = uv;
}

不过,我们确实使用了 Shadertoy 中的片段着色器,并将其添加到 FRAGMENT 中:

const FRAGMENT = `
// Edge detection Pass
#define Sensitivity (vec2(0.3, 1.5) * iResolution.y / 400.0)
float checkSame(vec4 center, vec4 samplef)
{
    vec2 centerNormal = center.xy;
    float centerDepth = center.z;
    vec2 sampleNormal = samplef.xy;
    float sampleDepth = samplef.z;
    vec2 diffNormal = abs(centerNormal - sampleNormal) * Sensitivity.x;
    bool isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
    float diffDepth = abs(centerDepth - sampleDepth) * Sensitivity.y;
    bool isSameDepth = diffDepth < 0.1;
    return (isSameNormal && isSameDepth) ? 1.0 : 0.0;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    vec4 sample0 = texture(iChannel0, fragCoord / iResolution.xy);
    vec4 sample1 = texture(iChannel0, (fragCoord + vec2(1.0, 1.0)) / iResolution.xy);
    vec4 sample2 = texture(iChannel0, (fragCoord + vec2(-1.0, -1.0)) / iResolution.xy);
    vec4 sample3 = texture(iChannel0, (fragCoord + vec2(-1.0, 1.0)) / iResolution.xy);
    vec4 sample4 = texture(iChannel0, (fragCoord + vec2(1.0, -1.0)) / iResolution.xy);
    float edge = checkSame(sample1, sample2) * checkSame(sample3, sample4);
    fragColor = vec4(edge, sample0.w, 1.0, 1.0);
}
`;

这基本上创建了着色器,但我们仍然需要解决以下问题:

顶点着色器坐标尚未在片段着色器中使用

  • 片段着色器使用当前 WebGL 上下文中未知的纹理
  • mainImage 必须重命名为 main
  • iResolution 尚未设置。

所以着色器还没有工作。

解决第一个问题会产生以下定义:

const FRAGMENT = `
// Edge detection Pass
varying vec2 vUv;
// ...

现在我们可以使用向量 vUv 代替 fragCoord / iResolution.xy。 这导致:

// ...
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec4 sample0 = texture(tDiffuse, vUv);
vec4 sample1 = texture(tDiffuse, vUv + (vec2(1.0, 1.0) / iResolution.xy));
vec4 sample2 = texture(tDiffuse, vUv + (vec2(-1.0, -1.0) / iResolution.xy));
vec4 sample3 = texture(tDiffuse, vUv + (vec2(-1.0, 1.0) / iResolution.xy));
vec4 sample4 = texture(tDiffuse, vUv + (vec2(1.0, -1.0) / iResolution.xy));
// ...

现在我们只需用texture2D 替换所有出现的纹理。

另外,我们将 mainImage 更改为不带参数的 main:

// void mainImage( out vec4 fragColor, in vec2 fragCoord ) {

void main () {
// ...

main 还应该返回 gl_FragColor 而不是 fragColor,它定义了着色器中的颜色。

void main () {
// ...
gl_FragColor = vec4(edge, sample0.w, 1.0, 1.0);
}

最后,我们需要通过将 iResolution 添加到uniforms来设置它。 我们通过定义一个存储宽度和高度的 ThreeJS 向量来做到这一点:

const resolution = new THREE.Vector2(width, height);

现在我们可以将分辨率添加到uniforms中:

const drawShader = {
    uniforms: {
        tDiffuse: { type: 't', value: null },
        iResolution: { type: 'v2', value: resolution },
    },
    vertexShader: VERTEX,
    fragmentShader: FRAGMENT,
};

我们需要增强调整大小功能:

const resize = () => {
// ...
pass.uniforms.iResolution.value.set(width, height);
};

重要的是我们使用实际渲染通道的uniforms。 原版已经被EffectComposer深度克隆; 更改变量分辨率不会产生任何影响。

由于我们确实定义了两个uniform,因此我们需要将它们引入片段着色器,因此我们定义它们:

const FRAGMENT = `
uniform sampler2D tDiffuse;
uniform vec2 iResolution;
// ...

这个着色器通道到此结束,如果一切顺利,我们会看到以下内容:
在这里插入图片描述

从蓝线我们看到它通常可以工作,但粉红色的部分仍然缺失。 让我们改变这一点。

6.3 解决阴影问题

粉色部分缺失,因为 Shadertoy 中的着色器秘密地将阴影渲染到一开始不可见的 Alpha 通道,如下图所示:

在这里插入图片描述

有多种方法可以解决这个问题 - 我们使用直接的方法,添加一种可以容纳阴影的材质。 这些必须在额外的渲染通道中处理。

让我们在 ThreeJS 中创建阴影:

// ...
renderer.shadowMap.enabled = true;
renderer.shadowMap.renderReverseSided = false;

// ...
floor.receiveShadow = true;

// ...
sphere.castShadow = true;
sphere.receiveShadow = true;

阴影需要光,在本例中是定向光:

const SHADOW_MAP_SIZE = 1024;

const directionalLight = new THREE.DirectionalLight( 0xffffff, 1.5 );
directionalLight.position.set( -1, 1.75, 1 );
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = SHADOW_MAP_SIZE;
directionalLight.shadow.mapSize.height = SHADOW_MAP_SIZE;
directionalLight.shadow.camera.far = 3500;
directionalLight.shadow.bias = -0.0001;

scene.add(directionalLight);

MeshPhongMaterial 可以容纳阴影。

const matShadow = new THREE.MeshPhongMaterial({
    color: 0xffffff,
    shininess: 0.0,
});

而新的渲染目标会保存它们。

const PARAMETERS = {
    minFilter: THREE.LinearFilter,
    magFilter: THREE.LinearFilter,
    format: THREE.RGBFormat,
    stencilBuffer: false
};

const shadowBuffer = new THREE.WebGLRenderTarget(1, 1, PARAMETERS);

同样,需要一个调整大小的函数:

shadowBuffer.setSize(width, height);

现在我们可以将阴影传输到新的渲染目标并为着色器做好准备:

const render () => {
    const tmpHeight = container.clientHeight;
    const tmpWidth = container.clientWidth;
    if (tmpHeight !== height || tmpWidth !== width) {
        height = tmpHeight;
        width = tmpWidth;
        resize(width, height);
    }
    controls.update();

    floor.material = matShadow;
    sphere.material = matShadow;
    renderer.render(scene, camera, shadowBuffer);
    pass.uniforms.tShadow.value = shadowBuffer.texture;

    floor.material = matNormal;
    sphere.material = matNormal;

    composer.render();

    requestAnimationFrame(render);
}

这些行设置材质、渲染场景、将阴影设置为统一并将材质更改回 MeshNormalMaterial。

现在着色器需要了解阴影才能处理它们,因此我们更改uniforms:

const drawShader = {
    uniforms: {
        tDiffuse: { type: 't', value: null },
        tShadow: { type: 't', value: null },
        iResolution: { type: 'v2', value: resolution },
    },
    vertexShader: VERTEX,
    fragmentShader: FRAGMENT,
};

片段着色器也是如此:

const FRAGMENT = `
uniform sampler2D tDiffuse;
uniform sampler2D tShadow;
uniform vec2 iResolution;
varying vec2 vUv;
//...

然后我们用阴影替换前一行。

// gl_FragColor = vec4(edge, sample0.w, 1.0, 1.0);
float shadow = texture2D(tShadow, vUv).x;
gl_FragColor = vec4(edge, shadow, 1.0, 1.0);

结果应该类似于 Shadertoy 上的第二步。
在这里插入图片描述

现在我们只差第二个着色器通道来完成此操作。

6.4 最终的着色器通道

对于最终的着色器通道,我们添加另一个 EffectComposer 实例。

让我们定义另一个着色器:

const FRAGMENT_FINAL = `
#define EdgeColor vec4(0.2, 0.2, 0.15, 1.0)
#define BackgroundColor vec4(1,0.95,0.85,1)
#define NoiseAmount 0.01
#define ErrorPeriod 30.0
#define ErrorRange 0.003
// Reference: https://www.shadertoy.com/view/MsSGD1
float triangle(float x)
{
    return abs(1.0 - mod(abs(x), 2.0)) * 2.0 - 1.0;
}
float rand(float x)
{
    return fract(sin(x) * 43758.5453);
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    float time = floor(iTime * 16.0) / 16.0;
    vec2 uv = fragCoord.xy / iResolution.xy;
    uv += vec2(triangle(uv.y * rand(time) * 1.0) * rand(time * 1.9) * 0.005,
    triangle(uv.x * rand(time * 3.4) * 1.0) * rand(time * 2.1) * 0.005);
    float noise = (texture(iChannel1, uv * 0.5).r - 0.5) * NoiseAmount;
    vec2 uvs[3];
    uvs[0] = uv + vec2(ErrorRange * sin(ErrorPeriod * uv.y + 0.0) + noise, ErrorRange * sin(ErrorPeriod * uv.x + 0.0) + noise);
    uvs[1] = uv + vec2(ErrorRange * sin(ErrorPeriod * uv.y + 1.047) + noise, ErrorRange * sin(ErrorPeriod * uv.x + 3.142) + noise);
    uvs[2] = uv + vec2(ErrorRange * sin(ErrorPeriod * uv.y + 2.094) + noise, ErrorRange * sin(ErrorPeriod * uv.x + 1.571) + noise);
    float edge = texture(iChannel0, uvs[0]).r * texture(iChannel0, uvs[1]).r * texture(iChannel0, uvs[2]).r;
    float diffuse = texture(iChannel0, uv).g;
    float w = fwidth(diffuse) * 2.0;
    vec4 mCol = mix(BackgroundColor * 0.5, BackgroundColor, mix(0.0, 1.0, smoothstep(-w, w, diffuse - 0.3)));
    fragColor = mix(EdgeColor, mCol, edge);
    fragColor = texture(iChannel0, uv);
    //fragColor = vec4(diffuse);
}`;

const finalShader = {
    uniforms: {
        tDiffuse: { type: 't', value: null},
    },
    vertexShader: VERTEX,
    fragmentShader: FRAGMENT_FINAL
};

const passFinal = new THREE.ShaderPass(finalShader);
passFinal.renderToScreen = true;
composer.addPass(passFinal);

我们停用前一个渲染通道的 renderToScreen:

const pass = new THREE.ShaderPass(drawShader);
// REMOVED FOR FINAL SHADER pass.renderToScreen = true;
composer.addPass(pass);

再次,引入更多变量; 随着时间的推移改变变量的时间和通道 1 添加噪声的时间。

在这里插入图片描述

我们为 iTime 使用 ThreeJS 时钟。

const clock = new THREE.Clock();

每次更改时,我们也会更新 iTime:

const render () => {
// ...
const elapsed = clock.getElapsedTime();
passFinal.uniforms.iTime.value = elapsed;

composer.render();

// ....
}

我们在uniforms中添加 iTime 和噪音:

const finalShader = {
    uniforms: {
        tDiffuse: { type: 't', value: null},
        iTime: { type: 'f', value: 0.0},
        tNoise: { type: 't', value: new THREE.TextureLoader().load('noise.png')}
    },
    vertexShader: VERTEX,
    fragmentShader: FRAGMENT_FINAL
};

噪声只是一种噪声纹理(例如来自 Shadertoy 的纹理),我们使用 ThreeJS 将其加载到 tNoise 中。

现在我们需要使片段着色器适应新变量,因此我们应用以下措施:

  • 将 mainImage 更改为 main
  • 定义uniform并调整变量
  • 定义 vUv 坐标
  • 将返回结果改为gl_FragColor
  • 用texture2D替换纹理

这给了我们:

const FRAGMENT_FINAL = `
uniform sampler2D tDiffuse;
uniform sampler2D tNoise;
uniform float iTime;
varying vec2 vUv;
#define EdgeColor vec4(0.2, 0.2, 0.15, 1.0)
#define BackgroundColor vec4(1,0.95,0.85,1)
#define NoiseAmount 0.01
#define ErrorPeriod 30.0
#define ErrorRange 0.003
// Reference: https://www.shadertoy.com/view/MsSGD1
float triangle(float x)
{
    return abs(1.0 - mod(abs(x), 2.0)) * 2.0 - 1.0;
}
float rand(float x)
{
    return fract(sin(x) * 43758.5453);
}
void main()
{
    float time = floor(iTime * 16.0) / 16.0;
    vec2 uv = vUv;
    uv += vec2(triangle(uv.y * rand(time) * 1.0) * rand(time * 1.9) * 0.005,
    triangle(uv.x * rand(time * 3.4) * 1.0) * rand(time * 2.1) * 0.005);
    float noise = (texture2D(tNoise, uv * 0.5).r - 0.5) * NoiseAmount;
    vec2 uvs[3];
    uvs[0] = uv + vec2(ErrorRange * sin(ErrorPeriod * uv.y + 0.0) + noise, ErrorRange * sin(ErrorPeriod * uv.x + 0.0) + noise);
    uvs[1] = uv + vec2(ErrorRange * sin(ErrorPeriod * uv.y + 1.047) + noise, ErrorRange * sin(ErrorPeriod * uv.x + 3.142) + noise);
    uvs[2] = uv + vec2(ErrorRange * sin(ErrorPeriod * uv.y + 2.094) + noise, ErrorRange * sin(ErrorPeriod * uv.x + 1.571) + noise);
    float edge = texture2D(tDiffuse, uvs[0]).r * texture2D(tDiffuse, uvs[1]).r * texture2D(tDiffuse, uvs[2]).r;
    float diffuse = texture2D(tDiffuse, uv).g;
    float w = fwidth(diffuse) * 2.0;
    vec4 mCol = mix(BackgroundColor * 0.5, BackgroundColor, mix(0.0, 1.0, smoothstep(-w, w, diffuse - 0.3)));
    gl_FragColor = mix(EdgeColor, mCol, edge);
}
`;

在这些更改之后,着色器仍然无法编译,因为该着色器需要特定的 WebGL 扩展。 值得庆幸的是,这很容易在 ThreeJS 中添加:

const passFinal = new THREE.ShaderPass(finalShader);
passFinal.renderToScreen = true;
passFinal.material.extensions.derivatives = true;
composer.addPass(passFinal);

这给了我们以下结果:
在这里插入图片描述

这与原始 Shadertoy 非常接近:
在这里插入图片描述

7、结束语

我们通过以下步骤成功地将复杂的 Shadertoy 着色器转移到 ThreeJS:

  • 了解具体shader的结构
  • 实施着色器通道
  • 解决可能的 GLSL 不兼容问题
  • 创建可选的着色器通道和/或材质
  • 激活可选扩展

我们预计,随着 ThreeJS 中即将推出的 WebGL2 支持,这些挑战将得到缓解,因为可能的 GLSL 不兼容性应该会消失。

完整的源代码在这里。


原文链接:ShaderToy移植到Three.js — BimAnt

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/838814.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot+vue网红酒店客房预定系统的设计与实现_ui9bt

随着计算机技术发展&#xff0c;计算机系统的应用已延伸到社会的各个领域&#xff0c;大量基于网络的广泛应用给生活带来了十分的便利。所以把网红酒店预定管理与现在网络相结合&#xff0c;利用计算机搭建网红酒店预定系统&#xff0c;实现网红酒店预定的信息化。则对于进一步…

【验证测试】未初始化的全局变量和局部变量的初值

验证目标&#xff1a; 未初始化的全局变量的初值为 0未初始化的局部变量的初值为随机值 测试用例&#xff1a; #include <stdio.h>char gval1; int gval2; static long gval3;int main() {unsigned char uchTmp1;unsigned int uTmp2;printf("%d\n", gval1)…

Word中如何断开表格中线段

Word中如何断开表格中线段_word表格断线怎么弄_仰望星空_LiDAR的博客-CSDN博客有时候为了美观&#xff0c;需要实现如下的效果&#xff0c;即第2条线段被断开成3段步骤如下&#xff1a;选中需要断开的格网&#xff0c;如下&#xff0c;再选择段落、针对下框标即可。_word表格断…

Verilog学习记录-自用

always语句块一定条件写完整&#xff0c;否则电平触发&#xff0c;综合生成锁存器 task不可综合&#xff0c;主要用于仿真/验证 大部分都是并行执行的&#xff0c;只有begin end块中阻塞语句是串行 if-else和case的区别 if-else面积小&#xff0c;但时延&#xff08;执…

Service not registered 异常导致手机重启分析

和你一起终身学习&#xff0c;这里是程序员Android 经典好文推荐&#xff0c;通过阅读本文&#xff0c;您将收获以下知识点: 一、Service not registered 异常导致手机重启二、Service not registered 解决方案 一、Service not registered 异常导致手机重启 1.重启 的部分Log如…

C++封装思想之二:友元机制和运算符重载(1W字详解)

目录 友元机制和运算符重载 友元机制 友元函数 友元的作用 友元类 前置声明 友元类的注意事项 友元成员函数&#xff08;类的某个成员函数 作为另一个类的友元&#xff09; 运算符重载 运算符重载的作用 运算符重载的注意事项 运算符重载的实现 成员函数重载 友…

2021-03-03 Multisim 14.0 电池充电防止反接保护

R2R3当作充电线电阻看,也可设置这2个电阻导线电阻,电阻取值依据充电电流范围确定,由于电池存在电压因此可以用光耦检测,发光二极管当作继电器看,可采用继电器自锁,当下次再次反接的话另一个继电器同样,2个继电器相互控制.本电路可验证极性变化时2路检测的变化,图中S1为模拟电池…

计算机视觉:替换万物Inpaint Anything

目录 1 Inpaint Anything介绍 1.1 为什么我们需要Inpaint Anything 1.2 Inpaint Anything工作原理 1.3 Inpaint Anything的功能是什么 1.4 Segment Anything模型&#xff08;SAM&#xff09; 1.5 Inpaint Anything 1.5.1 移除任何物体 1.5.2 填充任意内容 1.5.3 替换任…

Finalshell连接Linux超时之Connection timed out: connect

目录 &#x1f349;前言 &#x1f33c;报错 &#x1f33c;摸索 &#x1f4aa;解决措施 &#x1f349;前言 &#xff08;1&#xff09;福利&#xff1a;花了2小时才解决的BUG&#xff0c;希望本篇文章能帮你10分钟解决&#xff01; &#xff08;2&#xff09;tips&#xff1…

6.s081/6.1810(Fall 2022)Lab3: page tables

文章目录 前言其他篇章参考链接0. 前置环境1. Speed up system calls (easy)1.1 简单分析1.2 映射1.3 页分配1.4 页释放1.5 测试 2. Print a page table (easy)2.1 简单分析2.2 实现2.3 测试 3. Detect which pages have been accessed (hard)3.1 简单分析3.2 实现3.2.1 获取参…

DBSCAN聚类

一、概述 DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是一种基于密度的聚类算法&#xff0c;簇集的划定完全由样本的聚集程度决定。聚集程度不足以构成簇落的那些样本视为噪声点&#xff0c;因此DBSCAN聚类的方式也可以用于异常点的检测。 二、算法…

一零六七、JVM梳理

JVM&#xff1f; Java虚拟机&#xff0c;可以理解为Java程序的运行环境&#xff0c;可以执行Java字节码&#xff08;Java bytecode&#xff09;并提供了内存管理、垃圾回收、线程管理等功能 java内存区域划分?每块内存中都对应什么? 方法区&#xff1a;类的结构信息、常量池、…

5个顶级的开源有限元分析软件

每当我参加数值分析课程的教学时&#xff0c;都会回顾有限元方法的基础知识&#xff0c;很自然地就会出现使用哪种软件的问题。 以下讨论基于三个基本考虑&#xff1a; 在实际应用中&#xff0c;很少有人从头开始编写 FEM 代码。商业 FEM 软件通常在某些预定义的情况下非常易于…

生命在于学习——Linux安全加固以及基线检查

一、账号管理 1、口令锁定策略 基线检查&#xff1a; 查看文件more /etc/pam.d/password-auth判定条件&#xff1a;是否存在以下内容 auth required pam_tally2.so deny5 onerrfail unlock_time300 even_deny_root5 root_unlock_time600安全加固&#xff1a; &#xff08;1…

MyBatis的输入映射和输出映射

文章目录 前言案例总结resultMap的使用 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; MyBatis的输入映射和输出映射是将Java对象和数据库表的列进行映射&#xff0c;实现数据的自动转换。 输入映射&#xff08;Parameter Mapping&#xff09;&#x…

从小白到大神之路之学习运维第74天-------Docker网络模型详解

第四阶段 时 间&#xff1a;2023年8月4日 参加人&#xff1a;全班人员 内 容&#xff1a; Docker网络模型详解 目录 一、环境配置 &#xff08;一&#xff09;安装docker-ce&#xff08;Linux安装Docker&#xff09; 二、Docker网络基础 &#xff08;一&#xff09;端…

Linux 创建子进程

文章目录 前言一、进程&#xff0c;线程&#xff0c;程序 区分二、创建子进程三、创建多个进程1. 获取进程号2. 循环创建多个进程 四、进程工具。1. ps 查看当前进程.2. kill 进程终止. 总结 前言 在计算机科学中&#xff0c;进程&#xff08;Process&#xff09;、线程&#…

8.5day06 框架基础--反射+注解

文章目录 反射获取类的各种信息获取类的字节码文件 注解元注解 复习redis两道算法题 摆烂了&#xff0c;不想学啦&#xff01;&#xff01;&#xff01; 反射 反射主要用来做框架; 学习内容 获取类的各种信息 第一步 加载类&#xff0c;获取类的字节码文件 第二步 获取类的…

抖音seo矩阵系统源码搭建开发详解

抖音SEO矩阵系统是一个用于提高抖音视频在搜索引擎排名的工具。如果你想开发自己的抖音SEO矩阵系统&#xff0c;以下是详细的步骤&#xff1a; 开发步骤详解&#xff1a; 确定你需要的功能和算法 抖音SEO矩阵系统包含很多功能&#xff0c;比如关键词研究、内容优化、链接建设、…

【ASP.NET MVC】使用动软(一)(9)

一、解决的问题 前文为解决数据库操作设计的 TestMysql 类&#xff0c;仅简单地封装了一个Query函数&#xff0c;代码如下&#xff1a; public class TestMysql{public static string SqlserverConnectStr "server127.0.0.1;charsetutf8;user idroot;persistsecurityin…