🔗
- 中文翻译: 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)中, 我们将仔细研究纹理和交互.