什么是着色器?

实际上着色器是WebGL的主要组件之一。如果我们在没接触Three.js情况下开始学习WebGL,着色器将是我们首先且必须要学的知识,这也是为什么原生WebGL很难入门。
着色器是一种使用GLSL(OpenGL Shading Language)编写并在GPU上运行的程序。它们被用于定位几何体的每个顶点,并为该几何体的每个可见像素着色。使用“像素Pixel”来描述其实并不准确,因为渲染的每个点不一定与屏幕上的每个像素相匹配,因此我们更倾向于使用术语“片元fragment”。
之后我们会向着色器发送大量数据,如顶点坐标、网格变换、摄像机及其视野范围的信息、颜色、纹理、灯光、雾等参数。然后,GPU会按照着色器的指示处理所有的这些数据,接着几何体便出现在渲染中。
有俩种类型的着色器,并且我们都会用到它们。

顶点着色器Vertex Shader

顶点着色器(Vertex Shader)的作用是定位几何体的顶点。其思想是发送顶点位置、网格变换(如定位position、旋转rotation和缩放scale)、摄影机信息(如定位position、旋转rotation和视野fov)。然后,GPU将按照顶点着色器中的指示处理所有这些信息,以便将顶点投影到2D空间,该空间将成为我们的渲染render,也就是我们的画布canvas
顶点着色器对顶点实现了一种通用的可编程方法。
使用顶点着色器时,其代码将应用于几何体的每个顶点。但有些数据(如顶点的位置)会在每个顶点之间发生变化。这种类型的数据(在顶点之间变化的数据)称为attribute属性变量。

  • attribute:使用顶点数组封装每个顶点的数据,一般用于每个顶点都各不相同的变量,如顶点的位置等。

但有些数据不需要像网格的位置那样在每个顶点之间进行变换,用于对同一组顶点组成的单个3D物体中所有顶点都相同的变量,如当前光源的位置,相机的位置,这种类型的数据(顶点之间不发生变化的数据)称为uniform统一变量。

  • uniform:顶点着色器使用的常量数据,不能被着色器修改,一般用于对同一组顶点组成的单个3D物体中所有顶点都相同的变量,如当前光源的位置。

顶点着色器会首先触发,当放置完顶点后,GPU会知道几何体的哪些像素是可见的,然后可以接着下去使用片元着色器。

片元着色器Fragment Shader

片元着色器的作用是为几何体的每个可见片元(像素)进行着色。
我们会创建片元着色器,可以通过使用uniform将数据(像是颜色)和着色器发送至GPU,之后GPU就会按照指令对每个片元进行着色。
片段着色器中最简单直接的指令是可以使用相同的颜色为所有片段着色。如果我们只设置了颜色属性,我们就得到了与MeshBasicMaterial等效的材质(每个可见像素都被着色白色)。
在这里插入图片描述

参考 着色器语言三种变量attribute、uniform 和 varying

简单总结

  • 顶点着色器渲染定位顶点位置
  • 片段着色器为该几何体的每个可见片元(像素)进行着色
  • 片段着色器在顶点着色器之后执行
  • 在每个顶点之间会有变化的数据(如顶点的位置)称为attribute,只能在顶点着色器中使用
  • 顶点之间不变的数据(如网格位置或颜色)称为uniform,可以在顶点着色器和片段着色器中使用
  • 从顶点着色器发送到片元着色器中的插值计算数据被称为varying

为什么要写着色器?

Three.js的内置材质试图涵盖尽可能多的情况,但是依旧有其局限性。如果我们想要突破限制,就必须自个写自己的着色器。

当然也可能是出于性能原因。像MeshStandardMaterial这样的材质非常精细,涉及大量代码和计算。如果我们编写自己的着色器,我们可以将功能和计算保持在最小限度,这样就可以更好控制与优化演算性能了。

编写我们自己的着色器也是将后期处理添加到渲染中的一个很好的方法,但我们将在后面专门的课程中再看到这一点。

一旦掌握了着色器,它们将成为所有项目中不可或缺的必备工具。

使用原始着色器材质(RawShaderMaterial)创建自己的第一个着色器

要创建第一个着色器,我们需要创建特定的材质。该材质可以是着色器材质ShaderMaterial或原始着色器材质RawShaderMaterial。这两者之间的区别在于前者会自动将一些代码添加到着色器代码中(内置attributes和uniforms),而后者正如其名,什么都不会添加。

