Shader & Program

在之前的文章中我们已经了解了 State 全局状态机,StateSet 状态集以及 StateGraph 之间的关系,也对整个渲染流程有了大致了解。 在这篇文章中我们将深入底层核心模块 Program & Shader 的创建。

State 的初始化

在 Render 初始化时,会向全局状态机 State 传入一个 Shader 生成器 Proxy:

// Render.js
this._state = new State(new osgShader.ShaderGeneratorProxy());

在这个 Proxy 中,内置了一组 Shader 生成器,这里仅列举默认和阴影两个:

var ShaderGeneratorProxy = function() {
    // object of shader generators
    this._generators = {};
    this.addShaderGenerator('default', new ShaderGenerator());
    this.addShaderGenerator('ShadowCast', new ShadowCastShaderGenerator());
}

在上一篇中提到 RenderLeaf 的 render 方法会调用全局状态机 State 的 applyStateSet 方法:

// State.js
applyStateSet: function(stateset) {
    var previousProgram = this.getLastProgramApplied();

    // 取得 Shader 生成器
    this._currentShaderGenerator = this.getCurrentShaderGeneratorStateSet(stateset);

    // 应用属性
    this._applyAttributeMapStateSet(this._attributeArray, stateset._attributeArray);
    this._applyTextureAttributeMapListStateSet(
        this._textureAttributeArrayList,
        stateset._textureAttributeArrayList
    );

    var lastApplied;
    if (this._currentShaderGenerator) {
        // 创建或者直接获取 Program
        var generatedProgram = this._currentShaderGenerator.getOrCreateProgram(this);
        this.applyAttribute(generatedProgram);
        lastApplied = generatedProgram;

        // will cache uniform and apply them with the program
        this._applyGeneratedProgramUniforms(generatedProgram, stateset);
    }
},

为了避免连续重复应用相同的状态改变,需要记录上一次应用的状态:

_applyAttributeStack: function(attribute, attributeStack) {
    if (attributeStack._lastApplied === attribute) return false;

    // 实际应用
    if (attribute.apply) attribute.apply(this);

    attributeStack._lastApplied = attribute;
    return true;
},

创建 Program

这里可以看出 OSG 中十分注意利用闭包缓存临时变量,减少对象分配。 值得注意的是,如果全局状态机中的状态没有改变,就不应该重新生成 Program。 这就要求对 State 生成 hash,只有当缓存中没有与之对应的 Program 才需要重新生成:

// ShaderGenerator.js
getOrCreateProgram: (function() {
    // 缓存
    var textureAttributes = [];
    var attributes = [];

    return function(state) {
        // 根据 attribute 生成 hash
        var hash =
            this.getActiveAttributeListCache(state) +
            this.getActiveTextureAttributeListCache(state);

        // cache hit 直接返回
        var cachedProgram = this._getProgram(hash, state, attributes, textureAttributes);
        if (cachedProgram !== undefined) {
            return cachedProgram;
        }

        // cache miss 重新生成
        var program = this._createProgram(hash, state, attributes, textureAttributes);

        return program;
    };
})(),

根据属性生成 hash

那么如何保证相同的状态属性生成 hash 不变,并且不受不同顺序的影响呢? 这里就需要预先规定所有可能出现属性的顺序,才能保证最终拼接而成的 hash 一致:

// ShaderGenerator.js
getActiveAttributeListCache: function(state) {
    var hash = '';

    // 按固定顺序排列的 type list,存储在 Compiler 中
    var cacheType = this._ShaderCompiler._validAttributeTypeMemberCache;
    for (var i = 0, l = cacheType.length; i < l; i++) {
        var type = cacheType[i];
        var attributeStack =
            type < state._attributeArray.length ? state._attributeArray[type] : undefined;
        // 只有 type list 中的属性才生成 hash
        if (attributeStack) {
            var attribute = attributeStack._lastApplied;
            if (!attribute || this.filterAttributeTypes(attribute)) continue;

            // 例如 Light0DIRECTIONtrueMaterial
            hash += attributeStack._lastApplied.getHash();
        }
    }

    return hash;
},

