05-mini版材质

源码:github.com/buglas/thre...

我们要理解Threejs材质的架构逻辑,可以先通过一个简单的材质示例来理解。

1-创建材质

1-1-示例简介

创建MeshMiniMaterial 材质,只要能渲染一个纯色三角形即可,其余全不考虑,比如深度检测、贴图、灯光等。

1-2-meshmini.glsl.js

直接下载three.js 源码,在里面写代码。

新建文件:src/renderers/shaders/ShaderLib/meshmini.glsl.js

ini 复制代码
export const vertex = /* glsl */`
void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`;
​
export const fragment = /* glsl */`
uniform vec3 diffuse;
void main() {
    gl_FragColor = vec4( diffuse, 1.0 );
}
`;
​

vertex 是顶点着色器;fragment 是片元着色器。

ShaderLib 文件夹中存储的是着色器文件。

1-3-接入 ShaderLib

1.在ShaderChunk.js 中注册meshmini 的vertex 和fragment 。

javascript 复制代码
import * as meshmini from './ShaderLib/meshmini.glsl.js';
​
export const ShaderChunk = {
    ...
    meshmini_vert: meshmini.vertex,
    meshmini_frag: meshmini.fragment,
};

ShaderChunk 是 Three.js 着色器代码片段的中央注册表。

2.在ShaderLib.js 中增加条目。

css 复制代码
const ShaderLib = { 
    ...
    mini: {
        uniforms: {
            diffuse: { value: /*@__PURE__*/ new Color( 0xffffff ) }
        },
        vertexShader: ShaderChunk.meshmini_vert,
        fragmentShader: ShaderChunk.meshmini_frag
    },
}

ShaderLib 是着色器库,其中的键,如mini,对应了Material.type。

ShaderLib 的uniforms中定义的是uniform 变量,MeshMiniMaterial 只用到了diffuse。

3.在WebGLPrograms.js 的 shaderIDs 映射中添加MeshMiniMaterial。

ini 复制代码
const shaderIDs = {
    ...
    MeshMiniMaterial: 'mini',
};

MeshMiniMaterial 对应材质类型Material.type,所以通过Material.type可以从shaderIDs 中获取mini,然后再通过mini 从ShaderLib 中获取相应的着色器。

1-4-创建MeshMiniMaterial

新建 src/materials/MeshMiniMaterial.js

kotlin 复制代码
import { Material } from './Material.js';
import { Color } from '../math/Color.js';
​
/**
 * A minimal material.
 * Not affected by lights, fog, or textures.
 *
 * @augments Material
 */
class MeshMiniMaterial extends Material {
    constructor( parameters ) {
        super();
        this.isMeshMiniMaterial = true;
        this.type = 'MeshMiniMaterial';
​
        /**
         * Color of the material. Maps to the `diffuse` uniform in meshmini.glsl.js.
         *
         * @type {Color}
         * @default (1,1,1)
         */
        this.color = new Color( 0xffffff );
​
        /**
         * MeshMiniMaterial has no fog support in its shader.
         *
         * @type {boolean}
         * @default false
         */
        this.fog = false;
        this.setValues( parameters );
    }
​
    copy( source ) {
        super.copy( source );
        this.color.copy( source.color );
        this.fog = source.fog;
        return this;
    }
}
​
export { MeshMiniMaterial };

1-5-更新WebGLMaterials的uniform

在WebGLMaterials.js 中的refreshMaterialUniforms 方法中,根据MeshMiniMaterial材质更新相应的uniform 变量。

scss 复制代码
function refreshMaterialUniforms( uniforms, material, pixelRatio, height, transmissionRenderTarget ) {
    if ( material.isNodeMaterial ) {
    ...
    
    } else if ( material.isMeshMiniMaterial ) {
        uniforms.diffuse.value.copy( material.color );
    }
}

1-6- JSON 转Material

在src/loaders/MaterialLoader.js 中添加MeshMinMaterial 。

scala 复制代码
import { MeshMinMaterial } from '../materials/MeshMinMaterial.js';
class MaterialLoader extends Loader {
    ...
    static createMaterialFromType( type ) {
        const materialLib = {
            ...
            MeshMiniMaterial
        };
        return new materialLib[ type ]();
    }
}

