分析构成模型对象的另一个重要元素:Material(材质)。

主要介绍:

  • Material的属性及WebGLRenderer的处理: 属性分类、预处理宏与自定义标记
  • 部分属性解读(Todo): 融合属性、深度测试、模板测试、裁剪、多边形偏移等

本文所参考的Three.js版本为0.116.1

Material

一个表示物体的Mesh对象需要Geometry与Material对象,前者用于设置该物体的顶点、面和法线信息,后者用于设置片元着色时一些渲染属性,影响color等的计算。

Material的属性从顶层数据对象的角度来看可以认为是一种标记,在创建WebGLProgram实例时中会根据这些标记决定是否在片元着色器中添加指定的GLSL代码定义,影响最终的着色效果。

属性

Material属性主要包含:

  • 基础属性: shading(平滑/平面着色)、wireframe(线框表示)、side、vertexColors、fog
  • 融合属性(如何与背景融合): blending、blendSrc、blendDst、blendEquation…
  • 深度测试属性: depthFunc、depthTest、depthWrite
  • 模板测试属性: stencilFunc、stencilWriteMask、stencilRef、stencilFuncMask…
  • 裁剪属性: clippingPlanes、clipIntersection、clipShadows
  • 其他属性: precision(shader精度)、polygonOffset(多边形偏移,处理z-fighting)、dithering(颜色抖动,获取额外颜色信息,处理锯齿)

WebGLRenderer对于材质属性的处理

看看在WebGL渲染器的着色器中是如何处理材质属性的。

GLSL预处理宏

在编写着色器时,可以通过GLSL的预处理宏来加载模块化的着色器代码:

  • #define: 定义预处理宏
  • #ifdef: 若存在#ifdef后的宏定义,则保留#ifdef/#endif间包含的宏代码
  • #if: 若满足#if后的条件,则保留#if/#endif间包含的宏代码

