three.js 着色器学习笔记

着色器顾名思义就是给物体增加颜色的工具。我们也知道,创建物体时可以设置该物体的材质,材质决定了物体的外观。

其实使用着色器,就是给物体设置一种特殊的材质,这个材质是由着色器来决定显示效果。

下面以实际代码来展示不同材质的效果。

基本材质

function init () {
    // 创建渲染器
    const renderer = new THREE.WebGLRenderer({
        canvas: document.getElementById('main')
    })
    const width = window.innerWidth
    const height = window.innerHeight
    renderer.setClearColor(0xffffff)            
    renderer.setSize(width, height)

    // 创建场景
    const scene = new THREE.Scene()

    // 原点
    const origin = new THREE.Vector3(0, 0, 0)

    // 创建照相机
    const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000)
    camera.position.set(0, 10, 10)
    camera.lookAt(origin)
    scene.add(camera)

    // 创建光源
    const light = new THREE.DirectionalLight(0xffffff, 0.8)
    light.position.set(0, 10, 10)
    scene.add(light)

    // 创建基本材质的球体
    const baseSphere = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 20),
        new THREE.MeshBasicMaterial({
            color: 0xff0000
        })
    )
    scene.add(baseSphere)

    renderer.render(scene, camera)
}

init()

代码很简单,渲染出了一个基本材质MeshBasicMaterial的球体。

《three.js 着色器学习笔记》

Lambert材质

同样增加一个球体,其他参数相同仅仅是材质不同。

// 创建 Lambert 材质球体
const lambertSphere = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 20),
                                     new THREE.MeshLambertMaterial({
    color: 0xff0000
})
                                    )
// 将该球体放置在基本材质球体的左边
lambertSphere.position.set(-3, 0, 0)
scene.add(lambertSphere)

《three.js 着色器学习笔记》

在基本材质的左边可以看到一个新的小球,这就是 Lambert 材质的小球。

Phong材质

// 创建 Phong材质 材质球体
const phongSphere = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 20),
                                   new THREE.MeshPhongMaterial({
    color: 0xff0000
})
                                  )
phongSphere.position.set(3, 0, 0)
scene.add(phongSphere)

《three.js 着色器学习笔记》

除了上面三种外,还可以贴图。下面就开始进入正题,着色器,或者说着色器材质?

着色器

着色器分为顶点着色器与片元着色器,这里也不具体介绍,因为导师的书籍和博客都已经介绍得非常清楚了。

重点在这两句话:

  • 顶点着色器就是每个顶点调用一次的程序。在顶点着色器中,可以访问到顶点的三维位置、颜色、法向量等信息。可以通过修改这些值,或者将其传递到片元着色器中,实现特定的渲染效果。
  • 片元着色器就是每个片元调用一次的程序。在片元着色器中,可以访问到片元在二维屏幕上的坐标、深度信息、颜色等信息。通过改变这些值,可以实现特定的渲染效果。

OK,话不多说,再新建一个小球,这次先不设置材质,看看会怎么样。

// 没有材质的球体
const shaderSphere = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 20))
shaderSphere.position.set(0, 3, 0)
scene.add(shaderSphere)

《three.js 着色器学习笔记》

而且刷新会有一个随机的颜色,因为似乎默认使用了基本材质?

使用基本材质(BasicMaterial)的物体,渲染后物体的颜色始终为该材质的颜色,而不会由于光照产生明暗、阴影效果。如果没有指定材质的颜色,则颜色是随机的。

接下来就来添加材质。

// 新建着色器材质
    var material = new THREE.ShaderMaterial({
        vertexShader: `
            uniform vec3 color;
            uniform vec3 light;

            varying vec3 vColor;
            varying vec3 vNormal;
            varying vec3 vLight;

            void main()
            {
                // pass to fs
                vColor = color;
                vNormal = normalize(normalMatrix * normal);

                vec4 viewLight = viewMatrix * vec4(light, 1.0);
                vLight = viewLight.xyz;

                gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
            }`,
        fragmentShader: `
            varying vec3 vColor;
            varying vec3 vNormal;
            varying vec3 vLight;

            void main() {
                float diffuse = dot(normalize(vLight), vNormal);
                if (diffuse > 0.8) {
                    diffuse = 1.0;
                }
                else if (diffuse > 0.5) {
                    diffuse = 0.6;
                }
                else if (diffuse > 0.2) {
                    diffuse = 0.4;
                }
                else {
                    diffuse = 0.2;
                }

                gl_FragColor = vec4(vColor * diffuse, 1.0);
            }`,
        uniforms: {
            color: { 
                type: 'v3', // 指定变量类型为三维向量
                value: new THREE.Color('#ff0000') // 要传递给着色器的颜色值
            },
            light: {       
                type: 'v3',
                value: light.position // 光源位置
            }
        }
    })