这里是 Compiler 中初始化的属性顺序列表,可见 OSG 中最多支持 8 个光源:

// Compiler.js
Compiler.setStateAttributeConfig(Compiler, {
    attribute: [
        'Light0',
        'ShadowReceive0',
        'Light1',
        'ShadowReceive1',
        'Light2',
        'ShadowReceive2',
        'Light3',
        'ShadowReceive3',
        'Light4',
        'Light5',
        'Light6',
        'Light7',
        'Material',
        'PointSize',
        'Billboard',
        'Morph',
        'Skinning'
    ],
    textureAttribute: ['Texture']
});

我们以 Light 属性为例,添加一个平行光后,生成的 hash 就是 Light0DIRECTIONtrue

// Light.js
getHash: function() {
    if (!this._dirtyHash) return this._hash;

    this._hash = this._computeInternalHash();
    this._dirtyHash = false;
    return this._hash;
},
_computeInternalHash: function() {
    // Light0DIRECTIONtrue
    return this.getTypeMember() + this.getLightType() + this.isEnabled().toString();
},

创建 Compiler

这里有一个小问题 getActiveAttributeList() 内部仍旧进行了上述 hash 的拼接,但 OSG 中并没有使用到,其实是多余的操作。

_createProgram: function(hash, state, attributes, textureAttributes) {
    attributes.length = 0;
    textureAttributes.length = 0;

    // 获得属性
    this.getActiveAttributeList(state, attributes);
    this.getActiveTextureAttributeList(state, textureAttributes);

    // 创建编译器,传入属性
    var ShaderCompiler = this._ShaderCompiler;
    var shaderGen = new ShaderCompiler(attributes, textureAttributes, this._shaderProcessor);

    // 生成 vs & fs,创建 Program
    var fragmentshader = shaderGen.createFragmentShader();
    var vertexshader = shaderGen.createVertexShader();
    var program = new Program(
        new Shader(Shader.VERTEX_SHADER, vertexshader),
        new Shader(Shader.FRAGMENT_SHADER, fragmentshader)
    );

    program.hash = hash;
    // 设置 Uniforms
    program.setActiveUniforms(this.getActiveUniforms(state, attributes, textureAttributes));
    program.generated = true;
    // 放入缓存
    this._cache[hash] = program;

    program.apply(state);

    return program;
},

首先来看 Compiler 的创建,对于传入的属性列表进行了分类:

// Compiler.js
initAttributes: function() {
    var attributes = this._attributes;
    var lights = this._lights;
    var shadows = this._shadows;
    for (var i = 0, l = attributes.length; i < l; i++) {
        var type = attributes[i].className();

        if (type === 'Light') {
            lights.push(attributes[i]);
        } else if (type === 'Material') {
            this._material = attributes[i];
        } else if (type === 'ShadowReceiveAttribute') {
            shadows.push(attributes[i]);
        } // 省略其他类型的属性
    }
},

对于纹理属性的初始化也是类似,这里就省略了。

编译需要构建语法树,其中委托了 nodeFactory 工厂进行节点的创建工作:

// Compiler.js
getNode: function(/*name, arg1, etc*/) {
    var n = factory.getNode.apply(factory, arguments);
    var cacheID = n.getID();
    this._activeNodeMap[cacheID] = n;
    return n;
},

而 nodeFactory 本身是一个单例,会根据节点名称调用对应的构造函数(注册在 _nodes 中):

// nodeFactory.js
getNode: function(name) {
    // _nodes: Map
    var Constructor = this._nodes.get(name);
    var instance = window.Object.create(Constructor.prototype);
    Constructor.apply(instance, Array.prototype.slice.call(arguments, 1));
    return instance;
}

