threejs系列:着色器学习和例子

1,876 阅读4分钟

本文内容:介绍web着色器的基本概念,以及通过一个着色器demo来进一步理解着色器的用法和作用。

顶点着色器 & 片元着色器

首先我们需要知道threejs绘制3D物体包含了两个主要属性:顶点以及材质,可以理解为就是位置和颜色。着色器决定了3D物体每个顶点最终在屏幕上渲染出来的位置和颜色。

所以着色器分为顶点着色器(用于决定位置)片元着色器(用于决定颜色)

在具体的绘制过程先执行顶点着色器,再执行片元着色器,因此片元着色器中的变量需要从顶点着色器中传入。

顶点着色器示例:

attribute float size;  //顶点大小,由geometry的属性传入
attribute vec3 customColor; //顶点自定义颜色,由geometry的属性传入
varying vec3 vColor; //插值颜色
void main() {
    vColor = customColor;    //插值颜色,由geometry的属性传入
    gl_PointSize = 10 ; // 给内置变量gl_PointSize赋值像素大小
    //gl_Position的计算总是固定为 投影矩阵*模型视图矩阵*位置向量
    gl_Position =  projectionMatrix*modelViewMatrix * vec4( position, 1.0 );
}

顶点着色器定义了顶点的渲染位置和点的渲染像素大小,对组成物体的每个顶点都执行一次。

投影矩阵:不同的观察者位置和视角看到的物体不一样,所以需要一个投影矩阵变换操作。

模型视图矩阵:分为模型矩阵和视图矩阵,模型矩阵决定物体在世界空间矩阵的位置,比如操作物品平移,便是变更了模型矩阵。视图矩阵是从不同角度看到物品到二维平面的投影不一样,增加视图矩阵才能构建物体的全貌。

通过投影矩阵*模型视图矩阵*位置向量来计算出每个顶点在三维矩阵中的位置。

片元着色器示例:

uniform vec3 color;   //顶点颜色 ,由shader构造材质时引入
uniform sampler2D pointTexture; //顶点纹理采样器
varying vec3 vColor;  //顶点插值颜色
void main() {
    gl_FragColor = vec4( color * vColor, 1.0 ) ;
}

片元着色器定义了点的渲染结果像素的颜色值,对组成的每个像素执行一次。

逐顶点&逐片元

理解内置变量gl_Position需要建立逐顶点的概念,在javascript语言中出现一个变量赋值,可以理解为仅仅执行一次,但是对于着色器中不能直接这么理解,如果有多个顶点,则每个顶点都要执行一遍顶点着色器主函数main中的程序。

对于内置变量gl_FragColor而言,需要建立逐片元的概念。顶点经过片元着色器片元化以后,得到一个个片元,或者说像素点,然后通过内置变量gl_FragColor给每一个片元设置颜色值,所有片元可以使用同一个颜色值,也可能不是同一个颜色值,可以通过特定算法计算或者纹理像素采样。

关键字

varing 可用于在顶点着色器和片元着色器中传递变量的。

uniform 可读属性,不能在着色器代码中更改,用于从js中传递一些参数。

attribute 可从geometry中的attribute中获取相应的参数。一些比较复杂的参数可以通过这个参数传入,注意这里也要符合逐片元。

着色器用于图元的材质示例:

var material = new THREE.ShaderMaterial({
  uniforms: {
    color: { value: new THREE.Color(0xf0ffff) },
    pointTexture: { value: new THREE.TextureLoader().load("../images/red_line.png") }
  },
  vertexShader: v,
  fragmentShader: f,
  // blending: THREE.AdditiveBlending,
  depthTest: false,
  depthWrite: false,
  transparent: true 
});

vertexShader是顶点着色器,fragmentShader是片元着色器,支持js脚本引入或字符串(在工程化中,字符串比较方便),弊病就是目前不知道如何调试着色器代码。

uniforms,用于定义传入着色器的变量,可以改变uniforms中的值来动态变换物体的位置形状。变量需要在着色器代码中定义同名变量,例如:uniform sampler2D pointTexture;

mesh.vertices中存储的position值,因为它们是模型空间矩阵的,而变换是在世界空间矩阵中进行的,所以在世界空间矩阵中进行的变换,不会更改着色器中的position的值,但是会变更gl_Position的值。gl_Position是模型顶点在世界空间矩阵最终的位置。

