1 前言

在three.js已经预置了很多材质,比如MeshStandardMaterial、MeshPhongMaterial、MeshLambertMaterial等等,这些预置的材质其实在内部使用了Shader已经为我们创建了固定的渲染管线,我们可以直接使用而不用再去重新编写Shader重新实现这些渲染效果。

对熟悉OpenGL的特别是熟悉OpenGL3.3之后版本的可编程渲染管线的同学们来说,Shader不是一个陌生的东西。在图形学算法实现的过程中,我们就是需要编写一条一条冰冷的Shader代码来构建绚丽的渲染效果。如果涉及到性能调优,更需要在Shader层面对代码进行优化以达到更少的计算复杂度达到差不多的渲染效果。

2 Three.js中的Shader

本文以下的示例全部基于three.js r144版本。

2.1 ShaderMaterial和RawShaderMaterial

Three.js中提供两种自定义Shader的方式,一种是ShaderMaterial,另一种则是RawShaderMaterial

ShaderMaterial是Three.js将一些内置的attributes和uniforms已经添加到了Shader中,添加的内置变量如下,也可参考:https://threejs.org/docs/#api/zh/renderers/webgl/WebGLProgram

顶点着色器中内置的uniforms

// 模型矩阵 object.matrixWorld
uniform mat4 modelMatrix;

// 模型视图矩阵 camera.matrixWorldInverse * object.matrixWorld
uniform mat4 modelViewMatrix;

// 投影矩阵 camera.projectionMatrix
uniform mat4 projectionMatrix;

// 视图矩阵 camera.matrixWorldInverse
uniform mat4 viewMatrix;

// 模型视图矩阵的逆转置矩阵 inverse transpose of modelViewMatrix
uniform mat3 normalMatrix;

// 世界坐标下的摄像机坐标 camera position in world space
uniform vec3 cameraPosition;

顶点着色器中内置的attributes

attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

片段着色器中内置的attributes

uniform mat4 viewMatrix;
uniform vec3 cameraPosition;

而与ShaderMaterial相比,RawShaderMaterial不会将上述预置的uniforms和attributes自动添加到Shader中,而是需要我们自己去定义,这两种方式的区别会造成我们最后写出来的Shader不一样。

2.2 顶点着色器Vertex Shader和片段着色器Fragment Shaders

与OpenGL中一样,我们可以在Three.js中为ShaderMaterialRawShaderMaterial都指定两个Shader,一个是顶点着色器Vertex Shader,另一个是片段着色器Fragment Shaders,简而言之,顶点着色负责模型顶点数据变换,片段着色器负责模型颜色生成。

在Three.js中,Shader有三种类型的变量:uniform, attribute和varying。

  • uniform是所有顶点都具有相同的值的变量。 比如灯光,雾,和阴影贴图就是被储存在uniforms中的数据。 uniforms可以通过顶点着色器和片元着色器来访问。
  • attribute与每个顶点关联的变量。例如,顶点位置,法线和顶点颜色都是存储在attributes中的数据。attributes 只可以在顶点着色器中访问。
  • varying是从顶点着色器传递到片元着色器的变量。对于每一个片元,每一个varying的值将是相邻顶点值的平滑插值。

2.3 uniform与Javascript的类型对应

GLSL uniform Javascript size
float Number 1
vec2 THREE.Vector2 2
vec3 THREE.Vector3 3
vec3 THREE.Color 3
vec4 THREE.Vector4 4

2.4 精度设置

如果在three.js中使用RawShaderMaterial,我们需要设置默认数据类型的精度,如果不指定则会在编译Shader的时候报错,或者顶点着色器与片段着色器某个数据类型精度不一致也会报错,比如

ThreeJS – 使用自定义Shader-StubbornHuang Blog

上述图片显示的就是没有指定float精度的错误。

我们可以通过highp、mediump、lowp三个关键字对应精度的高、中、低,

顶点着色器默认精度

precision highp float;
precision highp int;
precision lowp sampler2D;
precision lowp samplerCube;

片元着色器默认精度

precision mediump int;
precision lowp sampler2D;
precision lowp samplerCube;

3 Shader使用示例

3.1 ShaderMaterial和RawShaderMaterial使用的差异

我们在这个示例中主要想实现以内置的THREE.BoxGeometry的法向量作为片段着色器着色的颜色,效果如下图所示

ThreeJS – 使用自定义Shader-StubbornHuang Blog

ShaderMaterial的示例

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>three.js shader test</title>
    <style type="text/css">
        html,body {
            margin: 0;
            height: 100%;
            overflow: hidden;
        }
    </style>
