材质

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

材质分类

PBRMaterial

引擎和编辑器全面提倡使用 PBR 材质 PBRMaterial 。PBR 全称是 Physically Based Rendering,中文意思是基于物理的渲染,最早由迪士尼在 2012 年提出,后来被游戏界广泛使用。跟传统的 Blinn-Phong 等渲染方法相比,PBR 遵循能量守恒,符合物理规则,美术们只需要调整几个简单的参数,即使在复杂的场景中也能保证正确的渲染效果。

/**
 * @title PBR Helmet
 * @category Material
 */
import { AmbientLight, AssetType, Camera, Color, DirectLight, EnvironmentMapLight, GLTFResource, SkyBox, TextureCubeMap, 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();

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

const color2glColor = (color) => new Color(color[0] / 255, color[1] / 255, color[2] / 255);
const glColor2Color = (color) => new Color(color[0] * 255, color[1] * 255, color[2] * 255);
const gui = new dat.GUI();
gui.domElement.style = "position:absolute;top:0px;left:50vw";

let envLightNode = rootEntity.createChild("env_light");
let envLight = envLightNode.addComponent(EnvironmentMapLight);
let envFolder = gui.addFolder("EnvironmentMapLight");
envFolder.add(envLight, "enabled");
envFolder.add(envLight, "specularIntensity", 0, 1);
envFolder.add(envLight, "diffuseIntensity", 0, 1);

let directLightColor = { color: [255, 255, 255] };
let directLightNode = rootEntity.createChild("dir_light");
let directLight = directLightNode.addComponent(DirectLight);
directLight.color = new Color(1, 1, 1);
let dirFolder = gui.addFolder("DirectionalLight1");
dirFolder.add(directLight, "enabled");
dirFolder.addColor(directLightColor, "color").onChange((v) => (directLight.color = color2glColor(v)));
dirFolder.add(directLight, "intensity", 0, 1);

const ambient = rootEntity.addComponent(AmbientLight);
ambient.color = new Color(0.2, 0.2, 0.2, 1);

//-- create camera
let cameraNode = rootEntity.createChild("camera_node");
cameraNode.transform.position = new Vector3(0, 0, 5);
cameraNode.addComponent(Camera);
cameraNode.addComponent(OrbitControl);

Promise.all([
  engine.resourceManager
    .load<GLTFResource>("https://gw.alipayobjects.com/os/bmw-prod/150e44f6-7810-4c45-8029-3575d36aff30.gltf")
    .then((gltf) => {
      rootEntity.addChild(gltf.defaultSceneRoot);
      console.log(gltf);
    }),
  engine.resourceManager
    .load<TextureCubeMap>({
      urls: [
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*Bk5FQKGOir4AAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_cPhR7JMDjkAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*trqjQp1nOMQAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_RXwRqwMK3EAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*q4Q6TroyuXcAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*DP5QTbTSAYgAAAAAAAAAAAAAARQnAQ"
      ],
      type: AssetType.TextureCube
    })
    .then((cubeMap) => {
      envLight.diffuseTexture = cubeMap;
    }),
  engine.resourceManager
    .load<TextureCubeMap>({
      urls: [
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*4ebgQaWOLaIAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*i56eR6AbreUAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*3wYERKsel5oAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*YiG7Srwmb3QAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*VUUwQrAv47sAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*Dn2qSoqzfwoAAAAAAAAAAAAAARQnAQ"
      ],
      type: AssetType.TextureCube
    })
    .then((cubeMap) => {
      envLight.specularTexture = cubeMap;
      rootEntity.addComponent(SkyBox).skyBoxMap = cubeMap;
    })
]).then(() => {
  engine.run();
});

引擎提供了 金属-粗糙度/高光-光泽度 两种工作流,分别对应 PBRMaterialPBRSpecularMaterial

/**
 * @title PBR Base
 * @category Material
 */
import { AssetType, Camera, EnvironmentMapLight, MeshRenderer, PBRMaterial, PrimitiveMesh, SkyBox, TextureCubeMap, Vector3, WebGLEngine } from "oasis-engine";
import { OrbitControl } from "@oasis-engine/controls";
import * as dat from "dat.gui";

/**
 * use PBR material
 */
function usePBR(rows = 5, cols = 5, radius = 1, gap = 1) {
  const deltaGap = radius * 2 + gap;
  const minX = (-deltaGap * (cols - 1)) / 2;
  const maxY = (deltaGap * (rows - 1)) / 2;
  const deltaMetal = 1 / (cols - 1);
  const deltaRoughness = 1 / (rows - 1);

  // create model mesh
  const mesh = PrimitiveMesh.createSphere(engine, radius, 64);

  // create renderer
  for (let i = 0, count = rows * cols; i < count; i++) {
    const entity = rootEntity.createChild();
    const renderer = entity.addComponent(MeshRenderer);
    const material = new PBRMaterial(engine);
    const currentRow = Math.floor(i / cols);
    const currentCol = i % cols;

    renderer.mesh = mesh;
    renderer.setMaterial(material);
    entity.transform.setPosition(minX + currentCol * deltaGap, maxY - currentRow * deltaGap, 0);

    // pbr metallic
    material.metallicFactor = 1 - deltaMetal * currentRow;

    // pbr roughness
    material.roughnessFactor = deltaRoughness * currentCol;

    // base color
    if (currentRow === 0) {
      material.baseColor.setValue(186 / 255, 110 / 255, 64 / 255, 1.0);
    } else if (currentRow === rows - 1) {
      material.baseColor.setValue(0, 0, 0, 1);
    }
  }
}

const gui = new dat.GUI();
const guiDebug = {
  env: "forrest",
  introX: "从左到右粗糙度递增",
  introY: "从上到下金属度递减"
};
gui.add(guiDebug, "introX");
gui.add(guiDebug, "introY");

// 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, 20);
cameraEntity.addComponent(Camera);
const control = cameraEntity.addComponent(OrbitControl);
control.maxDistance = 20;
control.minDistance = 2;

// create skybox
const skybox = rootEntity.addComponent(SkyBox);

// create env light
const envLight = rootEntity.addComponent(EnvironmentMapLight);

// load env texture
engine.resourceManager
  .load([
    {
      urls: [
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*Bk5FQKGOir4AAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_cPhR7JMDjkAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*trqjQp1nOMQAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_RXwRqwMK3EAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*q4Q6TroyuXcAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*DP5QTbTSAYgAAAAAAAAAAAAAARQnAQ"
      ],
      type: AssetType.TextureCube
    },
    {
      urls: [
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*4ebgQaWOLaIAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*i56eR6AbreUAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*3wYERKsel5oAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*YiG7Srwmb3QAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*VUUwQrAv47sAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*Dn2qSoqzfwoAAAAAAAAAAAAAARQnAQ"
      ],
      type: AssetType.TextureCube
    },
    {
      urls: [
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*5w6_Rr6ML6IAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*TiT2TbN5cG4AAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*8GF6Q4LZefUAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*D5pdRqUHC3IAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_FooTIp6pNIAAAAAAAAAAAAAARQnAQ",
        "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*CYGZR7ogZfoAAAAAAAAAAAAAARQnAQ"
      ],
      type: AssetType.TextureCube
    }
  ])
  .then((cubeMaps: TextureCubeMap[]) => {
    envLight.diffuseTexture = cubeMaps[0];
    envLight.specularTexture = cubeMaps[1];
    skybox.skyBoxMap = cubeMaps[1];

    gui.add(guiDebug, "env", ["forrest", "road"]).onChange((v) => {
      switch (v) {
        case "forrest":
          envLight.specularTexture = cubeMaps[1];
          skybox.skyBoxMap = cubeMaps[1];
          break;
        case "road":
          envLight.specularTexture = cubeMaps[2];
          skybox.skyBoxMap = cubeMaps[2];
          break;
      }
    });
  });

// run engine
engine.run();

// show pbr materials
usePBR();

通用参数介绍

参数应用
baseColor基础颜色。基础颜色 * 基础颜色纹理最后的基础颜色。基础颜色是物体的反照率值,与传统的漫反射颜色不同,它会同时贡献镜面反射和漫反射的颜色,我们可以通过上面提到过的金属度、粗糙度,来控制贡献比。
emissiveColor自发光颜色。使得即使没有光照也能渲染出颜色。
opacity透明度。当设置为透明模式后,可以通过透明度来调整透明度。
baseTexture基础颜色纹理。搭配基础颜色使用,是个相乘的关系。
opacityTexture透明度纹理。搭配透明度使用,是相乘的关系,注意透明度模式的切换。
normalTexture法线纹理。可以设置法线纹理 ,在视觉上造成一种凹凸感,还可以通过法线强度来控制凹凸程度。
emissiveTexture自发射光纹理。我们可以设置自发光纹理和自发光颜色(emissiveFactor)达到自发光的效果,即使没有光照也能渲染出颜色。
occlusionTexture阴影遮蔽纹理。我们可以设置阴影遮蔽纹理来提升物体的阴影细节。
tilingOffset纹理坐标的缩放与偏移。是一个 Vector4 数据,分别控制纹理坐标在 uv 方向上的缩放和偏移。

tilingOffset 案例:

/**
 * @title Tilling Offset
 * @category Material
 */
import { AssetType, BlinnPhongMaterial, Camera, MeshRenderer, PrimitiveMesh, RenderFace, Script, 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();

// Load texture
engine.resourceManager
  .load<Texture2D>({
    url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*Umw_RJGiZLYAAAAAAAAAAAAAARQnAQ",
    type: AssetType.Texture2D
  })
  .then((texture) => {
    const scene = engine.sceneManager.activeScene;
    const rootEntity = scene.createRootEntity();

    // Create camera
    const cameraEntity = rootEntity.createChild("Camera");
    cameraEntity.transform.position = new Vector3(0, 0, 20);
    cameraEntity.addComponent(Camera);
    cameraEntity.addComponent(OrbitControl);

    // Create plane
    const entity = rootEntity.createChild();
    const renderer = entity.addComponent(MeshRenderer);
    const mesh = PrimitiveMesh.createPlane(engine, 10, 10);
    const material = new BlinnPhongMaterial(engine);

    texture.anisoLevel = 16;
    material.renderFace = RenderFace.Double;
    material.emissiveTexture = texture;
    material.emissiveColor.setValue(1, 1, 1, 1);

    renderer.mesh = mesh;
    renderer.setMaterial(material);

    // Add animation script
    const animationScript = rootEntity.addComponent(AnimateScript);

    // Add data GUI
    const guiData = addDataGUI(material, animationScript);
    animationScript.guiData = guiData;
    animationScript.material = material;

    // Run engine
    engine.run();
  });

/**
 * Add data GUI.
 */
function addDataGUI(material: BlinnPhongMaterial, animationScript: AnimateScript): any {
  const gui = new dat.GUI();
  const guiData = {
    tilingX: 1,
    tilingY: 1,
    offsetX: 0,
    offsetY: 0,
    reset: function () {
      guiData.tilingX = 1;
      guiData.tilingY = 1;
      guiData.offsetX = 0;
      guiData.offsetY = 0;
      material.tilingOffset.setValue(1, 1, 0, 0);
    },
    pause: function () {
      animationScript.enabled = false;
    },
    resume: function () {
      animationScript.enabled = true;
    }
  };

  gui
    .add(guiData, "tilingX", 0, 10)
    .onChange((value: number) => {
      material.tilingOffset.x = value;
    })
    .listen();
  gui
    .add(guiData, "tilingY", 0, 10)
    .onChange((value: number) => {
      material.tilingOffset.y = value;
    })
    .listen();
  gui
    .add(guiData, "offsetX", 0, 1)
    .onChange((value: number) => {
      material.tilingOffset.z = value;
    })
    .listen();
  gui
    .add(guiData, "offsetY", 0, 1)
    .onChange((value: number) => {
      material.tilingOffset.w = value;
    })
    .listen();
  gui.add(guiData, "reset").name("重置");
  gui.add(guiData, "pause").name("暂停动画");
  gui.add(guiData, "resume").name("继续动画");

  return guiData;
}

/**
 * Animation script.
 */
class AnimateScript extends Script {
  guiData: any;
  material: BlinnPhongMaterial;

  /**
   * The main loop, called frame by frame.
   * @param deltaTime - The deltaTime when the script update.
   */
  onUpdate(deltaTime: number): void {
    const { material, guiData } = this;
    // material.tilingOffset.x = guiData.tilingX = ((guiData.tilingX - 1 + deltaTime * 0.001) % 9) + 1;
    // material.tilingOffset.y = guiData.tilingY = ((guiData.tilingY - 1 + deltaTime * 0.001) % 9) + 1;
    material.tilingOffset.x = ((  deltaTime * 0.001) % 9) + 1;
    material.tilingOffset.y = ((  deltaTime * 0.001) % 9) + 1;
  }
}

金属-粗糙度模式 参数介绍

参数应用
metallicFactor金属度。模拟材质的金属程度,金属值越大,镜面反射越强,即能反射更多周边环境。
roughnessFactor粗糙度。模拟材质的粗糙程度,粗糙度越大,微表面越不平坦,镜面反射越模糊。
metallicRoughnessTexture金属粗糙度纹理。搭配金属粗糙度使用,是相乘的关系。
metallicTexture金属度纹理。搭配金属度使用,是相乘的关系。
roughnessTexture粗糙度纹理。搭配粗糙度使用,是相乘的关系。

高光-光泽度 参数介绍

参数应用
specularColor高光度。不同于金属粗糙度工作流的根据金属度和基础颜色计算镜面反射,而是直接使用高光度来表示镜面反射颜色。(注,只有关闭金属粗糙工作流才生效)
glossinessFactor光泽度。模拟光滑程度,与粗糙度相反。(注,只有关闭金属粗糙工作流才生效)
specularGlossinessTexture高光光泽度纹理。搭配高光光泽度使用,是相乘的关系。

:如果您使用了 PBR 材质,千万别忘了往场景中添加一个EnvironmentMapLight ~只有添加了之后,属于 PBR 的金属粗糙度、镜面反射、物理守恒、全局光照才会展现出效果。**

BlinnPhongMaterial

BlinnPhongMaterial 虽然不是基于物理渲染,但是其高效的渲染算法和基本齐全的光学部分,还是有很多的应用场景。

常用参数介绍

参数应用
baseColor基础颜色。 基础颜色 * 基础纹理 = 最后的基础颜色。
baseTexture基础纹理。搭配基础颜色使用,是个相乘的关系。
specularColor镜面反射颜色。镜面反射颜色 * 镜面反射纹理 = 最后的镜面反射颜色。
specularTexture镜面反射纹理。搭配镜面反射颜色使用,是个相乘的关系。
normalTexture法线纹理。可以设置法线纹理 ,在视觉上造成一种凹凸感,还可以通过法线强度来控制凹凸程度。
normalIntensity 法线强度。法线强度,用来控制凹凸程度。
emissiveColor自发光颜色。自发光颜色 * 自发光纹理 = 最后的自发光颜色。即使没有光照也能渲染出颜色。
emissiveTexture自发光纹理。搭配自发光颜色使用,是个相乘的关系。
shininess镜面反射系数。值越大镜面反射效果越聚拢。
tilingOffset纹理坐标的缩放与偏移。是一个 Vector4 数据,分别控制纹理坐标在 uv 方向上的缩放和偏移。

UnlitMaterial

在一些简单的场景中,可能不希望计算光照,引擎提供了 UnlitMaterial,使用了最精简的 shader 代码,只需要提供颜色或者纹理即可渲染。

参数应用
baseColor基础颜色。基础颜色 * 基础颜色纹理 = 最后的颜色。
baseTexture基础纹理。搭配基础颜色使用,是个相乘的关系。
tilingOffset纹理坐标的缩放与偏移。是一个 Vector4 数据,分别控制纹理坐标在 uv 方向上的缩放和偏移。

如何使用材质

用户在 Unity、3ds Max、C4D、Blender 等建模软件调试后可以输出 GLTF 文件,GLTF文件里面包含了场景、模型实体、纹理、动画、材质等资源,Oasis 支持使用资源管理器加载解析这个 GLTF 文件,解析后模型已经自动赋予了对应的材质,我们也可以拿到模型的材质,进行一些后期加工,比如修改颜色。

// 获取想要修改的 renderer
const renderer = entity.getComponent(MeshRenderer);
// 通过 `getMaterial` 获取当前 renderer 的第 i 个材质, 默认第 0 个。
const material = renderer.getMaterial();
// 修改材质颜色
material.baseColor.r = 0;

我们也可以直接替换材质类型,比如将模型重新赋予一个 blinn-phong 材质:

// 获取想要修改的 renderer
const renderer = entity.getComponent(MeshRenderer);

// 创建 blinn-phong 材质
const material = new BlinnPhongMaterial(engine);
material.baseColor.r = 0;

// 通过 `setMaterial` 设置当前 renderer 的第 i 个材质, 默认第 0 个。
const material = renderer.setMaterial(material);

材质通用属性

以下属性都可以直接在 UnlitMaterialBlinnPhongMaterialPBRMaterialPBRSpecularMaterial 材质中使用。

参数应用
isTransparent是否透明。可以设置材质是否透明。如果设置为透明,可以通过 BlendMode 来设置颜色混合模式。
alphaCutoff透明度裁剪值。可以设置裁剪值,来指定在着色器中,裁剪透明度小于此数值的片元。
renderFace渲染面。可以决定渲染正面、背面、双面。
blendMode颜色混合模式。当设置材质为透明后,可以设置此枚举来决定颜色混合模式。

常见 QA

1. 透明渲染异常?

  • 请先确保材质开启了透明度模式,即材质的 isTransparent 属性设置为了 true
  • 相应的材质的 baseColor 需要设置正确的透明度。如 material.baseColor.a = 0.5。透明度范围为 【0~1】,数值越小,越透明。
  • 如果还上传了透明度纹理,请先确保透明纹理是否含有透明通道,即是正常的 png 图片,如果不是的话,可以开启 getOpacityFromRGB 为 true 代表希望采样亮度值作为透明度。
  • 如果透明度渲染仍有异常,请确保材质的颜色混合度模式(blendMode)为期望的组合。
  • 有一些透明度渲染异常可能是因为没有关闭背面剔除,可以通过 renderFace 来设置想要渲染的面。
  • 如果是自定义材质,请确保设置了正确的混合模式,混合因子,关闭了深度写入,设置了正确的渲染队列。

2. 为什么材质是黑的?

  • 场景中需要有光才能照亮物体,请确保您往场景中添加了全局光(EnvironmentMapLight)或者直接光如方向光(DirectLight)。

3. 为什么模型背面没有渲染?

  • 请确保关闭背面了剔除,可以通过 RasterState.cullMode 来设置,也可以通过材质内置的 renderFace 来设置想要渲染的面。

4. 一般需要打几个光?

  • 一般场景只需要添加一个全局光(EnvironmentMapLight)就可以了,它可以基于图片的照明实现直接光照和间接光照。
  • 如果出于美术流程的困难度, 1 个 EnvironmentMapLight 无法满足需求,可以适当添加方向光(DirectLight)和点光源(PointLight)来补充光照细节。
  • 出于性能考虑,尽量不要超过 1 个 EnvironmentMapLight + 2 个直接光 。

5. 为什么渲染的不够立体?