例子

下面以一个自定义的着色器的例子看看着色器的使用。

平面扩散波纹

创建顶点着色器和片元着色器

// 顶点着色器代码
const vertexShader = `  
  varying vec2 vUv;
  void main() { 
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
  }`;
// 片元着色器代码
const fragmentShader = `
  varying vec2 vUv;
  // 下面是传进来的变量
  uniform vec3 uColor;
  uniform float uOpacity;
  uniform float uRange;
  uniform float uSpeed; 
  uniform float uSge;
  uniform float time;
  float PI = 3.14159265;
  float drawCircle(float index, float range) {
      float opacity = 1.0;
      if (index >= 1.0 - range) {
          opacity = 1.0 - (index - (1.0 - range)) / range;
      } else if(index <= range) {
          opacity = index / range;
      } 
      return opacity;
  }
  void main() { 
      float iTime = -time * uSpeed;
      float opacity = 0.0;
      float len = distance(vec2(0.5, 0.5), vec2(vUv.x, vUv.y)); 

      float size = 1.0 / uSge;
      float rSize = uRange / 2.0;
      vec2 range = vec2(0.7 - rSize, 0.7 + rSize);
      float index = mod(iTime + len, size); 
      // 中心圆 
      vec2 cRadius = vec2(0.06, 0.12); 
      
      if (index < size && len <= 0.5) {   
          float i = sin(index / size * PI); 

          // 处理边缘锯齿
          if (i >= range.x && i <= range.y){
              // 归一
              float t = (i - range.x) / (range.y - range.x);
              // 边缘锯齿范围
              float r = 0.3;
              opacity = drawCircle(t, r);
              
          }
          // 渐变
          opacity *=  1.0 - len / 0.5;
      }; 
      
      gl_FragColor = vec4(uColor, uOpacity * opacity);
  }`;

构建threejs渲染环境

// 创建three环境
const container = document.getElementById('td-container');
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
camera.position.x = -3;
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
const lightGroup = new THREE.Group();
const ambientLight = new THREE.AmbientLight(0x404040);
lightGroup.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(-10, 6, 20);
lightGroup.add(directionalLight);
scene.add(light)
scene.background = new THREE.Color(0.9, 0.9, 0.9);

着色器使用

着色器是在图元材质中进行使用,分别对应vertexShader和fragmentShader,着色器中定义的变量通过uniforms传入。uniforms中的参数可以直接修改,threejs实例会监听uniforms的变量变化重新渲染图形。

// 相关的配置参数
const config = {
  width: 8,
  widthSegments: 8,
  color: 0xff91c2,
  opacity: 1,
  range: 0.5,
  speed: 0.1,
  seg: 6,
};

const geometry = new THREE.PlaneBufferGeometry(
  config.width,
  config.width,
  config.widthSegments,
  config.widthSegments,
);
// 使用自定义着色器构建材质
const material = new THREE.ShaderMaterial({
  uniforms: {
    uColor: { value: new THREE.Color(config.color) },
    uOpacity: { value: config.opacity },
    uRange: { value: config.range }, // 圆环的大小
    uSpeed: { value: config.speed }, // 扩散的速度
    uSge: { value: config.seg }, // 圆环个数
    uRadius: { value: config.width / 2 },
    time: { value: 0 },
  },
  // opacity:0.5,
  transparent: true,
  vertexShader: vertexShader, // 顶点着色器
  fragmentShader: fragmentShader, // 片元着色器
  side: THREE.DoubleSide,
});
// 将材质绘制到一个平面上
const model = new THREE.Mesh(geometry,material);
scene.add(model);
// 执行动画
function animate() {
  requestAnimationFrame(animate);
  // 控制时间参数变化让圆圈动起来
  material.uniforms['time'].value += 0.02;
  renderer.render(scene, camera);
}
animate();

说明:在一个plane上,绘制出扩散波纹的材质,控制参数的变化,使得同心环移动起来。可以尝试修改uniforms的参数看图形的变化。 这里其实变换的是每一帧像素的不同颜色达到波纹扩散移动的效果。效果如下:

参考资料:

WebGL教程

www.jianshu.com/p/5e0930089…