</head>
<body>
<script src="js/three.js-r144/build/three.js"></script>
<script src="js/three.js-r144/examples/js/controls/OrbitControls.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
    varying vec3 vNormal;

    void main() {
        vNormal = normal;
        vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
        gl_Position = projectionMatrix * modelViewPosition;
    }
</script>

<script id="fragmentShader" type="x-shader/x-fragment">

      varying vec3 vNormal;

      void main() {
           gl_FragColor = vec4(vNormal,1.0);
      }
</script>
<script>
    let scene,camera,renderer,controls

    function init() {
        // 创建绘制上下文
        renderer = new THREE.WebGLRenderer({
            antialias:true,
            //alpha:true
        });
        renderer.setPixelRatio( window.devicePixelRatio );
        renderer.setSize(window.innerWidth,window.innerHeight);
        renderer.gammaOutput = true;
        renderer.shadowMap.enabled = true;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.outputEncoding = THREE.sRGBEncoding
        renderer.toneMappingExposure = 2.2;
        document.body.appendChild(renderer.domElement)

        // 创建场景
        scene = new THREE.Scene();
        //scene.background = new THREE.Color(0xFFdddd);

        // 创建摄像头
        camera = new THREE.PerspectiveCamera(60,window.innerWidth/window.innerHeight,0.1,5000);
        camera.position.set(0,5,5);
        controls = new THREE.OrbitControls(camera,renderer.domElement);

        // 创建灯光
        //// 半球光
        hemiLight = new THREE.HemisphereLight(0xffeeb1, 0x080820, 1);
        scene.add(hemiLight)

        // 创建几何体
        const geometry = new THREE.BoxGeometry(1, 1, 1)
        uniforms = {
            colorB: {type: 'vec3', value: new THREE.Color(0xACB6E5)},
            colorA: {type: 'vec3', value: new THREE.Color(0x74ebd5)}
        };

        // 创建自定义shader
        const material = new THREE.ShaderMaterial( {
            uniforms: uniforms,
            vertexShader: document.getElementById( 'vertexShader' ).textContent,
            fragmentShader: document.getElementById( 'fragmentShader' ).textContent

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

        window.addEventListener( 'resize', onWindowResize, false );
    }

    function animate() {
        requestAnimationFrame(animate)
        controls.update()
        renderer.render(scene,camera)

    }

    init();
    animate()


    function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize( window.innerWidth, window.innerHeight );

    }

</script>
</body>
</html>

RawShaderMaterial的示例

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>three.js shader test</title>
    <style type="text/css">
        html,body {
            margin: 0;
            height: 100%;
            overflow: hidden;
        }
    </style>
</head>
<body>
<script src="js/three.js-r144/build/three.js"></script>
<script src="js/three.js-r144/examples/js/controls/OrbitControls.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
    attribute vec3 position;
    attribute vec3 normal;

    uniform mat4 projectionMatrix;
    uniform mat4 modelViewMatrix;

    varying vec3 vNormal;

    void main() {
        vNormal = normal;
        vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
        gl_Position = projectionMatrix * modelViewPosition;
    }
</script>

<script id="fragmentShader" type="x-shader/x-fragment">
      precision highp float;

      varying vec3 vNormal;

      void main() {
           gl_FragColor = vec4(vNormal,1.0);
      }
</script>
<script>
    let scene,camera,renderer,controls

    function init() {
        // 创建绘制上下文
        renderer = new THREE.WebGLRenderer({
            antialias:true,
            //alpha:true
        });
        renderer.setPixelRatio( window.devicePixelRatio );
        renderer.setSize(window.innerWidth,window.innerHeight);
        renderer.gammaOutput = true;
        renderer.shadowMap.enabled = true;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.outputEncoding = THREE.sRGBEncoding
        renderer.toneMappingExposure = 2.2;
        document.body.appendChild(renderer.domElement)

        // 创建场景
        scene = new THREE.Scene();
        //scene.background = new THREE.Color(0xFFdddd);

        // 创建摄像头
        camera = new THREE.PerspectiveCamera(60,window.innerWidth/window.innerHeight,0.1,5000);
        camera.position.set(0,5,5);
        controls = new THREE.OrbitControls(camera,renderer.domElement);

        // 创建灯光
        //// 半球光
        hemiLight = new THREE.HemisphereLight(0xffeeb1, 0x080820, 1);
        scene.add(hemiLight)

        // 创建几何体
        const geometry = new THREE.BoxGeometry(1, 1, 1)
        uniforms = {
            colorB: {type: 'vec3', value: new THREE.Color(0xACB6E5)},
            colorA: {type: 'vec3', value: new THREE.Color(0x74ebd5)}
        };

        // 创建自定义shader
        const material = new THREE.RawShaderMaterial( {
            uniforms: uniforms,
            vertexShader: document.getElementById( 'vertexShader' ).textContent,
            fragmentShader: document.getElementById( 'fragmentShader' ).textContent

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

        window.addEventListener( 'resize', onWindowResize, false );
    }

    function animate() {
        requestAnimationFrame(animate)
        controls.update()
        renderer.render(scene,camera)

    }

    init();
    animate()


    function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize( window.innerWidth, window.innerHeight );

    }

</script>
</body>
</html>

通过上述ShaderMaterial和RawShaderMaterial程序代码的不同,由于ShaderMaterial已经内置了一些uniforms和attributes,所以不需要对一些常用的变量进行设置便可以使用,而RawShaderMaterial则需要自己设置数据类型精度以及投影视图矩阵等等才能实现相同的效果,不过RawShaderMaterial有利于实现自由度更高的Shader,不必拘束于three.js的内置变量。

3.2 将图片传入到Shader中作为模型贴图

首先我们需要使用THREE.TextureLoader加载图片,然后将图片作为uniform传递给片段着色器进行模型表面贴图,基本上和OpenGL中一摸一样的流程,完整的代码如下:

这里我们使用ShaderMaterial,

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>three.js shader test</title>
    <style type="text/css">
        html,body {
            margin: 0;
            height: 100%;
            overflow: hidden;
        }
    </style>
</head>
<body>
<script src="js/three.js-r144/build/three.js"></script>
<script src="js/three.js-r144/examples/js/controls/OrbitControls.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
    varying vec3 vNormal;
    varying vec2 vUV;

    void main() {
        vUV = uv;
        vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
        gl_Position = projectionMatrix * modelViewPosition;
    }
</script>

<script id="fragmentShader" type="x-shader/x-fragment">
      varying vec2 vUV;
      uniform sampler2D uTexture;


      void main() {
           vec4 mapColor = texture2D(uTexture, vUV);
           gl_FragColor = mapColor;
      }
</script>
<script>
    let scene,camera,renderer,controls

    function init() {
        // 创建绘制上下文
        renderer = new THREE.WebGLRenderer({
            antialias:true,
            //alpha:true
        });
        renderer.setPixelRatio( window.devicePixelRatio );
        renderer.setSize(window.innerWidth,window.innerHeight);
        renderer.gammaOutput = true;
        renderer.shadowMap.enabled = true;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.outputEncoding = THREE.sRGBEncoding
        renderer.toneMappingExposure = 2.2;
        document.body.appendChild(renderer.domElement)

        // 创建场景
        scene = new THREE.Scene();
        //scene.background = new THREE.Color(0xFFdddd);

        // 创建摄像头
        camera = new THREE.PerspectiveCamera(60,window.innerWidth/window.innerHeight,0.1,5000);
        camera.position.set(0,5,5);
        controls = new THREE.OrbitControls(camera,renderer.domElement);

        // 创建灯光
        //// 半球光
        hemiLight = new THREE.HemisphereLight(0xffeeb1, 0x080820, 1);
        scene.add(hemiLight)

        // 创建几何体
        const geometry = new THREE.BoxGeometry(1, 1, 1)

        // 创建自定义shader
        const textureLoader = new THREE.TextureLoader();
        const texture = textureLoader.load('./texture.jpg')
        uniforms = {
            uTexture: {
                value: texture
            }
        };
        const material = new THREE.ShaderMaterial( {
            uniforms: uniforms,
            vertexShader: document.getElementById( 'vertexShader' ).textContent,
            fragmentShader: document.getElementById( 'fragmentShader' ).textContent
        } );
        const mesh = new THREE.Mesh( geometry, material );
        scene.add( mesh );

        window.addEventListener( 'resize', onWindowResize, false );
    }

    function animate() {
        requestAnimationFrame(animate)
        controls.update()
        renderer.render(scene,camera)

    }

    init();
    animate()


    function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize( window.innerWidth, window.innerHeight );

    }

</script>
</body>
</html>

贴图文件如下:

ThreeJS – 使用自定义Shader-StubbornHuang Blog

three.js效果如下:

ThreeJS – 使用自定义Shader-StubbornHuang Blog

参考链接