nodeFactory 在初始化时注册了一系列内置:

// nodeFactory.js
var Factory = function() {
    this._nodes = new window.Map();

    this.extractFunctions(shaderLib, 'lights.glsl');
    this.extractFunctions(shaderLib, 'lightCommon.glsl');
    this.extractFunctions(shaderLib, 'skinning.glsl');
    this.extractFunctions(shaderLib, 'morphing.glsl');
    this.extractFunctions(shaderLib, 'billboard.glsl');
    this.extractFunctions(shaderLib, 'functions.glsl');
    this.extractFunctions(shaderLib, 'textures.glsl');

    this.extractFunctions(shadowLib, 'shadowCast.glsl');
    this.extractFunctions(shadowLib, 'shadowReceive.glsl');

    this.registerNodes(data);
    this.registerNodes(operations);
};

那么如何将 glsl 通过语法分析构建出语法树呢?

语法树

其实就是一个对 GLSL 进行语法分析生成 AST 最后再生成 GLSL 的过程。glslify Clay.gl 也都是这么做的。

首先来看一个例子感受一下从 glsl 文本转换成最终语法树的效果。 可以看到基本的函数名(首字母大写),输入和输出都是一一对应的:

---- glsl ----
#pragma DECLARE_FUNCTION
vec3 myFunc(const in float myVarIn, out float myVarOut) {
    // do stuffs
}
---- js ----
this.getNode('MyFunc')
    .inputs({
        myVarIn: var1
    })
    .outputs({
        result: var2,
        myVarOut: var3
    });

在一个更复杂的例子中可以看出,函数名、输入和输出都是可以重载的:

---- glsl ----
#pragma DECLARE_FUNCTION NODE_NAME:myFuncTiti result:resOut myVarIn:totoIn optVariable:optVar DERIVATIVES:enable
vec3 myFunc(const in float myVarIn, out float myVarOut, OPT_ARG_optVariable) {
    // do stuffs
}
---- js ----
this.getNode('myFuncTiti')
    .inputs({
        totoIn: var1,
        optVar: var2
    })
    .outputs({
        resOut: var4,
        myVarOut: var5
    })
    .addDefines(['#define TOTO']);

接下来我们来看一下节点的定义。

方法声明节点定义

语法树节点最重要的就是输入和输出了:

var Node = function() {
    this._name = 'AbstractNode';
    this._inputs = [];
    this._outputs = null;
}

对于输入支持以下三种形式:

// accepts inputs like that:
// inputs( [ a, b, c , d] )
// inputs( { a: x, b: y } )
// inputs( a, b, c, d )
inputs: function() {
    // 省略具体实现
},

另外实际使用中的 NodeCustom 继承自 Node 并进行了扩展,包括开启扩展和定义,在上面的那个复杂例子中可以看到 addDefines 的用法:

addExtensions: function(exts) {
    this._extensions.push.apply(this._extensions, exts);
    return this;
},
addDefines: function(defines) {
    this._defines.push.apply(this._defines, defines);
    return this;
},

有了节点的定义,下面来看如何解析原始的 glsl 文本。

内置变量节点定义

除了用户自定义的方法声明,glsl 中还包含例如 attribute、varying、const、uniform 以及一些特殊变量的声明。 它们的共同基类定义如下:

