This updates the tutorial markdown, fixes up the literate programming Python script, and updates the web site itself. The doc build script now uses a Pipfile instead of "requirements.txt", which I find less frustrating since it does not interfere with other Python projects on your machine. Fixes #2483.
244 lines
24 KiB
HTML
244 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en"><head>
|
|
<link href="https://google.github.io/filament/favicon.png" rel="icon" type="image/x-icon" />
|
|
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700|Tangerine:700|Inconsolata" rel="stylesheet">
|
|
<link href="main.css" rel="stylesheet" type="text/css">
|
|
</head>
|
|
<body class="verbiage">
|
|
<div class="demo_frame"><iframe src="demo_triangle.html"></iframe><a href="demo_triangle.html">🔗</a></div>
|
|
<h2>Literate programming</h2>
|
|
<p>The markdown source for this tutorial is not only used to generate this
|
|
web page, it's also used to generate the JavaScript for the above demo.
|
|
We use a small Python script for weaving (generating HTML) and tangling
|
|
(generating JS). In the code samples, you'll often see
|
|
<code>// TODO: <some task></code>. These are special markers that get replaced by
|
|
subsequent code blocks.</p>
|
|
<h2>Start your project</h2>
|
|
<p>First, create a text file called <code>triangle.html</code> and fill it with the following HTML. This creates
|
|
a mobile-friendly page with a full-screen canvas.</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #BC7A00"><!DOCTYPE html></span>
|
|
<<span style="color: #008000; font-weight: bold">html</span> <span style="color: #7D9029">lang</span><span style="color: #666666">=</span><span style="color: #BA2121">"en"</span>>
|
|
<<span style="color: #008000; font-weight: bold">head</span>>
|
|
<<span style="color: #008000; font-weight: bold">title</span>>Filament Tutorial</<span style="color: #008000; font-weight: bold">title</span>>
|
|
<<span style="color: #008000; font-weight: bold">meta</span> <span style="color: #7D9029">charset</span><span style="color: #666666">=</span><span style="color: #BA2121">"utf-8"</span>>
|
|
<<span style="color: #008000; font-weight: bold">meta</span> <span style="color: #7D9029">name</span><span style="color: #666666">=</span><span style="color: #BA2121">"viewport"</span> <span style="color: #7D9029">content</span><span style="color: #666666">=</span><span style="color: #BA2121">"width=device-width,user-scalable=no,initial-scale=1"</span>>
|
|
<<span style="color: #008000; font-weight: bold">style</span>>
|
|
<span style="color: #008000; font-weight: bold">body</span> { <span style="color: #008000; font-weight: bold">margin</span>: <span style="color: #666666">0</span>; <span style="color: #008000; font-weight: bold">overflow</span>: <span style="color: #008000; font-weight: bold">hidden</span>; }
|
|
<span style="color: #008000; font-weight: bold">canvas</span> { touch-action: <span style="color: #008000; font-weight: bold">none</span>; <span style="color: #008000; font-weight: bold">width</span>: <span style="color: #666666">100</span><span style="color: #B00040">%</span>; <span style="color: #008000; font-weight: bold">height</span>: <span style="color: #666666">100</span><span style="color: #B00040">%</span>; }
|
|
</<span style="color: #008000; font-weight: bold">style</span>>
|
|
</<span style="color: #008000; font-weight: bold">head</span>>
|
|
<<span style="color: #008000; font-weight: bold">body</span>>
|
|
<<span style="color: #008000; font-weight: bold">canvas</span>></<span style="color: #008000; font-weight: bold">canvas</span>>
|
|
<<span style="color: #008000; font-weight: bold">script</span> <span style="color: #7D9029">src</span><span style="color: #666666">=</span><span style="color: #BA2121">"//unpkg.com/filament/filament.js"</span>></<span style="color: #008000; font-weight: bold">script</span>>
|
|
<<span style="color: #008000; font-weight: bold">script</span> <span style="color: #7D9029">src</span><span style="color: #666666">=</span><span style="color: #BA2121">"//unpkg.com/gl-matrix@2.8.1/dist/gl-matrix-min.js"</span>></<span style="color: #008000; font-weight: bold">script</span>>
|
|
<<span style="color: #008000; font-weight: bold">script</span> <span style="color: #7D9029">src</span><span style="color: #666666">=</span><span style="color: #BA2121">"triangle.js"</span>></<span style="color: #008000; font-weight: bold">script</span>>
|
|
</<span style="color: #008000; font-weight: bold">body</span>>
|
|
</<span style="color: #008000; font-weight: bold">html</span>>
|
|
</pre></div>
|
|
|
|
<p>The above HTML loads three JavaScript files:</p>
|
|
<ul>
|
|
<li><code>filament.js</code> does a couple things:
|
|
<ul>
|
|
<li>Downloads assets and compiles the Filament WASM module.</li>
|
|
<li>Contains high-level utilities, e.g. to simplify loading KTX textures from JavaScript.</li>
|
|
</ul>
|
|
</li>
|
|
<li><code>gl-matrix-min.js</code> is a small library that provides vector math functionality.</li>
|
|
<li><code>triangle.js</code> will contain your application code.</li>
|
|
</ul>
|
|
<p>Go ahead and create <code>triangle.js</code> with the following content.</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">class</span> App {
|
|
constructor() {
|
|
<span style="color: #408080; font-style: italic">// TODO: create entities</span>
|
|
<span style="color: #008000; font-weight: bold">this</span>.render <span style="color: #666666">=</span> <span style="color: #008000; font-weight: bold">this</span>.render.bind(<span style="color: #008000; font-weight: bold">this</span>);
|
|
<span style="color: #008000; font-weight: bold">this</span>.resize <span style="color: #666666">=</span> <span style="color: #008000; font-weight: bold">this</span>.resize.bind(<span style="color: #008000; font-weight: bold">this</span>);
|
|
<span style="color: #008000">window</span>.addEventListener(<span style="color: #BA2121">'resize'</span>, <span style="color: #008000; font-weight: bold">this</span>.resize);
|
|
<span style="color: #008000">window</span>.requestAnimationFrame(<span style="color: #008000; font-weight: bold">this</span>.render);
|
|
}
|
|
render() {
|
|
<span style="color: #408080; font-style: italic">// TODO: render scene</span>
|
|
<span style="color: #008000">window</span>.requestAnimationFrame(<span style="color: #008000; font-weight: bold">this</span>.render);
|
|
}
|
|
resize() {
|
|
<span style="color: #408080; font-style: italic">// TODO: adjust viewport and canvas</span>
|
|
}
|
|
}
|
|
|
|
Filament.init([<span style="color: #BA2121">'triangle.filamat'</span>], () => { <span style="color: #008000">window</span>.app <span style="color: #666666">=</span> <span style="color: #008000; font-weight: bold">new</span> App() } );
|
|
</pre></div>
|
|
|
|
<p>The two calls to <code>bind()</code> allow us to pass instance methods as callbacks for animation and resize
|
|
events.</p>
|
|
<p><code>Filament.init()</code> consumes two things: a list of asset URLs and a callback.</p>
|
|
<p>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 <code>App</code> object, since we'll do most of the
|
|
work in its constructor. We also set the app instance into a <code>Window</code> property to make it accessible
|
|
from the developer console.</p>
|
|
<p>Go ahead and download <a href="triangle.filamat">triangle.filamat</a> and place it in your project folder.
|
|
This is a <em>material package</em>, 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.</p>
|
|
<h2>Spawn a local server</h2>
|
|
<p>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:</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span>python3 -m http.server <span style="color: #408080; font-style: italic"># Python 3</span>
|
|
python -m SimpleHTTPServer <span style="color: #408080; font-style: italic"># Python 2.7</span>
|
|
npx http-server -p <span style="color: #666666">8000</span> <span style="color: #408080; font-style: italic"># nodejs</span>
|
|
</pre></div>
|
|
|
|
<p>To see if this works, navigate to <a href="http://localhost:8000">http://localhost:8000</a> and check if you
|
|
can load the page without any errors appearing in the developer console.</p>
|
|
<p>Take care not to use Python's simple server in production since it does not serve WebAssembly files
|
|
with the correct MIME type.</p>
|
|
<h2>Create the Engine and Scene</h2>
|
|
<p>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.</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">this</span>.canvas <span style="color: #666666">=</span> <span style="color: #008000">document</span>.getElementsByTagName(<span style="color: #BA2121">'canvas'</span>)[<span style="color: #666666">0</span>];
|
|
<span style="color: #008000; font-weight: bold">const</span> engine <span style="color: #666666">=</span> <span style="color: #008000; font-weight: bold">this</span>.engine <span style="color: #666666">=</span> Filament.Engine.create(<span style="color: #008000; font-weight: bold">this</span>.canvas);
|
|
</pre></div>
|
|
|
|
<p>The above snippet creates the <code>Engine</code> by passing it a canvas DOM object. The engine needs the
|
|
canvas in order to create a WebGL 2.0 context in its contructor.</p>
|
|
<p>The engine is a factory for many Filament entities, including <code>Scene</code>, which is a flat container of
|
|
entities. Let's go ahead and create a scene, then add a blank entity called <code>triangle</code> into the
|
|
scene.</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">this</span>.scene <span style="color: #666666">=</span> engine.createScene();
|
|
<span style="color: #008000; font-weight: bold">this</span>.triangle <span style="color: #666666">=</span> Filament.EntityManager.get().create();
|
|
<span style="color: #008000; font-weight: bold">this</span>.scene.addEntity(<span style="color: #008000; font-weight: bold">this</span>.triangle);
|
|
</pre></div>
|
|
|
|
<p>Filament uses an <a href="//en.wikipedia.org/wiki/Entity-component-system">Entity-Component System</a>.
|
|
The triangle entity in the above snippet does not yet have an associated component. Later in the
|
|
tutorial we will make it into a <em>renderable</em>. Renderables are entities that have associated draw
|
|
calls.</p>
|
|
<h2>Construct typed arrays</h2>
|
|
<p>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.</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">const</span> TRIANGLE_POSITIONS <span style="color: #666666">=</span> <span style="color: #008000; font-weight: bold">new</span> Float32Array([
|
|
<span style="color: #666666">1</span>, <span style="color: #666666">0</span>,
|
|
<span style="color: #008000">Math</span>.cos(<span style="color: #008000">Math</span>.PI <span style="color: #666666">*</span> <span style="color: #666666">2</span> <span style="color: #666666">/</span> <span style="color: #666666">3</span>), <span style="color: #008000">Math</span>.sin(<span style="color: #008000">Math</span>.PI <span style="color: #666666">*</span> <span style="color: #666666">2</span> <span style="color: #666666">/</span> <span style="color: #666666">3</span>),
|
|
<span style="color: #008000">Math</span>.cos(<span style="color: #008000">Math</span>.PI <span style="color: #666666">*</span> <span style="color: #666666">4</span> <span style="color: #666666">/</span> <span style="color: #666666">3</span>), <span style="color: #008000">Math</span>.sin(<span style="color: #008000">Math</span>.PI <span style="color: #666666">*</span> <span style="color: #666666">4</span> <span style="color: #666666">/</span> <span style="color: #666666">3</span>),
|
|
]);
|
|
|
|
<span style="color: #008000; font-weight: bold">const</span> TRIANGLE_COLORS <span style="color: #666666">=</span> <span style="color: #008000; font-weight: bold">new</span> Uint32Array([<span style="color: #666666">0xffff0000</span>, <span style="color: #666666">0xff00ff00</span>, <span style="color: #666666">0xff0000ff</span>]);
|
|
</pre></div>
|
|
|
|
<p>Next we'll use the positions and colors buffers to create a single <code>VertexBuffer</code> object.</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">const</span> VertexAttribute <span style="color: #666666">=</span> Filament.VertexAttribute;
|
|
<span style="color: #008000; font-weight: bold">const</span> AttributeType <span style="color: #666666">=</span> Filament.VertexBuffer$AttributeType;
|
|
<span style="color: #008000; font-weight: bold">this</span>.vb <span style="color: #666666">=</span> Filament.VertexBuffer.Builder()
|
|
.vertexCount(<span style="color: #666666">3</span>)
|
|
.bufferCount(<span style="color: #666666">2</span>)
|
|
.attribute(VertexAttribute.POSITION, <span style="color: #666666">0</span>, AttributeType.FLOAT2, <span style="color: #666666">0</span>, <span style="color: #666666">8</span>)
|
|
.attribute(VertexAttribute.COLOR, <span style="color: #666666">1</span>, AttributeType.UBYTE4, <span style="color: #666666">0</span>, <span style="color: #666666">4</span>)
|
|
.normalized(VertexAttribute.COLOR)
|
|
.build(engine);
|
|
|
|
<span style="color: #008000; font-weight: bold">this</span>.vb.setBufferAt(engine, <span style="color: #666666">0</span>, TRIANGLE_POSITIONS);
|
|
<span style="color: #008000; font-weight: bold">this</span>.vb.setBufferAt(engine, <span style="color: #666666">1</span>, TRIANGLE_COLORS);
|
|
</pre></div>
|
|
|
|
<p>The above snippet first creates aliases for two enum types, then constructs the vertex buffer using
|
|
its <code>Builder</code> method. After that, it pushes two buffer objects into the appropriate slots using
|
|
<code>setBufferAt</code>.</p>
|
|
<p>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.</p>
|
|
<p>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.</p>
|
|
<p>Next we'll construct an index buffer. The index buffer for our triangle is trivial: it simply holds
|
|
the integers 0,1,2.</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">this</span>.ib <span style="color: #666666">=</span> Filament.IndexBuffer.Builder()
|
|
.indexCount(<span style="color: #666666">3</span>)
|
|
.bufferType(Filament.IndexBuffer$IndexType.USHORT)
|
|
.build(engine);
|
|
|
|
<span style="color: #008000; font-weight: bold">this</span>.ib.setBuffer(engine, <span style="color: #008000; font-weight: bold">new</span> Uint16Array([<span style="color: #666666">0</span>, <span style="color: #666666">1</span>, <span style="color: #666666">2</span>]));
|
|
</pre></div>
|
|
|
|
<p>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).</p>
|
|
<h2>Finish up initialization</h2>
|
|
<p>Next let's construct an actual <code>Material</code> from the material package that was downloaded (the
|
|
material is an object; the package is just a binary blob), then extract the default
|
|
<code>MaterialInstance</code> 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.</p>
|
|
<p>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.</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">const</span> mat <span style="color: #666666">=</span> engine.createMaterial(<span style="color: #BA2121">'triangle.filamat'</span>);
|
|
<span style="color: #008000; font-weight: bold">const</span> matinst <span style="color: #666666">=</span> mat.getDefaultInstance();
|
|
Filament.RenderableManager.Builder(<span style="color: #666666">1</span>)
|
|
.boundingBox({ center<span style="color: #666666">:</span> [<span style="color: #666666">-1</span>, <span style="color: #666666">-1</span>, <span style="color: #666666">-1</span>], halfExtent<span style="color: #666666">:</span> [<span style="color: #666666">1</span>, <span style="color: #666666">1</span>, <span style="color: #666666">1</span>] })
|
|
.material(<span style="color: #666666">0</span>, matinst)
|
|
.geometry(<span style="color: #666666">0</span>, Filament.RenderableManager$PrimitiveType.TRIANGLES, <span style="color: #008000; font-weight: bold">this</span>.vb, <span style="color: #008000; font-weight: bold">this</span>.ib)
|
|
.build(engine, <span style="color: #008000; font-weight: bold">this</span>.triangle);
|
|
</pre></div>
|
|
|
|
<p>Next let's wrap up the initialization routine by creating the swap chain, renderer, camera, and
|
|
view.</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">this</span>.swapChain <span style="color: #666666">=</span> engine.createSwapChain();
|
|
<span style="color: #008000; font-weight: bold">this</span>.renderer <span style="color: #666666">=</span> engine.createRenderer();
|
|
<span style="color: #008000; font-weight: bold">this</span>.camera <span style="color: #666666">=</span> engine.createCamera();
|
|
<span style="color: #008000; font-weight: bold">this</span>.view <span style="color: #666666">=</span> engine.createView();
|
|
<span style="color: #008000; font-weight: bold">this</span>.view.setCamera(<span style="color: #008000; font-weight: bold">this</span>.camera);
|
|
<span style="color: #008000; font-weight: bold">this</span>.view.setScene(<span style="color: #008000; font-weight: bold">this</span>.scene);
|
|
|
|
<span style="color: #408080; font-style: italic">// Set up a blue-green background:</span>
|
|
<span style="color: #008000; font-weight: bold">this</span>.renderer.setClearOptions({clearColor<span style="color: #666666">:</span> [<span style="color: #666666">0.0</span>, <span style="color: #666666">0.1</span>, <span style="color: #666666">0.2</span>, <span style="color: #666666">1.0</span>], clear<span style="color: #666666">:</span> <span style="color: #008000; font-weight: bold">true</span>});
|
|
|
|
<span style="color: #408080; font-style: italic">// Adjust the initial viewport:</span>
|
|
<span style="color: #008000; font-weight: bold">this</span>.resize();
|
|
</pre></div>
|
|
|
|
<p>At this point, we're done creating all Filament entities, and the code should run without errors.
|
|
However the canvas is still blank!</p>
|
|
<h2>Render and resize handlers</h2>
|
|
<p>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.</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span>render() {
|
|
<span style="color: #408080; font-style: italic">// TODO: render scene</span>
|
|
<span style="color: #008000">window</span>.requestAnimationFrame(<span style="color: #008000; font-weight: bold">this</span>.render);
|
|
}
|
|
</pre></div>
|
|
|
|
<p>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.</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #408080; font-style: italic">// Rotate the triangle.</span>
|
|
<span style="color: #008000; font-weight: bold">const</span> radians <span style="color: #666666">=</span> <span style="color: #008000">Date</span>.now() <span style="color: #666666">/</span> <span style="color: #666666">1000</span>;
|
|
<span style="color: #008000; font-weight: bold">const</span> transform <span style="color: #666666">=</span> mat4.fromRotation(mat4.create(), radians, [<span style="color: #666666">0</span>, <span style="color: #666666">0</span>, <span style="color: #666666">1</span>]);
|
|
<span style="color: #008000; font-weight: bold">const</span> tcm <span style="color: #666666">=</span> <span style="color: #008000; font-weight: bold">this</span>.engine.getTransformManager();
|
|
<span style="color: #008000; font-weight: bold">const</span> inst <span style="color: #666666">=</span> tcm.getInstance(<span style="color: #008000; font-weight: bold">this</span>.triangle);
|
|
tcm.setTransform(inst, transform);
|
|
inst.<span style="color: #008000; font-weight: bold">delete</span>();
|
|
|
|
<span style="color: #408080; font-style: italic">// Render the frame.</span>
|
|
<span style="color: #008000; font-weight: bold">this</span>.renderer.render(<span style="color: #008000; font-weight: bold">this</span>.swapChain, <span style="color: #008000; font-weight: bold">this</span>.view);
|
|
</pre></div>
|
|
|
|
<p>The first half of our render method obtains the transform component of the triangle entity and uses
|
|
gl-matrix to generate a rotation matrix.</p>
|
|
<p>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 <code>if</code> statement.</p>
|
|
<p>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 <code>devicePixelRatio</code> into account for high-DPI
|
|
displays. It also adjusts the camera frustum accordingly.</p>
|
|
<div class="highlight" style="background: #f8f8f8"><pre style="line-height: 125%"><span></span><span style="color: #008000; font-weight: bold">const</span> dpr <span style="color: #666666">=</span> <span style="color: #008000">window</span>.devicePixelRatio;
|
|
<span style="color: #008000; font-weight: bold">const</span> width <span style="color: #666666">=</span> <span style="color: #008000; font-weight: bold">this</span>.canvas.width <span style="color: #666666">=</span> <span style="color: #008000">window</span>.innerWidth <span style="color: #666666">*</span> dpr;
|
|
<span style="color: #008000; font-weight: bold">const</span> height <span style="color: #666666">=</span> <span style="color: #008000; font-weight: bold">this</span>.canvas.height <span style="color: #666666">=</span> <span style="color: #008000">window</span>.innerHeight <span style="color: #666666">*</span> dpr;
|
|
<span style="color: #008000; font-weight: bold">this</span>.view.setViewport([<span style="color: #666666">0</span>, <span style="color: #666666">0</span>, width, height]);
|
|
|
|
<span style="color: #008000; font-weight: bold">const</span> aspect <span style="color: #666666">=</span> width <span style="color: #666666">/</span> height;
|
|
<span style="color: #008000; font-weight: bold">const</span> Projection <span style="color: #666666">=</span> Filament.Camera$Projection;
|
|
<span style="color: #008000; font-weight: bold">this</span>.camera.setProjection(Projection.ORTHO, <span style="color: #666666">-</span>aspect, aspect, <span style="color: #666666">-1</span>, <span style="color: #666666">1</span>, <span style="color: #666666">0</span>, <span style="color: #666666">1</span>);
|
|
</pre></div>
|
|
|
|
<p>You should now have a spinning triangle! The completed JavaScript is available
|
|
<a href="tutorial_triangle.js">here</a>.</p>
|
|
<p>In the <a href="tutorial_redball.html">next tutorial</a>, we'll take a closer look at Filament materials and 3D rendering.</p>
|
|
|
|
</body>
|
|
</html>
|