准备工作

我们的初始场景有一块MeshBasicMaterial材质的简单平面,将之材质替换为RawShaderMaterial原始着色器材质。然后你会发现报错了,这是因为我们还没添加着色器。如前所述,我们需要同时提供顶点着色器和片元着色器,并在里面书写程序:

const material = new THREE.RawShaderMaterial({
    vertexShader: `
        uniform mat4 projectionMatrix;
        uniform mat4 viewMatrix;
        uniform mat4 modelMatrix;

        attribute vec3 position;

        void main()
        {
            gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        precision mediump float;

        void main()
        {
            gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
        }
    `
})

得到这样的一个结果:
在这里插入图片描述

分离两种着色器

新建一个shaders文件夹,在里面新建俩个文件vertex.glslfragment.glsl,将上边代码拷贝到对应文件中,再在vscode的插件商店安装shader languages support for VS code,这样我们的.glsl文件也就有了语法高亮提示。
之后导入:

import testVertexShader from './shaders/test/vertex.glsl'
import testFragmentShader from './shaders/test/fragment.glsl'

然而我们会报一个webpack错误,因为需要告诉webpack如何处理.glsl文件。
/bundler/webpack.common.js,修改如下:

module.exports = {
    // ...

    module:
    {
        rules:
        [
            // ...

            // Shaders
            {
                test: /\.(glsl|vs|fs|vert|frag)$/,
                exclude: /node_modules/,
                use: [
                    'raw-loader'
                ]
            }
        ]
    }
}

这个规则仅告诉webpack提供文件的原始内容。然后重启服务,会看到与前面一样的场景。

我们在其他材质中介绍的大多数常见属性(如wireframe、side、transparent、flatShading)仍然适用于RawShaderMaterial

const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    wireframe: true
})

在这里插入图片描述
但是像mapalphaMapopacitycolor等属性将不再生效,因为我们需要自己在着色器中编写这些特性。

GLSL

OpenGL着色语言(OpenGL Shading Language)是用来在OpenGL中着色编程的语言,它是一种类C语言,下面先学习一下基础语法。

日志

没有控制台,因此无法记录值。这是因为代码针对每个顶点和每个片段执行。记录一个值是没有意义的。

缩进

缩进不重要,可以随意。

分号

任何指令的结尾都需要分号。哪怕忘记一个分号都可能导致编译错误,使得整个材料都不起作用。

变量

GLSL是一种强类型语言,如C语言一般。

不能在操作中混合使用floatint,会报错:

float a = 1.0;
int b = 2;
float c = a * b;

但是可以通过类型转换进行操作:

float a = 1.0;
int b = 2;
float c = a * float(b);

Vector 2

如果我们想存储具有x和y属性的2个坐标这样的值,我们可以使用vec2

vec2 foo = vec2(1.0, 2.0);

空的vec2将导致错误:

vec2 foo = vec2();

我们可以在创建vec2后更改这些属性:

vec2 foo = vec2(0.0);
foo.x = 1.0;
foo.y = 2.0;

执行vec2与浮点相乘等操作将同时操作x和y属性:

vec2 foo = vec2(1.0, 2.0);
foo *= 2.0;

Vector 3

vec3与vec2类似,但具有第三个名为z的属性。当需要3D坐标时,使用它非常方便:

vec3 foo = vec3(0.0);
vec3 bar = vec3(1.0, 2.0, 3.0);
bar.z = 4.0;

虽然我们可以使用x、y和z,但我们也可以使用r、g和b。这只是语法糖,结果完全一样。当我们使用vec3存储颜色时,它非常有效:

vec3 purpleColor = vec3(0.0);
purpleColor.r = 0.5;
purpleColor.b = 1.0;

vec3可以通过部分使用vec2来创建:

vec2 foo = vec2(1.0, 2.0);
vec3 bar = vec3(foo, 3.0);

我们也可以使用vec3的一部分来生成vec2:

vec3 foo = vec3(1.0, 2.0, 3.0);
vec2 bar = foo.xy;

Vector 4

最后,vec4的原理与前两个类似,但第四个值命名为wa(颜色alpha)

vec4 foo = vec4(1.0, 2.0, 3.0, 4.0);
vec4 bar = vec4(foo.zw, vec2(5.0, 6.0));

还有其他类型的变量,如mat2mat3mat4sampler2D,我们将在后面看到这些变量。