// osgShader/node/data.js
var Variable = function(type, prefix) {
    Node.call(this);
    this._name = 'Variable';
    this._prefix = prefix;
    this._type = type;
    this._value = undefined;
};
utils.createPrototypeObject(
    Variable,
    utils.objectInherit(Node.prototype, {
        getType: function() {
            return this._type;
        },
        getVariable: function() {
            return this._prefix;
        },
        setValue: function(value) {
            this._value = value;
            return this;
        },

完整变量如下:

export default {
    Output: Output,
    glPointSize: glPointSize,
    glPosition: glPosition,
    glFragColor: glFragColor,
    Sampler: Sampler,
    Variable: Variable,
    Constant: Constant,
    Attribute: Attribute,
    Varying: Varying,
    Uniform: Uniform,
    Define: Define
};

变量操作定义

支持以下两种语法,最终生成的 glsl 语句如下:

// new Add( output, input0, input1, ... )
// new Add( output, [ inputs ] )
// glsl code output = input0 + input1 +...

我们以加法为例,通过输入、输出变量拼接成最终的 GLSL 语句:

// osgShader/node/operations.js
type: 'Add',
operator: '+',
computeShader: function() {
    var outputType = this._outputs.getType();
    var addType = '';

    // 根据输出变量的类型,决定后续每个输入变量的分量
    if (outputType === 'vec4') addType = '.rgba';
    else if (outputType === 'vec3') addType = '.rgb';
    else if (outputType === 'vec2') addType = '.rg';

    // 对于第一个输入变量特殊处理
    var firstVariable = this._getFirstVariableCast();
    var str = this._outputs.getVariable() + ' = ' + firstVariable;

    // 使用操作符连接后续输入变量
    for (var i = 1, l = this._inputs.length; i < l; i++) {
        var input = this._inputs[i];
        // + var
        str += this.operator + input.getVariable();
        // + var.rgb
        var inType = input.getType();
        if (inType !== 'float' && inType !== outputType) {
            str += addType;
        }
    }
    str += ';';
    return str;
}

这里需要注意对于第一个输入变量的处理,但我认为对于后续每个变量都应该做 float 转成 vec 的处理,而不仅仅是第一个:

// osgShader/node/operations.js
_getFirstVariableCast: function() {
    var variable = this._inputs[0].getVariable();
    var inType = this._inputs[0].getType();
    var outType = this._outputs.getType();

    if (outType === inType) return variable;

    // 将 float 转成 vec2 vec3 vec4 等
    if (inType === 'float') return outType + '(' + variable + ')';

    // downcast vector,同后续输入变量的处理
    if (outType === 'vec3') return variable + '.rgb';
    if (outType === 'vec2') return variable + '.rg';
    if (outType === 'float') return variable + '.r';
    return variable;
},

基于加法,乘法和变量赋值就很简单了:

// glsl code output = input0 * input1 * ...
var Mult = function() {
    Add.call(this);
};
utils.createPrototypeObject(
    Mult,
    utils.objectInherit(Add.prototype, {
        type: 'Mult',
        operator: '*'
    }),
);

// glsl code output = input0
var SetFromNode = function() {
    Add.call(this);
};
utils.createPrototypeObject(
    SetFromNode,
    utils.objectInherit(Add.prototype, {
        type: 'SetFromNode'
    }),
);

解析 glsl 文本

shaderLib 暴露了 OSG 内置的 glsl:

import common from 'osgShader/node/common.glsl';
import functions from 'osgShader/node/functions.glsl';
import lightCommon from 'osgShader/node/lightCommon.glsl';
import lights from 'osgShader/node/lights.glsl';

export default {
    'common.glsl': common,
    'functions.glsl': functions,
    'lightCommon.glsl': lightCommon,
    'lights.glsl': lights,
};

在 OSG.js 中对于 glsl 配置了 raw-loader 进行处理,即当作普通文本读取:

module: {
    loaders: [
        {
            test: /\.(frag|vert|glsl)$/,
            loader: 'raw-loader'
        }
    ]
},

我们以 functions.glsl 为例,这里截取一部分:

#pragma DECLARE_FUNCTION
float linearTosRGB(const in float color) { return LIN_SRGB(color); }
#pragma DECLARE_FUNCTION
vec3 linearTosRGB(const in vec3 color) { return vec3(LIN_SRGB(color.r), LIN_SRGB(color.g), LIN_SRGB(color.b)); }

在解析出每一个方法后,还需要进一步提取方法签名,因为其中涉及到重载机制:

// osgShader/utils.js
var extractFunctions = function(shaderLib, fileName) {
    // 提取 function 声明
    var signatures = shaderLib[fileName].split(/#pragma DECLARE_FUNCTION(.*)[\r\n|\r|\n]/);
    var nbSignatures = (signatures.length - 1) / 2;

    var shaderNodeClassLocal = {};
    for (var i = 0; i < nbSignatures; ++i) {
        // 提取方法签名
        var result = extractSignature(signatures[i * 2 + 1], signatures[i * 2 + 2]);
        var nodeName = result.nodeName;
        // 创建新节点
        shaderNode = createNode(result, fileName);
        shaderNodeClassLocal[nodeName] = shaderNode;
        shaderNodeClassGlobal[nodeName] = shaderNode;
    }
    return shaderNodeClassLocal;
};

具体解析方法签名的过程就省略了,只需要关注解析结果:

// osgShader/utils.js
// extractSignature
return {
    nodeName: nodeName,
    functionName: nameFunc,
    signature: {
        returnVariable: returnVariable,
        orderedArgs: orderedArgs,
        outputs: outputs,
        inputs: inputs,
        extensions: extensions
    }
};

拿到签名之后就可以创建节点了,这里返回的是节点的构造函数:

// osgShader/utils.js
var createNode = function(res, fileName) {
    var NodeCustom = function() {
        Node.call(this);
        this._defines = [];
        this._extensions = [];
        this._missingArgs = false;
    };

    utils.createPrototypeObject(
        NodeCustom,
        utils.objectInherit(Node.prototype, {
            type: res.nodeName,
            signatures: [res.signature],
            globalDeclare: '#pragma include "' + fileName + '"',
    //...

    return NodeCustom;
}

代码生成

有了语法树中各类节点的定义,在 Compiler 中就可以进行这些节点的创建工作,例如创建一个 const 节点:

// Compiler.js
getOrCreateConstant: function(type, varname) {
    var nameID = varname;
    var exist = this._variables[nameID];
    if (exist) {
        return exist;
    }
    var v = this.getNode('Constant', type, nameID);
    this._variables[nameID] = v;
    return v;
},

接下来我们来看看 vs 和 fs 的生成,由于 Compiler 分别继承了 CompilerVertex 和 CompilerFragment,这部分会交由它们完成:

// CompilerVertex.js
_createVertexShader: function() {
    // 添加声明
    var roots = this.declareVertexMain();
    // 定义 shader 名称
    var vname = this.getVertexShaderName();
    if (vname) roots.push(this.getNode('Define', 'SHADER_NAME').setValue(vname));
    // 继续编译
    var shader = this.createShaderFromGraphs(roots);
    return shader;
},

变量和函数声明

对于 vs 的创建,首先在头部添加:

declareVertexMain: function() {
    // 声明 glPointSize
    var roots = [this.declarePointSize(), this.declareVertexPosition()];

    // 声明 varying
    this.declareVertexVaryings(roots);

    return roots;
},

对于 glPointSize 的赋值,这里用到了 InlineCode 这种自定义操作的节点类型。:

declarePointSize: function() {
    // 1. 获取 glPointSize 节点
    var glPointSize = this.getNode('glPointSize');
    // 2. 如果不开启,则默认 glPointSize = float(1.0);
    if (!this._pointSizeAttribute || !this._pointSizeAttribute.isEnabled()) {
        this.getNode('SetFromNode')
            .inputs(this.getOrCreateConstantOne('float'))
            .outputs(glPointSize);
        return glPointSize;
    }
    // 3. 否则使用 uPointSize 和 uModelViewMatrix 计算
    this.getNode('InlineCode')
        .code('%pointSize = min(64.0, max(1.0, -%size / %position.z));')
        .inputs({
            position: this.getOrCreateViewVertex(),
            size: this.getOrCreateUniform('float', 'uPointSize')
        })
        .outputs({
            pointSize: glPointSize
        });

    return glPointSize;
},

再来看 glPosition 的计算,使用 uProjectionMatrix 进行计算:

declareScreenVertex: function(glPosition) {
    // glsl code output = matrix * vector4(vec.xyz, 1)
    this.getNode('MatrixMultPosition')
        .inputs({
            matrix: this.getOrCreateProjectionMatrix(),
            vec: this.getOrCreateViewVertex()
        })
        .outputs({
            vec: glPosition
        });
},

最后是 varying 的赋值:

declareVertexVaryings: function(roots) {
    var varyings = this._varyings;
    // vModelVertex = uModelVertex
    if (varyings.vModelVertex) {
        this.getNode('SetFromNode')
            .inputs(this.getOrCreateModelVertex())
            .outputs(varyings.vModelVertex);
    }
}

主体部分

主要在 createShaderFromGraphs 中完成:

// defines and extensions are added by process shader
var extensions = this.evaluateExtensions(roots);
var defines = this.evaluateDefines(roots);

// 收集节点中所有的 extensions
evaluateExtensions: function(roots) {
    return this.evaluateAndGatherField(roots, 'getExtensions');
},

遍历语法树,收集每个节点的 extensions/defines 保存在 _text 数组中:

evaluateAndGatherField: function(nodes, field) {
    var func = this._getAndInitFunctor(this._functorEvaluateAndGatherField.bind(this, field));
    for (var j = 0, jl = nodes.length; j < jl; j++) {
        this.traverse(func, nodes[j]);
    }
    return func._text;
},

接下来是拼接全局变量声明,遍历节点调用 globalDeclaration,然后使用字符串数组的默认排序,以 uniform 为例:

globalDeclaration: function() {
    if (this._size) {
        return sprintf('uniform %s %s[%s];', [this._type, this.getVariable(), this._size]);
    } else {
        return sprintf('uniform %s %s;', [this._type, this.getVariable()]);
    }
}

然后拼接全局方法声明,和变量声明类似,这里就省略了。

然后是 main 方法,包括内部的变量声明:

shaderStack.push('void main() {');
if (variables.length !== 0) {
    shaderStack.push('// vars\n');
    shaderStack.push(variables.join(' '));
    shaderStack.push('\n// end vars\n');
}
// 省略 main 内部语句拼接
shaderStack.push('}');

遍历节点,调用 computeShader 得到 main 方法内部的语句,这部分在操作节点中已经介绍过了。 至此 shader 的 glsl 主体文本已经拼接完成了,接下来需要完成一些模块化相关的工作。

模块化

defines/extension 的处理,包括模块化(include)的处理都交由 Processor 完成:

// Process defines, add precision, resolve include pragma
var shader = this._shaderProcessor.processShader(
    shaderStr,
    defines,
    extensions,
    this._fragmentShaderMode ? 'fragment' : 'vertex'
);

首先进行 defines/extension 的去重:

defines = this._getSortedUnique(defines);
extensions = this._getSortedUnique(extensions);

对于 include 语句可以分成简单和根据 define 条件判断两种。 关于后者,不同于一些 3D 引擎使用 #ifdef/#endif 包裹一并输出到最终的 glsl 中,OSG 的做法是在生成代码时就进行判断,不符合的代码则不会输出。 另外,为了避免重复 include 以及循环依赖问题,会记录当前已经引入的 glsl 片段名:

// pure include is
// \#pragma include "name";

// conditionnal include is name included if _PCF defined
// \#pragma include "name" "_PCF";

由于之前用于分割函数声明的标记是非标准的,因此需要去除。这里通过注释的方式简单去除:

strCore = strCore.replace(/#pragma DECLARE_FUNCTION/g, '//#pragma DECLARE_FUNCTION');

GLSL 转译

下一步是 WebGL 性能优化中很重要的一步,就是如果当前环境支持 WebGL2,需要进行 glsl 100 到 glsl 330 es 的转译:

if (convertToWebGL2) {
    strExtensions = this._convertExtensionsToWebGL2(strExtensions);
    strDefines = this._convertToWebGL2(strDefines, isFragment);
    strCore = this._convertToWebGL2(strCore, isFragment);
}

首先是对于 extension 的处理。对于 WebGL2 已经内置支持的原 WebGL1 扩展,首先需要移除 #extension GL_OES_standard_derivatives : enable。然后转成 define 并加上前缀,例如 #define core_GL_EXT_shader_texture_lod

_convertExtensionsToWebGL2: (function() {
    var cbRenamer = function(match, extension) {
        return 'core_' + extension;
    };
    var cbDefiner = function(match, extension) {
        return '#define ' + extension;
    };
    var extensions =
        '(GL_EXT_shader_texture_lod|GL_OES_standard_derivatives|GL_EXT_draw_buffers|GL_EXT_frag_depth)';
    var definer = new RegExp('#\\s*extension\\s+' + extensions + '.*', 'g');
    var renamer = new RegExp(extensions, 'g');

    return function(strShader) {
        // 移除例如 #extension GL_OES_standard_derivatives : enable
        strShader = strShader.replace(definer, cbDefiner);
        // 加上 core_ 前缀
        strShader = strShader.replace(renamer, cbRenamer);
        return strShader;
    };
})(),

然后转译 define 和主体,通过字符串替换。例如 attribute -> in

strShader = strShader.replace(/attribute\s+/g, 'in ');
strShader = strShader.replace(/varying\s+/g, isFragment ? 'in ' : 'out ');
strShader = strShader.replace(/(texture2D|textureCube)\s*\(/g, 'texture(');
strShader = strShader.replace(/(textureCubeLodEXT)\s*\(/g, 'textureLod(');

特别的,对于 fs 中的 gl_FragColor

if (!frags.length) frags.push('out vec4 glFragColor_0;');
strShader = strShader.replace(/gl_FragColor/g, 'glFragColor_0');

对于 Multi render target 的支持,将原本开启 WEBGL_draw_buffers 扩展的 glsl 100 写法进行改写:

var frags = [];
var replaceMRT = function(match, number) {
    var varName = 'glFragData_' + number;
    frags[number] = 'layout(location = ' + number + ') out vec4 ' + varName + ';';
    return varName;
};
// gl_FragData[0] = vec4(0.25);
// gl_FragData[1] = vec4(0.5);
strShader = strShader.replace(/gl_FragData\s*\[\s*(\d+)\s*\]/g, replaceMRT);

如果没有指定精度,需要使用默认精度:

_precisionR: /precision\s+(high|low|medium)p\s+float/,
_globalDefaultprecision: `
    #ifdef GL_FRAGMENT_PRECISION_HIGH\n precision highp float;
    #else
    precision mediump float;
    #endif`,

最后在拼接转译后的代码时,需要注意各部分的顺序,尤其是 extension:

// See https://khronos.org/registry/gles/specs/2.0/GLSL_ES_Specification_1.0.17.pdf
// (p14-15: extension before any non-processor token)
return strVersion + strExtensions + strPrecision + strDefines + strCore;

一些问题

OSG.js shader 中还存在 #ifdef GL_ES,但是按照 WebGL best practices ,这个判断其实是无意义的:

You should never use #ifdef GL_ES in your WebGL shaders; although some early examples used this, it’s not necessary, since this condition is always true in WebGL shaders.

总结

ShaderLib 在任何 3D 引擎中都是至关重要的,它提供的模块化和 glsl 转译功能不光是引擎内部需要使用,开发者在自定义 Shader 时也需要便捷地引用。 OSG.js 采用的是完整的 AST 解析方案,与此类似的还有 Clay.gl。

在下一篇文章中我们将从一些关键特性的角度分析源码,例如视锥裁剪、LOD 等。 这些特性在地理信息展示场景下会发挥至关重要的作用。