自定义材质

上次修改时间:2021-05-29 00:11:75

业务中可能有一些特殊的渲染需求,例如水流特效,这时候就需要“自定义材质”去实现。通过使用 material 这个模块中的 MaterialShader 这两个类,就可以将自己定义的 Shader 代码整合进入引擎的渲染流程。

/**
 * @title Water
 * @category Shader
 */
import { AssetType, Camera, Color, Engine, Material, MeshRenderer, PrimitiveMesh, Script, Shader, Texture2D, Vector3, WebGLEngine } from "oasis-engine";
import { OrbitControl } from "@oasis-engine/controls";
import * as dat from "dat.gui";

//-- create engine object
const engine = new WebGLEngine("canvas");
engine.canvas.resizeByClientSize();

const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();

//-- create camera
const cameraEntity = rootEntity.createChild("camera_entity");
cameraEntity.transform.position = new Vector3(0, 0, 15);
cameraEntity.addComponent(Camera);
const orbitControl = cameraEntity.addComponent(OrbitControl);
orbitControl.minDistance = 15;
orbitControl.maxDistance = 15;

// 自定义材质
const vertexSource = `
uniform mat4 u_MVPMat;
attribute vec3 POSITION;
attribute vec2 TEXCOORD_0;
attribute vec3 NORMAL;

varying vec2 v_uv;
varying vec3 v_position;
varying vec3 v_normal;


uniform float uTime;

uniform sampler2D u_texture;

void main() {

  gl_Position = u_MVPMat  *  vec4( POSITION, 1.0 );
  v_uv = TEXCOORD_0;
  v_normal = NORMAL;
  v_position = POSITION;
}
 `;

const fragSource = `
varying vec2 v_uv;
varying vec3 v_position;
varying vec3 v_normal;

uniform float u_time;
uniform sampler2D u_texture;
uniform vec3 u_cameraPos;

#define EPS 0.001
#define MAX_ITR 100
#define MAX_DIS 100.0
#define PI	 	  3.141592

uniform float u_water_scale;
uniform float u_water_speed;

uniform vec3 u_sea_base;
uniform vec3 u_water_color;
uniform float u_sea_height;

// Distance Functions
float sd_sph(vec3 p, float r) { return length(p) - r; }

// Distance Map
float map(vec3 p, vec2 sc)
{    
    float l = cos(length(p * 2.0));
    vec2 u = vec2(l, sc.y);
    vec2 um = u * 0.3;
    um.x += u_time * 0.1 * u_water_speed;
    um.y += -u_time * 0.025 * u_water_speed;
    um.x += (um.y) * 2.0;    
    float a1 = texture2D(u_texture, (p.yz  *  .4 + um) * u_water_scale).x;
    float a2 = texture2D(u_texture, (p.zx  *  .4 + um) * u_water_scale).x;
    float a3 = texture2D(u_texture, (p.xy  *  .4 + um) * u_water_scale).x;
    
    float t1 = a1 + a2 + a3;
    t1 /= 15.0 * u_water_scale;
    
    float b1 = texture2D(u_texture, (p.yz  *  1. + u) * u_water_scale).x;
    float b2 = texture2D(u_texture, (p.zx  *  1. + u) * u_water_scale).x;
    float b3 = texture2D(u_texture, (p.xy  *  1. + u) * u_water_scale).x;
    
    float t2 = b1 + b2 + b3;
    t2 /= 15.0  *  u_water_scale;
    
    float comb = t1 * 0.4 + t2 * 0.1 * (1.0 - t1);
    
    return comb + sd_sph(p, 3.); // sd_box(p, vec3(1., 1., 1.)) + sdPlane(p, vec4(0., 0., 1.0, 0.));//
}

float diffuse(vec3 n,vec3 l,float p) {
    return pow(dot(n,l) * 0.4 + 0.6,p);
}

float specular(vec3 n,vec3 l,vec3 e,float s) {    
    float nrm = (s + 8.0) / (PI * 8.0);
    return pow(max(dot(reflect(e,n),l),0.0),s) * nrm;
}

// sky
vec3 getSkyColor(vec3 e) {
    e.y = max(e.y,0.0);
    return vec3(pow(1.0-e.y,2.0), 1.0-e.y, 0.6+(1.0-e.y)*0.4);
}

vec3 getSeaColor(vec3 p, vec3 n, vec3 l, vec3 eye, vec3 dist) {  
    float fresnel = clamp(1.0 - dot(n,-eye), 0.0, 1.0);
    fresnel = pow(fresnel,3.0) * 0.65;

    vec3 reflected = getSkyColor(reflect(eye,n));    
    vec3 refracted = u_sea_base + diffuse(normalize(n),l,80.0) * u_water_color * 0.12; 

    vec3 color =  mix(refracted,reflected,fresnel);

    float atten = max(1.0 - dot(dist,dist) * 0.001, 0.0);
    color += u_water_color * (length(p) - u_sea_height) * 0.18 * atten; // 

    color += vec3(specular(n,l,eye,20.0));

    return color;
}

void main (void) {

    vec2 uv = vec2(v_uv.x * 0.5, v_uv.y * 0.5);//  / iResolution.xy;
    
    vec3 pos = v_position; 
    vec3 dist = pos - u_cameraPos;

    float dis = EPS;
    vec3 rayDir = normalize(dist);
    
    // Ray marching
    for(int i = 0; i < MAX_ITR; i++)
    {
        if(dis < EPS || dis > MAX_DIS)
          break;
        dis = map(pos, uv);
        pos += dis * rayDir;
    }
    
    if (dis >= EPS) 
    {
      discard;
    }
    
    vec3 lig = normalize(vec3(-1., -3, -4.5));
    vec2 eps = vec2(0.0, EPS);
    vec3 normal = normalize(vec3(
        map(pos + eps.yxx, uv) - map(pos - eps.yxx, uv),
        map(pos + eps.xyx, uv) - map(pos - eps.xyx, uv),
        map(pos + eps.xxy, uv) - map(pos - eps.xxy, uv)
    ));
    
    vec3 col = getSeaColor(pos, normal, lig, rayDir, dist);
    
    gl_FragColor = vec4(col,1.0);
}
`;