在着色器内,一般命名以gl_开头的变量是着色器的内置变量,除此之外webgl__webgl还是着色器保留字,自定义变量不能以webgl__webgl开头。变量声明一般包含<存储限定符><数据类型><变量名称>,以attribute vec4 a_Position为例,attribute表示存储限定符,vec是数据类型,a_Position为变量名称。

原生函数

GLSL有许多内置的经典函数,如sin、cos、max、min、pow、exp、mod、clamp,也有非常实用的函数,如cross、dot、mix、step、smoothstep、length、distance、reflect、refract、normalize。

不幸的是,没有对初学者友好的文档,而且大多数时候,我们在网络上进行搜索,结果通常出现在以下网站:

  • Kronos Group OpenGL reference pages:本文档涉及OpenGL,但您将看到的大多数标准函数都与WebGL兼容。不要忘了WebGL只是一个访问OpenGL的JavaScript API。
  • Book of shaders documentation:《Book of shaders》主要关注片元着色器,与Three.js无关。但是这是一个很好的学习资源,它有自己的词汇表。

理解顶点着色器

下面开始尝试理解着色器里面的内容。

首先注意的是,顶点着色器的目的是将几何体的每个顶点放置在2D渲染空间上。换句话说,顶点着色器将3D顶点坐标转换为2D画布坐标。

main函数

它会自动调用,并且不会返回return任何内容

void main()
{
}

gl_Position

变量gl_Position原本就已经声明好,是一个内置变量,我们只需重新分配它。
这个变量将会包含屏幕上的顶点的位置,main函数中的代码便是要正确设置该变量。
这条长代码将返回一个vec4,这意味着我们可以直接在gl_Position变量上使用其x、y、z和w属性。

void main()
{
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
    gl_Position.x += 0.5;
    gl_Position.y += 0.5;
}

会看到平面往右上角移动,但是要注意,我们并没有在3D空间里边移动平面,虽然看起来像我们在Three.js里面控制位置一样。不过,我们确实是在2D空间上移动了平面
在这里插入图片描述
如何理解上面的话,想象一下你在桌上画一幅带透视效果的图画,然后你将整张图画往右上角移动,但是图画中的透视效果并未因此而改变。

可能你想要知道如果gl_Position的目标是在2D空间上定位顶点,那为什么还需要四个值?实际上这些坐标并不是精确的位于二维空间,而是位于我们说的需要四个维度的剪裁空间Clip Space

剪裁空间Clip Space是指在-1到+1范围内的所有3个方向(x、y和z)上的空间。这就像把所有东西都放在3D盒子里一样,任何超出此范围的内容都将被“剪裁”并消失。第四个值(w)负责透视。

幸运的是,所有这些都是自动完成的,作为初学者,我们不需要掌握所有东西,只用大概了解下就行。

但是我们到底向gl_Position发送了什么?

Position attributes

首先,我们使用下面代码检索顶点位置:

attribute vec3 position;

请记住,相同的代码适用于几何体的每个顶点。属性变量attribute是顶点之间唯一会发生变化的变量。相同的顶点着色器将应用于每个顶点,“position”属性将包含该特定顶点的x、y和z坐标。
然后,如上面所示的代码,我们将这个vec3转换成一个vec4,因为矩阵和gl_Position都需要用到这个vec4:

gl_Position = /* ... */ vec4(position, 1.0);

Matrices uniforms

每个矩阵都将变换position,直到我们得到最终的剪裁空间坐标。
我们的代码中有3个矩阵,因为它们的值对于几何体的所有顶点都是相同的,所以我们使用uniform检索它们。

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

每个矩阵都将做一部分变换:

  • modelMatrix将应用与网格相关的所有变换。如果我们缩放、旋转或移动网格,这些变换将包含在modelMatrix中并应用于position
  • viewMatrix将应用相对于相机的变换。如果我们向左旋转相机,顶点应该在右边。如果我们沿着网格的方向移动相机,顶点会变大,等等。
  • projectionMatrix将我们的坐标转换为最终的剪裁空间坐标。

如果想了解更多关于矩阵和坐标的信息,可以看这篇文章Coordinate-Systems

要应用矩阵,我们需要将其相乘。如果要将mat4应用于变量,该变量必须是vec4

gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);

实际上有一个代码更少的版本,便是将viewMatrixmodelMatrix组合成modelViewMatrix,虽然代码更短但我们对每一步的控制权也就越弱:

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

attribute vec3 position;