通过在代码中预加载部分代码(#ifdef),当需要使用这部分代码时通过在开头定义该宏(#define)来启用。

#define USE_COLOR;
#ifdef USE_COLOR
	vColor.xyz = color.xyz;
#endif

这段代码处理后会保留颜色插值的那一行:

vColor.xyz = color.xyz;

简易处理流程

  1. WebGLRenderer在初始化时会创建初始的GLContext
  2. 处理GL上下文时会根据参数生成WebGLProgram
  3. 构建WebGLProgram对象时根据传入的材质属性处理最终的着色器字符串

主要看一下第3步,即WebProgram中对于材质属性和着色器代码的处理。

以雾化属性fog为例,看看它的处理过程。 假设此时material的fog属性为true,在生成program实例前会将材质属性存放在一个parameters对象中:

// src/renderers/webgl/WebGLPrograms.js
function WebGLPrograms( renderer, extensions, capabilities ) {
	this.getParameters = function ( material, lights, shadows, scene, nClipPlanes, nClipIntersection, object ) {
		var fog = scene.fog;
		var parameters = {
			fog: !! fog,
			useFog: material.fog,
			fogExp2: ( fog && fog.isFogExp2 ),
			...
		}
		return parameters
	}
}

在program中,最终的着色器代码由两部分组成:宏定义前缀(prefix*)与着色器代码(*Shader),如下:

// src/renderers/webgl/WebGLProgram.js
function WebGLProgram( renderer, cacheKey, parameters ) {
	...
	var vertexGlsl = prefixVertex + vertexShader;
	var glVertexShader = WebGLShader( gl, gl.VERTEX_SHADER, vertexGlsl );
	gl.attachShader( program, glVertexShader );
}

GLSL宏定义与宏代码

来看下GLSL宏定义与宏代码的处理:

  1. 宏定义: prefixVertex/prefixFragment

    可以看到,根据属性在保存宏定义前缀的字符串变量中(prefixVertex/prefixFragment)中添加对应的宏定义(#define USE_FOG):

    // src/renderers/webgl/WebGLProgram.js
    function WebGLProgram( renderer, cacheKey, parameters ) {
        var prefixVertex, prefixFragment;
        ...
        // prefixFragment包含类似的操作
        prefixVertex = [
            ...
            ( parameters.useFog && parameters.fog ) ? '#define USE_FOG' : '',
            ( parameters.useFog && parameters.fogExp2 ) ? '#define FOG_EXP2' : ''
        ].filter( filterEmptyLine ).join( '\n' );
        // GLSL 3.0 conversion
        prefixVertex = [
            '#version 300 es\n',
            '#define attribute in',
            '#define varying out',
            '#define texture2D texture'
        ].join( '\n' ) + '\n' + prefixVertex;
    }
    

    ``

  2. 宏代码: ShaderChunk

    此时宏定义已经准备好了,那么还需要对应的宏代码。

    Three在src/renderers/shaders/ShaderChunk路径下放置了多个GLSL宏代码片段,并使用ShaderChunk对象来统一管理。

    // src/renderers/shaders/ShaderChunk.js
    import fog_vertex from './ShaderChunk/fog_vertex.glsl.js';
    import fog_pars_vertex from './ShaderChunk/fog_pars_vertex.glsl.js';
    import fog_fragment from './ShaderChunk/fog_fragment.glsl.js';
    import fog_pars_fragment from './ShaderChunk/fog_pars_fragment.glsl.js';
    ...
    export var ShaderChunk = {
        fog_vertex: fog_vertex,
        fog_pars_vertex: fog_pars_vertex,
        fog_fragment: fog_fragment,
        fog_pars_fragment: fog_pars_fragment,
        ...
    }
    

    ``

    这些片段均为为单一功能提供的变量声明与计算处理。以雾化属性的片元着色器宏代码为例:

    // fog_pars_fragment.glsl
    #ifdef USE_FOG
        uniform vec3 fogColor;
        varying float fogDepth;
        #ifdef FOG_EXP2
            uniform float fogDensity;
        #else
            uniform float fogNear;
            uniform float fogFar;
        #endif
    #endif
    // fog_fragment.glsl
    #ifdef USE_FOG
        #ifdef FOG_EXP2
            float fogFactor = 1.0 - exp( - fogDensity * fogDensity * fogDepth * fogDepth );
        #else
            float fogFactor = smoothstep( fogNear, fogFar, fogDepth );
        #endif
        gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );
    #endif
    

    ``

    关于这里的代码段命名规则,经观察后应该是使用{NAME}_{SHADER}表示main函数中代码,使用{NAME}_pars_{SHADER}表示开头的变量声明。

    现在我们找到了宏定义与宏代码,还有一个问题就是这些宏代码是如何并且在哪里被加载进来的。

  3. 预置着色方案对象: ShaderLib

    我们可以看到ShaderChunk提供的是细粒度的GLSL预处理代码,距离最终完整的shader代码还差很远、

    Three将预置的着色方案代码放在了src/renderers/shaders/ShaderLib路径下,这些代码也是使用ShaderChunk统一保存。 在外层使用了一个ShaderLib对象来保存这些预置的着色方案(以lambert光照模型的材质为例):

    // src/renderers/shaders/ShaderLib.js
    var ShaderLib = {
        ...
        lambert: {
            uniforms: mergeUniforms( [
                UniformsLib.common,
                UniformsLib.specularmap,
                UniformsLib.envmap,
                UniformsLib.aomap,
                UniformsLib.lightmap,
                UniformsLib.emissivemap,
                UniformsLib.fog,
                UniformsLib.lights,
                {
                    emissive: { value: new Color( 0x000000 ) }
                }
            ] ),
    
            vertexShader: ShaderChunk.meshlambert_vert,
            fragmentShader: ShaderChunk.meshlambert_frag
    
        },
        ...
    }
    

    ``

    可以看看ShaderChunk.meshlambert_vert中的内容:

    #define LAMBERT
    
    varying vec3 vLightFront;
    varying vec3 vIndirectFront;
    
    #ifdef DOUBLE_SIDED
        varying vec3 vLightBack;
        varying vec3 vIndirectBack;
    #endif
    
    #include <common>
    ...
    
    void main() {
        #include <fog_vertex>
        ...
    }
    
    

    `` 在这里通过#include引入的东东即为ShaderChunk路径下的GLSL宏代码。此时应该会产生一个问号,不要着急接着往下看。

#include

如果看过three中ShaderLib的着色器代码,会发现其中有大量#include <common>这样的类C语句。本意应该是加载公共代码库,但问题在于GLSL中并不支持#include语句,因此猜测是有一些额外的处理。

在WebGLProgram中接着往下看的话,会发现有几行这样的代码:

vertexShader = resolveIncludes( vertexShader );
vertexShader = replaceLightNums( vertexShader, parameters );
vertexShader = replaceClippingPlaneNums( vertexShader, parameters );

fragmentShader = resolveIncludes( fragmentShader );
fragmentShader = replaceLightNums( fragmentShader, parameters );
fragmentShader = replaceClippingPlaneNums( fragmentShader, parameters );

这些函数均是使用正则处理的一些在GLSL中添加的自定义标记,看下resolveIncludes()函数就明白了:

// Resolve Includes
var includePattern = /^[ \t]*#include +<([\w\d./]+)>/gm;
function resolveIncludes( string ) {
	return string.replace( includePattern, includeReplacer );
}
function includeReplacer( match, include ) {
	var string = ShaderChunk[ include ];
	if ( string === undefined ) {
		throw new Error( 'Can not resolve #include <' + include + '>' );
	}
	return resolveIncludes( string );
}

其中ShaderChunk对象就是之前提到的包含多种GLSL宏代码片段的对象。这样一来,如果在shader中写了这样一行:

#include <common>

那么通过正则会滤出common,并在ShaderChunk对象中根据该键寻找到对应的代码段(ShaderChunk['common']),在此位置用该代码段替换该语句。

这下就一目了然了,#include是为了方便代码添加与管理的一种自定义标记,而不是能让GPU直接处理的命令,虚惊一场。

属性解读

Todo。通过一个扩展的自定义材质来测试这些属性的效果

融合属性

深度测试

模板测试

裁剪属性

多边形偏移

其他

光照贴图类

  • aomap: AO贴图
  • envMap: 环境贴图
  • emissiveMap: 自发光贴图
  • bumpMap: 凹凸贴图
  • normalMap: 法线贴图

参考