## Start your project First, create a text file called `triangle.html` and fill it with the following HTML. This creates a mobile-friendly page with a full-screen canvas. ```html Filament Tutorial ``` The above HTML loads three JavaScript files: - `filament.js` does a couple things: - Downloads assets and compiles the Filament WASM module. - Contains high-level utilities, e.g. to simplify loading KTX textures from JavaScript. - `gl-matrix-min.js` is a small library that provides vector math functionality. - `triangle.js` will contain your application code. Go ahead and create `triangle.js` with the following content. ```js {fragment="root"} class App { constructor() { // TODO: create entities this.render = this.render.bind(this); this.resize = this.resize.bind(this); window.addEventListener('resize', this.resize); window.requestAnimationFrame(this.render); } render() { // TODO: render scene window.requestAnimationFrame(this.render); } resize() { // TODO: adjust viewport and canvas } } Filament.init(['triangle.filamat'], () => { window.app = new App() } ); ``` The two calls to `bind()` allow us to pass instance methods as callbacks for animation and resize events. `Filament.init()` consumes two things: a list of asset URLs and a callback. The callback will be triggered only after all assets finish downloading and the Filament module has become ready. In our callback, we simply instantiated the `App` object, since we'll do most of the work in its constructor. We also set the app instance into a `Window` property to make it accessible from the developer console. Go ahead and download [triangle.filamat](triangle.filamat) and place it in your project folder. This is a *material package*, which is a binary file that contains shaders and other bits of data that define a PBR material. We'll learn more about material packages in the next tutorial. ## Spawn a local server Because of CORS restrictions, your web app cannot fetch the material package directly from the file system. One way around this is to create a temporary server using Python or node: ```bash python3 -m http.server # Python 3 python -m SimpleHTTPServer # Python 2.7 npx http-server -p 8000 # nodejs ``` To see if this works, navigate to [http://localhost:8000](http://localhost:8000) and check if you can load the page without any errors appearing in the developer console. Take care not to use Python's simple server in production since it does not serve WebAssembly files with the correct MIME type. ## Create the Engine and Scene We now have a basic skeleton that can respond to paint and resize events. Let's start adding Filament objects to the app. Insert the following code into the top of the app constructor. ```js {fragment="create entities"} this.canvas = document.getElementsByTagName('canvas')[0]; const engine = this.engine = Filament.Engine.create(this.canvas); ``` The above snippet creates the `Engine` by passing it a canvas DOM object. The engine needs the canvas in order to create a WebGL 2.0 context in its contructor. The engine is a factory for many Filament entities, including `Scene`, which is a flat container of entities. Let's go ahead and create a scene, then add a blank entity called `triangle` into the scene. ```js {fragment="create entities"} this.scene = engine.createScene(); this.triangle = Filament.EntityManager.get().create(); this.scene.addEntity(this.triangle); ``` Filament uses an [Entity-Component System](//en.wikipedia.org/wiki/Entity-component-system). The triangle entity in the above snippet does not yet have an associated component. Later in the tutorial we will make it into a *renderable*. Renderables are entities that have associated draw calls. ## Construct typed arrays Next we'll create two typed arrays: a positions array with XY coordinates for each vertex, and a colors array with a 32-bit word for each vertex. ```js {fragment="create entities"} const TRIANGLE_POSITIONS = new Float32Array([ 1, 0, Math.cos(Math.PI * 2 / 3), Math.sin(Math.PI * 2 / 3), Math.cos(Math.PI * 4 / 3), Math.sin(Math.PI * 4 / 3), ]); const TRIANGLE_COLORS = new Uint32Array([0xffff0000, 0xff00ff00, 0xff0000ff]); ``` Next we'll use the positions and colors buffers to create a single `VertexBuffer` object. ```js {fragment="create entities"} const VertexAttribute = Filament.VertexAttribute; const AttributeType = Filament.VertexBuffer$AttributeType; this.vb = Filament.VertexBuffer.Builder() .vertexCount(3) .bufferCount(2) .attribute(VertexAttribute.POSITION, 0, AttributeType.FLOAT2, 0, 8) .attribute(VertexAttribute.COLOR, 1, AttributeType.UBYTE4, 0, 4) .normalized(VertexAttribute.COLOR) .build(engine); this.vb.setBufferAt(engine, 0, TRIANGLE_POSITIONS); this.vb.setBufferAt(engine, 1, TRIANGLE_COLORS); ``` The above snippet first creates aliases for two enum types, then constructs the vertex buffer using its `Builder` method. After that, it pushes two buffer objects into the appropriate slots using `setBufferAt`. In the Filament API, the above builder pattern is often used for constructing objects in lieu of long argument lists. The daisy chain of function calls allows the client code to be somewhat self-documenting. Our app sets up two buffer slots in the vertex buffer, and each slot is associated with a single attribute. Alternatively, we could have interleaved or concatenated these attributes into a single buffer slot. Next we'll construct an index buffer. The index buffer for our triangle is trivial: it simply holds the integers 0,1,2. ```js {fragment="create entities"} this.ib = Filament.IndexBuffer.Builder() .indexCount(3) .bufferType(Filament.IndexBuffer$IndexType.USHORT) .build(engine); this.ib.setBuffer(engine, new Uint16Array([0, 1, 2])); ``` Note that constructing an index buffer is similar to constructing a vertex buffer, but it only has one buffer slot, and it can only contain two types of data (USHORT or UINT). ## Finish up initialization Next let's construct an actual `Material` from the material package that was downloaded (the material is an object; the package is just a binary blob), then extract the default `MaterialInstance` from the material object. Material instances have concrete values for their parameters, and they can be bound to renderables. We'll learn more about material instances in the next tutorial. After extracting the material instance, we can finally create a renderable component for the triangle by setting up a bounding box and passing in the vertex and index buffers. ```js {fragment="create entities"} const mat = engine.createMaterial('triangle.filamat'); const matinst = mat.getDefaultInstance(); Filament.RenderableManager.Builder(1) .boundingBox({ center: [-1, -1, -1], halfExtent: [1, 1, 1] }) .material(0, matinst) .geometry(0, Filament.RenderableManager$PrimitiveType.TRIANGLES, this.vb, this.ib) .build(engine, this.triangle); ``` Next let's wrap up the initialization routine by creating the swap chain, renderer, camera, and view. ```js {fragment="create entities"} this.swapChain = engine.createSwapChain(); this.renderer = engine.createRenderer(); this.camera = engine.createCamera(); this.view = engine.createView(); this.view.setCamera(this.camera); this.view.setScene(this.scene); this.view.setClearColor([0.1, 0.2, 0.3, 1.0]); // blue-green background this.resize(); // adjust the initial viewport ``` At this point, we're done creating all Filament entities, and the code should run without errors. However the canvas is still blank! ## Render and resize handlers Recall that our App class has a skeletal render method, which the browser calls every time it needs to repaint. Often this is 60 times a second. ```js render() { // TODO: render scene window.requestAnimationFrame(this.render); } ``` Let's flesh this out by rotating the triangle and invoking the Filament renderer. Add the following code to the top of the render method. ```js {fragment="render scene"} // Rotate the triangle. const radians = Date.now() / 1000; const transform = mat4.fromRotation(mat4.create(), radians, [0, 0, 1]); const tcm = this.engine.getTransformManager(); const inst = tcm.getInstance(this.triangle); tcm.setTransform(inst, transform); inst.delete(); // Render the frame. this.renderer.render(this.swapChain, this.view); ``` The first half of our render method obtains the transform component of the triangle entity and uses gl-matrix to generate a rotation matrix. The second half of our render method invokes the Filament renderer on the view, and tells the Filament engine to execute its internal command buffer. The Filament renderer can tell the app that it wants to skip a frame, hence the `if` statement. One last step. Add the following code to the resize method. This adjusts the resolution of the rendering surface when the window size changes, taking `devicePixelRatio` into account for high-DPI displays. It also adjusts the camera frustum accordingly. ```js {fragment="adjust viewport and canvas"} 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]); const aspect = width / height; const Projection = Filament.Camera$Projection; this.camera.setProjection(Projection.ORTHO, -aspect, aspect, -1, 1, 0, 1); ``` You should now have a spinning triangle! The completed JavaScript is available [here](tutorial_triangle.js). In the [next tutorial], we'll take a closer look at Filament materials and 3D rendering. [next tutorial]: tutorial_redball.html