void main()
{
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

为了更好理解和控制position,我们将如下面这样写更长些:

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

attribute vec3 position;

void main()
{
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;

    gl_Position = projectedPosition;
}

这些写法都是等效的,但是现在能够通过调整modelPosition的值来移动整个模型:

void main()
{
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    // 平面向上移动
    modelPosition.y += 1.0;

    // ...
}
void main()
{
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    // 平面变波浪形
    modelPosition.z += sin(modelPosition.x * 10.0) * 0.1;

    // ...
}

在这里插入图片描述

理解片元着色器

片元着色器的代码将应用于几何体的每个可见片元(像素)。这就是为什么片元着色器运行在顶点着色器之后。

Precision

在最顶部有这样一条指令:

precision mediump float;

这条指令可以让我们决定浮点数的精度,有如下可能值

  • highp:会造成性能影响,并可能在某些设备上无法工作。
  • mediump:我们通常使用这个。
  • lowp:会由于缺乏精确性而产生某些错误。

我们也可以设置顶点着色器的精度,但这不是必要的。

这一部分在使用着色器材质ShaderMaterial会被自动处理,但现在我们使用的是原始着色器材质。

gl_FragColor

gl_FragColorgl_Position相似只不过是用于颜色。跟前者一样是内置变量了,只需要在main函数中重新设置值。
这是一个vec4,前三个值是红色、绿色和蓝色通道(r、g、b),第四个值是alpha(a),下面这代码应用后会看到紫色几何体:

gl_FragColor = vec4(0.5, 0.0, 1.0, 1.0);

如果要降低alpha值使能够看出差异,还需要回到RawShaderMaterial中将transparent属性设置为true:

const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    transparent: true
})

在这里插入图片描述

Attributes

我们在前面已经有一个名为positionattribute属性变量,现在可以直接将这个属性添加到BufferGeometry中。我们将为每个顶点添加一个随机值,并根据这个值在z轴上移动该顶点。

回到js中,我们要在创建完几何体后,再new一个32位的浮点数型数组Float32Array,然后为了知道几何体中有多少个顶点,可以通过几何体的attributes属性获取:

const count = geometry.attributes.position.count
const randoms = new Float32Array(count)

然后用随机值填充该数组:

for(let i = 0; i < count; i++)
{
    randoms[i] = Math.random()
}

最后,我们在BufferAttribute中使用该数组,并将其添加到几何体的属性中:

geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1))

setAttribute的第一个参数是我们设置的attribute属性的名字,这也是下面在着色器中使用的名字。命名最好在前面加一个a前缀代表属性。
BufferAttribute的第一个参数是数据数组,第二个参数是组成一个属性的值的数量。如果我们要发送一个位置,我们会使用3,因为位置由3个值(x、y和z)组成。但在这里,每个顶点只有1个随机值,所以我们使用1。

现在,我们可以在顶点着色器中检索该属性,并使用它移动顶点:

// ...
attribute float aRandom;

void main()
{
    // ...
    modelPosition.z += aRandom * 0.1;

    // ...
}

最后得到一个尖锐峰峦状平面:
在这里插入图片描述

Varyings

现在我们还想用aRandom属性给片元着色。但可惜,我们没法直接在片元着色器中使用attribute属性变量。

但是,有一种方法可以将数据从顶点着色器发送到片元着色器,称之为varying可变量。我们必须在俩种着色器上都这样做。

在顶点着色器上,我们需要在main函数之前创建varying,这里将该变量命名为vRandom,用v作前缀好区分,然后在main函数中赋值:

// ...

varying float vRandom;

void main()
{
    // ...

    vRandom = aRandom;
}

然后在片元着色器用一样的方式:

precision mediump float;
varying float vRandom;
void main()
{
    gl_FragColor = vec4(0.5, vRandom, 1.0, 1.0);
}

最后观察到如下图:
在这里插入图片描述
varying的一个有趣之处是,顶点之间的值是线性插值的,以此实现平滑渐变。如果GPU正在两个顶点之间绘制一个片元,一个顶点的变量为1.0,另一个顶点的变量为0.0,那么片元值将为0.5。
最后重新回归最初的普通平面。

Uniforms

统一变量uniform是将数据从JavaScript发送到着色器的一种方式。

如果我们想使用同一个着色器但是参数不一样,以得到不同的结果,就可以使用uniform

两个着色器中都可以使用,其对每个顶点和片元的数据都是相同的。我们的代码中已经有了projectionMatrix、viewMatrix和modelMatrix,但我们没有创建它们,是Three.js创建的。

