web: refactor examples and tutorials (#9833)

* Refactor web examples and tutorials

- Consolidate web/docs and web/samples into web/examples
- Remove literal programming blocks from Markdown tutorials
- Replace tutorial_template.html with a fully embedded template in serve.py
- Move serve.py to web/examples/serve.py and update dependency rules
- Update filament-js wrapper to expose _malloc, _free and set GEN_MIPMAPPABLE usage
- Update sample materials and WebGL asset generation within web/examples/CMakeLists.txt
- Add python venv instructions and usage details to web/README.md
- Remove obsolete examples, Pipfile, and demo templates

* Transfer web tutorials and samples to docs_src

- Introduce copy_web_docs.py api to deploy and rewrite web sample outputs into the docs output tree
- Hook copy_web_docs into docs_src/build/run.py natively
- Configure duplicates.json to map WebGL outputs to embedded .md docs
- Strip html skeleton out of examples in run.py so they map into markdown cleanly
- Update update-docs workflow to pre-build the WebGL target
- Remove obsolete remote docs path from src_raw
- Add explanatory comments to copy_web_docs.py and run.py
- Add index page with thumbnails for Web Tutorials and samples
This commit is contained in:
Powei Feng
2026-03-25 16:52:48 -07:00
committed by GitHub
parent 541a4feae1
commit a73957a590
66 changed files with 1553 additions and 3333 deletions

View File

@@ -73,8 +73,10 @@ jobs:
- uses: ./.github/actions/linux-prereq
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- uses: ./.github/actions/web-prereq
- name: Prerequisites
run: pip install selenium
run: |
pip install selenium
- name: Run update script
env:
GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }}

View File

@@ -960,7 +960,7 @@ set(FILAMENT_SAMPLES_BINARY_DIR ${PROJECT_BINARY_DIR}/samples)
if (WEBGL)
add_subdirectory(web/filament-js)
add_subdirectory(web/samples)
add_subdirectory(web/examples)
endif()
if (IS_HOST_PLATFORM)

View File

@@ -76,6 +76,13 @@ located in `tools`. Some of tools also have README.md as description. We also co
The process for copying and processing these READMEs is outlined in [Introductory docs](#introductory-doc).
### Web Examples and Tutorials
Filament provides a number of WebGL tutorials and examples in the `web/` directory. These are compiled during the WebGL CMake build and are integrated into the documentation via `duplicates.json`. The process is entirely automated:
1. `run.py` maps the `.html` and `.md` WebGL outputs from the `out/cmake-webgl-release/...` directory into `docs_src/src_mdbook/src/samples/web/` using the instructions in `duplicates.json`.
2. While transferring `.html` to `.md`, `run.py` strips away the `<!DOCTYPE html>`, `<head>`, and `<html>` tags. By retaining only the `<style>` and `<body>` elements, the HTML samples can be embedded cleanly into the `mdbook` site template without corrupting the DOM. It additionally collapses double-newlines (`\n\n`) because Markdown parsers will mistakenly fragment and wrap multi-line HTML tags into `<p>` blocks.
3. After `mdbook build` concludes, `docs_src/build/copy_web_docs.py` is invoked. This script creates `web/lib` and `web/assets` directories inside the final `book/` output directory, copying in the compiled WebAssembly engine (`filament.wasm`/`filament.js`) and any necessary assets (`.filamat`, `.glb`, `.ktx`, etc.).
4. Finally, `copy_web_docs.py` performs a regex pass over all HTML pages in `book/samples/web/` and `book/remote/` to rewrite their inline resource URLs to securely point to the shared `web/lib` and `web/assets` directories. It also dynamically overrides the `asset.loadResources()` Javascript call with an absolute URL (`new URL(..., window.location.href)`) so that `.glb`/`.gltf` assets fetch their internal `.bin` chunks correctly.
### Other technical notes
These are technical documents that do not fit into a library, tool, or directory of the
Filament source tree. We collect them into the `docs_src/src_mdbook/src/notes` directory. No additional

View File

@@ -34,8 +34,11 @@ DOCS_SRC_DIR = os.path.abspath(os.path.join(CUR_DIR, '../'))
MARKDEEP_SRC_DIR = os.path.join(DOCS_SRC_DIR, 'src_markdeep')
MDBOOK_SRC_DIR = os.path.join(DOCS_SRC_DIR, 'src_mdbook')
RAW_SRC_DIR = os.path.join(DOCS_SRC_DIR, 'src_raw')
# Web samples and tutorials are processed and embedded into the docs output,
# so edits to the web directory must trigger a docs update.
WEB_SRC_DIR = os.path.abspath(os.path.join(CUR_DIR, '../../web/'))
SRC_SRC_DIRS = [MARKDEEP_SRC_DIR, MDBOOK_SRC_DIR, RAW_SRC_DIR]
SRC_SRC_DIRS = [MARKDEEP_SRC_DIR, MDBOOK_SRC_DIR, RAW_SRC_DIR, WEB_SRC_DIR]
def get_edited_files(commit_hash):
INSERT = '#####?????'

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
#
# Copyright (C) 2026 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import shutil
import glob
import re
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
OUT_DIR = os.path.join(CUR_DIR, "../../out/cmake-webgl-release/web/examples/examples")
BOOK_DIR = os.path.join(CUR_DIR, "../src_mdbook/book")
def setup_dirs():
os.makedirs(f"{BOOK_DIR}/web/assets", exist_ok=True)
os.makedirs(f"{BOOK_DIR}/web/lib", exist_ok=True)
def copy_libs():
"""
Copies shared WebGL javascript libraries and WebAssembly binaries from the build output
into a central web/lib directory in the mdbook output. This ensures all tutorials
and samples use the same built engine versions.
"""
libs = ['filament.js', 'filament.wasm', 'gl-matrix-min.js', 'gltumble.min.js']
for l in libs:
for root, dirs, files in os.walk(OUT_DIR):
if l in files:
src = os.path.join(root, l)
dst = f"{BOOK_DIR}/web/lib/{l}"
shutil.copy(src, dst)
break
def copy_assets():
"""
Finds all required web assets (textures, models, materials, stylesheets) generated
during the WebGL build and copies them into the web/assets directory.
It preserves the relative directory structure per sample so that each sample's
assets are isolated.
"""
exts = ['.filamat', '.filamesh', '.ktx', '.ktx2', '.png', '.jpg', '.bin', '.glb', '.gltf', '.hdr', '.css']
for root, dirs, files in os.walk(OUT_DIR):
for f in files:
if any(f.endswith(ext) for ext in exts):
src = os.path.join(root, f)
rel_path = os.path.relpath(src, OUT_DIR)
dst = os.path.join(BOOK_DIR, "web/assets", rel_path)
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copy(src, dst)
def fix_paths():
"""
Parses the generated HTML examples inside the mdbook output to rewrite their resource URLs.
Since we moved the libraries to `web/lib` and the assets to `web/assets`, the raw HTML
files (which assume assets are alongside them) need to be patched to point to the correct
relative paths based on their nesting depth.
"""
files_to_fix = glob.glob(f"{BOOK_DIR}/samples/web/*.html") + glob.glob(f"{BOOK_DIR}/remote/*.html")
exts = "filamat|filamesh|ktx|ktx2|png|jpg|bin|glb|gltf|hdr|css"
asset_pattern = r'([\'"`])((?:\.\.\/)?(?:[\w\-\/]+\.)(?:' + exts + r'))\1'
for f in files_to_fix:
with open(f, 'r') as file:
text = file.read()
sample_name = os.path.splitext(os.path.basename(f))[0]
# Compute the relative depth prefix depending on where the HTML is located
if "samples/web/" in f:
depth_prefix = "../../web/"
else:
depth_prefix = "../web/"
# Replace JS libs
def js_repl(m):
quote = m.group(1)
lib = m.group(3)
return f"{quote}{depth_prefix}lib/{lib}{quote}"
text = re.sub(r'([\'"`])(\.\.\/)?(filament\.js|gl-matrix-min\.js|gltumble\.min\.js|filament\.wasm)\1', js_repl, text)
# Replace assets
def asset_repl(m):
quote = m.group(1)
path = m.group(2)
if path.startswith("../"):
return f"{quote}{depth_prefix}assets/{path[3:]}{quote}"
else:
return f"{quote}{depth_prefix}assets/{sample_name}/{path}{quote}"
text = re.sub(asset_pattern, asset_repl, text)
# Fix loadResources base path for glTF models so they can find their external bins/pngs
# By passing a fully qualified absolute URL using window.location.href, we ensure the
# browser correctly fetches associated chunks from the web/assets directory.
def fix_load_resources(m):
prefix = m.group(1)
args_str = m.group(2).strip()
args = [a.strip() for a in args_str.split(',')] if args_str else []
while len(args) < 2:
args.append('null')
args.append(f"new URL('{depth_prefix}assets/{sample_name}/', window.location.href).href")
return f"{prefix}.loadResources({', '.join(args)});"
text = re.sub(r'(\w+(?:\.\w+)*)\.loadResources\(([^)]*)\);', fix_load_resources, text)
with open(f, 'w') as file:
file.write(text)
def run():
"""
Executes the entire web docs post-processing pipeline:
1. Sets up the destination directories
2. Copies the Javascript and WebAssembly libraries
3. Copies the WebGL generated assets
4. Rewrites the resource paths inside the HTML examples
"""
setup_dirs()
copy_libs()
copy_assets()
fix_paths()
if __name__ == "__main__":
run()

View File

@@ -102,5 +102,35 @@
},
"filament/backend/test/README.md": {
"dest": "dup/backend_test.md"
},
"out/cmake-webgl-release/web/examples/examples/triangle/triangle.md": {
"dest": "samples/web/triangle.md"
},
"out/cmake-webgl-release/web/examples/examples/redball/redball.md": {
"dest": "samples/web/redball.md"
},
"out/cmake-webgl-release/web/examples/examples/suzanne/suzanne.md": {
"dest": "samples/web/suzanne.md"
},
"out/cmake-webgl-release/web/examples/examples/animation/animation.html": {
"dest": "samples/web/animation.md"
},
"out/cmake-webgl-release/web/examples/examples/cube_fl0/cube_fl0.html": {
"dest": "samples/web/cube_fl0.md"
},
"out/cmake-webgl-release/web/examples/examples/helmet/helmet.html": {
"dest": "samples/web/helmet.md"
},
"out/cmake-webgl-release/web/examples/examples/morphing/morphing.html": {
"dest": "samples/web/morphing.md"
},
"out/cmake-webgl-release/web/examples/examples/parquet/parquet.html": {
"dest": "samples/web/parquet.md"
},
"out/cmake-webgl-release/web/examples/examples/skinning/skinning.html": {
"dest": "samples/web/skinning.md"
},
"out/cmake-webgl-release/web/examples/examples/remote/remote.html": {
"dest": "remote/remote.html"
}
}
}

View File