// 初始化 shader
Shader.create("water", vertexSource, fragSource);

class ShaderMaterial extends Material {
  constructor(engine: Engine) {
    super(engine, Shader.find("water"));

    this.shaderData.setFloat("u_sea_height", 0.6);
    this.shaderData.setFloat("u_water_scale", 0.2);
    this.shaderData.setFloat("u_water_speed", 3.5);
    this.shaderData.setColor("u_sea_base", new Color(0.1, 0.2, 0.22));
    this.shaderData.setColor("u_water_color", new Color(0.8, 0.9, 0.6));
  }
}
const material = new ShaderMaterial(engine);

// 创建球体形的海面
const sphereEntity = rootEntity.createChild("sphere");
const renderer = sphereEntity.addComponent(MeshRenderer);
renderer.mesh = PrimitiveMesh.createSphere(engine, 3, 50);
renderer.setMaterial(material);

// 加载噪声纹理
engine.resourceManager
  .load({
    type: AssetType.Texture2D,
    url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*AC4IQZ6mfCIAAAAAAAAAAAAAARQnAQ"
  })
  .then((texture: Texture2D) => {
    material.shaderData.setTexture("u_texture", texture);
    engine.run();
  });

// u_time 更新脚本
class WaterScript extends Script {
  onUpdate() {
    material.shaderData.setFloat("u_time", engine.time.timeSinceStartup * 0.001);
  }
}
sphereEntity.addComponent(WaterScript);


