⚡Three.js-使用Shader绘制国旗

1,490 阅读6分钟

1. 什么是shader?

  • WebGL中重要的一部分内容
  • 开发原生WebGL必须学习的知识

比如你不依赖任何库去开发,例如绘制三角形啥的,就必须要掌握shader

下面简述一下什么是shader?

  1. shader也可以说是一种程序,使用GLSL编写。
  2. 编写的shader会发送到GPU
  3. shader会定位几何体的每个顶点
  4. shader会对几何体的每个像素进行着色

第4点用像素可能不太准确,因为在渲染的时候,每个点不一定对应显示器的每个像素,使用fragment(片段)更合适一点。

我们在shader中需要编写很多内容,诸如

  • 顶点定位
  • 几何体的变换
  • 摄像机的信息
  • 颜色
  • 纹理
  • 灯光
  • etc

编写完这些内容后,GPU会按照shader的操作处理这些内容。

shader可以分为两种类型

  • vertex shader(顶点shader),作用是定位几何体的每个顶点。
  • fragment shader(片段shader),作用是对几何体的每个片段进行着色。

2. 为什么我们需要编写shader?

  • three.js material是有限制的
  • 我们自己编写的shader可以很简单并且性能表现更好
  • 自己编写shader,我们可以对其做后期处理

3. 尝试使用RawShaderMaterial编写我们的第一个shader

3.1 使用vertextShaderfragmentShader属性

const material = new THREE.RawShaderMaterial({
    vertextShader: ``,
    fragmentShader: ``
})

尝试编写shader

// Geometry
const geometry = new THREE.PlaneGeometry(1, 1, 32, 32);

