业务中可能有一些特殊的渲染需求,例如水流特效,这时候就需要通过自定义着色器 (Shader)去实现。
Shader 类 封装了顶点着色器、片元着色器、着色器预编译、平台精度、平台差异性。他的创建和使用非常方便,用户只需要关注 shader 算法本身,而不用纠结于使用什么精度,亦或是使用 GLSL 哪个版本的写法。下面是一个简单的 demo:
import { Material, Shader, Color } from "@galacean/engine";
//-- Shader 代码
const vertexSource = `
uniform mat4 renderer_MVPMat;
attribute vec3 POSITION;
void main() {
gl_Position = renderer_MVPMat * vec4(POSITION, 1.0);
}
`;
const fragmentSource = `
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}
`;
// 创建自定义 shader(整个 runtime 只需要创建一次)
Shader.create("demo", vertexSource, fragmentSource);
// 创建材质
const material = new Material(engine, Shader.find("demo"));
Shader.create()
用来将 shader 添加到引擎的缓存池子中,因此整个 runtime 只需要创建一次,接下来就可以通过 Shader.find(name) 来反复使用.
注:引擎已经预先 create 了 blinn-phong、pbr、shadow-map、shadow、skybox、framebuffer-picker-color、trail。用户可以直接使用这些内置 shader,并且不能重名创建。
在上述的案例中,因为我们没有上传 u_color
变量,所以片元输出还是黑色的(uniform 默认值),接下来我们来介绍下引擎内置的 shader 变量以及如何上传自定义变量。
在上面,我们给 material 赋予了 shader,这个时候程序已经可以开始渲染了。
需要注意的是,shader 代码中有两种变量,一种是逐顶点的
attribute
变量,另一种是逐 shader 的uniform
变量。(在 GLSL300 后,统一为 in 变量)
引擎已经自动上传了一些常用变量,用户可以直接在 shader 代码中使用,如顶点数据和 mvp 数据,下面是引擎默认上传的变量。
逐顶点数据 | attribute name | 数据类型 |
---|---|---|
顶点 | POSITION | vec3 |
法线 | NORMAL | vec3 |
切线 | TANGENT | vec4 |
顶点颜色 | COLOR_0 | vec4 |
骨骼索引 | JOINTS_0 | vec4 |
骨骼权重 | WEIGHTS_0 | vec4 |
第一套纹理坐标 | TEXCOORD_0 | vec2 |
第二套纹理坐标 | TEXCOORD_1 | vec2 |
名字 | 类型 | 解释 |
---|---|---|
renderer_LocalMat | mat4 | 模型本地坐标系矩阵 |
renderer_ModelMat | mat4 | 模型世界坐标系矩阵 |
renderer_MVMat | mat4 | 模型视口矩阵 |
renderer_MVPMat | mat4 | 模型视口投影矩阵 |
renderer_NormalMat | mat4 | 法线矩阵 |
名字 | 类型 | 解释 |
---|---|---|
camera_ViewMat | mat4 | 视口矩阵 |
camera_ProjMat | mat4 | 投影矩阵 |
camera_VPMat | mat4 | 视口投影矩阵 |
camera_ViewInvMat | mat4 | 视口逆矩阵 |
camera_Position | vec3 | 相机位置 |
camera_DepthTexture | sampler2D | 深度信息纹理 |
camera_DepthBufferParams | Vec4 | 相机深度缓冲参数:(x: 1.0 - far / near, y: far / near, z: 0, w: 0) |
camera_ProjectionParams | Vec4 | 投影矩阵相关参数:(x: flipProjection ? -1 : 1, y: near, z: far, w: 0) |
名字 | 类型 | 解释 |
---|---|---|
scene_ElapsedTime | vec4 | 引擎启动后经过的总时间:(x: t, y: sin(t), z:cos(t), w: 0) |
scene_DeltaTime | vec4 | 距离上一帧的间隔时间:(x: dt, y: 0, z:0, w: 0) |
名字 | 类型 | 解释 |
---|---|---|
scene_FogColor | vec4 | 雾的颜色 |
scene_FogParams | vec4 | 雾的参数:(x: -1/(end-start), y: end/(end-start), z: density / ln(2), w: density / sqr(ln(2)) |
attribute 逐顶点数据的上传请参考 网格渲染器,这里不再赘述。
除了内置的变量,我们可以在着色器中上传任何自定义名字的变量,我们唯一要做的就是根据着色器数据类型,使用正确的接口。上传的接口全部保存在 ShaderData 中,而 shaderData 实例对象又分别保存在引擎的四大类 Scene、Camera、Renderer、Material 中,我们只需要分别往这些 shaderData 中调用接口,上传变量,引擎便会在底层自动帮我们组装这些数据,并进行判重等性能的优化。
着色器数据分别保存在引擎的四大类 Scene、Camera、Renderer、Material 中,这样做的好处之一就是底层可以根据上传时机上传某一块 uniform,提升性能;另外,将材质无关的着色器数据剥离出来,可以实现共享材质,比如两个 renderer ,共享了一个材质,虽然都要操控同一个 shader,但是因为这一部分 shader 数据的上传来源于两个 renderer 的 shaderData,所以是不会影响彼此的渲染结果的。
如:
const renderer1ShaderData = renderer1.shaderData;
const renderer2ShaderData = renderer2.shaderData;
const materialShaderData = material.shaderData;
materialShaderData.setColor("material_color", new Color(1, 0, 0, 1));
renderer1ShaderData.setFloat("u_progross", 0.5);
renderer2ShaderData.setFloat("u_progross", 0.8);
着色器数据的类型和分别调用的 API 如下:
shader 类型 | ShaderData API |
---|---|
bool 、 int | setInt( value: number ) |
float | setFloat( value: number )` |
bvec2 、ivec2 、vec2 | setVector2( value:Vector2 ) |
bvec3 、ivec3 、vec3 | setVector3( value:Vector3 ) |
bvec4 、ivec4 、vec4 | setVector4( value:Vector4 ) |
mat4 | setMatrix( value:Matrix ) |
float[] 、vec2[] 、vec3[] 、 vec4[] 、mat4[] | setFloatArray( value:Float32Array ) |
bool[] 、 int[] 、bvec2[] 、 bvec3[] 、bvec4[] 、 ivec2[] 、 ivec3[] 、ivec4[] | setIntArray( value:Int32Array ) |
sampler2D 、 samplerCube | setTexture( value:Texture ) |
sampler2D[] 、 samplerCube[] | setTextureArray( value:Texture[] ) |
代码演示如下:
// shader
uniform float u_float;
uniform int u_int;
uniform bool u_bool;
uniform vec2 u_vec2;
uniform vec3 u_vec3;
uniform vec4 u_vec4;
uniform mat4 u_matrix;
uniform int u_intArray[10];
uniform float u_floatArray[10];
uniform sampler2D u_sampler2D;
uniform samplerCube u_samplerCube;
uniform sampler2D u_samplerArray[2];
// GLSL 300:
// in float u_float;
// ...
// shaderData 可以分别保存在 scene 、camera 、renderer、 material 中。
const shaderData = material.shaderData;
shaderData.setFloat("u_float", 1.5);
shaderData.setInt("u_int", 1);
shaderData.setInt("u_bool", 1);
shaderData.setVector2("u_vec2", new Vector2(1, 1));
shaderData.setVector3("u_vec3", new Vector3(1, 1, 1));
shaderData.setVector4("u_vec4", new Vector4(1, 1, 1, 1));
shaderData.setMatrix("u_matrix", new Matrix());
shaderData.setIntArray("u_intArray", new Int32Array(10));
shaderData.setFloatArray("u_floatArray", new Float32Array(10));
shaderData.setTexture("u_sampler2D", texture2D);
shaderData.setTexture("u_samplerCube", textureCube);
shaderData.setTextureArray("u_samplerArray", [texture2D, textureCube]);
注:为了性能考虑,引擎暂不支持 结构体数组上传、数组单个元素上传。
除了 uniform 变量之外,引擎将 shader 中的宏定义也视为一种变量,因为宏定义的开启/关闭 将生成不同的着色器变种,也会影响渲染结果。
如 shader 中有这些宏相关的操作:
#ifdef DISCARD
discard;
#endif
#ifdef LIGHT_COUNT
uniform vec4 u_color[ LIGHT_COUNT ];
#endif
也是通过 ShaderData 来操控宏变量:
// 开启宏开关
shaderData.enableMacro("DISCARD");
// 关闭宏开关
shaderData.disableMacro("DISCARD");
// 开启变量宏
shaderData.enableMacro("LIGHT_COUNT", "3");
// 切换变量宏。这里底层会自动 disable 上一个宏,即 “LIGHT_COUNT 3”
shaderData.enableMacro("LIGHT_COUNT", "2");
// 关闭变量宏
shaderData.disableMacro("LIGHT_COUNT");
这部分的内容是结合上文所有内容,给用户一个简单的封装示例,希望对您有所帮助:
import { Material, Shader, Color, Texture2D, BlendFactor, RenderQueueType } from "@galacean/engine";
//-- Shader 代码
const vertexSource = `
uniform mat4 renderer_MVPMat;
attribute vec3 POSITION;
attribute vec2 TEXCOORD_0;
varying vec2 v_uv;
void main() {
gl_Position = renderer_MVPMat * vec4(POSITION, 1.0);
v_uv = TEXCOORD_0;
}
`;
const fragmentSource = `
uniform vec4 u_color;
varying vec2 v_uv;
#ifdef TEXTURE
uniform sampler2D u_texture;
#endif
void main() {
vec4 color = u_color;
#ifdef TEXTURE
color *= texture2D(u_texture, v_uv);
#endif
gl_FragColor = color;
}
`;
Shader.create("demo", vertexSource, fragmentSource);
export class CustomMaterial extends Material {
set texture(value: Texture2D) {
if (value) {
this.shaderData.enableMacro("TEXTURE");
this.shaderData.setTexture("u_texture", value);
} else {
this.shaderData.disableMacro("TEXTURE");
}
}
set color(val: Color) {
this.shaderData.setColor("u_color", val);
}
// make it transparent
set transparent() {
const target = this.renderState.blendState.targetBlendState;
const depthState = this.renderState.depthState;
target.enabled = true;
target.sourceColorBlendFactor = target.sourceAlphaBlendFactor = BlendFactor.SourceAlpha;
target.destinationColorBlendFactor = target.destinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha;
depthState.writeEnabled = false;
this.renderQueueType = RenderQueueType.Transparent;
}
constructor(engine: Engine) {
super(engine, Shader.find("demo"));
}
}