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
4
.github/workflows/postsubmit-main.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = '#####?????'
|
||||
|
||||
136
docs_src/build/copy_web_docs.py
Normal 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()
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 .
|
||||
|
||||
@@ -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()
|
||||
|
||||
111
docs_src/build/snapshot_samples.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
BIN
docs_src/src_mdbook/src/images/web_sample_animation.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
docs_src/src_mdbook/src/images/web_sample_cube_fl0.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
docs_src/src_mdbook/src/images/web_sample_helmet.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs_src/src_mdbook/src/images/web_sample_morphing.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
docs_src/src_mdbook/src/images/web_sample_parquet.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs_src/src_mdbook/src/images/web_sample_redball.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs_src/src_mdbook/src/images/web_sample_skinning.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
docs_src/src_mdbook/src/images/web_sample_suzanne.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
docs_src/src_mdbook/src/images/web_sample_triangle.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
@@ -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)
|
||||
65
docs_src/src_mdbook/src/samples/web/samples.md
Normal 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>
|
||||
53
docs_src/src_mdbook/src/samples/web/tutorials.md
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -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
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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">🔗</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()
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
triangle
|
||||
redball
|
||||
suzanne
|
||||
197
web/examples/CMakeLists.txt
Normal 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})
|
||||
@@ -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);
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -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];
|
||||
@@ -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);
|
||||
12
web/examples/materials/skinning.mat
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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";
|
||||
1
web/examples/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
mistletoe
|
||||
161
web/examples/serve.py
Executable 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.")
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
Before Width: | Height: | Size: 678 KiB After Width: | Height: | Size: 678 KiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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><filament-viewer></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>
|
||||
@@ -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>
|
||||