- 中文翻译: LainTea
# 简介
本教程示例如何创建一个 **红色球**, 主要介绍材质和纹理的相关知识.
对于初学者, 首先创建一个名为`redball.html`的文本文件, 并且复制我们在[上一个教程](https://jerkwin.github.io/filamentcn/T1_triangle.md.html)中使用的HTML. 然后将最后一个脚本的名称从`triangle.js`更改为`redball.js`.
接下来, 你需要使用一些命令行工具: `matc` 和 `cmgen`. 你可以从相应的[Filament发布](https://github.com/google/filament/releases)中获得这些工具. 你应该选择与开发机器相对应的版本, 而不是用于Web的版本.
# 定义塑料材质
`matc` 工具会基于一个包含PBR材质高级描述的文本文件生成一个二进制材质包, 其中包含着色器代码和相关元数据. 有关更多信息, 请参阅描述[Filament材质系统](https://jerkwin.github.io/filamentcn/Materials.md.html)的官方文档.
让我们试试`matc`. 在你常用的文本编辑器中创建以下内容的文件, 并将其命名为 `plastic.mat`.
```text
material {
    name : Lit,
    shadingModel : lit,
    parameters : [
        { type : float3, name : baseColor },
        { type : float,  name : roughness },
        { type : float,  name : clearCoat },
        { type : float,  name : clearCoatRoughness }
    ],
}
fragment {
    void material(inout MaterialInputs material) {
        prepareMaterial(material);
        material.baseColor.rgb = materialParams.baseColor;
        material.roughness = materialParams.roughness;
        material.clearCoat = materialParams.clearCoat;
        material.clearCoatRoughness = materialParams.clearCoatRoughness;
    }
}
```
接下来, 使用如下命令调用 `matc`:
```
matc -a opengl -p mobile -o plastic.filamat plastic.mat
```
现在你的工作目录中应该有一个材质包文件了, 稍后我们将在本教程中使用它.
# 烘焙环境贴图
接下来, 我们将使用Filament的 `cmgen` 工具, 以 latlong 格式根据HDR环境贴图生成两个立方体贴图文件: mipmapped IBL和模糊天空盒.
下载HDR环境贴图[pillars_2k.hdr](https://github.com/google/filament/blob/master/third_party/environments/pillars_2k.hdr), 并在终端中使用如下命令.
```bash
cmgen -x . --format=ktx --size=256 --extract-blur=0.1 pillars_2k.hdr
```
现在, 你应该得到了一个名为 `pillars_2k` 的文件夹, 其中包含一些用于IBL和天空盒的KTX文件, 以及一个带有球谐函数系数的文本文件. 你可以删除包含球谐函数系数的文本文件, 因为IBL KTX在其元数据中已经包含了这些系数.
# 创建JavaScript代码
接下来, 创建 `redball.js` 文件, 内容如下.
```js {fragment="root"}
const environ = 'pillars_2k';
const ibl_url = `${environ}/${environ}_ibl.ktx`;
const sky_url = `${environ}/${environ}_skybox.ktx`;
const filamat_url = 'plastic.filamat'
Filament.init([ filamat_url, ibl_url, sky_url ], () => {
  // 为方便起见, 为枚举项创建一些全局别名.
  window.VertexAttribute = Filament.VertexAttribute;
  window.AttributeType = Filament.VertexBuffer$AttributeType;
  window.PrimitiveType = Filament.RenderableManager$PrimitiveType;
  window.IndexType = Filament.IndexBuffer$IndexType;
  window.Fov = Filament.Camera$Fov;
  window.LightType = Filament.LightManager$Type;
  // 获取canvas DOM对象并将其传递给App.
  const canvas = document.getElementsByTagName('canvas')[0];
  window.app = new App(canvas);
} );
class App {
  constructor(canvas) {
    this.canvas = canvas;
    const engine = this.engine = Filament.Engine.create(canvas);
    const scene = engine.createScene();
    // TODO: 创建材质
    // TODO: 创建球体
    // TODO: 创建灯光
    // TODO: 创建IBL
    // TODO: 创建天空盒
    this.swapChain = engine.createSwapChain();
    this.renderer = engine.createRenderer();
    this.camera = engine.createCamera();
    this.view = engine.createView();
    this.view.setCamera(this.camera);
    this.view.setScene(scene);
    this.resize();
    this.render = this.render.bind(this);
    this.resize = this.resize.bind(this);
    window.addEventListener('resize', this.resize);
    window.requestAnimationFrame(this.render);
  }
  render() {
    const eye = [0, 0, 4], center = [0, 0, 0], up = [0, 1, 0];
    const radians = Date.now() / 10000;
    vec3.rotateY(eye, eye, center, radians);
    this.camera.lookAt(eye, center, up);
    this.renderer.render(this.swapChain, this.view);
    window.requestAnimationFrame(this.render);
  }
  resize() {
    const dpr = window.devicePixelRatio;
    const width = this.canvas.width = window.innerWidth * dpr;
    const height = this.canvas.height = window.innerHeight * dpr;
    this.view.setViewport([0, 0, width, height]);
    this.camera.setProjectionFov(45, width / height, 1.0, 10.0, Fov.VERTICAL);
  }
}
```
你应该在上一个教程中就熟悉了上述模板, 尽管它加载了一组新的资源. 我们还为相机添加了一些动画.
接下来, 让我们为在教程开始时构建的材质包创建一个材质实例. 将 **创建材质** 注释替换为以下代码片段.
```js {fragment="create material"}
const material = engine.createMaterial(filamat_url);
const matinstance = material.createInstance();
const red = [0.8, 0.0, 0.0];
matinstance.setColor3Parameter('baseColor', Filament.RgbType.sRGB, red);
matinstance.setFloatParameter('roughness', 0.5);
matinstance.setFloatParameter('clearCoat', 1.0);
matinstance.setFloatParameter('clearCoatRoughness', 0.3);
```
下一步是创建球体的可渲染对象. 为了解决这个问题, 我们将使用 `IcoSphere` 实用程序类, 其构造函数采用LOD. 它的功能是对二十面体进行细分, 生成三个数组:
- `icosphere.vertices` Float32Array XYZ坐标.
- `icosphere.tangents` Uint16Array (视为半浮点数) 以四元数编码的表面方向
- `icosphere.triangles` Uint16Array 三角形索引.
让我们使用这些数组来构建顶点缓冲区和索引缓冲区. 将 **创建球体** 替换为以下代码片段.
```js {fragment="create sphere"}
const renderable = Filament.EntityManager.get().create();
scene.addEntity(renderable);
const icosphere = new Filament.IcoSphere(5);
const vb = Filament.VertexBuffer.Builder()
  .vertexCount(icosphere.vertices.length / 3)
  .bufferCount(2)
  .attribute(VertexAttribute.POSITION, 0, AttributeType.FLOAT3, 0, 0)
  .attribute(VertexAttribute.TANGENTS, 1, AttributeType.SHORT4, 0, 0)
  .normalized(VertexAttribute.TANGENTS)
  .build(engine);
const ib = Filament.IndexBuffer.Builder()
  .indexCount(icosphere.triangles.length)
  .bufferType(IndexType.USHORT)
  .build(engine);
vb.setBufferAt(engine, 0, icosphere.vertices);
vb.setBufferAt(engine, 1, icosphere.tangents);
ib.setBuffer(engine, icosphere.triangles);
Filament.RenderableManager.Builder(1)
  .boundingBox({ center: [-1, -1, -1], halfExtent: [1, 1, 1] })
  .material(0, matinstance)
  .geometry(0, PrimitiveType.TRIANGLES, vb, ib)
  .build(engine, renderable);
```
此时, 应用程序正在渲染一个球体, 但它是黑色的, 因此不会显示出来. 为了证实那里有一个球体, 你可以尝试使用 `setClearColor` 将背景颜色更改为蓝色, 就像我们在第一个教程中所做的那样.
# 添加灯光
在本节中, 我们将创建一些方向光, 以及由我们在教程开始时构建的KTX文件之一定义的基于图像的光源(IBL). 首先, 将 **创建灯光** 注释替换为以下代码片段.
```js {fragment="create lights"}
const sunlight = Filament.EntityManager.get().create();
scene.addEntity(sunlight);
Filament.LightManager.Builder(LightType.SUN)
  .color([0.98, 0.92, 0.89])
  .intensity(110000.0)
  .direction([0.6, -1.0, -0.8])
  .sunAngularRadius(1.9)
  .sunHaloSize(10.0)
  .sunHaloFalloff(80.0)
  .build(engine, sunlight);
const backlight = Filament.EntityManager.get().create();
scene.addEntity(backlight);
Filament.LightManager.Builder(LightType.DIRECTIONAL)
        .direction([-1, 0, 1])
        .intensity(50000.0)
        .build(engine, backlight);
```
`SUN` 光源类似于 `DIRECTIONAL` 光源, 但具有一些额外的参数, 因为Filament会自动在天空盒中绘制一个光盘.
接下来, 我们需要从KTX IBL创建一个 `IndirectLight` 对象. 一种方法是这样做(不要输入这些代码, 我们有更简单的方法).
```js
const format = Filament.PixelDataFormat.RGB;
const datatype = Filament.PixelDataType.UINT_10F_11F_11F_REV;
// 为 mipmapped 立方体贴图创建Texture对象
const ibl_package = Filament.Buffer(Filament.assets[ibl_url]);
const iblktx = new Filament.KtxBundle(ibl_package);
const ibltex = Filament.Texture.Builder()
  .width(iblktx.info().pixelWidth)
  .height(iblktx.info().pixelHeight)
  .levels(iblktx.getNumMipLevels())
  .sampler(Filament.Texture$Sampler.SAMPLER_CUBEMAP)
  .format(Filament.Texture$InternalFormat.RGBA8)
  .build(engine);
for (let level = 0; level < iblktx.getNumMipLevels(); ++level) {
  const uint8array = iblktx.getCubeBlob(level).getBytes();
  const pixelbuffer = Filament.PixelBuffer(uint8array, format, datatype);
  ibltex.setImageCube(engine, level, pixelbuffer);
}
// 解析球谐函数元数据.
const shstring = iblktx.getMetadata('sh');
const shfloats = shstring.split(/\s/, 9 * 3).map(parseFloat);
// 构建IBL对象并将其插入场景中.
const indirectLight = Filament.IndirectLight.Builder()
  .reflections(ibltex)
  .irradianceSh(3, shfloats)
  .intensity(50000.0)
  .build(engine);
scene.setIndirectLight(indirectLight);
```
Filament提供了一个JavaScript应用程序来简化这一过程, 只要用以下代码片段替换 **创建IBL** 注释即可.
```js {fragment="create IBL"}
const indirectLight = engine.createIblFromKtx(ibl_url);
indirectLight.setIntensity(50000);
scene.setIndirectLight(indirectLight);
```
# 添加背景
此时你可以运行示例, 应该看到黑色背景下的一个红色塑料球. 如果没有天盒, 球上的反射就不能正确地表示其周围环境. 以下是为天空盒创建纹理的一种方法:
```js
const sky_package = Filament.Buffer(Filament.assets[sky_url]);
const skyktx = new Filament.KtxBundle(sky_package);
const skytex = Filament.Texture.Builder()
  .width(skyktx.info().pixelWidth)
  .height(skyktx.info().pixelHeight)
  .levels(1)
  .sampler(Filament.Texture$Sampler.SAMPLER_CUBEMAP)
  .format(Filament.Texture$InternalFormat.RGBA8)
  .build(engine);
const uint8array = skyktx.getCubeBlob(0).getBytes();
const pixelbuffer = Filament.PixelBuffer(uint8array, format, datatype);
skytex.setImageCube(engine, 0, pixelbuffer);
```
Filament提供了一个Javascript实用程序来简化这一过程. 将 **创建天空盒** 替换为以下内容.
```js {fragment="create skybox"}
const skybox = engine.createSkyFromKtx(sky_url);
scene.setSkybox(skybox);
```
就是这样, 我们现在得到了一个闪闪发光的红色球, 漂浮在环境之中!
完整的JavaScript文件在[这里](https://google.github.io/filament/webgl/tutorial_redball.js)下载.
在[下一个教程](https://google.github.io/filament/webgl/tutorial_suzanne.html)中, 我们将仔细研究纹理和交互.