自定义着色器
本教程使用 ShaderMaterial API 创建带燃烧边缘的溶解效果,同时支持 WebGL 和 WebGPU。完整项目见此处。
将三维模型导入 PlayCanvas 时,默认使用物理材质。这是一种用途广泛的材质,可以满足许多渲染需求。
但在需要特殊视觉效果或特殊材质行为时,往往需要编写自定义着色器。
ShaderMaterial API
PlayCanvas 提供 ShaderMaterial API,简化自定义着色器创建,同时支持 WebGL(GLSL)和 WebGPU(WGSL)。此 API 自动处理图形 API 之间的差异,提供更清晰的着色器开发接口。
跨平台着色器支持
为确保自定义着色器适用于各类设备和浏览器,应同时提供 GLSL 和 WGSL 版本:
- GLSL(OpenGL 着色语言):用于 WebGL。
- WGSL(WebGPU 着色语言):用于 WebGPU。
顶点着色器
GLSL 顶点着色器
attribute vec3 aPosition;
attribute vec2 aUv0;
uniform mat4 matrix_model;
uniform mat4 matrix_viewProjection;
varying vec2 vUv0;
void main(void)
{
vUv0 = aUv0;
gl_Position = matrix_viewProjection * matrix_model * vec4(aPosition, 1.0);
}
WGSL 顶点着色器
attribute aPosition: vec3f;
attribute aUv0: vec2f;
uniform matrix_viewProjection: mat4x4f;
uniform matrix_model: mat4x4f;
varying vUv0: vec2f;
@vertex
fn vertexMain(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
output.vUv0 = aUv0;
output.position = uniform.matrix_viewProjection * uniform.matrix_model * vec4<f32>(aPosition, 1.0);
return output;
}
片元着色器
GLSL 片元着色器
varying vec2 vUv0;
uniform sampler2D uDiffuseMap;
uniform sampler2D uHeightMap;
uniform float uTime;
void main(void)
{
float height = texture2D(uHeightMap, vUv0).r;
vec4 color = texture2D(uDiffuseMap, vUv0);
if (height < uTime) {
discard;
}
// Burning band width
float edgeWidth = 0.05;
if (height < (uTime + edgeWidth)) {
// 0 at inner edge → 1 at outer edge
float t = (height - uTime) / edgeWidth;
// Fire gradient: yellow to dark orange
vec3 burnColor = mix(
vec3(1.0, 0.7, 0.2),
vec3(0.6, 0.1, 0.0),
t
);
// Blend the burn color with the original texture
color = vec4(mix(burnColor, color.rgb, t), 1.0);
}
gl_FragColor = color;
}
WGSL 片元着色器
varying vUv0: vec2f;
uniform uTime: f32;
var uDiffuseMap: texture_2d<f32>;
var uDiffuseMapSampler: sampler;
var uHeightMap: texture_2d<f32>;
var uHeightMapSampler: sampler;
@fragment
fn fragmentMain(input: FragmentInput) -> FragmentOutput {
var output: FragmentOutput;
let height = textureSample(uHeightMap, uHeightMapSampler, vUv0).r;
var color = textureSample(uDiffuseMap, uDiffuseMapSampler, vUv0);
if (height < uniform.uTime) {
discard;
}
// Burning band width
let edgeWidth = 0.05;
if (height < (uniform.uTime + edgeWidth)) {
// t goes from 0 (just inside edge) to 1 (outer edge)
let t = (height - uniform.uTime) / edgeWidth;
// Fire color: bright yellow fading to dark orange
let burnColor = mix(
vec3f(1.0, 0.7, 0.2),
vec3f(0.6, 0.1, 0.0),
t
);
// Blend burn color with original texture (more burn at the outer edge)
color = vec4f(mix(burnColor, color.rgb, t), 1.0);
}
output.color = color;
return output;
}
上述着色器创建带火焰燃烧边缘的溶解效果。顶点着色器将网格顶点变换到屏幕空间,片元着色器根据高度图纹理生成溶解效果。当 uTime 大于某像素处的高度图值时,会丢弃该像素,使模型对应位置透明。在溶解边缘附近混入火焰色渐变,营造逼真的燃烧效果。
创建 ShaderMaterial
// Create a new ShaderMaterial with both GLSL and WGSL versions
this.material = new ShaderMaterial({
uniqueName: 'Dissolve',
vertexGLSL: this.vertexGLSL.resource,
fragmentGLSL: this.fragmentGLSL.resource,
vertexWGSL: this.vertexWGSL.resource,
fragmentWGSL: this.fragmentWGSL.resource,
attributes: {
aPosition: SEMANTIC_POSITION,
aUv0: SEMANTIC_TEXCOORD0
}
});
ShaderMaterial 构造函数同时接收 GLSL 和 WGSL 着色器代码。PlayCanvas 会根据当前图形 API 自动选择合适版本,attributes 对象则指定着色器所需的顶点属性。
设置着色器参数
// Set the initial time parameter
this.material.setParameter('uTime', 0);
// Set the diffuse texture
const diffuseTexture = this.diffuseMap.resource;
this.material.setParameter('uDiffuseMap', diffuseTexture);
// Set the height map texture
const heightTexture = this.heightMap.resource;
this.material.setParameter('uHeightMap', heightTexture);
使用 setParameter() 方法设置 uniform,与普通材质的方式相同。ShaderMaterial 会自动处理 GLSL 和 WGSL 的 uniform 语法差异。
着色器资源的脚本属性
/**
* GLSL vertex shader.
*
* @attribute
* @title GLSL Vertex Shader
* @type {pc.Asset}
*/
vertexGLSL;
/**
* GLSL fragment shader.
*
* @attribute
* @title GLSL Fragment Shader
* @type {pc.Asset}
*/
fragmentGLSL;
/**
* WGSL vertex shader.
*
* @attribute
* @title WGSL Vertex Shader
* @type {pc.Asset}
*/
vertexWGSL;
/**
* WGSL fragment shader.
*
* @attribute
* @title WGSL Fragment Shader
* @type {pc.Asset}
*/
fragmentWGSL;
/**
* Diffuse Map
*
* @attribute
* @title Diffuse Map
* @type {pc.Asset}
*/
diffuseMap;
/**
* Height Map
*
* @attribute
* @title Height Map
* @type {pc.Asset}
*/
heightMap;
需要创建四个着色器资源,GLSL 和 WGSL 各两个,并在 PlayCanvas 编辑器中分配到这些脚本属性。
更新 uniform
update(dt) {
this.time += dt;
// Create a smooth oscillation using sine wave
const t = (Math.sin(this.time) + 1) / 2;
// Update the time value in the material
this.material.setParameter('uTime', t);
}
为实现溶解效果,我们将高度图值与随时间变化的阈值比较。此版本使用正弦波在 0 和 1 之间平滑振荡,使溶解动画更自然。
完整脚本
import { Script, ShaderMaterial, SEMANTIC_POSITION, SEMANTIC_TEXCOORD0 } from 'playcanvas';
/**
* Apply a dissolve shader material to an entity's render components.
*/
export class CustomShader extends Script {
scriptName = 'dissolveShader';
/**
* GLSL vertex shader.
*
* @attribute
* @title GLSL Vertex Shader
* @type {pc.Asset}
*/
vertexGLSL;
/**
* GLSL fragment shader.
*
* @attribute
* @title GLSL Fragment Shader
* @type {pc.Asset}
*/
fragmentGLSL;
/**
* WGSL vertex shader.
*
* @attribute
* @title WGSL Vertex Shader
* @type {pc.Asset}
*/
vertexWGSL;
/**
* WGSL fragment shader.
*
* @attribute
* @title WGSL Fragment Shader
* @type {pc.Asset}
*/
fragmentWGSL;
/**
* Diffuse Map
*
* @attribute
* @title Diffuse Map
* @type {pc.Asset}
*/
diffuseMap;
/**
* Height Map
*
* @attribute
* @title Height Map
* @type {pc.Asset}
*/
heightMap;
time = 0;
// initialize code called once per entity
initialize() {
// Create a new material and set the shader
this.material = new ShaderMaterial({
uniqueName: 'Dissolve',
vertexGLSL: this.vertexGLSL.resource,
fragmentGLSL: this.fragmentGLSL.resource,
vertexWGSL: this.vertexWGSL.resource,
fragmentWGSL: this.fragmentWGSL.resource,
attributes: {
aPosition: SEMANTIC_POSITION,
aUv0: SEMANTIC_TEXCOORD0
}
});
// Set the initial time parameter
this.material.setParameter('uTime', 0);
// Set the diffuse texture
const diffuseTexture = this.diffuseMap.resource;
this.material.setParameter('uDiffuseMap', diffuseTexture);
// Set the height map texture
const heightTexture = this.heightMap.resource;
this.material.setParameter('uHeightMap', heightTexture);
// Replace the material on all render components
const renders = this.entity.findComponents('render');
for (let i = 0; i < renders.length; ++i) {
const meshInstances = renders[i].meshInstances;
for (let j = 0; j < meshInstances.length; j++) {
meshInstances[j].material = this.material;
}
}
}
// update code called every frame
update(dt) {
this.time += dt;
// Create a smooth oscillation using sine wave
const t = (Math.sin(this.time) + 1) / 2;
// Update the time value in the material
this.material.setParameter('uTime', t);
}
}
此脚本演示如何通过 ShaderMaterial API 创建跨平台自定义着色器。溶解效果依据高度图决定丢弃哪些像素,并随溶解推进生成燃烧边缘。
GLSL 与 WGSL 的差异
同时为两种 API 编写着色器时,需要注意以下差异:
- 语法:WGSL 的类型更显式,例如
vec3f、f32,而 GLSL 会推断部分类型。 - 属性与插值变量:WGSL 使用结构化输入输出,GLSL 使用全局变量。
- 纹理:WGSL 分离纹理与采样器,GLSL 将两者组合。
- 入口点:WGSL 使用
@vertex、@fragment标记,GLSL 使用main()。
ShaderMaterial API 自动处理这些差异,让你专注于着色器逻辑,而非特定 API 的细节。