这样就新建了一个材质,并且该材质的显示效果由vertexShaderfragmentShader两个着色器决定。

将该材质用在我们没有材质的小球上。

shaderSphere.material = material

再看看浏览器,发现和之前完全不同了。

《three.js 着色器学习笔记》

可以看到上面的球体颜色有很明显的变化,上面颜色浅下面深,因为光是从顶上照射下来,所以有这种效果,如果改变光照方向,效果也不一样。

实现了需要的效果没有太大意义,更需要的是理解为什么会有这种效果。

顶点着色器

uniform vec3 color;
uniform vec3 light;

varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vLight;

void main() {
    // pass to fs
    vColor = color;
    vNormal = normalize(normalMatrix * normal);

    vec4 viewLight = viewMatrix * vec4(light, 1.0);
    vLight = viewLight.xyz;

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

一开始看的时候也完全不明白写的是什么,以学习另一种语言的心理再来看就慢慢能够理解了。

uniformvarying就是和 js 中的 var、let一样,用来声明变量的。所以这段代码一开始就声明了五个变量

  • color
  • light
  • vColor
  • vNormal
  • vLight

使用uniform声明的变量将会从我们的 js 代码中传递过来,所以color就是#ff0000,light就是光源位置。

然后是main()函数,表示会执行这里面的代码,第一行vColor = color;很简单就是一个赋值操作。
接下来就复杂了。。。

vNormal = normalize(normalMatrix * normal);

虽然能知道是给vNormal变量赋值,但是normalizenormalMatrixnormal都是哪里来的?

回头看看之前说的重点,顶点着色器,也就是这段代码能够访问到顶点的三维位置、颜色、法向量等信息。normalMatrixnormal就是顶点信息,而normalize则是全局方法。

OK,现在知道变量是怎么来的,但是为啥要这么做呢?。。。。
竟无语凝噎,还是看导师的博客吧。

  • 算法理解

总之,就是得到了 vNormalvColorvLight这三个变量并传递给片元着色器。

片元着色器

varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vLight;

void main() {
    float diffuse = dot(normalize(vLight), vNormal);
    if (diffuse > 0.8) {
        diffuse = 1.0;
    }
    else if (diffuse > 0.5) {
        diffuse = 0.6;
    }
    else if (diffuse > 0.2) {
        diffuse = 0.4;
    }
    else {
        diffuse = 0.2;
    }

    gl_FragColor = vec4(vColor * diffuse, 1.0);
}

最为核心的代码就是main()函数中的第一句:

float diffuse = dot(normalize(vLight), vNormal);

调用normalize方法得到的结果,与vNormal作为参数再传入dot方法,最后得到的结果是片元的亮度值。

并且,将亮度值固定为1.00.60.40.2这四个值,所以,最终渲染出来的结果肯定只有四种颜色。

如果不将亮度值规定为四个值,而是直接使用,又是什么效果呢?

《three.js 着色器学习笔记》

结果和预想的一样,diffuse是一个0 ~ 1的值,并且是逐渐变化的。

总结

物体的外观是由材质决定的,three.js 中有提供现成的材质,也可以自己使用着色器来自定义材质。

着色器分为顶点着色器与片元着色器,这两种着色器其实本质上就是以另一种语言实现的函数方法。由于是另一种语言所以也是有自己的变量声明规则以及全局方法,同时 three.js 也能够传递变量给着色器。

重点在于着色器中能够直接取到关于物体的顶点信息与片元信息,正是借助这些信息才能够定制显示效果。

参考

本文仅作webgl学习技术参考,点击阅读原文

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注