@@ -19,6 +19,7 @@ FILAMENT_BOT_TOKEN=$2
set -ex
function update_to_main() {
./build.sh -p webgl release
python3 docs_src/build/run.py
mkdir -p tmp
pushd .

View File

@@ -35,6 +35,14 @@ FILAMENT_MD = 'Filament.md.html'
MATERIALS_MD = 'Materials.md.html'
def transform_dup_file_link(line, transforms):
"""
Transforms markdown links in duplicated files to point to the correct relative locations
within the generated mdbook site.
Args:
line: The markdown line containing potential links.
transforms: A dictionary mapping original link prefixes to their new destinations.
"""
URL_CONTENT = '[-a-zA-Z0-9()@:%_\+.~#?&//=]+'
res = re.findall(f'\[(.+)\]\(({URL_CONTENT})\)', line)
for text, url in res:
@@ -47,6 +55,12 @@ def transform_dup_file_link(line, transforms):
return line
def pull_duplicates():
"""
Reads `duplicates.json` and copies files from the project root into the mdbook source directory.
This allows us to maintain a single source of truth for files like README.md or CONTRIBUTING.md,
while seamlessly embedding them into the generated documentation site.
It also converts raw HTML examples to Markdown by extracting just the body and styles.
"""
if not os.path.exists(DUP_DIR):
os.mkdir(DUP_DIR)
@@ -59,11 +73,24 @@ def pull_duplicates():
link_transforms = config[fin].get('link_transforms', {})
fpath = os.path.join(ROOT_DIR, fin)
new_fpath = os.path.join(SRC_DIR, new_name)
os.makedirs(os.path.dirname(new_fpath), exist_ok=True)
with open(fpath, 'r') as in_file:
with open(new_fpath, 'w') as out_file:
for line in in_file.readlines():
out_file.write(transform_dup_file_link(line, link_transforms))
content = in_file.read()
if fpath.endswith('.html') and new_fpath.endswith('.md'):
# We replace double newlines so mdbook does not treat it as separated markdown paragraphs
content = content.replace("\n\n", "\n")
import re
style_match = re.search(r'<style>(.*?)</style>', content, re.DOTALL | re.IGNORECASE)
style_str = f"<style>{style_match.group(1)}</style>\n" if style_match else ""
body_match = re.search(r'<body[^>]*>(.*?)</body>', content, re.DOTALL | re.IGNORECASE)
if body_match:
content = style_str + body_match.group(1)
with open(new_fpath, 'w') as out_file:
for line in content.splitlines(True):
out_file.write(transform_dup_file_link(line, link_transforms))
def pull_markdeep_docs():
import http.server
@@ -166,8 +193,9 @@ if __name__ == "__main__":
res, err = execute('mdbook build', cwd=MDBOOK_DIR)
assert res == 0, f"failed to execute `mdbook`. return-code={res} err=\"{err}\""
RAW_IGNORES = shutil.ignore_patterns('update_remote_ui.sh')
shutil.copytree(RAW_COPIES_DIR, BOOK_OUPUT_DIR, ignore=RAW_IGNORES, dirs_exist_ok=True)
shutil.copytree(RAW_COPIES_DIR, BOOK_OUPUT_DIR, dirs_exist_ok=True)
shutil.copy(os.path.join(MARKDEEP_DIR, FILAMENT_MD), BOOK_OUPUT_DIR)
shutil.copy(os.path.join(MARKDEEP_DIR, MATERIALS_MD), BOOK_OUPUT_DIR)
import copy_web_docs
copy_web_docs.run()

View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
#
# Copyright (C) 2026 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Automated WebGL Snapshot Generator
This script starts a local HTTP server pointing to the built mdbook output directory
and uses a headless Chrome browser (via Selenium) to load the generated WebGL examples.
For each sample and tutorial:
1. It waits for the scene and its assets (textures, models, materials) to fully render.
2. It explicitly targets the `<canvas>` DOM element to take an isolated screenshot.
3. It crops the raw screenshot into a perfect square based on its aspect ratio.
4. It uses Lanczos resampling to scale the image down to a 100x100 pixel thumbnail.
5. It saves the final thumbnail to `docs_src/src_mdbook/src/images/` for embedding in the Markdown.
These thumbnails are used directly by the "Web Tutorials" and "Web Samples" index pages.
"""
import os
import time
from PIL import Image
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
import http.server
import socketserver
import threading
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
BOOK_DIR = os.path.join(CUR_DIR, '../src_mdbook/book')
IMAGES_DIR = os.path.join(CUR_DIR, '../src_mdbook/src/images')
os.makedirs(IMAGES_DIR, exist_ok=True)
class Server(socketserver.ThreadingMixIn, http.server.HTTPServer):
pass
class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=BOOK_DIR, **kwargs)
def start_server(port):
httpd = Server(("", port), Handler)
server_thread = threading.Thread(target=httpd.serve_forever)
server_thread.daemon = True
server_thread.start()
print(f"Server started on port {port}...")
return httpd
def snapshot_samples():
port = 8081
httpd = start_server(port)
time.sleep(2)
chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument("--window-size=800,600")
driver = webdriver.Chrome(options=chrome_options)
samples = ['animation', 'cube_fl0', 'helmet', 'morphing', 'parquet', 'skinning', 'triangle', 'redball', 'suzanne']
for sample in samples:
print(f"Taking snapshot of {sample}...")
driver.get(f"http://localhost:{port}/samples/web/{sample}.html")
time.sleep(4) # Wait for models to load and render
canvas = driver.find_element(By.TAG_NAME, "canvas")
if not canvas:
print(f"Failed to find canvas for {sample}")
continue
# Get raw screenshot of the canvas element
png = canvas.screenshot_as_png
import io
img = Image.open(io.BytesIO(png))
# Crop to square
width, height = img.size
size = min(width, height)
left = (width - size) / 2
top = (height - size) / 2
right = (width + size) / 2
bottom = (height + size) / 2
img = img.crop((left, top, right, bottom))
# Resize to 100x100
img = img.resize((100, 100), Image.Resampling.LANCZOS)
img.save(os.path.join(IMAGES_DIR, f"web_sample_{sample}.png"))
print(f"Saved snapshot for {sample}")
driver.quit()
httpd.shutdown()
if __name__ == "__main__":
snapshot_samples()

View File

@@ -10,7 +10,17 @@
- [Materials](./main/materials.md)
- [Tutorials and Samples](./samples/README.md)
- [iOS Tutorial](./samples/ios.md)
- [Web Tutorial](./samples/web.md)
- [Web Tutorials](./samples/web/tutorials.md)
- [triangle](./samples/web/triangle.md)
- [redball](./samples/web/redball.md)
- [suzanne](./samples/web/suzanne.md)
- [Web Samples](./samples/web/samples.md)
- [animation](./samples/web/animation.md)
- [cube_fl0](./samples/web/cube_fl0.md)
- [helmet](./samples/web/helmet.md)
- [morphing](./samples/web/morphing.md)
- [parquet](./samples/web/parquet.md)
- [skinning](./samples/web/skinning.md)
- [Technical Notes](./notes/README.md)
- [Material Properties](./notes/material_properties.md)
- [Release](./release/README.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -1,19 +0,0 @@
# Web Docs
## [This page is under construction. Links are not working at the moment]
## tutorials
1. [Triangle Tutorial](tutorial_triangle.html)
2. [Redball Tutorial](tutorial_redball.html)
3. [Suzanne Tutorial](tutorial_suzanne.html)
## demos
- [parquet](parquet.html)
- [suzanne](suzanne.html)
- [helmet](helmet.html)
- [knotess](https://prideout.net/knotess/)
## other documentation
- [WebGL Meetup Slides](https://prideout.net/slides/filawasm) (2018)

View File

@@ -0,0 +1,65 @@
# Web Samples
Here are some additional standalone examples demonstrating Filament's capabilities in WebGL:
<style>
.sample-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 20px;
margin-top: 20px;
}
.sample-card {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
text-decoration: none;
color: inherit;
border: 1px solid var(--sidebar-bg);
border-radius: 8px;
padding: 15px;
transition: transform 0.2s, box-shadow 0.2s;
background-color: var(--bg);
}
.sample-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
text-decoration: none;
}
.sample-card img {
border-radius: 4px;
margin-bottom: 10px;
width: 100px;
height: 100px;
object-fit: cover;
}
</style>
<div class="sample-grid">
<a href="animation.md" class="sample-card">
<img src="../../images/web_sample_animation.png" alt="animation" />
<span>animation</span>
</a>
<a href="cube_fl0.md" class="sample-card">
<img src="../../images/web_sample_cube_fl0.png" alt="cube_fl0" />
<span>cube_fl0</span>
</a>
<a href="helmet.md" class="sample-card">
<img src="../../images/web_sample_helmet.png" alt="helmet" />
<span>helmet</span>
</a>
<a href="morphing.md" class="sample-card">
<img src="../../images/web_sample_morphing.png" alt="morphing" />
<span>morphing</span>
</a>
<a href="parquet.md" class="sample-card">
<img src="../../images/web_sample_parquet.png" alt="parquet" />
<span>parquet</span>
</a>
<a href="skinning.md" class="sample-card">
<img src="../../images/web_sample_skinning.png" alt="skinning" />
<span>skinning</span>
</a>
</div>

View File

@@ -0,0 +1,53 @@
# Web Tutorials
Here are the step-by-step tutorials to get you started with Filament on the Web.
<style>
.sample-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 20px;
margin-top: 20px;
}
.sample-card {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
text-decoration: none;
color: inherit;
border: 1px solid var(--sidebar-bg);
border-radius: 8px;
padding: 15px;
transition: transform 0.2s, box-shadow 0.2s;
background-color: var(--bg);
}
.sample-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
text-decoration: none;
}
.sample-card img {
border-radius: 4px;
margin-bottom: 10px;
width: 100px;
height: 100px;
object-fit: cover;
}
</style>
<div class="sample-grid">
<a href="triangle.md" class="sample-card">
<img src="../../images/web_sample_triangle.png" alt="triangle" />
<span>triangle</span>
</a>
<a href="redball.md" class="sample-card">
<img src="../../images/web_sample_redball.png" alt="redball" />
<span>redball</span>
</a>
<a href="suzanne.md" class="sample-card">
<img src="../../images/web_sample_suzanne.png" alt="suzanne" />
<span>suzanne</span>
</a>
</div>

File diff suppressed because one or more lines are too long

View File

@@ -1,472 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Filament Remote</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link href="../favicon.png" rel="icon" type="image/x-icon" />
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet">
<style>
html, body {
height: 100%;
}
body {
margin: 0;
overflow: hidden;
}
.container {
font-family: "Open Sans";
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.connection-settings {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
max-width: 640px;
max-height: 32px;
background: rgb(189, 189, 189);
border: solid 2px black;
border-bottom: none;
font-size: 12px;
}
.connection-settings input {
font-family: "Open Sans";
background-color: rgb(240, 240, 240);
border: solid 2px black;
outline: none;
text-align: center;
height: 20px;
width: 120px;
}
.connection-settings input:invalid { background-color: #ff9090; }
.connection-settings .connect-to-label {
display: inline-block;
margin-right: 15px;
}
.connection-settings .port-label {
display: inline-block;
margin-left: 7px;
}
canvas {
flex-direction: column;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
max-width: 640px;
max-height: 640px;
border: solid 2px black;
}
.dropbox-area {
flex-direction: column;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
max-width: 640px;
max-height: 160px;
background: rgb(189, 189, 189);
border: solid 2px black;
border-top: none;
}
.dropbox-area p { text-align: center; }
.instructions-area { margin-top: 12px; }
a { text-decoration: none; }
a:visited { color: rgb(26, 65, 78); }
.bad { background: lightcoral; }
.good { background: #45d48d; }
</style>
</head>
<body>
<div class="container">
<div class="connection-settings">
<label class="connect-to-label">Connect to</label>
<input id="connection-url" type="text" value="localhost" />
<label class="port-label">: 8082</label>
</div>
<canvas id="webgl2-canvas"></canvas>
<div id="dropbox" class="dropbox-area"><p>Disconnected.</p></div>
<div id="instructions" class="instructions-area">
<button id="copyButton">Copy adb command to clipboard</button>
</div>
</div>
<script src="filament.js"></script>
<script>
"use strict";
document.getElementById("copyButton").addEventListener("click", () => {
navigator.clipboard.writeText("adb forward tcp:8082 tcp:8082");
});
["dragenter", "dragover", "dragleave", "drop"].forEach(eventName => {
dropbox.addEventListener(eventName, e => { e.preventDefault(); e.stopPropagation() }, false)
})
Filament.init([], () => window.app = new App() );
const debounce = (callback, delay) => {
let timeout;
return () => {
clearTimeout(timeout);
timeout = setTimeout(callback, delay);
};
};
/**
* Manages a WebSocket connection. If connection fails, continuously tries to reconnect.
* This class mantains the invariant that at most one socket connection is open at any given time.
*/
class Connection {
constructor() {
this.retryInterval = 3000;
this.poll = null;
this.connectionOpen = false;
this.listeners = {};
}
/**
* Attempt to connect to url through a WebSocket. The url should be a valid WebSocket URL,
* using either the ws:// or ws:// scheme. Calling 'connect' cancels any previous connection
* attempts.
* If the connection succeeds, the 'open' event is fired.
* If the connection fails or is closed, the 'close' event is fired. A connection will be
* continuously re-attempted until either the connection succeeds, or 'disconnect' is called.
*/
connect(url) {
this.disconnect();
const websocket = this.ws = new WebSocket(url);
this.ws.addEventListener("open", () => {
if (websocket.ignoreEvents) {
return;
}
clearTimeout(this.poll);
this.connectionOpen = true;
this.listeners["open"] && this.listeners["open"]();
});
this.ws.addEventListener("close", () => {
if (websocket.ignoreEvents) {
return;
}
if (this.connectionOpen) {
this.connectionOpen = false;
this.listeners["close"] && this.listeners["close"]();
}
// Reset the polling timeout. It's important that we clear the previous timeout, as
// we're about to lose its handle.
clearTimeout(this.poll);
this.poll = setTimeout(() => this.connect(url), this.retryInterval);
});
this.ws.addEventListener("message", event => {
this.listeners["message"] && this.listeners["message"](event);
});
this.poll = setTimeout(() => this.connect(url), this.retryInterval);
}
send(what) {
if (!this.ws || !this.connectionOpen) {
return;
}
this.ws.send(what);
}
/**
* Immediately disconnects a WebSocket or cancels any connection attempt. If a connection was
* active, the 'close' event is fired.
*/
disconnect() {
// Calling ws.close() causes the 'close' event to fire. We set a flag to ensure the listener
// callbacks return immediately, without any further action.
if (this.ws) {
this.ws.ignoreEvents = true;
this.ws.close();
}
this.ws = null;
if (this.connectionOpen) {
this.connectionOpen = false;
this.listeners['close'] && this.listeners['close']();
}
clearTimeout(this.poll);
}
isConnected() {
return this.connectionOpen;
}
addEventListener(type, listener) {
this.listeners[type] = listener;
}
}
class App {
constructor(canvas) {
this.connection = new Connection();
this.cachedFile = null;
this.connectionUrl = document.getElementById("connection-url");
this.dropbox = document.getElementById("dropbox");
this.canvas = document.getElementsByTagName("canvas")[0];
const engine = this.engine = Filament.Engine.create(this.canvas);
const scene = this.scene = engine.createScene();
const view = this.view = engine.createView();
const uiview = this.uiview = engine.createView();
this.swapChain = engine.createSwapChain();
this.renderer = engine.createRenderer();
this.camera = engine.createCamera(Filament.EntityManager.get().create());
this.serializer = new Filament.JsonSerializer();
this.previousSettingsJson = "";
this.urlRegex =
/^((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$|localhost$/;
view.setScene(scene);
view.setCamera(this.camera);
view.setPostProcessingEnabled(false);
// For now, we initialize the "sidebar" such that it stretches across the entire viewport.
// In the future we might want to draw stuff in the central 3D viewport.
const kInitialSidebarWidth = this.canvas.clientWidth * window.devicePixelRatio;
// Clear the central 3D viewport to light gray, which is not visible by default.
const L = 189 / 255;
const kBackgroundColor = [L, L, L, 1];
this.renderer.setClearOptions({clearColor: kBackgroundColor, clear: true});
this.render = this.render.bind(this);
this.simpleViewer = new Filament.ViewerGui(engine, scene, view, kInitialSidebarWidth);
this.mouseX = -1;
this.mouseY = -1;
this.mouseButton = null;
this.mouseButtonEvents = [];
this.mouseWheelY = 0;
Object.seal(this);
this.canvas.addEventListener("pointermove", e => {
this.mouseX = e.offsetX * window.devicePixelRatio;
this.mouseY = e.offsetY * window.devicePixelRatio;
});
this.canvas.addEventListener("pointerup", e => this.mouseButtonEvents.push(null));
this.canvas.addEventListener("pointerdown", e => this.mouseButtonEvents.push(e));
this.canvas.addEventListener("mousewheel", e => this.mouseWheelY = e.deltaY / 8);
this.canvas.addEventListener("contextmenu", event => event.preventDefault());
document.addEventListener("keydown", e => this.simpleViewer.keyDownEvent(e.keyCode));
document.addEventListener("keyup", e => this.simpleViewer.keyUpEvent(e.keyCode));
document.addEventListener("keypress", e => this.simpleViewer.keyPressEvent(e.charCode));
const resize = () => {
const dpr = window.devicePixelRatio;
const width = this.canvas.clientWidth * dpr;
const height = this.canvas.clientHeight * dpr;
this.resize(width, height);
};
window.addEventListener("resize", resize);
resize();
const dropbox = this.dropbox;
const upload = this.uploadFile.bind(this);
dropbox.addEventListener("dragover", dragEvent => {
dropbox.classList.add("bad");
if (!event.dataTransfer) return;
if (event.dataTransfer.items[0].kind !== "file") return;
dropbox.classList.remove("bad");
dropbox.classList.add("good");
}, false);
dropbox.addEventListener("dragleave", () => {
dropbox.classList.remove("good", "bad");
}, false);
dropbox.addEventListener("drop", dragEvent => {
dropbox.classList.remove("good", "bad");
if (!event.dataTransfer) return;
if (event.dataTransfer.items[0].kind !== "file") return;
const file = event.dataTransfer.items[0].getAsFile();
const is_glb = file.name.match(/\.(glb)$/i);
const is_zip = file.name.match(/\.(zip)$/i);
const is_hdr = file.name.match(/\.(hdr)$/i);
if (!is_glb && !is_zip && !is_hdr) return;
const files = event.dataTransfer.files;
([...files]).forEach(upload);
}, false);
const url = this.connectionUrl;
// The pattern attribute only serves as a visual indication that the inputted URL is valid.
// We'll check the URL against the regex inside of startConnection.
url.pattern = this.urlRegex.source;
url.addEventListener("input", debounce(this.startConnection.bind(this), 1000));
const connectionUrlString = localStorage.getItem("connectionUrlString");
if (connectionUrlString) {
url.value = connectionUrlString;
}
this.startConnection();
window.requestAnimationFrame(this.render);
}
uploadFile(file) {
if (this.connection.isConnected()) {
console.info(`Uploading ${file.name}`);
file.arrayBuffer().then(buffer => {
this.connection.send(file.name);
this.connection.send(buffer);
});
}
this.cachedFile = file;
}
updateDom() {
const instructions = document.getElementById("instructions");
if (this.connection.isConnected()) {
instructions.style.visibility = "hidden";
this.dropbox.innerHTML = `<div>
<p>Connected.</p>
<p>Drop a <b>glb</b>, <b>zip</b>, or <b>hdr</b> file here.</p>
</div>`;
} else {
instructions.style.visibility = "visible";
this.dropbox.innerHTML = `<div>
<p>Disconnected.</p>
<p>Ensure app is active and port forwarding is enabled.</p>
<p><b>adb forward tcp:8082 tcp:8082</b></p>
</div>`;
}
}
startConnection() {
const urlInput = this.connectionUrl.value;
if (!urlInput.match(this.urlRegex)) {
// This is an invalid URL. We'll stop trying to connect for now, and try again upon
// the next edit to the url input.
console.info(`Invalid URL: ${urlInput}`);
this.connection.disconnect();
return;
}
// Given a valid URL, we'll save it in local storage to persist it even if the user reloads
// the page.
localStorage.setItem("connectionUrlString", urlInput);
const url = `ws://${urlInput}:8082`;
this.connection.connect(url);
this.connection.addEventListener("open", () => {
this.updateDom();
this.previousSettingsJson = "";
if (this.cachedFile) {
this.uploadFile(this.cachedFile);
}
});
this.connection.addEventListener("close", () => {
this.updateDom();
});
this.connection.addEventListener("message", (event) => {
if (!event.data) return;
let commands = [];
try {
commands = JSON.parse(event.data);
}
catch (err) {
console.error(err, event.data);
return;
}
for (const command of commands) {
// TODO: process incoming messages here.
// Currently we do not send messages from the app on the device to the web client.
}
});
}
render() {
// Process only a single mouse button event to ensure that ImGui detects a touch event
// even when a down-up pair occurs between consecutive frames.
let mouseButton = this.mouseButton;
if (this.mouseButtonEvents.length > 0) {
this.mouseButton = this.mouseButtonEvents.shift();
}
const mouseWheel = this.mouseWheelY;
this.mouseWheelY = 0;
this.simpleViewer.mouseEvent(this.mouseX, this.mouseY, !!this.mouseButton,
mouseWheel, this.mouseButton && this.mouseButton.ctrlKey);
// If there's no connection, let Filament clear the canvas, but do not render the UI view.
if (!this.connection.isConnected()) {
this.renderer.beginFrame(this.swapChain);
this.renderer.renderView(this.view);
this.renderer.endFrame();
this.engine.execute();
window.requestAnimationFrame(this.render);
return;
}
// Draw the UI and potentially mutate the settings object.
const deltaTime = 1.0 / 60.0;
this.simpleViewer.renderUserInterface(deltaTime, this.uiview, window.devicePixelRatio);
// Check if the user has changed any settings.
const settingsJson = this.serializer.writeJson(this.simpleViewer.getSettings()).slice();
if (this.previousSettingsJson != settingsJson) {
if (this.connection.isConnected()) {
this.connection.send("settings.json");
this.connection.send(settingsJson);
}
this.previousSettingsJson = settingsJson;
}
// Use Filament to render the 3D viewport and the UI.
this.renderer.beginFrame(this.swapChain);
this.renderer.renderView(this.view);
this.renderer.renderView(this.uiview);
this.renderer.endFrame();
this.engine.execute();
window.requestAnimationFrame(this.render);
}
resize(width, height) {
const Projection = Filament.Camera$Projection;
this.canvas.width = width;
this.canvas.height = height;
this.view.setViewport([0, 0, width, height]);
this.uiview.setViewport([0, 0, width, height]);
this.camera.setProjection(Projection.ORTHO, 0, width, height, 0, 0, 1);
}
}
</script>
</body>
</html>

View File

@@ -1,17 +0,0 @@
#!/bin/bash
pushd "$(dirname "$0")" > /dev/null
curl -OL https://nightly.link/google/filament/workflows/web-continuous/main/filament-web.zip
unzip -q filament-web.zip
tar -xvzf filament-release-web.tgz
rm filament-release-web.tgz
rm filament-web.zip
rm filament.d.ts
cp ../../../web/samples/remote.html index.html
popd
git status
echo ""
echo "All done! Next, commit the changed files"

View File

@@ -38,12 +38,12 @@ filament-viewer::part(canvas) {
<p>
<filament-viewer
enableDrop="true"
ibl="../webgl/default_env/default_env_ibl.ktx"
ibl="../web/assets/helmet/default_env/default_env_ibl.ktx"
intensity="20000" />
</p>
</main>
<script src="https://unpkg.com/filament@1.51.6/filament.js"></script>
<script src="../web/lib/filament.js"></script>
<script src="https://unpkg.com/gltumble"></script>
<script src="filament-viewer.js" type="module"></script>
</body>

63
web/README.md Normal file
View File

@@ -0,0 +1,63 @@
# Web Samples and Tutorials
This directory contains the web-based samples, tutorials, and the JavaScript wrapper for Filament.
## Structure
- `examples/`
Contains the source for the WebGL tutorials, HTML samples, materials, and assets.
These examples use `filament.js` and `filament.wasm` to demonstrate various features of the engine.
- `filament-js/`
Contains the JavaScript bindings for Filament, generated via Emscripten.
## Building the Web Examples
To build the WebGL targets and compile all required materials and assets, you will need the Emscripten SDK installed and activated.
1. **Activate Emscripten SDK:**
Make sure you have `EMSDK` in your environment.
```bash
cd path/to/emsdk
./emsdk activate latest
source ./emsdk_env.sh
```
2. **Run the Build Script:**
From the root directory of the repository, execute the build script targeting WebGL:
```bash
./build.sh -p webgl release
```
This will:
- Compile the C++ engine to `filament.wasm` and `filament.js`.
- Build all materials (`.mat` to `.filamat`) and process textures required by the examples.
- Output everything into `out/cmake-webgl-release/examples/`.
## Running the Examples
Because of CORS restrictions and the need to serve WebAssembly files with the correct MIME type, you must serve the files via a local web server.
1. **Install Python Requirements:**
The serve script dynamically renders Markdown tutorials on-the-fly using `mistletoe`. It is highly recommended to use a Python virtual environment to install dependencies.
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r web/examples/requirements.txt
```
2. **Start the Server:**
From the root of the repository, run the `serve.py` script:
```bash
./web/examples/serve.py
```
3. **View the Examples:**
Open your browser and navigate to `http://localhost:8000`. You will see an index page listing all the generated tutorials and samples.
## Python Script: `serve.py`
The custom `serve.py` script in the `web/examples/` directory performs the following:
- Automatically detects the built files in the `out/` directory.
- Generates a main entry `index.html` listing all the available samples and tutorials.
- Handles server-side rendering of `.md` tutorial files into HTML using an embedded template.

View File

@@ -1,14 +0,0 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
jsbeautifier = "*"
mistletoe = "*"
Pygments = ">=2.7.4"
[requires]
python_version = "3.9"

View File

@@ -1,546 +0,0 @@
#!/usr/bin/env python3
# To run this script, I recommend using pipenv to create an isolated Python environment with the
# correct packages so that you do not interfere with other Python projects in your system.
# After installing pipenv, run the following commands from the current folder:
#
# pipenv --python /usr/local/opt/python@3.9/bin/python3
# pipenv install
# pipenv shell
# ./build.py
"""Converts markdown into HTML and extracts JavaScript code blocks.
For each markdown file, this script invokes mistletoe twice: once
to generate HTML, and once to extract JavaScript code blocks.
Literate code fragments are marked by "// TODO: <name>". These get
replaced according to extra properties on the code fences.
For example, the following snippet would replace "// TODO: create
wombat".
```js {fragment="create wombat"}
var wombat = new Wombat();
```
This script also generates reference documentation by extracting
doxygen style comments of the form:
/// [name] ::tags:: brief description
/// detailed description
Where "tags" consists of one or more of the following words:
class, core, method, argument, retval, function
These docstrings are used to build a JSON hierarchy where the roots
are classes, free functions, and enums. The JSON is then traversed to
generate a markdown string, which then produces HTML.
"""
import glob
import os
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) + '/'
ROOT_DIR = SCRIPT_DIR + '../../'
OUTPUT_DIR = ROOT_DIR + 'docs/webgl/'
ENABLE_EMBEDDED_DEMO = True
BUILD_DIR = ROOT_DIR + 'out/cmake-webgl-release/'
TOOLS_DIR = ROOT_DIR + 'out/cmake-release/tools/'
TUTORIAL_PREAMBLE = """
## Literate programming
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
`// TODO: <some task>`. These are special markers that get replaced by
subsequent code blocks.
"""
REFERENCE_PREAMBLE = """
All type names in this reference belong to the Filament namespace.
For example, **[init](#init)** actually refers to **Filament.init**.
""".strip().replace("\n", " ")
import argparse
import jsbeautifier
import mistletoe
import pygments
import re
import shutil
from itertools import chain
from mistletoe import HTMLRenderer
from mistletoe.base_renderer import BaseRenderer
from mistletoe import span_token
from mistletoe.block_token import CodeFence as CF
from pygments import highlight
from pygments.formatters.html import HtmlFormatter
from pygments.lexers import get_lexer_by_name as get_lexer
from pygments.styles import get_style_by_name as get_style
class PygmentsRenderer(HTMLRenderer):
"Extends HTMLRenderer by adding syntax highlighting"
formatter = HtmlFormatter()
formatter.noclasses = True
def __init__(self, *extras, style='default'):
super().__init__(*extras)
self.formatter.style = get_style(style)
def render_block_code(self, token):
code = token.children[0].content
lexer = get_lexer(token.language)
return highlight(code, lexer, self.formatter)
class CodeFence(CF):
"Extends the standard CodeFence with optional properties"
ppattern = r' *(\{ *(.+) *\= *\"(.+)\" *\})?'
pattern = re.compile(r'( {0,3})((?:`|~){3,}) *(\S*)' + ppattern)
_open_info = None
def __init__(self, match):
lines, open_info = match
self.language = span_token.EscapeSequence.strip(open_info[2])
self.children = (span_token.RawText(''.join(lines)),)
self.properties = open_info[3]
@classmethod
def start(cls, line):
match_obj = cls.pattern.match(line)
if not match_obj:
return False
prepend, leader, lang, props = match_obj.groups()[:4]
if leader[0] in lang or leader[0] in line[match_obj.end():]:
return False
cls._open_info = len(prepend), leader, lang, props
return True
class JsRenderer(BaseRenderer):
ppattern = re.compile(CodeFence.ppattern)
def __init__(self, *extras):
self.root = ''
self.fragments = {}
super().__init__(*chain((CodeFence,), extras))
def __exit__(self, *args): super().__exit__(*args)
def render_strong(self, token): return ''
def render_emphasis(self, token): return ''
def render_inline_code(self, token): return ''
def render_strikethrough(self, token): return ''
def render_image(self, token): return ''
def render_link(self, token): return ''
def render_auto_link(self, token): return ''
def render_escape_sequence(self, token): return ''
def render_raw_text(self, token): return ''
def render_heading(self, token): return ''
def render_quote(self, token): return ''
def render_paragraph(self, token): return ''
def render_list(self, token): return ''
def render_list_item(self, token): return ''
def render_document(self, token):
for child in token.children:
self.render(child)
label_pattern = re.compile(r'\s*// TODO:\ (.+)')
result = ''
for line in self.root.split('\n'):
m = label_pattern.match(line)
label = m.group(1) if m else None
if label in self.fragments:
result += self.fragments[label]
else:
result += line + '\n'
opts = jsbeautifier.default_options()
opts.indent_size = 2
opts.end_with_newline = True
opts.preserve_newlines = False
opts.break_chained_methods = True
opts.wrap_line_length = 100
return jsbeautifier.beautify(result, opts)
def render_code_fence(self, token):
if token.language != 'js' or not token.properties:
return ''
match = JsRenderer.ppattern.match(token.properties)
key = match.groups()[2]
if key == 'root':
self.root = token.children[0].content
return
fragments = self.fragments
val = token.children[0].content
fragments[key] = fragments.get(key, '') + val
return ''
def weave(name):
with open(SCRIPT_DIR + f'tutorial_{name}.md', 'r') as fin:
markdown = fin.read()
if ENABLE_EMBEDDED_DEMO:
if name == 'triangle':
markdown = TUTORIAL_PREAMBLE + markdown
markdown = '<div class="demo_frame">' + \
f'<iframe src="demo_{name}.html"></iframe>' + \
f'<a href="demo_{name}.html">&#x1F517;</a>' + \
'</div>\n' + markdown
rendered = mistletoe.markdown(markdown, PygmentsRenderer)
template = open(SCRIPT_DIR + 'tutorial_template.html').read()
rendered = template.replace('$BODY', rendered)
outfile = os.path.join(OUTPUT_DIR, f'tutorial_{name}.html')
with open(outfile, 'w') as fout:
fout.write(rendered)
def generate_demo_html(name):
template = open(SCRIPT_DIR + 'demo_template.html').read()
rendered = template.replace('$SCRIPT', f'tutorial_{name}.js')
outfile = os.path.join(OUTPUT_DIR, f'demo_{name}.html')
with open(outfile, 'w') as fout:
fout.write(rendered)
def tangle(name):
with open(SCRIPT_DIR + f'tutorial_{name}.md', 'r') as fin:
rendered = mistletoe.markdown(fin, JsRenderer)
outfile = os.path.join(OUTPUT_DIR, f'tutorial_{name}.js')
with open(outfile, 'w') as fout:
fout.write(rendered)
def build_filamat(name):
matsrc = SCRIPT_DIR + name + '.mat'
matdst = os.path.join(OUTPUT_DIR, name + '.filamat')
flags = '-a opengl -p mobile'
matc_exec = os.path.join(TOOLS_DIR, 'matc/matc')
retval = os.system(f"{matc_exec} {flags} -o {matdst} {matsrc}")
if retval != 0:
exit(retval)
def copy_built_file(pattern, destfolder=None):
outdir = OUTPUT_DIR
if destfolder:
outdir = os.path.join(outdir, destfolder)
if not os.path.exists(outdir):
os.mkdir(outdir)
pattern = os.path.join(BUILD_DIR, pattern)
for src in glob.glob(pattern):
dst = os.path.join(outdir, os.path.basename(src))
shutil.copyfile(src, dst)
def copy_src_file(src):
src = os.path.join(ROOT_DIR, src)
dst = os.path.join(OUTPUT_DIR, os.path.basename(src))
shutil.copyfile(src, dst)
def spawn_local_server():
import http.server
import socketserver
Handler = http.server.SimpleHTTPRequestHandler
Handler.extensions_map.update({ '.wasm': 'application/wasm' })
Handler.directory = OUTPUT_DIR
os.chdir(OUTPUT_DIR)
socketserver.TCPServer.allow_reuse_address = True
port = 8000
print(f"serving docs at http://localhost:{port}")
with socketserver.TCPServer(("", port), Handler) as httpd:
httpd.allow_reuse_address = True
httpd.serve_forever()
def expand_refs(comment_line):
"""Adds hrefs to markdown links that do not already have them; e.g.
expands [Foo] to [Foo](#Foo) but leaves [Foo](https://foo) alone.
"""
result = comment_line
result = re.sub(r"\[(\S+)\]([^(])", r"[\1](#\1)\2", result)
result = re.sub(r"\[(\S+)\]$", r"[\1](#\1)", result)
return result
def gather_docstrings(paths):
"""Given a list of paths to JS and CPP files, builds a JSON tree of
type descriptions."""
result = []
stack = [{"tags": ["root"]}]
previous = stack[0]
docline = re.compile(r' */// (.+)')
enumline = re.compile(r' *enum_.*\"(.*)\"')
enumvalue = re.compile(r' *\.value\("(.*)\"')
tagged = re.compile(r'(\S+)? *::(.+):: *(.*)')
lines = []
enumerating = False
current_enumeration = None
for path in paths:
lines += open(path).readlines()
for line in lines:
match_obj = docline.match(line)
if not match_obj:
match_obj = enumline.match(line)
if match_obj:
result.append({
"name": match_obj.groups()[0],
"tags": "enum",
"brief": "",
"detail": None,
"children": [],
})
current_enumeration = result[-1]["children"]
enumerating = True
continue
match_obj = enumvalue.match(line)
if match_obj:
val = match_obj.groups()[0]
current_enumeration.append(val)
continue
ln = match_obj.groups()[0]
match_obj = tagged.match(ln)
if match_obj:
name = match_obj.groups()[0]
tags = match_obj.groups()[1].split()
brief = match_obj.groups()[2]
entity = {
"name": name,
"tags": tags,
"brief": brief,
"detail": None,
"children": []
}
# Check if this is continuation of a previous type.
if brief == '':
for existing_type in result:
if existing_type['name'] == name:
entity = existing_type
result.remove(existing_type)
break
top = stack[-1]["tags"]
if 'root' in top:
result.append(entity)
stack.append(entity)
elif 'class' in tags or 'function' in tags:
result.append(entity)
stack[-1] = entity
elif 'method' in tags and 'class' in top:
stack[-1]["children"].append(entity)
stack.append(entity)
elif 'method' in tags:
stack[-2]["children"].append(entity)
stack[-1] = entity
elif 'retval' in tags or 'argument' in tags:
stack[-1]["children"].append(entity)
previous = entity
else:
brief = previous["brief"]
detail = previous["detail"]
if brief.endswith("\\"):
previous["brief"] = brief[:-1] + ln
elif not detail:
previous["detail"] = ln
else:
previous["detail"] += "\n" + ln
return result
def generate_class_reference(entity):
name = entity["name"]
brief, detail = entity["brief"], entity["detail"]
brief = expand_refs(brief)
result = f"\n## class <a id='{name}' href='#{name}'>{name}</a>\n\n"
result += brief + "\n\n"
entity["children"].sort(key = lambda t: t["name"])
for method in entity["children"]:
result += "- **"
if "static" in method["tags"]:
# Write the class name before the method name.
result += name + "."
else:
# Instances are lowercase by convention.
result += name[0].lower() + name[1:] + "."
mname = method.get("name")
assert mname, f"Missing method name on {name}"
args = []
for child in method["children"]:
if "argument" in child["tags"]:
cname = child.get("name")
assert cname, f"Missing arg name on {mname}"
args.append(cname)
result += f"{mname}(" + ", ".join(args) + ")**\n"
if method["brief"] != "":
result += " - " + method["brief"] + "\n"
for child in method["children"]:
argname = child["name"]
argbrief = expand_refs(child["brief"])
if "argument" in child["tags"]:
result += f" - *{argname}* {argbrief}\n"
elif "retval" in child["tags"]:
result += f" - *returns* {argbrief}\n"
result += "\n"
if detail:
result += expand_refs(detail) + "\n"
return result
def generate_function_reference(entity):
name = entity["name"]
brief, detail = entity["brief"], entity["detail"]
brief = expand_refs(brief)
result = f"\n## function <a id='{name}' href='#{name}'>{name}</a>("
args = []
for child in entity["children"]:
if "argument" in child["tags"]:
args.append(child["name"])
result += ", ".join(args)
result += ")\n\n"
result += brief + "\n\n"
for child in entity["children"]:
argname = child["name"]
argbrief = expand_refs(child["brief"])
if "argument" in child["tags"]:
result += f"- **{argname}**\n - {argbrief}\n"
else:
result += f"- **returns**\n - {argbrief}\n"
result += "\n"
if detail:
result += expand_refs(detail) + "\n"
return result
def generate_enum_reference(entity):
name = entity["name"]
result = f"\n## enum <a id='{name}' href='#{name}'>{name}</a>\n\n"
for valname in entity["children"]:
result += f"- {valname}\n"
result += "\n"
return result
def build_reference_markdown(doctree):
result = REFERENCE_PREAMBLE
# Generate table of contents
result += """
### <a id="classes" href="#classes">Classes</a>
| | |
| --- | --- |
"""
doctree.sort(key = lambda t: t['name'])
for entity in doctree:
name = entity["name"]
brief = expand_refs(entity["brief"])
if "class" in entity["tags"]:
result += f"| [{name}](#{name}) | {brief} |\n"
result += """
### <a id="functions" href="#functions">Free Functions</a>
| | |
| --- | --- |
"""
for entity in doctree:
name = entity["name"]
brief = expand_refs(entity["brief"])
if "function" in entity["tags"]:
result += f"| [{name}](#{name}) | {brief} |\n"
result += """
### <a id="enums" href="#enums">Enumerations</a>
| | |
| --- | --- |
"""
for entity in doctree:
name = entity["name"]
brief = expand_refs(entity["brief"])
if "enum" in entity["tags"]:
result += f"| [{name}](#{name}) | {brief} |\n"
result += "\n<br>\n"
# Generate actual reference
for entity in doctree:
if "class" in entity["tags"]:
result += "\n<div class='classdoc'>\n"
result += generate_class_reference(entity)
result += "\n</div>\n"
for entity in doctree:
if "function" in entity["tags"]:
result += "\n<div class='funcdoc'>\n"
result += generate_function_reference(entity)
result += "\n</div>\n"
for entity in doctree:
if "enum" in entity["tags"]:
result += "\n<div class='enumdoc'>\n"
result += generate_enum_reference(entity)
result += "\n</div>\n"
return result
def build_api_reference():
doctree = gather_docstrings([
ROOT_DIR + 'web/filament-js/jsbindings.cpp',
ROOT_DIR + 'web/filament-js/jsenums.cpp',
ROOT_DIR + 'web/filament-js/utilities.js',
ROOT_DIR + 'web/filament-js/wasmloader.js',
ROOT_DIR + 'web/filament-js/extensions.js',
])
markdown = build_reference_markdown(doctree)
rendered = mistletoe.markdown(markdown, PygmentsRenderer)
template = open(SCRIPT_DIR + 'ref_template.html').read()
rendered = template.replace('$BODY', rendered)
outfile = os.path.join(OUTPUT_DIR, f'reference.html')
with open(outfile, 'w') as fout:
fout.write(rendered)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("-d", "--disable-demo",
help="omit the embedded WebGL demo",
action="store_true")
parser.add_argument("-s", "--server",
help="start small server in output folder",
action="store_true")
parser.add_argument("-b", "--build-folder", type=str,
default=BUILD_DIR,
help="set the cmake webgl build folder")
parser.add_argument("-t", "--tools-folder", type=str,
default=TOOLS_DIR,
help="set the cmake host build folder for tools")
parser.add_argument("-o", "--output-folder", type=str,
default=OUTPUT_DIR,
help="set the output folder")
args = parser.parse_args()
BUILD_DIR = args.build_folder
TOOLS_DIR = args.tools_folder
OUTPUT_DIR = args.output_folder
ENABLE_EMBEDDED_DEMO = not args.disable_demo
os.makedirs(os.path.realpath(OUTPUT_DIR), exist_ok=True)
for name in open(SCRIPT_DIR + 'tutorials.txt').read().split():
weave(name)
tangle(name)
generate_demo_html(name)
copy_src_file(ROOT_DIR + 'web/docs/main.css')
copy_src_file(ROOT_DIR + 'third_party/gl-matrix/gl-matrix-min.js')
copy_built_file('web/filament-js/filament.js')
copy_built_file('web/filament-js/filament.wasm')
build_filamat('triangle')
build_filamat('plastic')
build_filamat('textured')
build_api_reference()
# Copy resources from "samples" to "docs"
copy_built_file('web/samples/helmet.html')
copy_built_file('web/samples/parquet.html')
copy_built_file('web/samples/parquet.filamat')
copy_built_file('web/samples/suzanne.html')
copy_built_file('web/samples/suzanne.filamesh')
copy_built_file('web/samples/metallic*.ktx')
copy_built_file('web/samples/normal*.ktx')
copy_built_file('web/samples/roughness*.ktx')
copy_built_file('web/samples/ao*.ktx')
copy_built_file('web/samples/albedo*.ktx')
copy_built_file('web/samples/metallic*.ktx2')
copy_built_file('web/samples/normal*.ktx2')
copy_built_file('web/samples/roughness*.ktx2')
copy_built_file('web/samples/ao*.ktx2')
copy_built_file('web/samples/albedo*.ktx2')
copy_built_file('web/samples/default_env/default_env*.ktx', 'default_env')
# TODO: We do not normally build the following env maps so we should update the tutorials.
# copy_built_file('web/samples/pillars_2k/pillars_2k_*.ktx', 'pillars_2k')
# copy_built_file('web/samples/venetian_crossroads_2k/venetian_crossroads*.ktx', 'venetian_crossroads_2k')
if args.server:
spawn_local_server()

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Filament Demo</title>
<link href="https://google.github.io/filament/favicon.png" rel="icon" type="image/x-icon" />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<style>
body { margin: 0; overflow: hidden; }
canvas { touch-action: none; width: 100%; height: 100%; }
</style>
</head>
<body>
<canvas></canvas>
<script src="filament.js"></script>
<script src="//unpkg.com/gl-matrix@2.8.1"></script>
<script src="//unpkg.com/gltumble"></script>
<script src="$SCRIPT"></script>
</body>
</html>

View File

@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en"><head>
<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">
<style>
li > p { margin: 0 }
li { list-style-type: none }
div.classdoc, div.funcdoc, div.enumdoc {
background: #eee;
padding-left: 10px;
padding-top: 1px;
padding-bottom: 1px;
margin-bottom: 10px;
}
div.classdoc > h2, div.funcdoc > h2, div.enumdoc > h2 { margin-top: 10px; }
.verbiage h3 a, .verbiage h2 a { color: #567 }
</style>
</head>
<body class="verbiage">
$BODY
</body>
</html>

View File

@@ -1,10 +0,0 @@
<!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">
$BODY
</body>
</html>

View File

@@ -1,3 +0,0 @@
triangle
redball
suzanne

197
web/examples/CMakeLists.txt Normal file
View File

@@ -0,0 +1,197 @@
cmake_minimum_required(VERSION 3.19)
project(webexamples)
if (FILAMENT_SKIP_SAMPLES)
return()
endif()
set(SERVER_DIR ${PROJECT_BINARY_DIR}/examples)
set(ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../..)
if (CMAKE_CROSSCOMPILING)
include(${IMPORT_EXECUTABLES})
endif()
function(build_material MAT_FILE TARGET_DIR MAT_NAME OUT_LIST)
set(MATC_FLAGS -a opengl -p mobile)
if (NOT CMAKE_BUILD_TYPE MATCHES Release)
set(MATC_FLAGS -g ${MATC_FLAGS})
endif()
set(mat_src "${CMAKE_CURRENT_SOURCE_DIR}/${MAT_FILE}")
set(output_path "${SERVER_DIR}/${TARGET_DIR}/${MAT_NAME}.filamat")
add_custom_command(
OUTPUT ${output_path}
COMMAND matc ${MATC_FLAGS} -o ${output_path} ${mat_src}
MAIN_DEPENDENCY ${mat_src}
DEPENDS matc
COMMENT "Compiling material ${mat_src} to ${output_path}")
set(${OUT_LIST} ${${OUT_LIST}} ${output_path} PARENT_SCOPE)
endfunction()
function(build_material_fl0 MAT_FILE TARGET_DIR MAT_NAME OUT_LIST)
if (FILAMENT_ENABLE_FEATURE_LEVEL_0)
set(MATC_FLAGS -a opengl -p mobile)
if (NOT CMAKE_BUILD_TYPE MATCHES Release)
set(MATC_FLAGS -g ${MATC_FLAGS})
endif()
set(mat_src "${CMAKE_CURRENT_SOURCE_DIR}/${MAT_FILE}")
set(output_path "${SERVER_DIR}/${TARGET_DIR}/${MAT_NAME}_fl0.filamat")
add_custom_command(
OUTPUT ${output_path}
COMMAND matc ${MATC_FLAGS} -PfeatureLevel=0 -o ${output_path} ${mat_src}
MAIN_DEPENDENCY ${mat_src}
DEPENDS matc
COMMENT "Compiling material FL0 ${mat_src} to ${output_path}")
set(${OUT_LIST} ${${OUT_LIST}} ${output_path} PARENT_SCOPE)
endif()
endfunction()
function(add_ktxfiles SOURCE TARGET EXTRA_ARGS TARGET_DIR OUT_LIST)
set(source_path "${ROOT_DIR}/${SOURCE}")
set(target_path "${SERVER_DIR}/${TARGET_DIR}/${TARGET}")
add_custom_command(
OUTPUT ${target_path}
COMMAND mipgen --quiet --strip-alpha ${EXTRA_ARGS} ${source_path} ${target_path}
MAIN_DEPENDENCY ${source_path}
DEPENDS mipgen)
set(${OUT_LIST} ${${OUT_LIST}} ${target_path} PARENT_SCOPE)
endfunction()
function(add_rawfile SOURCE TARGET TARGET_DIR OUT_LIST)
set(source_path "${ROOT_DIR}/${SOURCE}")
set(target_path "${SERVER_DIR}/${TARGET_DIR}/${TARGET}")
add_custom_command(
OUTPUT ${target_path}
COMMAND ${CMAKE_COMMAND} -E copy ${source_path} ${target_path}
MAIN_DEPENDENCY ${source_path})
set(${OUT_LIST} ${${OUT_LIST}} ${target_path} PARENT_SCOPE)
endfunction()
function(add_localfile SOURCE TARGET TARGET_DIR OUT_LIST)
set(source_path "${CMAKE_CURRENT_SOURCE_DIR}/${SOURCE}")
set(target_path "${SERVER_DIR}/${TARGET_DIR}/${TARGET}")
add_custom_command(
OUTPUT ${target_path}
COMMAND ${CMAKE_COMMAND} -E copy ${source_path} ${target_path}
MAIN_DEPENDENCY ${source_path})
set(${OUT_LIST} ${${OUT_LIST}} ${target_path} PARENT_SCOPE)
endfunction()
function(add_mesh SOURCE TARGET TARGET_DIR OUT_LIST)
set(source_mesh "${ROOT_DIR}/${SOURCE}")
set(target_mesh "${SERVER_DIR}/${TARGET_DIR}/${TARGET}")
add_custom_command(
OUTPUT ${target_mesh}
COMMAND filamesh --compress ${source_mesh} ${target_mesh}
MAIN_DEPENDENCY ${source_mesh}
DEPENDS filamesh)
set(${OUT_LIST} ${${OUT_LIST}} ${target_mesh} PARENT_SCOPE)
endfunction()
set(CMGEN_ARGS --quiet --format=ktx --size=256 --extract-blur=0.1)
set(CMGEN_ARGS_TINY --quiet --format=ktx --size=64 --extract-blur=0.1)
function(add_envmap SOURCE TARGET TARGET_DIR OUT_LIST)
set(source_envmap "${ROOT_DIR}/${SOURCE}")
set(target_skybox "${SERVER_DIR}/${TARGET_DIR}/${TARGET}/${TARGET}_skybox.ktx")
set(target_envmap "${SERVER_DIR}/${TARGET_DIR}/${TARGET}/${TARGET}_ibl.ktx")
set(target_skybox_tiny "${SERVER_DIR}/${TARGET_DIR}/${TARGET}/${TARGET}_skybox_tiny.ktx")
add_custom_command(OUTPUT ${target_skybox} ${target_skybox_tiny} ${target_envmap}
COMMAND ${CMAKE_COMMAND} -E make_directory "${SERVER_DIR}/${TARGET_DIR}/${TARGET}"
COMMAND cmgen -x ${TARGET} ${CMGEN_ARGS_TINY} ${source_envmap}
COMMAND ${CMAKE_COMMAND} -E rename "${SERVER_DIR}/${TARGET_DIR}/${TARGET}/${TARGET}_skybox.ktx" ${target_skybox_tiny}
COMMAND cmgen -x ${TARGET} ${CMGEN_ARGS} ${source_envmap}
WORKING_DIRECTORY "${SERVER_DIR}/${TARGET_DIR}"
MAIN_DEPENDENCY ${source_envmap}
DEPENDS cmgen)
set(${OUT_LIST} ${${OUT_LIST}} ${target_skybox} ${target_skybox_tiny} ${target_envmap} PARENT_SCOPE)
endfunction()
set(ALL_DEMO_ASSETS)
add_custom_command(OUTPUT ${SERVER_DIR}/filament.js DEPENDS filament-js COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/../filament-js/filament.js ${SERVER_DIR})
add_custom_command(OUTPUT ${SERVER_DIR}/filament.wasm DEPENDS filament-js COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/../filament-js/filament.wasm ${SERVER_DIR})
add_custom_command(OUTPUT ${SERVER_DIR}/gl-matrix-min.js COMMAND ${CMAKE_COMMAND} -E copy ${ROOT_DIR}/third_party/gl-matrix/gl-matrix-min.js ${SERVER_DIR})
add_custom_command(OUTPUT ${SERVER_DIR}/gltumble.min.js COMMAND ${CMAKE_COMMAND} -E copy ${ROOT_DIR}/third_party/gltumble/gltumble.min.js ${SERVER_DIR})
add_custom_command(OUTPUT ${SERVER_DIR}/main.css COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/main.css ${SERVER_DIR})
add_custom_command(OUTPUT ${SERVER_DIR}/favicon.png COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/assets/favicon.png ${SERVER_DIR})
list(APPEND ALL_DEMO_ASSETS ${SERVER_DIR}/filament.js ${SERVER_DIR}/filament.wasm ${SERVER_DIR}/gl-matrix-min.js ${SERVER_DIR}/gltumble.min.js ${SERVER_DIR}/main.css ${SERVER_DIR}/favicon.png)
set(TARGET_DIR "triangle")
add_localfile("triangle.md" "triangle.md" ${TARGET_DIR} ALL_DEMO_ASSETS)
build_material("materials/triangle.mat" ${TARGET_DIR} "triangle" ALL_DEMO_ASSETS)
set(TARGET_DIR "redball")
add_localfile("redball.md" "redball.md" ${TARGET_DIR} ALL_DEMO_ASSETS)
build_material("materials/plastic.mat" ${TARGET_DIR} "plastic" ALL_DEMO_ASSETS)
add_envmap("third_party/environments/pillars_2k.hdr" "pillars_2k" ${TARGET_DIR} ALL_DEMO_ASSETS)
set(TARGET_DIR "suzanne")
add_localfile("suzanne.md" "suzanne.md" ${TARGET_DIR} ALL_DEMO_ASSETS)
build_material("materials/textured.mat" ${TARGET_DIR} "textured" ALL_DEMO_ASSETS)
add_mesh("assets/models/monkey/monkey.obj" "suzanne.filamesh" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_ktxfiles("assets/models/monkey/color.png" "albedo.ktx2" "--compression=uastc" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_ktxfiles("assets/models/monkey/normal.png" "normal.ktx2" "--compression=uastc_normals;--kernel=NORMALS;--linear" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_ktxfiles("assets/models/monkey/roughness.png" "roughness.ktx2" "--compression=uastc;--grayscale;--linear" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_ktxfiles("assets/models/monkey/metallic.png" "metallic.ktx2" "--compression=uastc;--grayscale;--linear" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_ktxfiles("assets/models/monkey/ao.png" "ao.ktx2" "--compression=uastc;--grayscale;--linear" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_envmap("third_party/environments/venetian_crossroads_2k.hdr" "venetian_crossroads_2k" ${TARGET_DIR} ALL_DEMO_ASSETS)
set(TARGET_DIR "animation")
add_localfile("animation.html" "animation.html" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/AnimatedTriangle/animation.bin" "animation.bin" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/AnimatedTriangle/simpleTriangle.bin" "simpleTriangle.bin" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/AnimatedTriangle/AnimatedTriangle.gltf" "AnimatedTriangle.gltf" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_envmap("third_party/environments/lightroom_14b.hdr" "default_env" ${TARGET_DIR} ALL_DEMO_ASSETS)
set(TARGET_DIR "cube_fl0")
add_localfile("cube_fl0.html" "cube_fl0.html" ${TARGET_DIR} ALL_DEMO_ASSETS)
build_material_fl0("materials/nonlit.mat" ${TARGET_DIR} "nonlit" ALL_DEMO_ASSETS)
set(TARGET_DIR "helmet")
add_localfile("helmet.html" "helmet.html" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_baseColor.png" "FlightHelmet_baseColor.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_baseColor1.png" "FlightHelmet_baseColor1.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_baseColor2.png" "FlightHelmet_baseColor2.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_baseColor3.png" "FlightHelmet_baseColor3.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_baseColor4.png" "FlightHelmet_baseColor4.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_normal.png" "FlightHelmet_normal.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_normal1.png" "FlightHelmet_normal1.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_normal2.png" "FlightHelmet_normal2.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_normal3.png" "FlightHelmet_normal3.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_normal4.png" "FlightHelmet_normal4.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_occlusionRoughnessMetallic.png" "FlightHelmet_occlusionRoughnessMetallic.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_occlusionRoughnessMetallic1.png" "FlightHelmet_occlusionRoughnessMetallic1.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_occlusionRoughnessMetallic2.png" "FlightHelmet_occlusionRoughnessMetallic2.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_occlusionRoughnessMetallic3.png" "FlightHelmet_occlusionRoughnessMetallic3.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_occlusionRoughnessMetallic4.png" "FlightHelmet_occlusionRoughnessMetallic4.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet.bin" "FlightHelmet.bin" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/FlightHelmet/FlightHelmet.gltf" "FlightHelmet.gltf" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_envmap("third_party/environments/lightroom_14b.hdr" "default_env" ${TARGET_DIR} ALL_DEMO_ASSETS)
set(TARGET_DIR "morphing")
add_localfile("morphing.html" "morphing.html" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_rawfile("third_party/models/AnimatedMorphCube/AnimatedMorphCube.glb" "AnimatedMorphCube.glb" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_envmap("third_party/environments/lightroom_14b.hdr" "default_env" ${TARGET_DIR} ALL_DEMO_ASSETS)
set(TARGET_DIR "parquet")
add_localfile("parquet.html" "parquet.html" ${TARGET_DIR} ALL_DEMO_ASSETS)
build_material("materials/parquet.mat" ${TARGET_DIR} "parquet" ALL_DEMO_ASSETS)
add_mesh("third_party/models/shader_ball/shader_ball.obj" "shader_ball.filamesh" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_localfile("textures/floor_ao_roughness_metallic.png" "floor_ao_roughness_metallic.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_localfile("textures/floor_basecolor.jpg" "floor_basecolor.jpg" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_localfile("textures/floor_normal.png" "floor_normal.png" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_envmap("third_party/environments/lightroom_14b.hdr" "default_env" ${TARGET_DIR} ALL_DEMO_ASSETS)
set(TARGET_DIR "skinning")
add_localfile("skinning.html" "skinning.html" ${TARGET_DIR} ALL_DEMO_ASSETS)
build_material("materials/skinning.mat" ${TARGET_DIR} "skinning" ALL_DEMO_ASSETS)
add_rawfile("third_party/models/AnimatedTriangle/simpleTriangle.bin" "simpleTriangle.bin" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_envmap("third_party/environments/lightroom_14b.hdr" "default_env" ${TARGET_DIR} ALL_DEMO_ASSETS)
set(TARGET_DIR "remote")
add_localfile("remote.html" "remote.html" ${TARGET_DIR} ALL_DEMO_ASSETS)
add_custom_target(${PROJECT_NAME} ALL DEPENDS ${ALL_DEMO_ASSETS})

View File

@@ -4,16 +4,16 @@
<title>glTF Animation</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link href="favicon.png" rel="icon" type="image/x-icon" />
<link href="../favicon.png" rel="icon" type="image/x-icon" />
<style>
body { margin: 0; overflow: hidden; }
canvas { touch-action: none; width: 100%; height: 100%; }
canvas { touch-action: none; width: 100%; height: 400px; border: 1px solid black; }
</style>
</head>
<body>
<canvas></canvas>
<script src="filament.js"></script>
<script src="gl-matrix-min.js"></script>
<script src="../filament.js"></script>
<script src="../gl-matrix-min.js"></script>
<script>
const mesh_url = "AnimatedTriangle.gltf";
@@ -84,8 +84,8 @@ class App {
resize() {
const dpr = window.devicePixelRatio;
const width = this.canvas.width = window.innerWidth * dpr;
const height = this.canvas.height = window.innerHeight * dpr;
const width = this.canvas.width = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * dpr;
this.view.setViewport([0, 0, width, height]);
const eye = [0, 0, 5], center = [0, 0, 0], up = [0, 1, 0];
this.camera.lookAt(eye, center, up);

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -5,7 +5,7 @@
<title>Filament Cube</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link href="favicon.png" rel="icon" type="image/x-icon" />
<link href="../favicon.png" rel="icon" type="image/x-icon" />
<style>
body {
margin: 0;
@@ -15,15 +15,16 @@
canvas {
touch-action: none;
width: 100%;
height: 100%;
height: 400px;
border: 1px solid black;
}
</style>
</head>
<body>
<canvas></canvas>
<script src="filament.js"></script>
<script src="gl-matrix-min.js"></script>
<script src="../filament.js"></script>
<script src="../gl-matrix-min.js"></script>
<script>
Filament.init(['nonlit_fl0.filamat'], () => {
@@ -169,8 +170,8 @@
resize() {
const dpr = window.devicePixelRatio;
const width = this.canvas.width = window.innerWidth * dpr;
const height = this.canvas.height = window.innerHeight * dpr;
const width = this.canvas.width = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * dpr;
this.view.setViewport([0, 0, width, height]);
const eye = [0, 0, 5], center = [0, 0, 0], up = [0, 1, 0];

View File

@@ -4,13 +4,11 @@
<title>FlightHelmet</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link href="favicon.png" rel="icon" type="image/x-icon" />
<link href="../favicon.png" rel="icon" type="image/x-icon" />
<style>
html, body { height: 100%; }
body { margin: 0; overflow: hidden; }
#container { position: relative; height: 100%; }
canvas { position: absolute; width: 100%; height: 100%; }
#messages { position: absolute; width: 100%; height: 100%; padding-left: 10px; color:blue; pointer-events: none; }
#container { position: relative; width: 100%; height: 400px; border: 1px solid black; }
canvas { position: absolute; width: 100%; height: 100%; touch-action: none; }
#messages { position: absolute; width: 100%; height: 100%; padding-left: 10px; color:blue; pointer-events: none; overflow: hidden; }
</style>
</head>
<body>
@@ -18,14 +16,13 @@ canvas { position: absolute; width: 100%; height: 100%; }
<canvas></canvas>
<pre id="messages"></pre>
</div>
<script src="filament.js"></script>
<script src="gl-matrix-min.js"></script>
<script src="gltumble.min.js"></script>
<script src="../filament.js"></script>
<script src="../gl-matrix-min.js"></script>
<script src="../gltumble.min.js"></script>
<script>
const env = 'default_env';
const ibl_url = `${env}/${env}_ibl.ktx`;
const sky_url = `${env}/${env}_skybox.ktx`;
const ibl_url = 'default_env/default_env_ibl.ktx';
const sky_url = 'default_env/default_env_skybox.ktx';
const mesh_url = 'FlightHelmet.gltf';
Filament.init([mesh_url, ibl_url, sky_url], () => {
@@ -174,8 +171,8 @@ class App {
resize() {
const dpr = window.devicePixelRatio;
const width = this.canvas.width = window.innerWidth * dpr;
const height = this.canvas.height = window.innerHeight * dpr;
const width = this.canvas.width = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * dpr;
this.view.setViewport([0, 0, width, height]);
const y = -0.125, eye = [0, y, 2], center = [0, y, 0], up = [0, 1, 0];
this.camera.lookAt(eye, center, up);

View File

@@ -0,0 +1,12 @@
material {
name : skinning,
shadingModel : unlit,
culling : none
}
fragment {
void material(inout MaterialInputs material) {
prepareMaterial(material);
material.baseColor = float4(0.8, 0.8, 0.8, 1.0);
}
}

View File

@@ -4,22 +4,24 @@
<title>glTF Morphing</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link href="favicon.png" rel="icon" type="image/x-icon" />
<link href="../favicon.png" rel="icon" type="image/x-icon" />
<style>
body { margin: 0; overflow: hidden; }
canvas { touch-action: none; width: 100%; height: 100%; }
canvas { touch-action: none; width: 100%; height: 400px; border: 1px solid black; }
</style>
</head>
<body>
<canvas></canvas>
<script src="filament.js"></script>
<script src="gl-matrix-min.js"></script>
<script src="gltumble.min.js"></script>
<script src="../filament.js"></script>
<script src="../gl-matrix-min.js"></script>
<script src="../gltumble.min.js"></script>
<script>
const mesh_url = "AnimatedMorphCube.glb";
const ibl_url = "default_env/default_env_ibl.ktx";
const sky_url = "default_env/default_env_skybox.ktx";
Filament.init([mesh_url], () => {
Filament.init([mesh_url, ibl_url, sky_url], () => {
window.gltfio = Filament.gltfio;
window.Fov = Filament.Camera$Fov;
window.app = new App(document.querySelector("canvas"));
@@ -30,6 +32,18 @@ class App {
this.canvas = canvas;
const engine = this.engine = Filament.Engine.create(this.canvas);
const scene = this.scene = engine.createScene();
const sunlight = Filament.EntityManager.get().create();
Filament.LightManager.Builder(Filament.LightManager$Type.SUN).direction([0, 0, -1]).build(engine, sunlight);
this.scene.addEntity(sunlight);
const indirectLight = engine.createIblFromKtx1(ibl_url);
indirectLight.setIntensity(50000);
this.scene.setIndirectLight(indirectLight);
const skybox = engine.createSkyFromKtx1(sky_url);
this.scene.setSkybox(skybox);
this.trackball = new Trackball(canvas, {startSpin: 0.035});
const loader = engine.createAssetLoader();
@@ -38,7 +52,7 @@ class App {
const onDone = () => {
loader.delete();
scene.addEntities(asset.getEntities());
this.animator = asset.getAnimator();
this.animator = asset.getInstance().getAnimator();
this.animationStartTime = Date.now();
};
asset.loadResources(onDone);
@@ -78,8 +92,8 @@ class App {
resize() {
const dpr = window.devicePixelRatio;
const width = this.canvas.width = window.innerWidth * dpr;
const height = this.canvas.height = window.innerHeight * dpr;
const width = this.canvas.width = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * dpr;
this.view.setViewport([0, 0, width, height]);
const eye = [0, 0, 5], center = [0, 0, 0], up = [0, 1, 0];
this.camera.lookAt(eye, center, up);

View File

@@ -4,16 +4,16 @@
<title>Filament Parquet</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link href="favicon.png" rel="icon" type="image/x-icon" />
<link href="../favicon.png" rel="icon" type="image/x-icon" />
<style>
body { margin: 0; overflow: hidden; }
canvas { touch-action: none; width: 100%; height: 100%; }
canvas { touch-action: none; width: 100%; height: 400px; border: 1px solid black; }
</style>
</head>
<body>
<canvas></canvas>
<script src="filament.js"></script>
<script src="gl-matrix-min.js"></script>
<script src="../filament.js"></script>
<script src="../gl-matrix-min.js"></script>
<script>
const iblfile = 'default_env/default_env_ibl.ktx';
@@ -72,9 +72,10 @@ class App {
Filament.MagFilter.LINEAR,
Filament.WrapMode.REPEAT);
const ao = engine.createTextureFromPng('floor_ao_roughness_metallic.png');
const basecolor = engine.createTextureFromJpeg('floor_basecolor.jpg', {'srgb': true});
const normal = engine.createTextureFromPng('floor_normal.png');
const texargs = { usage: Filament.Texture$Usage.DEFAULT.value | Filament.Texture$Usage.GEN_MIPMAPPABLE.value };
const ao = engine.createTextureFromPng('floor_ao_roughness_metallic.png', texargs);
const basecolor = engine.createTextureFromJpeg('floor_basecolor.jpg', Object.assign({srgb: true}, texargs));
const normal = engine.createTextureFromPng('floor_normal.png', texargs);
matinstance.setTextureParameter('aoRoughnessMetallic', ao, sampler)
matinstance.setTextureParameter('baseColor', basecolor, sampler)
matinstance.setTextureParameter('normal', normal, sampler)
@@ -109,8 +110,8 @@ class App {
resize() {
const dpr = window.devicePixelRatio;
const width = this.canvas.width = window.innerWidth * dpr;
const height = this.canvas.height = window.innerHeight * dpr;
const width = this.canvas.width = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * dpr;
this.view.setViewport([0, 0, width, height]);
const eye = [0, 1.8, 5], center = [0, 1, -1], up = [0, 1, 0];
this.camera.lookAt(eye, center, up);

View File

@@ -1,9 +1,140 @@
<div class="demo_frame" style="width:100%; height:400px; border: 1px solid black; position: relative;">
<canvas id="demo-canvas" style="width:100%; height:100%; touch-action: none;"></canvas>
</div>
<script src="../filament.js"></script>
<script src="../gl-matrix-min.js"></script>
<script>
// We wrap the demo code so it applies to demo-canvas instead of generic canvas
(function() {
const originalGetElementsByTagName = document.getElementsByTagName;
document.getElementsByTagName = function(tag) {
if (tag === 'canvas') return [document.getElementById('demo-canvas')];
return originalGetElementsByTagName.call(document, tag);
};
//
const ibl_url = 'pillars_2k/pillars_2k_ibl.ktx';
const sky_url = 'pillars_2k/pillars_2k_skybox.ktx';
const filamat_url = 'plastic.filamat'
//
Filament.init([ filamat_url, ibl_url, sky_url ], () => {
// Create some global aliases to enums for convenience.
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;
//
// Obtain the canvas DOM object and pass it to the 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();
//
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);
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);
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);
const indirectLight = engine.createIblFromKtx1(ibl_url);
indirectLight.setIntensity(50000);
scene.setIndirectLight(indirectLight);
const skybox = engine.createSkyFromKtx1(sky_url);
scene.setSkybox(skybox);
//
this.swapChain = engine.createSwapChain();
this.renderer = engine.createRenderer();
this.camera = engine.createCamera(Filament.EntityManager.get().create());
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 = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * dpr;
this.view.setViewport([0, 0, width, height]);
this.camera.setProjectionFov(45, width / height, 1.0, 10.0, Fov.VERTICAL);
}
}
//
})();
</script>
This tutorial will describe how to create the **redball** demo, introducing you to materials and
textures.
For starters, create a text file called `redball.html` and copy over the HTML that we used in the
[previous tutorial]. Change the last script tag from `triangle.js` to `redball.js`.
previous tutorial. Change the last script tag from `triangle.js` to `redball.js`.
Next you'll need to get a couple command-line tools: `matc` and `cmgen`. You can find these in the
appropriate [Filament release](//github.com/google/filament/releases). You should choose the
@@ -71,10 +202,9 @@ IBL KTX contains these coefficients in its metadata.
Next, create `redball.js` with the following content.
```js {fragment="root"}
const environ = 'pillars_2k';
const ibl_url = `${environ}/${environ}_ibl.ktx`;
const sky_url = `${environ}/${environ}_skybox.ktx`;
```js
const ibl_url = 'pillars_2k/pillars_2k_ibl.ktx';
const sky_url = 'pillars_2k/pillars_2k_skybox.ktx';
const filamat_url = 'plastic.filamat'
Filament.init([ filamat_url, ibl_url, sky_url ], () => {
@@ -97,11 +227,63 @@ class App {
const engine = this.engine = Filament.Engine.create(canvas);
const scene = engine.createScene();
// TODO: create material
// TODO: create sphere
// TODO: create lights
// TODO: create IBL
// TODO: create skybox
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);
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);
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);
const indirectLight = engine.createIblFromKtx1(ibl_url);
indirectLight.setIntensity(50000);
scene.setIndirectLight(indirectLight);
const skybox = engine.createSkyFromKtx1(sky_url);
scene.setSkybox(skybox);
this.swapChain = engine.createSwapChain();
this.renderer = engine.createRenderer();
@@ -127,8 +309,8 @@ class App {
resize() {
const dpr = window.devicePixelRatio;
const width = this.canvas.width = window.innerWidth * dpr;
const height = this.canvas.height = window.innerHeight * dpr;
const width = this.canvas.width = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * dpr;
this.view.setViewport([0, 0, width, height]);
this.camera.setProjectionFov(45, width / height, 1.0, 10.0, Fov.VERTICAL);
}
@@ -141,7 +323,7 @@ new set of assets. We also added some animation to the camera.
Next let's create a material instance from the package that we built at the beginning the tutorial.
Replace the **create material** comment with the following snippet.
```js {fragment="create material"}
```js
const material = engine.createMaterial(filamat_url);
const matinstance = material.createInstance();
@@ -164,7 +346,7 @@ three arrays:
Let's go ahead use these arrays to build the vertex buffer and index buffer. Replace **create
sphere** with the following snippet.
```js {fragment="create sphere"}
```js
const renderable = Filament.EntityManager.get().create();
scene.addEntity(renderable);
@@ -204,7 +386,7 @@ In this section we will create some directional light sources, as well as an ima
defined by one of the KTX files we built at the start of the demo. First, replace the **create
lights** comment with the following snippet.
```js {fragment="create lights"}
```js
const sunlight = Filament.EntityManager.get().create();
scene.addEntity(sunlight);
Filament.LightManager.Builder(LightType.SUN)
@@ -269,7 +451,7 @@ scene.setIndirectLight(indirectLight);
Filament provides a JavaScript utility to make this simpler,
simply replace the **create IBL** comment with the following snippet.
```js {fragment="create IBL"}
```js
const indirectLight = engine.createIblFromKtx1(ibl_url);
indirectLight.setIntensity(50000);
scene.setIndirectLight(indirectLight);
@@ -300,20 +482,15 @@ skytex.setImageCube(engine, 0, pixelbuffer);
Filament provides a Javascript utility to make this easier.
Replace **create skybox** with the following.
```js {fragment="create skybox"}
```js
const skybox = engine.createSkyFromKtx1(sky_url);
scene.setSkybox(skybox);
```
That's it, we now have a shiny red ball floating in an environment! The complete JavaScript file is
available [here](tutorial_redball.js).
In the [next tutorial], we'll take a closer look at textures and interaction.
That's it, we now have a shiny red ball floating in an environment!
[pillars_2k.hdr]:
//github.com/google/filament/blob/main/third_party/environments/pillars_2k.hdr
[next tutorial]: tutorial_suzanne.html
[previous tutorial]: tutorial_triangle.html
[Filament release]: //github.com/google/filament/releases
[Filament Material System]: https://google.github.io/filament/Materials.md.html

View File

@@ -110,7 +110,7 @@ a:visited { color: rgb(26, 65, 78); }
</div>
</div>
<script src="filament.js"></script>
<script src="../filament.js"></script>
<script>
"use strict";

View File

@@ -0,0 +1 @@
mistletoe

161
web/examples/serve.py Executable file
View File

@@ -0,0 +1,161 @@
#!/usr/bin/env python3
#
# Copyright (C) 2026 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys
import http.server
import socketserver
from pathlib import Path
try:
import mistletoe
from mistletoe.html_renderer import HTMLRenderer
except ImportError:
print("mistletoe is not installed. Please install it using `pip install mistletoe`.", file=sys.stderr)
sys.exit(1)
SCRIPT_DIR = Path(__file__).parent.resolve()
ROOT_DIR = SCRIPT_DIR.parent.parent
def find_examples_dir():
# search for filament.js in out/
out_dir = ROOT_DIR / 'out'
if out_dir.exists():
for root, dirs, files in os.walk(out_dir):
if 'filament.js' in files:
return root
return None
examples_dir = find_examples_dir()
if not examples_dir:
print("Could not find the built examples directory in out/. Please build the webgl target first.", file=sys.stderr)
# fallback to web/examples for testing, though it won't have the built assets
examples_dir = str(SCRIPT_DIR)
# Read template
HTML_TEMPLATE = """<!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">
$BODY
</body>
</html>"""
class CustomHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=examples_dir, **kwargs)
def do_GET(self):
# Serve main index if root
if self.path == '/' or self.path == '/index.html':
self.serve_index()
return
# If it's a markdown file, render it
if self.path.endswith('.md'):
self.serve_markdown()
return
# Otherwise serve normally
super().do_GET()
def serve_index(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
# Generate a list of all samples and tutorials
html = ["<!DOCTYPE html><html><head><title>Filament Examples</title>"]
html.append("<style>body { font-family: sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; line-height: 1.6; } ")
html.append("h1 { border-bottom: 1px solid #eee; padding-bottom: 10px; } </style>")
html.append("</head><body><h1>Filament Examples and Tutorials</h1><ul>")
# Look for subdirectories in examples_dir
for item in sorted(os.listdir(examples_dir)):
item_path = os.path.join(examples_dir, item)
if os.path.isdir(item_path):
# check for .html or .md
entry_point = None
for file in os.listdir(item_path):
if file.endswith('.html') or file.endswith('.md'):
# Prefer markdown if multiple exist (tutorials have both usually)
if file.endswith('.md'):
entry_point = f"{item}/{file}"
break
entry_point = f"{item}/{file}"
if entry_point:
html.append(f"<li><a href='{entry_point}'>{item}</a></li>")
html.append("</ul></body></html>")
self.wfile.write("".join(html).encode('utf-8'))
def serve_markdown(self):
# path looks like /triangle/triangle.md
file_path = os.path.join(examples_dir, self.path.lstrip('/'))
if not os.path.exists(file_path):
self.send_error(404, "File not found")
return
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
with open(file_path, 'r') as f:
md_content = f.read()
import re
blocks = []
def repl(match):
blocks.append(match.group(0))
return f'<!-- BLOCK_{len(blocks)-1} -->'
md_content = re.sub(r'<script.*?</script>', repl, md_content, flags=re.DOTALL)
md_content = re.sub(r'<div class="demo_frame".*?</div>', repl, md_content, flags=re.DOTALL)
rendered_html = mistletoe.markdown(md_content, HTMLRenderer)
for i, block in enumerate(blocks):
rendered_html = rendered_html.replace(f'<!-- BLOCK_{i} -->', block)
# fix relative links, since we are serving at /dir/file.md but resources are at /dir/
# base url should just be the directory of the markdown
dir_name = os.path.dirname(self.path.lstrip('/'))
base_tag = f'<base href="/{dir_name}/" />'
# insert base tag into HEAD
if '<head>' in HTML_TEMPLATE:
final_html = HTML_TEMPLATE.replace('<head>', f'<head>\n{base_tag}')
else:
final_html = HTML_TEMPLATE
final_html = final_html.replace('$BODY', rendered_html)
self.wfile.write(final_html.encode('utf-8'))
if __name__ == '__main__':
port = 8000
print(f"Serving at http://localhost:{port}")
print(f"Serving directory: {examples_dir}")
with socketserver.TCPServer(("", port), CustomHandler) as httpd:
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nShutting down server.")

View File

@@ -4,16 +4,16 @@
<title>Skinning</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link href="favicon.png" rel="icon" type="image/x-icon" />
<link href="../favicon.png" rel="icon" type="image/x-icon" />
<style>
body { margin: 0; overflow: hidden; }
canvas { touch-action: none; width: 100%; height: 100%; }
canvas { touch-action: none; width: 100%; height: 400px; border: 1px solid black; }
</style>
</head>
<body>
<canvas></canvas>
<script src="filament.js"></script>
<script src="gl-matrix-min.js"></script>
<script src="../filament.js"></script>
<script src="../gl-matrix-min.js"></script>
<script>
let buffer0 = "AAABAAMAAAADAAIAAgADAAUAAgAFAAQABAAFAAcABAAHAAYABgAHAAkABgAJAAgAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAD8AAAAAAACAPwAAAD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAwD8AAAAAAACAPwAAwD8AAAAAAAAAAAAAAEAAAAAAAACAPwAAAEAAAAAA";
@@ -57,7 +57,10 @@ This demo is heavily inspired by gltfTutorial_019_SimpleSkin:
*/
Filament.init([ 'nonlit.filamat' ], () => {
const ibl_url = 'default_env/default_env_ibl.ktx';
const sky_url = 'default_env/default_env_skybox.ktx';
Filament.init([ 'skinning.filamat', ibl_url, sky_url ], () => {
window.AttributeType = Filament.VertexBuffer$AttributeType;
window.Fov = Filament.Camera$Fov;
window.Projection = Filament.Camera$Projection;
@@ -70,6 +73,14 @@ class App {
this.canvas = canvas;
const engine = this.engine = Filament.Engine.create(this.canvas);
this.scene = engine.createScene();
const indirectLight = engine.createIblFromKtx1(ibl_url);
indirectLight.setIntensity(50000);
this.scene.setIndirectLight(indirectLight);
const skybox = engine.createSkyFromKtx1(sky_url);
this.scene.setSkybox(skybox);
this.mesh = Filament.EntityManager.get().create();
this.scene.addEntity(this.mesh);
@@ -92,7 +103,7 @@ class App {
this.vb.setBufferAt(engine, 1, bufview2.subarray(0, 160));
this.vb.setBufferAt(engine, 2, bufview2.subarray(160, 320));
const mat = engine.createMaterial('nonlit.filamat');
const mat = engine.createMaterial('skinning.filamat');
const matinst = mat.getDefaultInstance();
Filament.RenderableManager.Builder(1)
.boundingBox({ center: [-1, -1, -1], halfExtent: [1, 1, 1] })
@@ -164,8 +175,8 @@ class App {
resize() {
const dpr = window.devicePixelRatio;
const width = this.canvas.width = window.innerWidth * dpr;
const height = this.canvas.height = window.innerHeight * dpr;
const width = this.canvas.width = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * dpr;
this.view.setViewport([0, 0, width, height]);
const aspect = width / height;

View File

@@ -1,4 +1,123 @@
<div class="demo_frame" style="width:100%; height:400px; border: 1px solid black; position: relative;">
<canvas id="demo-canvas" style="width:100%; height:100%; touch-action: none;"></canvas>
</div>
<script src="../filament.js"></script>
<script src="../gl-matrix-min.js"></script>
<script src="../gltumble.min.js"></script>
<script>
// We wrap the demo code so it applies to demo-canvas instead of generic canvas
(function() {
const originalGetElementsByTagName = document.getElementsByTagName;
document.getElementsByTagName = function(tag) {
if (tag === 'canvas') return [document.getElementById('demo-canvas')];
return originalGetElementsByTagName.call(document, tag);
};
//
const albedo_suffix = Filament.getSupportedFormatSuffix('astc s3tc_srgb');
const texture_suffix = Filament.getSupportedFormatSuffix('etc');
//
const ibl_url = 'venetian_crossroads_2k/venetian_crossroads_2k_ibl.ktx';
const sky_small_url = 'venetian_crossroads_2k/venetian_crossroads_2k_skybox_tiny.ktx';
const sky_large_url = 'venetian_crossroads_2k/venetian_crossroads_2k_skybox.ktx';
const albedo_url = `albedo.ktx2`;
const ao_url = `ao.ktx2`;
const metallic_url = `metallic.ktx2`;
const normal_url = `normal.ktx2`;
const roughness_url = `roughness.ktx2`;
const filamat_url = 'textured.filamat';
const filamesh_url = 'suzanne.filamesh';
//
Filament.init([ filamat_url, filamesh_url, sky_small_url, ibl_url ], () => {
window.app = new App(document.getElementsByTagName('canvas')[0]);
});
//
class App {
constructor(canvas) {
this.canvas = canvas;
this.engine = Filament.Engine.create(canvas);
this.scene = this.engine.createScene();
//
const material = this.engine.createMaterial(filamat_url);
this.matinstance = material.createInstance();
//
const filamesh = this.engine.loadFilamesh(filamesh_url, this.matinstance);
this.suzanne = filamesh.renderable;
//
this.skybox = this.engine.createSkyFromKtx1(sky_small_url);
this.scene.setSkybox(this.skybox);
this.indirectLight = this.engine.createIblFromKtx1(ibl_url);
this.indirectLight.setIntensity(100000);
this.scene.setIndirectLight(this.indirectLight);
this.trackball = new Trackball(canvas, {startSpin: 0.035});
Filament.fetch([sky_large_url, albedo_url, roughness_url, metallic_url, normal_url, ao_url], () => {
const albedo = this.engine.createTextureFromKtx2(albedo_url, {srgb: true});
const roughness = this.engine.createTextureFromKtx2(roughness_url);
const metallic = this.engine.createTextureFromKtx2(metallic_url);
const normal = this.engine.createTextureFromKtx2(normal_url);
const ao = this.engine.createTextureFromKtx2(ao_url);
//
const sampler = new Filament.TextureSampler(
Filament.MinFilter.LINEAR_MIPMAP_LINEAR,
Filament.MagFilter.LINEAR,
Filament.WrapMode.CLAMP_TO_EDGE);
//
this.matinstance.setTextureParameter('albedo', albedo, sampler);
this.matinstance.setTextureParameter('roughness', roughness, sampler);
this.matinstance.setTextureParameter('metallic', metallic, sampler);
this.matinstance.setTextureParameter('normal', normal, sampler);
this.matinstance.setTextureParameter('ao', ao, sampler);
//
// Replace low-res skybox with high-res skybox.
this.engine.destroySkybox(this.skybox);
this.skybox = this.engine.createSkyFromKtx1(sky_large_url);
this.scene.setSkybox(this.skybox);
//
this.scene.addEntity(this.suzanne);
});
//
this.swapChain = this.engine.createSwapChain();
this.renderer = this.engine.createRenderer();
this.camera = this.engine.createCamera(Filament.EntityManager.get().create());
this.view = this.engine.createView();
this.view.setCamera(this.camera);
this.view.setScene(this.scene);
this.render = this.render.bind(this);
this.resize = this.resize.bind(this);
window.addEventListener('resize', this.resize);
//
const eye = [0, 0, 4], center = [0, 0, 0], up = [0, 1, 0];
this.camera.lookAt(eye, center, up);
//
this.resize();
window.requestAnimationFrame(this.render);
}
//
render() {
const tcm = this.engine.getTransformManager();
const inst = tcm.getInstance(this.suzanne);
tcm.setTransform(inst, this.trackball.getMatrix());
inst.delete();
this.renderer.render(this.swapChain, this.view);
window.requestAnimationFrame(this.render);
}
//
resize() {
const dpr = window.devicePixelRatio;
const width = this.canvas.width = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * dpr;
this.view.setViewport([0, 0, width, height]);
//
const aspect = width / height;
const Fov = Filament.Camera$Fov, fov = aspect < 1 ? Fov.HORIZONTAL : Fov.VERTICAL;
this.camera.setProjectionFov(45, aspect, 1.0, 10.0, fov);
}
}
//
})();
</script>
This tutorial will describe how to create the **suzanne** demo, introducing you to compressed
textures, mipmap generation, asynchronous texture loading, and trackball rotation.
@@ -116,8 +235,20 @@ Create a text file called `suzanne.html` and copy over the HTML that we used in
tutorial]. Change the last script tag from `redball.js` to `suzanne.js`. Next, create `suzanne.js`
with the following content.
```js {fragment="root"}
// TODO: declare asset URLs
```js
const albedo_suffix = Filament.getSupportedFormatSuffix('astc s3tc_srgb');
const texture_suffix = Filament.getSupportedFormatSuffix('etc');
const ibl_url = 'venetian_crossroads_2k/venetian_crossroads_2k_ibl.ktx';
const sky_small_url = 'venetian_crossroads_2k/venetian_crossroads_2k_skybox_tiny.ktx';
const sky_large_url = 'venetian_crossroads_2k/venetian_crossroads_2k_skybox.ktx';
const albedo_url = `albedo.ktx2`;
const ao_url = `ao.ktx2`;
const metallic_url = `metallic.ktx2`;
const normal_url = `normal.ktx2`;
const roughness_url = `roughness.ktx2`;
const filamat_url = 'textured.filamat';
const filamesh_url = 'suzanne.filamesh';
Filament.init([ filamat_url, filamesh_url, sky_small_url, ibl_url ], () => {
window.app = new App(document.getElementsByTagName('canvas')[0]);
@@ -135,9 +266,37 @@ class App {
const filamesh = this.engine.loadFilamesh(filamesh_url, this.matinstance);
this.suzanne = filamesh.renderable;
// TODO: create sky box and IBL
// TODO: initialize gltumble
// TODO: fetch larger assets
this.skybox = this.engine.createSkyFromKtx1(sky_small_url);
this.scene.setSkybox(this.skybox);
this.indirectLight = this.engine.createIblFromKtx1(ibl_url);
this.indirectLight.setIntensity(100000);
this.scene.setIndirectLight(this.indirectLight);
this.trackball = new Trackball(canvas, {startSpin: 0.035});
Filament.fetch([sky_large_url, albedo_url, roughness_url, metallic_url, normal_url, ao_url], () => {
const albedo = this.engine.createTextureFromKtx2(albedo_url, {srgb: true});
const roughness = this.engine.createTextureFromKtx2(roughness_url);
const metallic = this.engine.createTextureFromKtx2(metallic_url);
const normal = this.engine.createTextureFromKtx2(normal_url);
const ao = this.engine.createTextureFromKtx2(ao_url);
const sampler = new Filament.TextureSampler(
Filament.MinFilter.LINEAR_MIPMAP_LINEAR,
Filament.MagFilter.LINEAR,
Filament.WrapMode.CLAMP_TO_EDGE);
this.matinstance.setTextureParameter('albedo', albedo, sampler);
this.matinstance.setTextureParameter('roughness', roughness, sampler);
this.matinstance.setTextureParameter('metallic', metallic, sampler);
this.matinstance.setTextureParameter('normal', normal, sampler);
this.matinstance.setTextureParameter('ao', ao, sampler);
// Replace low-res skybox with high-res skybox.
this.engine.destroySkybox(this.skybox);
this.skybox = this.engine.createSkyFromKtx1(sky_large_url);
this.scene.setSkybox(this.skybox);
this.scene.addEntity(this.suzanne);
});
this.swapChain = this.engine.createSwapChain();
this.renderer = this.engine.createRenderer();
@@ -157,15 +316,18 @@ class App {
}
render() {
// TODO: apply gltumble matrix
const tcm = this.engine.getTransformManager();
const inst = tcm.getInstance(this.suzanne);
tcm.setTransform(inst, this.trackball.getMatrix());
inst.delete();
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;
const width = this.canvas.width = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * dpr;
this.view.setViewport([0, 0, width, height]);
const aspect = width / height;
@@ -192,19 +354,18 @@ In our case, we know that our web server will have `astc` and `s3tc` variants fo
variants for the other textures. The uncompressed variants (empty string) are always available as a
last resort. Go ahead and replace the **declare asset URLs** comment with the following snippet.
```js {fragment="declare asset URLs"}
```js
const albedo_suffix = Filament.getSupportedFormatSuffix('astc s3tc_srgb');
const texture_suffix = Filament.getSupportedFormatSuffix('etc');
const environ = 'venetian_crossroads_2k'
const ibl_url = `${environ}/${environ}_ibl.ktx`;
const sky_small_url = `${environ}/${environ}_skybox_tiny.ktx`;
const sky_large_url = `${environ}/${environ}_skybox.ktx`;
const albedo_url = `albedo${albedo_suffix}.ktx`;
const ao_url = `ao${texture_suffix}.ktx`;
const metallic_url = `metallic${texture_suffix}.ktx`;
const normal_url = `normal${texture_suffix}.ktx`;
const roughness_url = `roughness${texture_suffix}.ktx`;
const ibl_url = 'venetian_crossroads_2k/venetian_crossroads_2k_ibl.ktx';
const sky_small_url = 'venetian_crossroads_2k/venetian_crossroads_2k_skybox_tiny.ktx';
const sky_large_url = 'venetian_crossroads_2k/venetian_crossroads_2k_skybox.ktx';
const albedo_url = `albedo.ktx2`;
const ao_url = `ao.ktx2`;
const metallic_url = `metallic.ktx2`;
const normal_url = `normal.ktx2`;
const roughness_url = `roughness.ktx2`;
const filamat_url = 'textured.filamat';
const filamesh_url = 'suzanne.filamesh';
```
@@ -213,7 +374,7 @@ const filamesh_url = 'suzanne.filamesh';
Next, let's create the low-resolution skybox and IBL in the `App` constructor.
```js {fragment="create sky box and IBL"}
```js
this.skybox = this.engine.createSkyFromKtx1(sky_small_url);
this.scene.setSkybox(this.skybox);
this.indirectLight = this.engine.createIblFromKtx1(ibl_url);
@@ -234,7 +395,7 @@ In our callback, we'll make several `setTextureParameter` calls on the material
recreate the skybox using a higher-resolution texture. As a last step we unhide the renderable that
was created in the app constructor.
```js {fragment="fetch larger assets"}
```js
Filament.fetch([sky_large_url, albedo_url, roughness_url, metallic_url, normal_url, ao_url], () => {
const albedo = this.engine.createTextureFromKtx2(albedo_url, {srgb: true});
const roughness = this.engine.createTextureFromKtx2(roughness_url);
@@ -274,22 +435,20 @@ listens for drag events and computes a rotation matrix.
Next, replace the **initialize gltumble** and **apply gltumble matrix** comments with the following
two code snippets.
```js {fragment="initialize gltumble"}
```js
this.trackball = new Trackball(canvas, {startSpin: 0.035});
```
```js {fragment="apply gltumble matrix"}
```js
const tcm = this.engine.getTransformManager();
const inst = tcm.getInstance(this.suzanne);
tcm.setTransform(inst, this.trackball.getMatrix());
inst.delete();
```
That's it, we now have a fast-loading interactive demo. The complete JavaScript file is available
[here](tutorial_suzanne.js).
That's it, we now have a fast-loading interactive demo.
[Filament release]: //github.com/google/filament/releases
[previous tutorial]: tutorial_redball.html
[Filament Material System]: https://google.github.io/filament/Materials.md.html
[this OBJ file]: https://github.com/google/filament/blob/main/assets/models/monkey/monkey.obj
[monkey folder]: https://github.com/google/filament/blob/main/assets/models/monkey

View File

Before

Width:  |  Height:  |  Size: 678 KiB

After

Width:  |  Height:  |  Size: 678 KiB

View File

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 226 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1,3 +1,104 @@
<div class="demo_frame" style="width:100%; height:400px; border: 1px solid black; position: relative;">
<canvas id="demo-canvas" style="width:100%; height:100%; touch-action: none;"></canvas>
</div>
<script src="../filament.js"></script>
<script src="../gl-matrix-min.js"></script>
<script>
// We wrap the demo code so it applies to demo-canvas instead of generic canvas
(function() {
const originalGetElementsByTagName = document.getElementsByTagName;
document.getElementsByTagName = function(tag) {
if (tag === 'canvas') return [document.getElementById('demo-canvas')];
return originalGetElementsByTagName.call(document, tag);
};
//
class App {
constructor() {
this.canvas = document.getElementsByTagName('canvas')[0];
const engine = this.engine = Filament.Engine.create(this.canvas);
this.scene = engine.createScene();
this.triangle = Filament.EntityManager.get().create();
this.scene.addEntity(this.triangle);
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]);
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);
this.ib = Filament.IndexBuffer.Builder()
.indexCount(3)
.bufferType(Filament.IndexBuffer$IndexType.USHORT)
.build(engine);
//
this.ib.setBuffer(engine, new Uint16Array([0, 1, 2]));
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);
this.swapChain = engine.createSwapChain();
this.renderer = engine.createRenderer();
this.camera = engine.createCamera(Filament.EntityManager.get().create());
this.view = engine.createView();
this.view.setCamera(this.camera);
this.view.setScene(this.scene);
//
// Set up a blue-green background:
this.renderer.setClearOptions({clearColor: [0.0, 0.1, 0.2, 1.0], clear: true});
//
// Adjust the initial viewport:
this.resize();
this.render = this.render.bind(this);
this.resize = this.resize.bind(this);
window.addEventListener('resize', this.resize);
window.requestAnimationFrame(this.render);
}
render() {
// 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);
window.requestAnimationFrame(this.render);
}
resize() {
const dpr = window.devicePixelRatio;
const width = this.canvas.width = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * 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);
}
}
//
Filament.init(['triangle.filamat'], () => { window.app = new App() } );
//
})();
</script>
## Start your project
First, create a text file called `triangle.html` and fill it with the following HTML. This creates
@@ -33,21 +134,85 @@ The above HTML loads three JavaScript files:
Go ahead and create `triangle.js` with the following content.
```js {fragment="root"}
```js
class App {
constructor() {
// TODO: create entities
this.canvas = document.getElementsByTagName('canvas')[0];
const engine = this.engine = Filament.Engine.create(this.canvas);
this.scene = engine.createScene();
this.triangle = Filament.EntityManager.get().create();
this.scene.addEntity(this.triangle);
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]);
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);
this.ib = Filament.IndexBuffer.Builder()
.indexCount(3)
.bufferType(Filament.IndexBuffer$IndexType.USHORT)
.build(engine);
this.ib.setBuffer(engine, new Uint16Array([0, 1, 2]));
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);
this.swapChain = engine.createSwapChain();
this.renderer = engine.createRenderer();
this.camera = engine.createCamera(Filament.EntityManager.get().create());
this.view = engine.createView();
this.view.setCamera(this.camera);
this.view.setScene(this.scene);
// Set up a blue-green background:
this.renderer.setClearOptions({clearColor: [0.0, 0.1, 0.2, 1.0], clear: true});
// Adjust the initial viewport:
this.resize();
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
// 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);
window.requestAnimationFrame(this.render);
}
resize() {
// TODO: adjust viewport and canvas
const dpr = window.devicePixelRatio;
const width = this.canvas.width = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * 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);
}
}
@@ -90,7 +255,7 @@ with the correct MIME type.
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"}
```js
this.canvas = document.getElementsByTagName('canvas')[0];
const engine = this.engine = Filament.Engine.create(this.canvas);
```
@@ -102,7 +267,7 @@ The engine is a factory for many Filament entities, including `Scene`, which is
entities. Let's go ahead and create a scene, then add a blank entity called `triangle` into the
scene.
```js {fragment="create entities"}
```js
this.scene = engine.createScene();
this.triangle = Filament.EntityManager.get().create();
this.scene.addEntity(this.triangle);
@@ -118,7 +283,7 @@ calls.
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"}
```js
const TRIANGLE_POSITIONS = new Float32Array([
1, 0,
Math.cos(Math.PI * 2 / 3), Math.sin(Math.PI * 2 / 3),
@@ -130,7 +295,7 @@ 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"}
```js
const VertexAttribute = Filament.VertexAttribute;
const AttributeType = Filament.VertexBuffer$AttributeType;
this.vb = Filament.VertexBuffer.Builder()
@@ -160,7 +325,7 @@ 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"}
```js
this.ib = Filament.IndexBuffer.Builder()
.indexCount(3)
.bufferType(Filament.IndexBuffer$IndexType.USHORT)
@@ -183,7 +348,7 @@ 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"}
```js
const mat = engine.createMaterial('triangle.filamat');
const matinst = mat.getDefaultInstance();
Filament.RenderableManager.Builder(1)
@@ -196,7 +361,7 @@ Filament.RenderableManager.Builder(1)
Next let's wrap up the initialization routine by creating the swap chain, renderer, camera, and
view.
```js {fragment="create entities"}
```js
this.swapChain = engine.createSwapChain();
this.renderer = engine.createRenderer();
this.camera = engine.createCamera(Filament.EntityManager.get().create());
@@ -221,7 +386,16 @@ to repaint. Often this is 60 times a second.
```js
render() {
// TODO: 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);
window.requestAnimationFrame(this.render);
}
```
@@ -229,7 +403,7 @@ 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"}
```js
// Rotate the triangle.
const radians = Date.now() / 1000;
const transform = mat4.fromRotation(mat4.create(), radians, [0, 0, 1]);
@@ -253,20 +427,13 @@ One last step. Add the following code to the resize method. This adjusts the res
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"}
```js
const dpr = window.devicePixelRatio;
const width = this.canvas.width = window.innerWidth * dpr;
const height = this.canvas.height = window.innerHeight * dpr;
const width = this.canvas.width = this.canvas.clientWidth * dpr;
const height = this.canvas.height = this.canvas.clientHeight * 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

View File

@@ -30,6 +30,7 @@ set(COPTS "${COPTS} -DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0")
set(LOPTS "${LOPTS} --bind")
set(LOPTS "${LOPTS} -s ALLOW_MEMORY_GROWTH=1")
set(LOPTS "${LOPTS} -s EXPORTED_FUNCTIONS=['_malloc','_free']")
set(LOPTS "${LOPTS} -s MODULARIZE=1")
set(LOPTS "${LOPTS} -s EXPORT_NAME=Filament")
set(LOPTS "${LOPTS} -s USE_WEBGL2=1")

View File

@@ -290,13 +290,18 @@ Filament._createTextureFromImageFile = function(fileContents, engine, options) {
pbtype = Filament.PixelDataType.UBYTE;
}
const tex = Filament.Texture.Builder()
let tex = Filament.Texture.Builder()
.width(decodedImage.width)
.height(decodedImage.height)
.levels(nomips ? 1 : 0xff)
.sampler(Sampler.SAMPLER_2D)
.format(texformat)
.build(engine);
.format(texformat);
if (options['usage'] !== undefined) {
tex.usage(options['usage']);
}
tex = tex.build(engine);
const pixelbuffer = Filament.PixelBuffer(decodedImage.data.getBytes(), pbformat, pbtype);
tex.setImage(engine, 0, pixelbuffer);

View File

@@ -1,264 +0,0 @@
cmake_minimum_required(VERSION 3.19)
project(websamples)
if (FILAMENT_SKIP_SAMPLES)
return()
endif()
set(SERVER_DIR ${PROJECT_BINARY_DIR})
set(ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../..)
if (CMAKE_CROSSCOMPILING)
include(${IMPORT_EXECUTABLES})
endif()
# ==================================================================================================
# Build Materials.
# ==================================================================================================
set(MATERIAL_NAMES
parquet
sandbox
textured
nonlit)
set(MATERIAL_FL0_NAMES
nonlit
)
set(MATC_FLAGS -a opengl -p mobile)
if (NOT CMAKE_BUILD_TYPE MATCHES Release)
set(MATC_FLAGS -g ${MATC_FLAGS})
endif()
set(MATERIAL_BINS)
foreach (NAME ${MATERIAL_NAMES})
set(mat_src "materials/${NAME}.mat")
get_filename_component(localname "${mat_src}" NAME_WE)
get_filename_component(fullname "${mat_src}" ABSOLUTE)
set(output_path "${SERVER_DIR}/${localname}.filamat")
add_custom_command(
OUTPUT ${output_path}
COMMAND matc ${MATC_FLAGS} -o ${output_path} ${fullname}
MAIN_DEPENDENCY ${mat_src}
DEPENDS matc
COMMENT "Compiling material ${mat_src} to ${output_path}")
list(APPEND MATERIAL_BINS ${output_path})
# --- Feature Level 0 variant ---
list(FIND MATERIAL_FL0_NAMES ${NAME} index)
if (${index} GREATER -1 AND FILAMENT_ENABLE_FEATURE_LEVEL_0)
string(REGEX REPLACE "[.]filamat$" "_fl0.filamat" output_path_fl0 ${output_path})
add_custom_command(
OUTPUT ${output_path_fl0}
COMMAND matc ${MATC_FLAGS} -PfeatureLevel=0 -o ${output_path_fl0} ${fullname}
MAIN_DEPENDENCY ${fullname}
DEPENDS matc
COMMENT "Compiling material ${fullname} (FL0)"
)
list(APPEND MATERIAL_BINS ${output_path_fl0})
endif()
endforeach()
add_custom_target(sample_materials DEPENDS ${MATERIAL_BINS})
# ==================================================================================================
# Build Assets.
# ==================================================================================================
# Generate mipmapped KTX files from various PNG files using mipgen.
function(add_ktxfiles SOURCE TARGET EXTRA_ARGS)
set(source_path "${ROOT_DIR}/${SOURCE}")
set(target_path "${SERVER_DIR}/${TARGET}")
set(target_textures ${target_textures} ${target_path} PARENT_SCOPE)
add_custom_command(
OUTPUT ${target_path}
COMMAND mipgen --quiet --strip-alpha ${EXTRA_ARGS} ${source_path} ${target_path}
MAIN_DEPENDENCY ${source_path}
DEPENDS mipgen)
endfunction()
# Raw resource files can simply be copied into the server folder.
function(add_rawfile SOURCE TARGET)
set(source_path "${ROOT_DIR}/${SOURCE}")
set(target_path "${SERVER_DIR}/${TARGET}")
set(target_textures ${target_textures} ${target_path} PARENT_SCOPE)
add_custom_command(
OUTPUT ${target_path}
COMMAND ${CMAKE_COMMAND} -E copy ${source_path} ${target_path}
MAIN_DEPENDENCY ${source_path})
endfunction()
add_ktxfiles("assets/models/monkey/color.png" "albedo.ktx2" "--compression=uastc")
add_ktxfiles("assets/models/monkey/normal.png" "normal.ktx2" "--compression=uastc_normals;--kernel=NORMALS;--linear")
add_ktxfiles("assets/models/monkey/roughness.png" "roughness.ktx2" "--compression=uastc;--grayscale;--linear")
add_ktxfiles("assets/models/monkey/metallic.png" "metallic.ktx2" "--compression=uastc;--grayscale;--linear")
add_ktxfiles("assets/models/monkey/ao.png" "ao.ktx2" "--compression=uastc;--grayscale;--linear")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_baseColor.png" "FlightHelmet_baseColor.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_baseColor1.png" "FlightHelmet_baseColor1.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_baseColor2.png" "FlightHelmet_baseColor2.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_baseColor3.png" "FlightHelmet_baseColor3.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_baseColor4.png" "FlightHelmet_baseColor4.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_normal.png" "FlightHelmet_normal.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_normal1.png" "FlightHelmet_normal1.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_normal2.png" "FlightHelmet_normal2.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_normal3.png" "FlightHelmet_normal3.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_normal4.png" "FlightHelmet_normal4.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_occlusionRoughnessMetallic.png" "FlightHelmet_occlusionRoughnessMetallic.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_occlusionRoughnessMetallic1.png" "FlightHelmet_occlusionRoughnessMetallic1.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_occlusionRoughnessMetallic2.png" "FlightHelmet_occlusionRoughnessMetallic2.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_occlusionRoughnessMetallic3.png" "FlightHelmet_occlusionRoughnessMetallic3.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet_occlusionRoughnessMetallic4.png" "FlightHelmet_occlusionRoughnessMetallic4.png")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet.bin" "FlightHelmet.bin")
add_rawfile("third_party/models/FlightHelmet/FlightHelmet.gltf" "FlightHelmet.gltf")
add_rawfile("third_party/models/AnimatedTriangle/simpleTriangle.bin" "simpleTriangle.bin")
add_rawfile("third_party/models/AnimatedTriangle/animation.bin" "animation.bin")
add_rawfile("third_party/models/AnimatedTriangle/AnimatedTriangle.gltf" "AnimatedTriangle.gltf")
add_rawfile("third_party/models/AnimatedMorphCube/AnimatedMorphCube.glb" "AnimatedMorphCube.glb")
# Convert OBJ files into filamesh files.
function(add_mesh SOURCE TARGET)
set(source_mesh "${ROOT_DIR}/${SOURCE}")
set(target_mesh "${SERVER_DIR}/${TARGET}")
set(target_meshes ${target_meshes} ${target_mesh} PARENT_SCOPE)
add_custom_command(
OUTPUT ${target_mesh}
COMMAND filamesh --compress ${source_mesh} ${target_mesh}
MAIN_DEPENDENCY ${source_mesh}
DEPENDS filamesh)
endfunction()
add_mesh("assets/models/monkey/monkey.obj" "suzanne.filamesh")
add_mesh("third_party/models/shader_ball/shader_ball.obj" "shader_ball.filamesh")
# Generate IBL and skybox images using cmgen.
set(CMGEN_ARGS --quiet --format=ktx --size=256 --extract-blur=0.1)
set(CMGEN_ARGS_TINY --quiet --format=ktx --size=64 --extract-blur=0.1)
function(add_envmap SOURCE TARGET)
set(source_envmap "${ROOT_DIR}/${SOURCE}")
file(MAKE_DIRECTORY "${SERVER_DIR}/${TARGET}")
set(target_skybox "${SERVER_DIR}/${TARGET}/${TARGET}_skybox.ktx")
set(target_envmap "${SERVER_DIR}/${TARGET}/${TARGET}_ibl.ktx")
set(target_skybox_tiny "${SERVER_DIR}/${TARGET}/${TARGET}_skybox_tiny.ktx")
set(target_skyboxes ${target_skyboxes} ${target_skybox} ${target_skybox_tiny} PARENT_SCOPE)
set(target_envmaps ${target_envmaps} ${target_envmap} PARENT_SCOPE)
add_custom_command(OUTPUT ${target_skybox} ${target_skybox_tiny} ${target_envmap}
# Create a low-resolution skybox.
COMMAND cmgen -x ${TARGET} ${CMGEN_ARGS_TINY} ${source_envmap}
COMMAND ${CMAKE_COMMAND} -E rename ${target_skybox} ${target_skybox_tiny}
# Create KTX files for the full-size envmap.
COMMAND cmgen -x ${TARGET} ${CMGEN_ARGS} ${source_envmap}
MAIN_DEPENDENCY ${source_envmap}
DEPENDS cmgen)
endfunction()
# The pillars envmap is used only when building new HTML for the tutorials.
# add_envmap("third_party/environments/pillars_2k.hdr" "pillars_2k")
add_envmap("third_party/environments/lightroom_14b.hdr" "default_env")
add_custom_target(sample_assets DEPENDS
${target_textures}
${target_meshes}
${target_envmaps}
${target_skyboxes})
# ==================================================================================================
# Copy filament.{js,wasm} into the server folder.
# ==================================================================================================
add_custom_command(
OUTPUT ${SERVER_DIR}/filament.js
DEPENDS filament-js
COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/../filament-js/filament.js ${SERVER_DIR})
add_custom_command(
OUTPUT ${SERVER_DIR}/filament.wasm
DEPENDS filament-js
COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/../filament-js/filament.wasm ${SERVER_DIR})
add_custom_command(
OUTPUT ${SERVER_DIR}/filament-viewer.js
DEPENDS ${PROJECT_BINARY_DIR}/../filament-js/filament-viewer.js
COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/../filament-js/filament-viewer.js ${SERVER_DIR})
add_custom_target(filamentjs_public DEPENDS
${SERVER_DIR}/filament.js
${SERVER_DIR}/filament.wasm
${SERVER_DIR}/filament-viewer.js)
if (WEBGL_PTHREADS)
add_custom_command(
OUTPUT ${SERVER_DIR}/filament.worker.js
DEPENDS filament-js
COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/../filament-js/filament.worker.js ${SERVER_DIR})
add_dependencies(filamentjs_public ${SERVER_DIR}/filament.worker.js)
endif()
# ==================================================================================================
# The websamples target depends on all HTML files, assets, and filament.{js,wasm}
# ==================================================================================================
set(HTML_FILES
animation.html
cube_fl0.html
helmet.html
morphing.html
parquet.html
remote.html
skinning.html
suzanne.html
test-filament-viewer.html
triangle.html)
set(ASSET_FILES
assets/favicon.png)
set(DEMO_ASSETS)
foreach (NAME ${HTML_FILES} ${ASSET_FILES})
add_custom_command(
OUTPUT ${SERVER_DIR}/${NAME}
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/${NAME} ${SERVER_DIR}
MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/${NAME})
list(APPEND DEMO_ASSETS ${SERVER_DIR}/${NAME})
endforeach()
set(TEXTURE_FILES floor_ao_roughness_metallic.png floor_basecolor.jpg floor_normal.png)
foreach (NAME ${TEXTURE_FILES})
add_custom_command(
OUTPUT ${SERVER_DIR}/${NAME}
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/textures/${NAME} ${SERVER_DIR}
MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/textures/${NAME})
list(APPEND DEMO_ASSETS ${SERVER_DIR}/${NAME})
endforeach()
add_custom_command(
OUTPUT ${SERVER_DIR}/gl-matrix-min.js
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/gl-matrix/gl-matrix-min.js ${SERVER_DIR}/gl-matrix-min.js
MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/gl-matrix/gl-matrix-min.js)
list(APPEND DEMO_ASSETS ${SERVER_DIR}/gl-matrix-min.js)
add_custom_command(
OUTPUT ${SERVER_DIR}/gltumble.min.js
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/gltumble/gltumble.min.js ${SERVER_DIR}/gltumble.min.js
MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/../../third_party/gltumble/gltumble.min.js)
list(APPEND DEMO_ASSETS ${SERVER_DIR}/gltumble.min.js)
add_custom_target(${PROJECT_NAME} ALL DEPENDS
${DEMO_ASSETS}
sample_materials
sample_assets
filamentjs_public)

View File

@@ -1,45 +0,0 @@
material {
name : textured,
requires : [
uv0
],
shadingModel : lit,
parameters : [
{
type : sampler2d,
name : albedo
},
{
type : sampler2d,
name : roughness
},
{
type : sampler2d,
name : metallic
},
{
type : float,
name : clearCoat
},
{
type : sampler2d,
name : normal
},
{
type : sampler2d,
name : ao
}
],
}
fragment {
void material(inout MaterialInputs material) {
material.normal = texture(materialParams_normal, getUV0()).xyz * 2.0 - 1.0;
prepareMaterial(material);
material.baseColor = texture(materialParams_albedo, getUV0());
material.roughness = texture(materialParams_roughness, getUV0()).r;
material.metallic = texture(materialParams_metallic, getUV0()).r;
material.clearCoat = materialParams.clearCoat;
material.ambientOcclusion = texture(materialParams_ao, getUV0()).r;
}
}

View File

@@ -1,147 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Filament Suzanne</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link href="favicon.png" rel="icon" type="image/x-icon" />
<style>
body { margin: 0; overflow: hidden; }
canvas { touch-action: none; width: 100%; height: 100%; }
</style>
</head>
<body>
<canvas></canvas>
<script src="filament.js"></script>
<script src="gl-matrix-min.js"></script>
<script>
const env = 'default_env'
const ibl_url = `${env}/${env}_ibl.ktx`;
const sky_url = `${env}/${env}_skybox.ktx`;
const albedo_url = `albedo.ktx2`;
const ao_url = `ao.ktx2`;
const metallic_url = `metallic.ktx2`;
const normal_url = `normal.ktx2`;
const roughness_url = `roughness.ktx2`;
const filamat_url = 'textured.filamat';
const filamesh_url = 'suzanne.filamesh';
Filament.init([
filamat_url,
filamesh_url,
albedo_url,
ao_url,
metallic_url,
normal_url,
roughness_url,
ibl_url,
sky_url
], () => {
window.AttributeType = Filament.VertexBuffer$AttributeType;
window.Format = Filament.Texture$InternalFormat;
window.Fov = Filament.Camera$Fov;
window.IndexType = Filament.IndexBuffer$IndexType;
window.LightType = Filament.LightManager$Type;
window.PrimitiveType = Filament.RenderableManager$PrimitiveType;
window.VertexAttribute = Filament.VertexAttribute;
window.app = new App(document.getElementsByTagName('canvas')[0]);
});
class App {
constructor(canvas) {
this.canvas = canvas;
const engine = this.engine = Filament.Engine.create(this.canvas);
this.scene = engine.createScene();
const sunlight = Filament.EntityManager.get().create();
Filament.LightManager.Builder(LightType.SUN)
.color([0.98, 0.92, 0.89])
.intensity(80000.0)
.direction([0.6, -1.0, -0.8])
.castShadows(true)
.sunAngularRadius(1.9)
.sunHaloSize(10.0)
.sunHaloFalloff(80.0)
.build(engine, sunlight);
this.scene.addEntity(sunlight);
const indirectLight = this.ibl = engine.createIblFromKtx1(ibl_url);
this.scene.setIndirectLight(indirectLight);
indirectLight.setIntensity(80000);
const skybox = engine.createSkyFromKtx1(sky_url);
this.scene.setSkybox(skybox);
const material = engine.createMaterial(filamat_url);
const matinstance = material.createInstance();
const sampler = new Filament.TextureSampler(
Filament.MinFilter.LINEAR_MIPMAP_LINEAR,
Filament.MagFilter.LINEAR,
Filament.WrapMode.CLAMP_TO_EDGE);
// Filament requests support for the following extensions, but none are guaranteed.
// WEBGL_compressed_texture_s3tc, WEBGL_compressed_texture_s3tc_srgb
// WEBGL_compressed_texture_astc, WEBGL_compressed_texture_etc
const albedo = engine.createTextureFromKtx2(albedo_url, {
srgb: true,
formats: [ Format.DXT5_RGBA, Format.DXT5_SRGBA, Format.ETC2_EAC_SRGBA8 ]
});
console.assert(albedo, "Unable to create albedo texture.")
const roughness = engine.createTextureFromKtx2(roughness_url);
const metallic = engine.createTextureFromKtx2(metallic_url);
const ao = engine.createTextureFromKtx2(ao_url);
const normal = engine.createTextureFromKtx2(normal_url);
matinstance.setTextureParameter('albedo', albedo, sampler)
matinstance.setTextureParameter('roughness', roughness, sampler)
matinstance.setTextureParameter('metallic', metallic, sampler)
matinstance.setTextureParameter('normal', normal, sampler)
matinstance.setTextureParameter('ao', ao, sampler)
const mesh = engine.loadFilamesh(filamesh_url, matinstance);
this.suzanne = mesh.renderable;
this.scene.addEntity(mesh.renderable);
this.swapChain = engine.createSwapChain();
this.renderer = engine.createRenderer();
this.camera = engine.createCamera(Filament.EntityManager.get().create());
this.view = engine.createView();
this.view.setCamera(this.camera);
this.view.setScene(this.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 radians = Date.now() / 1000;
const transform = mat4.fromRotation(mat4.create(), radians, [0, 1, 0]);
const tcm = this.engine.getTransformManager();
const inst = tcm.getInstance(this.suzanne);
tcm.setTransform(inst, transform);
inst.delete();
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]);
const eye = [0, 0, 4], center = [0, 0, 0], up = [0, 1, 0];
this.camera.lookAt(eye, center, up);
const aspect = width / height;
const fov = aspect < 1 ? Fov.HORIZONTAL : Fov.VERTICAL;
this.camera.setProjectionFov(45, aspect, 1.0, 10.0, fov);
}
}
</script>
</body>
</html>

View File

@@ -1,97 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Filament Viewer</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<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" rel="stylesheet">
<style>
html, body {
height: 100%;
margin: 0;
}
h2 {
margin-top: 0
}
main {
max-width: 800px;
background: #eee;
font-family: "Open Sans";
margin: auto;
padding: 10px;
}
.expando {
width: 100%;
border: solid 2px gray;
}
.tall {
height: 400px;
}
.wide {
width: 500px;
height: 300px;
border: none;
}
.wide::part(canvas) {
border-radius: 10px;
}
</style>
</head>
<body>
<main>
<h2>Hello World</h2>
<p>
This is a demonstration of the <code>&lt;filament-viewer&gt;</code> web component.
</p>
<h3>No skybox, auto-expand, drag and drop</h3>
<p>
<filament-viewer
enableDrop="true"
class="expando"
ibl="default_env/default_env_ibl.ktx"
intensity="20000" />
</p>
<h3>No skybox, auto-expand</h3>
<p>
<filament-viewer
src="FlightHelmet.gltf"
class="expando"
ibl="default_env/default_env_ibl.ktx"
intensity="20000" />
</p>
<h3>Skybox, fixed size</h3>
<p>
<filament-viewer
src="https://prideout.net/models/CesiumMan.glb"
ibl="default_env/default_env_ibl.ktx"
sky="default_env/default_env_skybox.ktx"
class="tall"
/>
</p>
<h3>No skybox, no IBL, fixed size, round border</h3>
<p>
<filament-viewer
src="https://prideout.net/models/MaterialsVariantsShoe.glb"
materialVariant="1"
class="wide"
/>
</p>
</main>
<script src="filament.js"></script>
<script src="https://unpkg.com/gltumble"></script>
<script src="filament-viewer.js" type="module"></script>
</body>
</html>

View File

@@ -1,108 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Filament Triangle</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1">
<link href="favicon.png" rel="icon" type="image/x-icon" />
<style>
body { margin: 0; overflow: hidden; }
canvas { touch-action: none; width: 100%; height: 100%; }
</style>
</head>
<body>
<canvas></canvas>
<script src="filament.js"></script>
<script src="gl-matrix-min.js"></script>
<script>
Filament.init([ 'nonlit.filamat' ], () => {
window.VertexAttribute = Filament.VertexAttribute;
window.AttributeType = Filament.VertexBuffer$AttributeType;
window.Projection = Filament.Camera$Projection;
window.app = new App(document.getElementsByTagName('canvas')[0]);
});
class App {
constructor(canvas) {
this.canvas = canvas;
const engine = this.engine = Filament.Engine.create(this.canvas);
this.scene = engine.createScene();
this.triangle = Filament.EntityManager.get().create();
this.scene.addEntity(this.triangle);
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]);
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);
this.ib = Filament.IndexBuffer.Builder()
.indexCount(3)
.bufferType(Filament.IndexBuffer$IndexType.USHORT)
.build(engine);
this.ib.setBuffer(engine, new Uint16Array([0, 1, 2]));
const mat = engine.createMaterial('nonlit.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);
this.swapChain = engine.createSwapChain();
this.renderer = engine.createRenderer();
this.camera = engine.createCamera(Filament.EntityManager.get().create());
this.view = engine.createView();
this.view.setSampleCount(4);
this.view.setCamera(this.camera);
this.view.setScene(this.scene);
this.renderer.setClearOptions({clearColor: [0.0, 0.1, 0.2, 1.0], clear: true});
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 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();
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]);
const aspect = width / height;
this.camera.setProjection(Projection.ORTHO, -aspect, aspect, -1, 1, 0, 1);
}
}
</script>
</body>
</html>