MaterialLoader.js 是 Three.js 的材质反序列化加载器,负责把 JSON 格式的材质数据还原成运行时 Material 对象。它与 Material.toJSON() 配对,是场景/资源导入链路中的一环。

1-7-实例化MeshMiniMaterial

新建 examples/webgl_material_mini.html 文件。

ini 复制代码
scene = new THREE.Scene();
​
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array( [
    - 1.0, - 1.0, 0.0,
    1.0, - 1.0, 0.0,
    0.0, 1.0, 0.0
] );
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
​
const material = new THREE.MeshMiniMaterial( { color: 0x0000ff } );
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );

配置examples/files.json

json 复制代码
{
    "webgl": [
        ...
        "webgl_material_mini"
    ]
}
​

运行项目

arduino 复制代码
npm ci
nom run start

打开webgl_material_mini.html,效果如下:

1-8-总结

meshmini.glsl.js 中定义了vertex 和fragment 。

在ShaderChunk 中集中注册着色器。

在ShaderLib 中合成具体材质所需的着色器,并定义fragment 所需的uniform变量。

MeshMiniMaterial 通过WebGLPrograms.js 中的shaderIDs 与着色器相关联。

2-chunk

chunk 是着色器的代码块。

three.js 中有很多代码是共用的,就像vue里的公共组件一样,所以就有了chunk。

chunk 可以被three.js 拼合成着色器代码。

我们以上节课的MeshMiniMaterial 材质为例,举个chunk 的例子。

2-1-chunk 示例

在src/renderers/shaders/ShaderLib/meshmini.glsl.js 的fragment 中引入一个chunk。

arduino 复制代码
export const fragment = /* glsl */`
uniform vec3 diffuse;
void main() {
    gl_FragColor = vec4( diffuse, 1.0 );
    #include <colorspace_fragment>
}
`;

#include <> 是three.js 自定义的着色器编写规则,它可以引入外部定义的chunk。

#include <colorspace_fragment> 中的colorspace_fragment 就是一个chunk。

<colorspace_fragment> 的作用是把 gl_FragColor 从线性色域转换到渲染器输出色域(通常是 sRGB)。

<colorspace_fragment> 对应colorspace_fragment.glsl.js 文件。

ini 复制代码
export default /* glsl */`
    gl_FragColor = linearToOutputTexel( gl_FragColor );
`;

linearToOutputTexel 是由 WebGLProgram.js 在编译前动态注入到片元 shader 前缀里的。

scss 复制代码
ShaderChunk[ 'colorspace_pars_fragment' ],
getTexelEncodingFunction( 'linearToOutputTexel', parameters.outputColorSpace ),

最终,效果如下:

此三角形比上节课的三角形的颜色浅,这就是colorspace_fragment 的作用。

在当前示例中有2个重点:

  • #include 语法
  • shader 前缀

接下来我们对其进行详解。

2-2-#include 语法

WebGL 的GLSL 无法从外部引入GLSL ,所以Three.js 在字符串中用#include 模拟像C 语言那样的#include 预处理能力

在WebGLProgram.js 中可以看到#include 的解析方法。

scss 复制代码
function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
    //...
    fragmentShader = resolveIncludes( fragmentShader );
​
}
php 复制代码
// Resolve Includes
const includePattern = /^[ \t]*#include +<([\w\d./]+)>/gm;
function resolveIncludes( string ) {
    return string.replace( includePattern, includeReplacer );
}
const shaderChunkMap = new Map();
function includeReplacer( match, include ) {
    let string = ShaderChunk[ include ];
    if ( string === undefined ) {
        const newInclude = shaderChunkMap.get( include );
        if ( newInclude !== undefined ) {
            string = ShaderChunk[ newInclude ];
            warn( 'WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.', include, newInclude );
        } else {
            throw new Error( 'Can not resolve #include <' + include + '>' );
        }
    }
    return resolveIncludes( string );
}

正则 includePattern

arduino 复制代码
/^[ \t]*#include +<([\w\d./]+)>/gm
部分 含义
^[ \t]* 行首,允许前导空格/Tab
#include + 字面量 #include 加至少一个空格
<([\w\d./]+)> 尖括号内的 chunk 名,捕获为 include
g 全局替换
m 多行模式,使 ^ 匹配每一行行首

能匹配例如:

arduino 复制代码
#include <common>
#include <uv_pars_vertex>

