ShaderToy着色器移植到Three.js(着色器模型3.0)
liuian 2025-05-08 02:45 90 浏览
推荐:用 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 不兼容性应该会消失。
完整的源代码在这里。
原文链接:
http://www.bimant.com/blog/shadertoy-to-threejs/
相关推荐
- windows10官网打不开(win10系统官网打不开)
-
你可以通过以下步骤在Windows10官网上更新操作系统:1.打开windows官网,进入“下载和工具”页面。2.单击“立即下载工具”按钮,将下载“Windows10更新助手”。3.运行“...
- win7无线网卡插上没反应(win7无线网卡插上没反应怎么回事)
-
1、如果是路由器的问题,如果原来可以用,暂时不能用了,在有就是恢复出厂设置,从新设置就可以用了(这是在物理连接正确的前提下)。2、如果是宽带本身的问题,首先直接联接宽带网线测试,如果是宽带的问题,联系...
- 下载爱奇艺安装(下载爱奇艺安装包)
-
如果你的电脑无法安装爱奇艺,可能有以下原因,第一种原因可能是你的电脑系统版本太低,升级你的电脑操作系统,可以促进爱奇艺的下载,第二种情况是你下载的爱奇艺可能捆绑一些病毒软件,系统的杀毒软件识别有霸王软...
- 5000元左右的电脑配置单(5000左右的电脑配置推荐2021)
-
五千元至六千元价位电脑主机,如果组装机,可以配置配置很高的档次,电脑主机主板可以配置不低于十二代产品,可以设四个内存条插槽,相应的内存可以配置128GB内存条2至四根,电脑处理器也同样不低于十二代产品...
-
- 快速关机(快速关机按什么键)
-
1、我们直接长按手机右侧的电源键,大概5秒的时间,这时候手机页面会直接显示是否关机,选择关机就可以直接关机了。2、找到手机一侧的音量“+”键,再找到电源按键,之后只需同时按住音量“+”键和电源按钮,直到手机屏幕关闭即可强制关机。3、点击【设...
-
2025-12-25 08:05 liuian
- 云电脑免登录破解版(“云电脑破解版”)
-
虎牙YOWA云游戏平台便是一款完全免费的产品,只要玩家在自己的账号上购买过相关的产品即可通过云游戏平台直接登陆。但云游戏平台终归只是改变玩家的游戏方式,用户最终还是要回归于游戏中,如果难以保证游戏体验...
- 联想家庭版win7(联想家庭版笔记本电脑)
-
1、开机到欢迎界面时,按Ctrl+Alt+Delete,跳出帐号窗口,输入用户名:administrator,回车。2、如果这个帐号也有密码采用开机启动时按F8选“带命令行的安全模式”。3、选“Ad...
- 两台电脑怎么传文件最快(两台电脑怎么传文件比较快)
-
两台电脑之间传递文件可以有很多种方法。如果两台电脑同时在1栋楼或者一间办公室内,可以用U盘拷贝的方法传递文件。另外最快的方法还可以用通过邮箱、微信、QQ传送文件,那样速度更快,节省时间,又节省距离。将...
- win7计算机图标怎么弄出来(win7怎么设置计算机图标)
-
您好,如果您的Win7桌面图标不见了,可以尝试以下方法:1.右键点击桌面的空白处,点击查看之后点击显示桌面图标。2.如果第一种方法不起作用,可以使用组合键“ctrl键+alt键+delete键”,...
- usb打印机改wifi打印机(usb打印机改无线网络打印机)
-
首先要把打印机通过USB端口连接到路由器上,连接成功后路由器上的USB指示灯会亮。然后在需要使用网络打印机的电脑上安装打印机的驱动程序,这样才能够正常使用打印服务器连接的打印机。登录路由器,在左侧的系...
- windows7没pdf打印机(win7系统自带的打印pdf找不到了)
-
建议安装Acrobat9,并安装9.1.3的AdobeReader/Acrobat的更新,去官网搜索即可,如果现有版本是9.1.0,则9.1.2和9.1.3的更新均需要安装.我实验的结果时9.0...
- 有两台iphone一台忘记密码(有两台iphone一台忘记锁屏密码)
-
iphone的锁屏密码输入错误次数过多,显示iphone已停用。解决办法:第一步:电脑上装好iTunes,并打开。第二步:关手机,插上数据线,注意只插手机这一端,先不接电脑。第三步:按住手机上的Hom...
- 快用苹果助手官网进不去(快用苹果助手怎么下载不了)
-
要在指定的网址上登录下载,苹果手机没有自动授信不能下载
- 一周热门
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