// Material
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);
    }
  `,
});

// Mesh
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

现在我们自己编写的shader起作用了,后面我们会解释上述代码的含义。 截屏2023-03-05 10.42.12.png

3.2 将上述程序中的shader代码分离到不同文件中

// src/shaders/test/vertex.glsl
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

attribute vec3 position;

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

// src/shaders/test/fragment.glsl
precision mediump float;
void main() 
{
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

安装VSCode插件,使glsl语言高亮

截屏2023-03-05 10.50.17.png

使用分离的glsl文件

import testVertexShader from "./shaders/test/vertex.glsl";
import testGragmentShader from "./shaders/test/fragment.glsl";

// Material
const material = new THREE.RawShaderMaterial({
  vertexShader: testVertexShader,
  fragmentShader: testGragmentShader,
});

此时会提示编译错误,此时我们需要配置一下webpack

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

现在就完成了glsl文件分离啦

其他通用属性,像wireframesidetransparentflatShading仍然可以配置

// Material
const material = new THREE.RawShaderMaterial({
  vertexShader: testVertexShader,
  fragmentShader: testGragmentShader,
  wireframe: true
});

但是一些属性,像mapalphaMapopacitycolor等不能配置,这些我们需要自己写在glsl文件中。

4. 简述GLSL

我们编写的shader语言叫做GLSL,它很类似于C语言,GLSL其使用C语言作为基础高阶着色语言,避免了使用汇编语言或硬件规格语言的复杂性。

4.1 不能打印

诸如,print, log等打印语句是不生效的

4.2 忽略缩进

缩进是不影响程序运行结果的

4.3 分号是必须的

4.4 变量

glsl 和 c语言类似,是强类型语言

// 浮点数
float a = -0.123;
float b = 1.0;

// 整数
int c = 123;
// 可以在运算时进行类型转换
float d = b * float(c);

// 布尔值
bool e = true;
bool f = false;

// 二维向量
vet2 g = vec2(-1.0, 2.0);

vet2 h = vec2(0.0);
h.x = 1.0;

vet2 i = vec2(1.0, 2.0);
i *= 2;

// 三维向量
vec3 j = vec3(1.0, 2.0, 3.0);

vec3 k = vec3(0.0);
// 可以使用r代替x,g代替y,b代替z
k.r = 0.5;
k.g = 0.2;

vet3 l = vec3(1.0, 2.0, 3.0);
vec2 m = l.xy; // vec2 = vec2(1.0, 2.0)

// 四维向量
vec4 n = vec4(1.0, 2.0, 3.0, 4.0);
vec4 o = vec4(n.zw, vec2(5.0, 5.0));

除了这些类型还有mat2, mat3, mat4, sampler2D,后续再介绍这些,先从基本的入手。

4.5 函数

float caluate() {
    float a = 1.0;
    float b = 2.0;
    
    return a + b;
}

float add(float a, float b) {
    return a + b;
}

4.6 内置函数

内置函数有很多,例如 sin, cos, max, min, pow, exp, mod, clamp等,下面提供了一些网站,可以方便查询有哪些内置函数

Shaderific

截屏2023-03-05 11.55.10.png

book of shaders glossary

截屏2023-03-05 11.58.59.png

5. 理解GLSL

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

attribute vec3 position;

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

5.1 主函数 main

  • 主函数会自动执行
  • 不需要返回任何内容
void main() {

}

5.2 gl_Position

  • 已经存在,不需要声明
  • 包含屏幕中顶点的位置信息

我们可以改变其xy属性

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

但要注意,这是移动物体的错误方式,不要使用。

5.3 attribute position

attribute vec3 position;

convert it to vec4

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

5.4 matrices uniforms

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

每一个矩阵都会转换位置,知道得到正确的正标

  • 3个矩阵
  • 使用 uniform 是因为它们应用在几何体的每个顶点上
  • modelMatrix 相当于几何体位置的转换
  • viewMatrix 相当于摄像机的转换
  • projectionMatrix ,投影矩阵,相当于最终显示的位置

此外,我们还可以写简洁版本,将modelMatrixviewMatrix合并为modelViewMatrix

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

attribute vec3 position;

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

还可以根据矩阵分离一下我们的代码

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
}

现在可以修改modelPositionxy 属性来调整位置。

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    
    // modelPosition.y += 1.0;
    modelPosition.z += sin(modelPosition.x * 10.0)* 0.1;
		
    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;
    gl_Position = projectedPosition;
}

现在我们就绘制了一个类似挥舞的旗帜。而这不使用shader很难做到。 截屏2023-03-05 13.31.20.png

5.5 理解fragment shader

precision mediump float;
void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

precision

float的精度可以是

  • highp(会丢失性能)
  • mediump
  • lowp

通常使用mediump

gl_FragColor

    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    // 对应的是 r, g, b, alpha

5.6 创建自定义属性

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

for (let i = 0; i < count; i++) {
  randoms[i] = Math.random();
}
geometry.setAttribute("aRandom", new THREE.BufferAttribute(randoms, 1));

应用自定义属性到vertex shader

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

attribute vec3 position;
attribute float aRandom;
void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);

    modelPosition.z += aRandom* 0.1;

    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;
    gl_Position = projectedPosition;
}

在vertex shader中使用varying定义属性,并尝试在fragment shader中使用

// vertex shader
attribute float aRandom;
varying float vRandom;
void main() {
    // ...
    vRandom = aRandom;
}
// fragment shader
precision mediump float;
varying float vRandom;
void main() {
    gl_FragColor = vec4(vRandom, 0.0, 0.0, 1.0);
}

现在我们成功对每个顶点进行着色了。 截屏2023-03-05 14.43.54.png

uniforms

uniform很有用,例如

  • 可以使相同的着色器呈现不同的结果
  • 可以调整数值
  • 制作动画

material 添加 uniform属性

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

在vertx shader中使用

// vertex shader
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform float uFrequency;

attribute vec3 position;

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);

    modelPosition.z += sin(modelPosition.x * uFrequency) * 0.1;	

    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;
    gl_Position = projectedPosition;
}

此外我们还可以把值替换成Vector2来控制每个轴

const material = new THREE.RawShaderMaterial({
  vertexShader: testVertexShader,
  fragmentShader: testGragmentShader,
  uniforms: {
    uFrequency: { value: new THREE.Vector2(10, 5) },
  },
});
// vertex shader
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform vec2 uFrequency;

attribute vec3 position;

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);

    modelPosition.z += sin(modelPosition.x * uFrequency.x) * 0.1;	
    modelPosition.z += sin(modelPosition.y * uFrequency.y) * 0.1;	

    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;
    gl_Position = projectedPosition;
}

截屏2023-03-05 15.15.48.png

制作舞动的旗帜

首先添加uTime属性

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

const tick = () => {
  const elapsedTime = clock.getElapsedTime();
  material.uniforms.uTime.value = elapsedTime;
};

在vertex shader中使用

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

attribute vec3 position;

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);

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

    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;
    gl_Position = projectedPosition;
}

现在舞动的旗帜就完成啦 shader-flag.gif

注意在tick函数中,我们使用的是elapsedTime,而不是Date.now(),不使用的原因是因为它太大了,因为对shader来说,Date.now()太大了

给旗帜添加纹理


const flagTexture = textureLoader.load("/textures/flag-china.jpg");

const material = new THREE.RawShaderMaterial({
  vertexShader: testVertexShader,
  fragmentShader: testGragmentShader,
  uniforms: {
    uFrequency: { value: new THREE.Vector2(10, 5) },
    uTime: { value: 0 },
    uTexture: { value: flagTexture },
  },
});
// vartex shader
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform vec2 uFrequency;
uniform float uTime;


attribute vec3 position;

attribute vec2 uv; // 传递uv坐标

varying vec2 vUv; // 传递给fragment shader

void main() {
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);

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

    modelPosition.y *= 0.75;

    vec4 viewPosition = viewMatrix * modelPosition;
    vec4 projectedPosition = projectionMatrix * viewPosition;
    gl_Position = projectedPosition;
    vUv = uv;
}
precision mediump float;

uniform sampler2D uTexture; // 使用smapler2D来接受纹理

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

好啦,现在舞动的国旗就画好啦~ 中国国旗.gif demo预览地址

demo源码地址

最后

⚽️本文主要介绍了shader的基础知识,以及如何使用shader绘制国旗
⚾如果你对本专栏感兴趣欢迎点赞关注+收藏,后面会持续更新,更多精彩知识正在等你!😘
🏉此外笔者还有其他专栏,欢迎阅读~
🏐玩转CSS之美
🎳深入浅出JavaScript