不能匹配 #include "common"(Three.js 只用尖括号形式)。

resolveIncludes(string)

return string.replace( includePattern, includeReplacer );

对整段 shader 做一次扫描,每个 #include 都交给 includeReplacer 处理。

includeReplacer(match, include)

根据include,从ShaderChunk中取chunk,即ShaderChunk include

ShaderChunksrc/renderers/shaders/ShaderChunk.js,汇总了所有chunk。

关键:return resolveIncludes( string ) --- 对被插入的 chunk 再递归 做 include 解析

fragment

在上例中,#include <colorspace_fragment> 被替换后,fragment 如下:

ini 复制代码
uniform vec3 diffuse;
void main() {
    gl_FragColor = vec4( diffuse, 1.0 );
    gl_FragColor = linearToOutputTexel( gl_FragColor );
}

在当前的fragment 中并没有linearToOutputTexel 方法,这是因为fragment 还会引入shader 前缀。

2-3-shader 前缀

对于非 RawShaderMaterial 的着色器,会将版本信息、shader 前缀与着色器拼在一起。

在WebGLProgram.js 中可以看到相关代码。

ini 复制代码
prefixFragment = [
    ...
    ShaderChunk[ 'colorspace_pars_fragment' ], // this code is required here because it is used by the various encoding/decoding function defined below
    getTexelEncodingFunction( 'linearToOutputTexel', parameters.outputColorSpace ),
].filter( filterEmptyLine ).join( '\n' );
...
const vertexGlsl = versionString + prefixVertex + vertexShader;
const fragmentGlsl = versionString + prefixFragment + fragmentShader;

ShaderChunk 'colorspace_pars_fragment' 对应colorspace_pars_fragment.glsl.js 文件

scss 复制代码
export default /* glsl */`
vec4 LinearTransferOETF( in vec4 value ) {
    return value;
}
​
vec4 sRGBTransferEOTF( in vec4 value ) {
    return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a );
}
​
vec4 sRGBTransferOETF( in vec4 value ) {
    return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );
}
`;

getTexelEncodingFunction 会根据 renderer.outputColorSpace 生成特定的着色代码。

javascript 复制代码
function getTexelEncodingFunction( functionName, colorSpace ) {
    const components = getEncodingComponents( colorSpace );
    return [
        `vec4 ${functionName}( vec4 value ) {`,
        `   return ${components[ 1 ]}( vec4( value.rgb * ${components[ 0 ]}, value.a ) );`,
        '}',
    ].join( '\n' );
}

当renderer.outputColorSpace = THREE.SRGBColorSpace 时,会生成类似代码:

scss 复制代码
vec4 linearToOutputTexel( vec4 value ) {
    return sRGBTransferOETF( vec4( value.rgb * mat3(...), value.a ) );
}

总结

three.js中的着色器是通过#include <> 规则,将多个chunk 拼合而成的。

顶点着色器和片元着色器的构成如下:

ini 复制代码
const vertexGlsl = versionString + prefixVertex + vertexShader;
const fragmentGlsl = versionString + prefixFragment + fragmentShader;

这一章我们主要说的chunk在Material 中的架构原理,我们并没有细说着色器的内容,这个我们会作为另一快内容详解。

相关推荐
学以智用1 小时前
HTTP 请求头 Headers 完整详解
前端·后端
李伟_Li慢慢1 小时前
03-threejs架构原理浅析
前端·three.js
cidy_981 小时前
Codex、Hermes、Claude 使用共享知识库的对比
前端
牧艺1 小时前
浏览器 3D 交互实战:射线拾取、描边高亮与业务面板联动
前端·three.js·数据可视化
叉歪1 小时前
为 UnoCSS 扩展无限色阶与主题换色
前端·css
jiayong231 小时前
第 44 课:任务详情抽屉里的评论输入与操作记录联动
前端
ndsc_d1 小时前
前端实战复盘:用AI直接生成响应式官网落地页与React源码
前端·人工智能·react.js·ui·前端框架·aigc·paico
大家的林语冰2 小时前
✌️ Deno 2.9 来了,ESM 直接导入 CSS,甚至能开发桌面应用?!
前端·javascript·node.js
李顿波3 小时前
前端样式技术发展进程和解析(长期更新版)
前端