下面创建我们自己的统一变量uniform

为将统一变量添加到我们的material中,要使用uniforms属性,我们要做一个波浪平面,并且能够控制波形频率:

const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    uniforms:
    {
        frequency: { value: 10 }
    }
})

在这里我们将统一变量命名为frequency,虽然不是强制性的,但最好在前面加个u前缀以将统一变量和其他数据区分开来:

const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    uniforms:
    {
        uFrequency: { value: 10 }
    }
})

如果你也有在看其他的教程或示例,你可能会看到它是这样声明uniform的:

uFrequency: { value: 10, type: 'float' }

在之前我们必须为其指定类型,但现在已经被弃用了。

现在,我们可以在着色器代码中检索该值,并在main函数中使用它:

uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform float uFrequency;

attribute vec3 position;

void main()
{
    // ...
    modelPosition.z += sin(modelPosition.x * uFrequency) * 0.1;

    // ...
}

然后你会发现结果是一样的,但现在我们可以在js中控制波形频率了。

让我们把频率frequency改为vec2来控制水平和垂直方向的波,这里使用Three.js中一个简单的vec2:

const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    uniforms:
    {
        uFrequency: { value: new THREE.Vector2(10, 5) }
    }
})

然后回到着色器,将float改为vec2,在z轴上应用位移:

// ...
uniform vec2 uFrequency;

// ...

void main()
{
    // ...
    modelPosition.z += sin(modelPosition.x * uFrequency.x) * 0.1;
    modelPosition.z += sin(modelPosition.y * uFrequency.y) * 0.1;

    // ...
}

在这里插入图片描述
这些值现在由JavaScript控制,所以我们可以将它们添加到我们的Dat.GUI中:

gui.add(material.uniforms.uFrequency.value, 'x')
	.min(0)
	.max(20)
	.step(0.01)
	.name('frequencyX')
gui.add(material.uniforms.uFrequency.value, 'y')
	.min(0)
	.max(20)
	.step(0.01)
	.name('frequencyY')

在这里插入图片描述
现在让我们再新加一个uniform来让平面像在风中飘动的旗帜。
我们将使用统一变量向着色器发送一个时间值,并在sin函数中使用:

const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    uniforms:{
        uFrequency: { value: new THREE.Vector2(10, 5) },
        uTime: { value:0 }
    }
})

记得在tick函数中更新uTime:

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()

    // Update material
    material.uniforms.uTime.value = elapsedTime

    // ...
}

更新顶点着色器代码:

// ...
uniform float uTime;

// ...

void main()
{
    // ...
    modelPosition.z += sin(modelPosition.x * uFrequency.x + uTime) * 0.1;
    modelPosition.z += sin(modelPosition.y * uFrequency.y + uTime) * 0.1;

    // ...
}

在这里插入图片描述
在使用uTime时要注意,如果我们使用原生JavaScript的Date.now(),你会发现不起作用,因为它返回的数值对于着色器而言太过庞大了。注意,我们不能发送太小或太大的统一变量值。

不要忘记这仍然是一个平面,我们可以像以前一样变换网格。让我们给飞机做个旗子形状。

我们可以在着色器中通过乘以modelPosition.y来实现,但不要忘记,我们仍然可以直接在网格上更改位置position、缩放scale和旋转rotation

const mesh = new THREE.Mesh(geometry, material)
mesh.scale.y = 2 / 3
scene.add(mesh)

在这里插入图片描述
在片元着色器中也可以使用统一变量,下面使用Three.js的Color作为新的统一变量:

const material = new THREE.RawShaderMaterial({
    vertexShader: testVertexShader,
    fragmentShader: testFragmentShader,
    uniforms:
    {
        uFrequency: { value: new THREE.Vector2(10, 5) },
        uTime: { value: 0 },
        uColor: { value: new THREE.Color('orange') }
    }
})
precision mediump float;

uniform vec3 uColor;

void main()
{
    gl_FragColor = vec4(uColor, 1.0);
}

之后你会看到旗帜变为橘色。

Textures

下面将我们的旗帜贴图纹理应用于平面上(网上找一张图片1024*1024即可):

const textureLoader = new THREE.TextureLoader()
const flagTexture = textureLoader.load('/textures/flag-CCCP.jpg')

然后将它传给统一变量uTexture