// debug
function openDebug() {
  const shaderData = material.shaderData;
  const baseColor = shaderData.getColor("u_sea_base");
  const waterColor = shaderData.getColor("u_water_color");
  const debug = {
    sea_height: shaderData.getFloat("u_sea_height"),
    water_scale: shaderData.getFloat("u_water_scale"),
    water_speed: shaderData.getFloat("u_water_speed"),
    sea_base: [baseColor.r * 255, baseColor.g * 255, baseColor.b * 255],
    water_color: [waterColor.r * 255, waterColor.g * 255, waterColor.b * 255]
  };

  const gui = new dat.GUI();

  gui.add(debug, "sea_height", 0, 3).onChange((v) => {
    shaderData.setFloat("u_sea_height", v);
  });
  gui.add(debug, "water_scale", 0, 4).onChange((v) => {
    shaderData.setFloat("u_water_scale", v);
  });
  gui.add(debug, "water_speed", 0, 4).onChange((v) => {
    shaderData.setFloat("u_water_speed", v);
  });
  gui.addColor(debug, "sea_base").onChange((v) => {
    baseColor.r = v[0] / 255;
    baseColor.g = v[1] / 255;
    baseColor.b = v[2] / 255;
  });
  gui.addColor(debug, "water_color").onChange((v) => {
    waterColor.r = v[0] / 255;
    waterColor.g = v[1] / 255;
    waterColor.b = v[2] / 255;
  });
}

openDebug();

创建 shader

Shader 封装了顶点着色器、片元着色器、着色器预编译、平台精度、平台差异性。他的创建和使用非常方便,用户只需要关注 shader 算法本身,而不用纠结于使用什么精度,亦或是使用 GLSL 哪个版本的写法。 下面是一个简单的demo:

import { Material, Shader, Color } from "oasis-engine";

//-- Shader 代码
const vertexSource = `
  uniform mat4 u_MVPMat;

  attribute vec3 POSITION; 

  void main() {
    gl_Position = u_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 变量以及如何上传自定义变量。

内置 shader 变量

在上面,我们给 material 赋予了 shader,这个时候程序已经可以开始渲染了。需要注意的是,shader 代码中有两种变量,一种是逐顶点attribute 变量,另一种是逐 shaderuniform 变量。(在 GLSL300 后,统一为 in 变量) 引擎已经自动上传了一些常用变量,用户可以直接在 shader 代码中使用,如顶点数据和 mvp 数据,下面是引擎默认上传的变量。

逐顶点数据attribute name数据类型
顶点POSITIONvec3
法线NORMALvec3
切线TANGENTvec4
顶点颜色COLOR_0vec4
骨骼索引JOINTS_0vec4
骨骼权重WEIGHTS_0vec4
第一套纹理坐标TEXCOORD_0vec2
第二套纹理坐标TEXCOORD_1vec2
逐 shader 数据uniform name数据类型
canvas 分辨率u_resolutionvec2
视口矩阵u_viewMatmat4
投影矩阵u_projMatmat4
视口投影矩阵u_VPMatmat4
视口逆矩阵u_viewInvMatmat4
投影逆矩阵u_projInvMatmat4
相机位置u_cameraPosvec3
模型本地坐标系矩阵u_localMatmat4
模型世界坐标系矩阵u_modelMatmat4
模型视口矩阵u_MVMatmat4
模型视口投影矩阵u_MVPMatmat4
模型视口逆矩阵u_MVInvMatmat4
法线逆转置矩阵u_normalMatmat4

上传 shader 变量

attribute 逐顶点数据的上传请参考 网格渲染器,这里不再赘述。

除了内置的变量,我们可以在 shader 中上传任何自定义名字的变量(建议使用 u** 、 v** 分别表示 uniform、varying变量),我们唯一要做的就是根据 shader 的变量类型,使用正确的接口。 上传接口全部保存在 ShaderData 中,而 shaderData 实例对象又分别保存在引擎的四大类 SceneCameraRendererMaterial 中,我们只需要分别往这些 shaderData 中调用接口,上传变量,引擎便会在底层自动帮我们组装这些数据,并进行判重等性能的优化。

shaderData 分开的好处

shaderData 分别保存在引擎的四大类 SceneCameraRendererMaterial 中,这样做的好处之一就是底层可以根据上传时机上传某一块 uniform,提升性能;另外,将材质无关的 shaderData 剥离出来,可以实现共享材质,比如两个 renderer ,共享了一个材质,虽然都要操控同一个 shader,但是因为这一部分 shader 数据的上传来源于两个 renderer 的 shaderData,所以是不会影响彼此的渲染结果的。

如:

const renderer1ShaderData = renderer1.shaderData;
const renderer2ShaderData = renderer2.shaderData;
const materialShaderData = material.shaderData;

materialShaderData.setColor("u_color", new Color(1,0,0,1));
renderer1ShaderData.setFloat("u_progross",0.5);
renderer2ShaderData.setFloat("u_progross",0.8);

调用接口

shader 中变量的类型和调用的接口对应关系如下:

shader 类型ShaderData API
boolintsetInt( value: number )
floatsetFloat( value: number )`
bvec2ivec2vec2setVector2( value:Vector2 )
bvec3ivec3vec3setVector3( value:Vector3 )
bvec4ivec4vec4setVector4( value:Vector4 )
mat4setMatrix( value:Matrix )
float[]vec2[]vec3[]vec4[]mat4[]setFloatArray( value:Float32Array )
bool[]int[]bvec2[]bvec3[]bvec4[]ivec2[]ivec3[]ivec4[]setIntArray( value:Int32Array )
sampler2DsamplerCubesetTexture( 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");

渲染状态

我们通过材质的 shader,四块 shaderData 决定了材质的渲染表现。但是渲染管线除了着色器的操作,还提供了一些渲染状态来使我们对可编程的输入输出进行一些额外配置和处理。 因此,引擎提供了RenderState ,可以分别对混合状态(BlendState)深度状态(DepthState)模版状态(StencilState)光栅状态(RasterState)进行配置。 我们拿一个透明物体的标准渲染流程来举例,我们希望开启混合模式并设置混合因子,并且因为透明物体是叠加渲染的,所以我们还要关闭深度写入;

const renderState = material.renderState;

// 1. 设置颜色混合因子。
const blendState = renderState.blendState;
const target = blendState.targetBlendState;

// src 混合因子为(As,As,As,As)
target.sourceColorBlendFactor = target.sourceAlphaBlendFactor =BlendFactor.SourceAlpha;
// dst 混合因子为(1 - As,1 - As,1 - As,1 - As)。
target.destinationColorBlendFactor = target.destinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha;
// 操作方式为 src + dst  */
target.colorBlendOperation = target.alphaBlendOperation = BlendOperation.Add;

// 2. 开启颜色混合
target.enabled = true;

// 3. 关闭深度写入。
const depthState = renderState.depthState;
depthState.writeEnabled = false;

// 4. 设置透明渲染队列 (后面会讲为什么)
material.renderQueueType = RenderQueueType.Transparent;

有关渲染状态的更多选项可以分别查看相应的API 文档

渲染队列

至此,自定义材质已经非常完善了,但是也许我们还需要对物体的渲染顺序做一些处理,比如透明物体的渲染一般都是放在非透明队列后面的,因此,引擎提供了 渲染队列(RenderQueueType) ,我们设置材质的渲染队列,可以决定这个材质在当前场景中的渲染顺序,引擎底层会对不同范围的渲染队列进行一些特殊处理,如 RenderQueueType.Transparent 会从远到近进行渲染。值得注意的是渲染队列的值可以是枚举值加上任何自定义数字。

material.renderQueueType = RenderQueueType.Opaque + 1;

封装自定义材质

这部分的内容是结合上文所有内容,给用户一个简单的封装示例,希望对您有所帮助:

import { Material, Shader, Color, Texture2D, BlendFactor, RenderQueueType } from "oasis-engine";

//-- Shader 代码
const vertexSource = `
  uniform mat4 u_MVPMat;

  attribute vec3 POSITION; 
  attribute vec2 TEXCOORD_0;
  varying vec2 v_uv;

  void main() {
    gl_Position = u_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"))
  }
}