const material = new THREE.RawShaderMaterial({
    // ...
    uniforms:
    {
        // ...
        uTexture: { value: flagTexture }
    }
})

下面要将我们的旗帜纹理的颜色应用于所有可见片元上,为此我们必须使用texture2D()方法,该方法第一个参数就是应用的纹理,第二个参数是由在纹理上拾取的颜色的坐标组成,我们还没有这些坐标,而这听起来很熟悉,我们正在寻找可以帮助我们在几何体上投射纹理的坐标,也就是UV坐标。
PlaneBufferGeometry会自动生成这些坐标,可以通过打印geometry.attributes.uv看到。
因为它是一个属性,我们可以在顶点着色器中检索它:

attribute vec2 uv;

不过,我们需要在片元着色器中使用这些坐标。而要将数据从顶点着色器发送至片元着色器,我们要创建一个名为vUv的变量varying,并在main函数中对其赋值:

// ...
attribute vec2 uv;

varying vec2 vUv;

void main()
{
    // ...

    vUv = uv;
}

现在,我们可以在片元着色器中检索变量vUv传给texture2D()方法:

precision mediump float;

uniform vec3 uColor;
uniform sampler2D uTexture;

varying vec2 vUv;

void main()
{
    vec4 textureColor = texture2D(uTexture, vUv);
    gl_FragColor = textureColor;
}

texture2D(...)的输出值是一个vec4因为它包含rgb和a,即便这里没用到a。

在这里插入图片描述

颜色变化

旗帜的颜色变化不是很明显,如果有亮度的变化像阴影一般就好了,下面要用到的技术在物理上并不准确,但应该也能起到一点作用。
首先,在顶点着色器中,我们将把风的高度存储在一个变量elevation中,然后通过varying将之发送至片元着色器:

// ...
varying float vElevation;

void main()
{
    // ...

    float elevation = sin(modelPosition.x * uFrequency.x - uTime) * 0.1;
    elevation += sin(modelPosition.y * uFrequency.y - uTime) * 0.1;

    modelPosition.z += elevation;
    
	vElevation = elevation;
    // ...
}

用这个变量来改变textureColor的r,g,b属性:

// ...
varying float vElevation;

void main()
{
    vec4 textureColor = texture2D(uTexture, vUv);
    textureColor.rgb *= vElevation * 2.0 + 0.5;
    gl_FragColor = textureColor;
}

在这里插入图片描述

着色器材质

到目前为止,我们一直使用原始着色器材质RawShaderMaterial,而ShaderMaterial的工作原理其实是一样的,只不过其有内置attributes和uniforms,精度也会自动设置。
你可以将材质直接替换:

const material = new THREE.ShaderMaterial({
    // ...

然后在着色器中移除下面这些uniformattributeprecision

  • uniform mat4 projectionMatrix;
  • uniform mat4 viewMatrix;
  • uniform mat4 modelMatrix;
  • attribute vec3 position;
  • attribute vec2 uv;
  • precision mediump float;

之后着色器会跟之前一样正常运行,因为它会自动添加上上面这些。

调试

调试着色器很困难,我们不能像JavaScript那样记录数据,因为是GPU对每个顶点和每个片元都执行着色器代码。
然而,Three.js在编译中传递错误这方面做得很好。

查错

如果我们忘打一个分号,Three.js会打印整个着色器代码并提示我们在第几行出错,像这样:ERROR: 0:99: 'textureColor' : syntax error

阅读着色器

当使用ShaderMaterial时,在报错时查看控制台的整个着色器代码也是一种很好的方式,可以观察到Three.js为我们自动添加了哪些变量。

测试数值

调试数值的另一个解决方案是在gl_FragColor中使用它们。虽然并不精确,但是能看到颜色变化,有时候这也足够了。
如果这些值在顶点着色器中,我们可以使用一个变量将其传递给片元着色器。
假如我们想看uv长啥样,我们可以将其发送至片元着色器,然后在gl_FragColor中使用:

gl_FragColor = vec4(vUv, 1.0, 1.0);

在这里插入图片描述

最后

Glslify

GLSLify是一个node module模块,它改进了我们对glsl文件的处理。通过glslify,我们可以像模块一样导入和导出glsl代码。你可以使用glslify-loader并将之加到webpack配置中去。

网站

下面是学习着色器的一些网站和油管频道:

笔记都是边看视频边跟着敲代码边做笔记的,如果有哪里出错,还望指出来,好做改进,谢谢。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