Compare commits

..
Author SHA1 Message Date
Powei Feng 962abb7e8d material-parser: refactor computing, caching of crc32
- Make the two ways to get crc32 explicitly documented.
 - Ensure the slow path of computing crc32 from scratch is only
   happens on init, either
     - Creating MaterialParser for validation
     - Or, using crc32 val as the key for the first time in
       MaterialCache
 - In doing the above, move crc32 validation to
   MaterialDefinition::createParser
2026-03-06 11:26:06 -08:00
haroonq 0b8dbe9b0a Small fix to allow PlatformEGL to work on non-Android targets. (#9724)
- Include gl_headers.h since it defines BACKEND_OPENGL_VERSION_GL(ES)
    depending on which headers are available.
- Don't assume glGetString(GL_EXTENSIONS) returns a non-null value as
    null is a possible return value for OpenGL (desktop).
2026-03-05 15:38:49 -08:00
Powei Feng e595fd4b79 android-utils: ModelViewer camera manipulator can be null (#9766)
We add the ability for the maniuplator to be null. If it is null,
then the camera settings won't be determined by the manipulator.
This is useful for when we want to set the camera parameters
outside of the modelviewer, but still use it to do everything else.
2026-03-05 20:05:55 +00:00
Ben Doherty c5d36cff7f Prepare Gradle plugin for publishing (#9773) 2026-03-05 10:50:15 -08:00
Powei Feng f392e8be54 ppm: move stencil check right before driver calls (#9769)
The init block of addPass will be executed immediately, which means
that even for passes that are culled, we will trip on incorrect format
given to the pass.

We move the stencil check to the execute block of addPass so that
it'll only assert when the pass is actually present in the graph.

FIXES=489437881
2026-03-05 18:11:33 +00:00
Eliza 83653fb358 engine: encapsulate material cache handling (#9663)
* engine: encapsulate material cache handling

We will soon allow `MaterialInstance` to override the value of spec constants.
To avoid code duplication, we introduce a new class `MaterialPrograms` which
handles the chunky bits of managing the program cache and values of the spec
constants.

* MaterialPrograms: add explicit initialize method

* MaterialPrograms: backport fixes, address comments

* MaterialPrograms: set all constants at once

* MaterialPrograms: rename to LocalProgramCache
2026-03-05 06:42:05 +00:00
Eliza 8a3c48fef1 utils: add LRU cache to RefCountedMap (#9730)
* utils: add LRU cache to RefCountedMap

This change introduces a new data structure LruCache and uses it in
RefCountedMap to keep a fixed number of cache entries alive after their
reference count has dropped to zero in the main map.

* utils: address LRU cache comments
2026-03-05 06:04:16 +00:00
Powei Feng 7f6b9bb144 vk: reference rendertarget as part of fbo cache (#9771)
The FBO cache uses imageviews as its cache key. To ensure that these
views are valid for the lifetime of a key-value pair in the cache, we
need to also reference the rendertarget in the value part of the pair.
The RT contains attachments, which are wrappers around
which own the image views.

Fixes #9680
2026-03-04 09:59:05 -08:00
rafadevai 687c42583b VK: Only make a single copy of the descriptor with external samplers (#9765)
When more than one external sampler are present the
flow will clone the descriptor set twice and make a copy
of a binding that requires an immutable sampler. The copy
of this binding will cause a crash in some adreno GPUs.

This change will make sure the descriptor set is only cloned one
and after that only update binding operations are done for the
external sampled bindings.
2026-03-03 18:34:40 -08:00
Mathias Agopian 71e8cab08a Fix a race when garbage-collecting components (#9768)
Not all component managers are thread-safe for garbage-collection. In
particular, some need to access the DriverAPI which is never
thread-safe.

We refactor the garbage-collection code into FEngine, so it's not
duplicated in FRenderer. We make it more explicit that 
FRenderableManager::destroyComponent() needs the DriverAPI, and we
don't call its gc() from a job.

Besides the refactoring, the only change in this CL is that 
FRenderableManager::gc() is no longer called from a job.


FIXES=[489134910]
2026-03-03 13:21:06 -08:00
Ben Doherty afae31a975 Add Sonatype publishing step to release workflow (#9764) 2026-03-03 12:41:55 -08:00
Powei Feng 5ac5dc4c95 ci: fetch PR ref explicitly to extract commit message for forks (#9756) 2026-03-03 17:24:00 +00:00
Filament Bot e975572972 [automated] Updating /docs due to commit 29e91f0
Full commit hash is 29e91f0d3a

DOCS_ALLOW_DIRECT_EDITS
2026-03-03 16:21:50 +00:00
Sungun Park 29e91f0d3a Release Filament 1.69.5 2026-03-03 08:15:31 -08:00
Mathias Agopian 6f0d47f275 EVSM improvements (#9758)
- refactoring/clecanup to make some changes easier
- VSM mipmap generation was mistakenly disable when blur radius was 0
- analytic variance was disabled because the math only worked for VSM. Fixed the math.
- better handling of large blurs when using fp32
- implement EVSM equivalent of receiver plane normal bias
- use correct EVSM clearing color
- mipmapping with point lights works much better (no seams)
- min variance is computed automatically
- custom high precision mipmaping shader for VSM
2026-03-02 16:46:07 -08:00
Powei Feng cf66813f41 android: sample-render-validation with new UI and cli tool (#9751)
Android App:
- Refactored activity_main.xml to use modern Material 3 components.
- Grouped Export/Help buttons logically and moved ADB instructions
  to dedicated info icons.
- Added an in-app "Load Test" button to explicitly pick test bundles.
- Updated ValidationInputManager.kt to gracefully handle relative
  zip_path intent execution via ADB targeting app external storage.

Tooling & Documentation:
- Added a Python Textual TUI (validation_app.py) to automate device
  discovery, test execution, bundling, renaming, and
  downloading/uploading.
- Added README.md in test/render-validation documenting ADB intent
  parameter capabilities and TUI dashboard setup.
2026-03-02 23:51:07 +00:00
Powei Feng 5f89e8e711 vk: fix crash when resizing (#9762)
The problem is that we might flush when resizing happens. So
we need to ask for the command buffer "after" the resize/acquire
swapchain logic.

Fixes #9718
2026-02-27 18:14:59 +00:00
Anish Goyal be9e9298e1 Add a default fence value to VulkanCommands (#9759)
This is useful for the case of certain devices, where a filament::Sync
may be created BEFORE any commands are submitted.
2026-02-27 06:03:30 +00:00
dependabot[bot]andPowei Feng 9218b90c9c build(deps): bump the pip group across 1 directory with 2 updates (#9749)
Bumps the pip group with 2 updates in the /test/renderdiff/src directory: [flask](https://github.com/pallets/flask) and [werkzeug](https://github.com/pallets/werkzeug).


Updates `flask` from 3.1.2 to 3.1.3
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.1.2...3.1.3)

Updates `werkzeug` from 3.1.4 to 3.1.6
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.1.4...3.1.6)

---
updated-dependencies:
- dependency-name: flask
  dependency-version: 3.1.3
  dependency-type: direct:production
  dependency-group: pip
- dependency-name: werkzeug
  dependency-version: 3.1.6
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Powei Feng <powei@google.com>
2026-02-27 05:37:29 +00:00
Mathias Agopian 070a07679d Simplify the API of MaterialInstanceManager (#9757)
It's no longer necessary to preallocate a "tag" to reuse a material
instance. Instead, we can simply pass a unique tag when getting the
instance from the pool.
2026-02-26 15:11:31 -08:00
Andrew Wilson 10b7bd71f9 Avoid backend test when building without testing (#9727) 2026-02-26 22:49:58 +00:00
Powei Feng e4ae96a2a1 renderdiff: disable transmission + webgpu due to flake
RDIFF_ACCEPT_NEW_GOLDENS
2026-02-26 11:52:35 -08:00
SiyuandMathias Agopian 56ac08e353 Provide thread name when attaching to JVM on Android (#9755)
* Provide thread name when attaching to JVM on Android

When calling AttachCurrentThread on Android, pass a JavaVMAttachArgs structure. This allows providing the thread name, which is retrieved using pthread_getname_np, to the JVM.

* Fix Android build error: pthread_getname_np requires API 26+

---------

Co-authored-by: Mathias Agopian <mathias@google.com>
2026-02-26 10:44:24 -08:00
52b0b553b4 Marshall the name size when setting thread name with pthread_setname_np (#9753)
* Marshall the name size when setting thread name with pthread_setname_np.

[pthread_setname_np](https://source.corp.google.com/piper///depot/google3/third_party/android/ndk/stable/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/pthread.h;l=330-341) requires the caller to keep the name within 16 bytes.

After this change, Filament threads like `OpenGLTimerQuer`, `CompilerThreadP`, `CompilerThreadP`, `Filament Choreo`, `FrameInfoGpuCom` would be displayed correctly in the trace.

* Use constexpr MAX_PTHREAD_NAME_LEN

---------

Co-authored-by: Powei Feng <powei@google.com>
Co-authored-by: Mathias Agopian <mathias@google.com>
2026-02-26 10:43:53 -08:00
Filament Bot 00f3c7175c [automated] Updating /docs due to commit e4fa86f
Full commit hash is e4fa86fb01

DOCS_ALLOW_DIRECT_EDITS
2026-02-26 18:05:45 +00:00
Powei Feng e4fa86fb01 renderdiff: bump golden again
Transimssion.webgpu.TransmissionRoughnessTest seems to have small,
non-flaky differences.

RDIFF_ACCEPT_NEW_GOLDENS
2026-02-26 09:48:30 -08:00
Mathias AgopianandPowei Feng 7da2a08df6 replace Variant::VSM with MNT and S2D (#9750)
The VSM variant bit was overloaded, it meant two different things
depending on the DEP bit (depth).

For standard variants (DEP = 0), it decides the type of the shadow
sampler used (PCF or 2D).

For depth variants (DEP = 1), it decides what is written during the
shadow pass (nothing, i.e. depth only, or EVSM depth moments).

We now clearly separate the two bits throughout the code.

This change should be purely source-cosmetic, there shouldn't be any
behavior changes.

Co-authored-by: Powei Feng <powei@google.com>
2026-02-25 16:16:07 -08:00
Powei Feng 82246d934d renderdiff: fix update_golden.py
And fix other python bugs

RDIFF_ACCEPT_NEW_GOLDENS
2026-02-25 15:35:02 -08:00
Filament Bot c60969ef67 [automated] Updating /docs due to commit ce37f21
Full commit hash is ce37f216bc

DOCS_ALLOW_DIRECT_EDITS
2026-02-25 23:01:55 +00:00
Powei Feng ce37f216bc Update renderdiff README
RDIFF_ACCEPT_NEW_GOLDENS
2026-02-25 14:58:35 -08:00
Mathias Agopian 81c71fbbb9 Improve shadow normal bias calculation (#9734)
The previous code used the max of the texel's width or height footprint
in world space to compute the offset; this could both overestimate or
underestimate the bias causing some peter panning or acne.

The new code replaces a sqrt with a dot, but is otherwise similar.
2026-02-25 12:16:13 -08:00
Mathias Agopian 07a7c6003a better computation of the jacobian of a projection (#9731)
The previous code was a bit clunky and not generic, it made assumptions
about the shape of the projection matrix. 
This just uses the generic, correct calculation.

In addition the previous code used the wrong matrix, it assumed that
Wp wasn't needed, but it was because translations do change the
jacobian value at a given point.

RDIFF_ACCEPT_NEW_GOLDENS
2026-02-25 12:04:55 -08:00
Sungun Park 804a74c205 Remove unused member in OpenGLContext (#9741) 2026-02-25 19:46:34 +00:00
Anish Goyal dde49a410a Add inferred template to View.cpp (#9746)
In some cases, this missing template causes build issues when locally
building Impress prebuilts.
2026-02-25 09:19:19 -08:00
Filament Bot 770ce7f8ec [automated] Updating /docs due to commit 11714d3
Full commit hash is 11714d3adc

DOCS_ALLOW_DIRECT_EDITS
2026-02-25 03:44:07 +00:00
Powei Feng 11714d3adc Release Filament 1.69.4 2026-02-24 19:37:50 -08:00
Filament Bot 6aac9071b3 [automated] Updating /docs due to commit da9173e
Full commit hash is da9173e9dc

DOCS_ALLOW_DIRECT_EDITS
2026-02-25 03:15:24 +00:00
Powei Feng da9173e9dc ci: automate renderdiff golden image updates (#9740)
- Introduces the `RDIFF_ACCEPT_NEW_GOLDENS` commit message tag.
- Conditionally skip the `test-renderdiff` presubmit comparison
  if this tag is present.
- Extracts renderdiff generation into a reusable
  `.github/actions/renderdiff-generate` composite action.
- Modifies `postsubmit-main.yml` to automatically generate new
  goldens and push them to a temporary
  `accept-goldens-<short-hash>` branch before merging them into
  `main` when the tag is found.
2026-02-25 03:12:50 +00:00
Anish Goyal cd64d50408 Fixes FBO destroys for inflight command buffers (#9744)
If a framebuffer is destroyed while a command buffer is in flight (e.g.
during a window resize), it's possible for a command buffer to try to
use a framebuffer that no longer exists. This encapsulates framebuffers
and render passes in resource_ptr, to ensure they are never reclaimed
prematurely.
2026-02-25 01:13:51 +00:00
dependabot[bot] a3145cb96f Bump minimatch (#9747)
Bumps the npm_and_yarn group with 1 update in the /build/common/upload-release-assets directory: [minimatch](https://github.com/isaacs/minimatch).


Updates `minimatch` from 3.1.2 to 3.1.3
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.3)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-24 16:52:35 -08:00
Nick Fisher cdfb92e14a gltfio: Allow compile-time override of GLTFIO_USE_FILESYSTEM (#9733) 2026-02-24 17:47:49 +00:00
Doris Wu 55c16e6e7a call execute() under single threaded mode (#9738) 2026-02-23 22:20:03 +00:00
Powei Feng 65e3c3bfb9 backend: disable autoresolve test for gl+vk on CI (#9742)
BUGS=486954356
2026-02-23 21:57:12 +00:00
Ben Doherty 902f869721 Metal: recreate sidecar texture if sample count changes (#9430) 2026-02-23 09:54:21 -08:00
Eliza ad1bc6f360 engine: fix VSM (#9737) 2026-02-20 15:08:59 -08:00
Sungun Park 73c343635e Turn off UBO batching (#9736)
BUGS=[486200381]
2026-02-20 20:04:05 +00:00
Mathias Agopian 432e672022 Revert "Swap logic of how the EGL display is initialized. (#9634)" (#9729)
This reverts commit c35ae6571f.

BUGS=[481534922, 478925865]
2026-02-20 08:34:51 -08:00
Doris Wu b56b04c5f8 Fix translucent objects are pickable when skybox is disabled (#9688) 2026-02-20 11:36:58 +08:00
Filament Bot 99816d67c2 [automated] Updating /docs due to commit d6d4f92
Full commit hash is d6d4f92922

DOCS_ALLOW_DIRECT_EDITS
2026-02-19 20:03:46 +00:00
Mathias Agopian d6d4f92922 fix intensities (#9728)
DOCS_FORCE
2026-02-19 11:59:34 -08:00
Powei Feng 6a59a68622 gl: update record when detaching stream (#9712)
FIXES=483744050
2026-02-19 18:33:04 +00:00
Filament Bot 4580f57987 [automated] Updating /docs due to commit 38f7e57
Full commit hash is 38f7e579f1

DOCS_ALLOW_DIRECT_EDITS
2026-02-19 17:33:34 +00:00
156 changed files with 5501 additions and 1812 deletions
+17 -15
View File
@@ -27,17 +27,24 @@ runs:
echo "$HASH" > /tmp/commit_hash.txt
- name: Find commit message (PR)
shell: bash
id: checkout_code
if: github.event_name == 'pull_request'
run: |
BEFORE_HASH=$(git rev-parse HEAD)
echo "hash=$BEFORE_HASH" >> "$GITHUB_OUTPUT"
# Next we will checkout the actual head (not the merge commits) of the PR
AFTER_HASH="${{ github.event.pull_request.head.sha }}"
git checkout $AFTER_HASH
COMMIT_MESSAGE=$(git log -1 --no-merges)
echo "$COMMIT_MESSAGE" > /tmp/commit_msg.txt
echo "$AFTER_HASH" > /tmp/commit_hash.txt
PR_NUMBER="${{ github.event.pull_request.number }}"
# Fetch the head of the PR explicitly to handle forks. Depth 50 ensures we can traverse past recent merge commits.
git fetch --depth=50 origin "pull/$PR_NUMBER/head:pr-head"
COMMIT_HASH=$(git log -1 --no-merges pr-head --format=%H)
AUTHOR_NAME=$(git log -1 --no-merges pr-head --format=%an)
AUTHOR_EMAIL=$(git log -1 --no-merges pr-head --format=%ae)
TSTAMP=$(git log -1 --no-merges pr-head --format=%aI)
COMMIT_MESSAGE=$(git log -1 --no-merges pr-head --format=%B)
echo "commit $COMMIT_HASH" > /tmp/commit_msg.txt
echo "Author: ${AUTHOR_NAME}<${AUTHOR_EMAIL}>" >> /tmp/commit_msg.txt
echo "Date: ${TSTAMP}" >> /tmp/commit_msg.txt
echo "" >> /tmp/commit_msg.txt
echo "$COMMIT_MESSAGE" >> /tmp/commit_msg.txt
echo "$COMMIT_HASH" > /tmp/commit_hash.txt
- shell: bash
id: action_output
run: |
@@ -47,9 +54,4 @@ runs:
cat /tmp/commit_msg.txt >> "$GITHUB_OUTPUT"
echo "$DELIMITER" >> "$GITHUB_OUTPUT"
# Get the commit hash
echo "hash=$(cat /tmp/commit_hash.txt)" >> "$GITHUB_OUTPUT"
- name: Cleanup Find commit message (PR)
shell: bash
if: github.event_name == 'pull_request'
run: |
git checkout ${{ steps.checkout_code.outputs.hash }}
echo "hash=$(cat /tmp/commit_hash.txt)" >> "$GITHUB_OUTPUT"
@@ -0,0 +1,26 @@
name: 'Renderdiff Generate'
description: 'Sets up prerequisites and runs the generate script for renderdiff'
runs:
using: "composite"
steps:
- uses: ./.github/actions/mac-prereq
- uses: ./.github/actions/get-gltf-assets
- uses: ./.github/actions/get-mesa
- uses: ./.github/actions/get-vulkan-sdk
- name: Prerequisites
run: |
# Must have at least clang-16 for a webgpu/dawn build.
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
shell: bash
- name: Generate images
run: |
TEST_DIR=test/renderdiff
source ${TEST_DIR}/src/preamble.sh
set -eux
bash ${TEST_DIR}/generate.sh
set +eux
shell: bash
- name: Build diffimg tool
run: |
./build.sh release diffimg
shell: bash
+32 -7
View File
@@ -10,16 +10,27 @@ jobs:
# a branch on filament-assets.
update-renderdiff-goldens:
name: update-renderdiff-goldens
runs-on: 'ubuntu-24.04-4core'
runs-on: 'macos-14-xlarge'
steps:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/linux-prereq
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Build diffimg
run: ./build.sh release diffimg
- name: Check if accepting new goldens
id: check_accept
env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
run: |
if echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py --mode=accept_new_goldens; then
echo "accept=true" >> "$GITHUB_OUTPUT"
else
echo "accept=false" >> "$GITHUB_OUTPUT"
fi
shell: bash
- name: Renderdiff Generate for new goldens
if: steps.check_accept.outputs.accept == 'true'
uses: ./.github/actions/renderdiff-generate
- name: Run update script
env:
GH_TOKEN: ${{ secrets.FILAMENTBOT_TOKEN }}
@@ -27,10 +38,24 @@ jobs:
run: |
GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py)
COMMIT_HASH="${{ steps.get_commit_msg.outputs.hash }}"
git config --global user.email "filament.bot@gmail.com"
git config --global user.name "Filament Bot"
git config --global credential.helper cache
if [[ "${{ steps.check_accept.outputs.accept }}" == "true" ]]; then
SHORT_HASH="${COMMIT_HASH:0:8}"
GOLDEN_BRANCH="accept-goldens-${SHORT_HASH}"
echo "Generating new goldens for branch ${GOLDEN_BRANCH}"
python3 test/renderdiff/src/update_golden.py \
--branch=${GOLDEN_BRANCH} \
--source=$(pwd)/out/renderdiff/renders \
--commit-msg="Auto-update goldens from ${COMMIT_HASH}" \
--push-to-remote \
--golden-repo-token=${GH_TOKEN}
fi
if [[ "${GOLDEN_BRANCH}" != "main" ]]; then
git config --global user.email "filament.bot@gmail.com"
git config --global user.name "Filament Bot"
git config --global credential.helper cache
echo "branch==${GOLDEN_BRANCH}"
echo "hash==${COMMIT_HASH}"
python3 test/renderdiff/src/update_golden.py --branch=${GOLDEN_BRANCH} \
+15 -10
View File
@@ -126,18 +126,24 @@ jobs:
- uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
- uses: ./.github/actions/mac-prereq
- uses: ./.github/actions/get-gltf-assets
- uses: ./.github/actions/get-mesa
- uses: ./.github/actions/get-vulkan-sdk
- id: get_commit_msg
uses: ./.github/actions/get-commit-msg
- name: Prerequisites
- name: Check if accepting new goldens
id: check_accept
env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
run: |
# Must have at least clang-16 for a webgpu/dawn build.
sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
if echo "${COMMIT_MESSAGE}" | python3 test/renderdiff/src/commit_msg.py --mode=accept_new_goldens; then
echo "accept=true" >> "$GITHUB_OUTPUT"
else
echo "accept=false" >> "$GITHUB_OUTPUT"
fi
shell: bash
- name: Renderdiff Generate
if: steps.check_accept.outputs.accept != 'true'
uses: ./.github/actions/renderdiff-generate
- name: Render and compare
if: steps.check_accept.outputs.accept != 'true'
id: render_compare
env:
COMMIT_MESSAGE: ${{ steps.get_commit_msg.outputs.msg }}
@@ -146,9 +152,6 @@ jobs:
source ${TEST_DIR}/src/preamble.sh
set -eux
GOLDEN_BRANCH=$(echo "${COMMIT_MESSAGE}" | python3 ${TEST_DIR}/src/commit_msg.py)
bash ${TEST_DIR}/generate.sh
# Build diffimg tool
./build.sh release diffimg
python3 ${TEST_DIR}/src/golden_manager.py \
--branch=${GOLDEN_BRANCH} \
@@ -172,10 +175,12 @@ jobs:
fi
shell: bash
- uses: actions/upload-artifact@v4
if: steps.check_accept.outputs.accept != 'true'
with:
name: presubmit-renderdiff-result
path: ./out/renderdiff
- name: Compare result
if: steps.check_accept.outputs.accept != 'true'
run: |
ERROR_STR="${{ steps.render_compare.outputs.err }}"
if [ -n "${ERROR_STR}" ]; then
+48
View File
@@ -188,6 +188,54 @@ jobs:
const globber = await glob.create(['out/*.aar', 'out/*.apk', 'out/*.tgz'].join('\n'));
await upload({ github, context }, await globber.glob(), TAG);
sonatype-publish:
name: sonatype-publish
runs-on: 'ubuntu-24.04-16core'
# Depends on the the Android build for the Android binaries.
# Depends on the Mac, Linux, and Windows builds for host tools.
needs: [build-mac, build-linux, build-windows, build-android]
if: github.event_name == 'release' || github.event.inputs.platform == 'android'
steps:
- name: Decide Git ref
id: git_ref
run: |
REF=${RELEASE_TAG:-${GITHUB_REF}}
TAG=${REF##*/}
echo "ref=${REF}" >> $GITHUB_OUTPUT
echo "tag=${TAG}" >> $GITHUB_OUTPUT
- uses: actions/checkout@v4.1.6
with:
ref: ${{ steps.git_ref.outputs.ref }}
- uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- uses: ./.github/actions/linux-prereq
- name: Download Android Release
run: |
gh release download ${TAG} \
--repo ${{ github.repository }} \
--pattern 'filament-*-android-native.tgz'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.git_ref.outputs.tag }}
- name: Unzip Android Release
run: |
mkdir -p out/android-release
tar -xzvf filament-${TAG}-android-native.tgz -C out/android-release/
env:
TAG: ${{ steps.git_ref.outputs.tag }}
- name: Publish To Sonatype
run: |
cd android
./gradlew publishToSonatype closeSonatypeStagingRepository
env:
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USERNAME }}
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.MAVEN_SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.MAVEN_SIGNING_PASSWORD }}
build-ios:
name: build-ios
runs-on: macos-14-xlarge
-2
View File
@@ -6,5 +6,3 @@
appropriate header in [RELEASE_NOTES.md](./RELEASE_NOTES.md).
## Release notes for next branch cut
- engine: fix crash when using variance shadow maps
+2 -2
View File
@@ -31,7 +31,7 @@ repositories {
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.69.4'
implementation 'com.google.android.filament:filament-android:1.69.5'
}
```
@@ -50,7 +50,7 @@ Here are all the libraries available in the group `com.google.android.filament`:
iOS projects can use CocoaPods to install the latest release:
```shell
pod 'Filament', '~> 1.69.4'
pod 'Filament', '~> 1.69.5'
```
## Documentation
+9
View File
@@ -7,6 +7,15 @@ A new header is inserted each time a *tag* is created.
Instead, if you are authoring a PR for the main branch, add your release note to
[NEW_RELEASE_NOTES.md](./NEW_RELEASE_NOTES.md).
## v1.70.0
- engine: fix crash when using variance shadow maps
- materials: better shadow normal-bias calculations [⚠️ **New Material Version**]
## v1.69.5
- engine: fix crash when using variance shadow maps
## v1.69.4
+8 -2
View File
@@ -40,15 +40,21 @@
// - Build and upload artifacts with ./gradlew publish
// - Close and release staging repo on Nexus with ./gradlew closeAndReleaseStagingRepository
//
// The following is needed in ~/gradle/gradle.properties:
// The following properties need to be set (either in ~/gradle/gradle.properties, on the command
// line, or as environment variables, e.g.: ORG_GRADLE_PROJECT_property=value):
//
// sonatypeUsername=nexus_user
// sonatypePassword=nexus_password
//
// To sign with a key ring file:
// signing.keyId=pgp_key_id
// signing.password=pgp_key_password
// signing.secretKeyRingFile=/Users/user/.gnupg/maven_signing.key
//
// To sign with in-memory keys (useful for CI):,
// signingKey=ASCII armored key (begins with -----BEGIN PGP PRIVATE KEY BLOCK-----)
// signingPassword=key password
//
buildscript {
def path = providers
@@ -194,7 +200,7 @@ subprojects {
google()
}
if (!name.startsWith("sample") && name != "filament-tools") {
if (!name.startsWith("sample") && name != "filament-tools" && name != "gradle-plugin") {
apply plugin: 'com.android.library'
android {
-21
View File
@@ -1,21 +0,0 @@
plugins {
id 'groovy-gradle-plugin'
}
gradlePlugin {
plugins {
create("filament-plugin") {
id = "filament-plugin"
implementationClass = "com.google.android.filament.gradle.FilamentPlugin"
}
}
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation "com.google.gradle:osdetector-gradle-plugin:1.7.3"
}
@@ -2121,7 +2121,7 @@ public class View {
*/
public boolean highPrecision = false;
/**
* VSM minimum variance scale, must be positive.
* @deprecated has no effect.
*/
public float minVarianceScale = 0.5f;
/**
@@ -106,8 +106,8 @@ class ModelViewer(
var skyboxCubemap: Texture? = null
private lateinit var displayHelper: DisplayHelper
private lateinit var cameraManipulator: Manipulator
private lateinit var gestureDetector: GestureDetector
private var cameraManipulator: Manipulator? = null
private var gestureDetector: GestureDetector? = null
private var surfaceView: SurfaceView? = null
private var textureView: TextureView? = null
@@ -157,15 +157,13 @@ class ModelViewer(
surfaceView: SurfaceView,
engine: Engine = Engine.create(),
uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK),
manipulator: Manipulator? = null
manipulator: Manipulator? = defaultCameraManipulator(surfaceView.width, surfaceView.height)
) : this(engine, uiHelper) {
cameraManipulator = manipulator ?: Manipulator.Builder()
.targetPosition(kDefaultObjectPosition.x, kDefaultObjectPosition.y, kDefaultObjectPosition.z)
.viewport(surfaceView.width, surfaceView.height)
.build(Manipulator.Mode.ORBIT)
this.surfaceView = surfaceView
gestureDetector = GestureDetector(surfaceView, cameraManipulator)
cameraManipulator = manipulator
cameraManipulator?.let { c ->
gestureDetector = GestureDetector(surfaceView, c)
}
displayHelper = DisplayHelper(surfaceView.context)
uiHelper.renderCallback = SurfaceCallback()
uiHelper.attachTo(surfaceView)
@@ -177,15 +175,14 @@ class ModelViewer(
textureView: TextureView,
engine: Engine = Engine.create(),
uiHelper: UiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK),
manipulator: Manipulator? = null
manipulator: Manipulator? = defaultCameraManipulator(textureView.width, textureView.height)
) : this(engine, uiHelper) {
cameraManipulator = manipulator ?: Manipulator.Builder()
.targetPosition(kDefaultObjectPosition.x, kDefaultObjectPosition.y, kDefaultObjectPosition.z)
.viewport(textureView.width, textureView.height)
.build(Manipulator.Mode.ORBIT)
cameraManipulator = manipulator
this.textureView = textureView
gestureDetector = GestureDetector(textureView, cameraManipulator)
cameraManipulator = manipulator
cameraManipulator?.let { c ->
gestureDetector = GestureDetector(textureView, c)
}
displayHelper = DisplayHelper(textureView.context)
uiHelper.renderCallback = SurfaceCallback()
uiHelper.attachTo(textureView)
@@ -302,11 +299,13 @@ class ModelViewer(
asset?.let { populateScene(it) }
// Extract the camera basis from the helper and push it to the Filament camera.
cameraManipulator.getLookAt(eyePos, target, upward)
camera.lookAt(
cameraManipulator?.let { cm ->
cm.getLookAt(eyePos, target, upward)
camera.lookAt(
eyePos[0], eyePos[1], eyePos[2],
target[0], target[1], target[2],
upward[0], upward[1], upward[2])
}
// Render the scene, unless the renderer wants to skip the frame.
if (renderer.beginFrame(swapChain!!, frameTimeNanos)) {
@@ -398,7 +397,7 @@ class ModelViewer(
* Handles a [MotionEvent] to enable one-finger orbit, two-finger pan, and pinch-to-zoom.
*/
fun onTouchEvent(event: MotionEvent) {
gestureDetector.onTouchEvent(event)
gestureDetector?.onTouchEvent(event)
}
@SuppressWarnings("ClickableViewAccessibility")
@@ -451,7 +450,7 @@ class ModelViewer(
override fun onResized(width: Int, height: Int) {
view.viewport = Viewport(0, 0, width, height)
cameraManipulator.setViewport(width, height)
cameraManipulator?.setViewport(width, height)
updateCameraProjection()
synchronizePendingFrames(engine)
}
@@ -468,5 +467,11 @@ class ModelViewer(
companion object {
private val kDefaultObjectPosition = Float3(0.0f, 0.0f, -4.0f)
private fun defaultCameraManipulator(width: Int, height: Int) : Manipulator {
return Manipulator.Builder()
.targetPosition(kDefaultObjectPosition.x, kDefaultObjectPosition.y, kDefaultObjectPosition.z)
.viewport(width, height)
.build(Manipulator.Mode.ORBIT)
}
}
}
@@ -25,7 +25,7 @@ configured, the corresponding task will be disabled.
```groovy
plugins {
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
filament {
+31
View File
@@ -0,0 +1,31 @@
plugins {
id 'groovy-gradle-plugin'
id 'com.gradle.plugin-publish' version '2.1.0'
}
group = "com.google.android.filament"
version = "0.1.0"
gradlePlugin {
website = "https://github.com/google/filament/tree/main/android/gradle-plugin"
vcsUrl = "https://github.com/google/filament/tree/main/android/gradle-plugin"
plugins {
create("filament-tools") {
id = "com.google.android.filament-tools"
displayName = "Filament Tools Gradle Plugin"
description = "A plugin that helps integrate Filament into Android projects"
tags.addAll('android', 'graphics', 'rendering', 'filament', '3d', 'gltf', 'native')
implementationClass = "com.google.android.filament.gradle.FilamentPlugin"
}
}
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation "com.google.gradle:osdetector-gradle-plugin:1.7.3"
}
@@ -60,9 +60,10 @@ class ToolsLocator {
def classifier =
project.extensions.getByType(com.google.gradle.osdetector.OsDetector).classifier
// If com.google.android.filament.tools-dir is set, we'll use it as the tool's base path.
// If com.google.android.filament.tools-dir is set to a non-empty string, we'll use it as
// the tool's base path.
def toolsDirProp = project.providers.gradleProperty("com.google.android.filament.tools-dir")
if (toolsDirProp.isPresent()) {
if (toolsDirProp.isPresent() && !toolsDirProp.get().trim().isEmpty()) {
def toolsDir = toolsDirProp.get()
def path = OperatingSystem.current().isWindows() ?
"${toolsDir}/bin/${name}.exe" :
+1 -1
View File
@@ -1,5 +1,5 @@
GROUP=com.google.android.filament
VERSION_NAME=1.69.4
VERSION_NAME=1.69.5
POM_DESCRIPTION=Real-time physically based rendering engine for Android.
+7
View File
@@ -94,6 +94,13 @@ afterEvaluate { project ->
}
signing {
def signingKey = findProperty("signingKey")
def signingPassword = findProperty("signingPassword")
if (signingKey && signingPassword) {
println("Signing with in-memory keys")
useInMemoryPgpKeys(signingKey, signingPassword)
}
publishing.publications.all { publication ->
sign publication
}
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
+1 -1
View File
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -26,7 +26,8 @@ tasks.register('copyDamagedHelmetGltf', Copy) {
preBuild.dependsOn copyDamagedHelmetGltf
clean.doFirst {
delete "src/main/assets"
delete "src/main/assets/envs"
delete "src/main/assets/models"
}
android {
@@ -48,6 +49,7 @@ android {
dependencies {
implementation deps.kotlin
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.10.0'
implementation deps.coroutines.core
implementation project(':filament-android')
implementation project(':gltfio-android')
@@ -12,10 +12,11 @@
android:supportsRtl="true"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:theme="@android:style/Theme.NoTitleBar">
android:theme="@style/Theme.Material3.DayNight.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:screenOrientation="fullSensor">
<intent-filter>
@@ -0,0 +1,899 @@
{
"name": "Default Test",
"backends": [
"opengl"
],
"models": {
"DamagedHelmet": "helmet.glb"
},
"presets": [
{
"name": "base",
"models": [
"DamagedHelmet"
],
"rendering": {
"camera": {
"enabled": true,
"horizontalFov": 45.0,
"center": [
0,
0,
0
],
"lookAt": [
0,
0,
-1
]
},
"view": {
"postProcessingEnabled": true,
"dithering": "NONE"
},
"tolerance": {
"maxAbsDiff": 0.1,
"maxFailingPixelsFraction": 0.0
}
}
},
{
"name": "tilted",
"rendering": {
"camera": {
"enabled": true,
"horizontalFov": 45.0,
"center": [
-4,
-2,
-3
],
"lookAt": [
0,
0,
-4
]
}
}
}
],
"tests": [
{
"name": "basic",
"apply_presets": [
"base"
]
},
{
"name": "rotated",
"apply_presets": [
"base",
"tilted"
]
},
{
"name": "ssao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true
}
},
{
"name": "msaa",
"apply_presets": [
"base"
],
"rendering": {
"view.msaa.enabled": true
}
},
{
"name": "bloom",
"apply_presets": [
"base",
"tilted"
],
"rendering": {
"view.bloom.enabled": true
}
},
{
"name": "aa_none",
"apply_presets": [
"base"
],
"rendering": {
"view.antiAliasing": "NONE"
}
},
{
"name": "aa_fxaa",
"apply_presets": [
"base"
],
"rendering": {
"view.antiAliasing": "FXAA"
}
},
{
"name": "dithering_none",
"apply_presets": [
"base"
],
"rendering": {
"view.dithering": "NONE"
}
},
{
"name": "msaa_8",
"apply_presets": [
"base"
],
"rendering": {
"view.msaa.enabled": true,
"view.msaa.sampleCount": 8
}
},
{
"name": "taa_custom",
"apply_presets": [
"base"
],
"rendering": {
"view.taa.enabled": true,
"view.taa.feedback": 0.2,
"view.taa.jitterPattern": "HALTON_23_X16"
}
},
{
"name": "ssao_gtao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.aoType": "GTAO"
}
},
{
"name": "ssao_sao",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.aoType": "SAO"
}
},
{
"name": "ssao_radius_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.radius": 1.0
}
},
{
"name": "ssao_radius_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.radius": 0.1
}
},
{
"name": "ssao_power_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.power": 2.0
}
},
{
"name": "ssao_power_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.power": 0.5
}
},
{
"name": "ssao_bias_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.bias": 0.05
}
},
{
"name": "ssao_resolution_half",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.resolution": 0.5
}
},
{
"name": "ssao_resolution_full",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.resolution": 1.0
}
},
{
"name": "ssao_intensity_high",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.intensity": 2.0
}
},
{
"name": "ssao_bent_normals",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.bentNormals": true
}
},
{
"name": "ssao_quality_ultra",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.quality": "ULTRA"
}
},
{
"name": "ssao_quality_low",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.quality": "LOW"
}
},
{
"name": "ssao_lowPassFilter_ultra",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.lowPassFilter": "ULTRA"
}
},
{
"name": "ssao_upsampling_ultra",
"apply_presets": [
"base"
],
"rendering": {
"view.ssao.enabled": true,
"view.ssao.upsampling": "ULTRA"
}
},
{
"name": "ssr_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true
}
},
{
"name": "ssr_thickness_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.5
}
},
{
"name": "ssr_bias_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.bias": 0.1
}
},
{
"name": "ssr_maxDistance_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.maxDistance": 5.0
}
},
{
"name": "ssr_stride_high",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.stride": 4.0
}
},
{
"name": "ssr_stride_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.stride": 1.0
}
},
{
"name": "ssr_thickness_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.01
}
},
{
"name": "ssr_maxDistance_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.maxDistance": 1.0
}
},
{
"name": "ssr_bias_low",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.bias": 0.001
}
},
{
"name": "ssr_quality_combo",
"apply_presets": [
"base"
],
"rendering": {
"view.screenSpaceReflections.enabled": true,
"view.screenSpaceReflections.thickness": 0.2,
"view.screenSpaceReflections.stride": 2.0
}
},
{
"name": "bloom_levels_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.levels": 8
}
},
{
"name": "bloom_levels_low",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.levels": 4
}
},
{
"name": "bloom_resolution_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.resolution": 1024
}
},
{
"name": "bloom_strength_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.strength": 0.5
}
},
{
"name": "bloom_strength_low",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.strength": 0.01
}
},
{
"name": "bloom_blendMode_interpolate",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.blendMode": "INTERPOLATE"
}
},
{
"name": "bloom_no_threshold",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.threshold": false
}
},
{
"name": "bloom_quality_high",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.quality": "HIGH"
}
},
{
"name": "bloom_lensflare",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true
}
},
{
"name": "bloom_lensflare_no_starburst",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.starburst": false
}
},
{
"name": "bloom_lensflare_chromatic",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.chromaticAberration": 0.05
}
},
{
"name": "bloom_lensflare_ghosts",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.ghostCount": 8
}
},
{
"name": "bloom_lensflare_ghostSpacing",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.ghostSpacing": 0.8
}
},
{
"name": "bloom_halo_thick",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.haloThickness": 0.2
}
},
{
"name": "bloom_halo_radius",
"apply_presets": [
"base"
],
"rendering": {
"view.bloom.enabled": true,
"view.bloom.lensFlare": true,
"view.bloom.haloRadius": 0.5
}
},
{
"name": "dof_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true
}
},
{
"name": "dof_cocScale_high",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.cocScale": 2.0
}
},
{
"name": "dof_cocScale_low",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.cocScale": 0.5
}
},
{
"name": "dof_cocAspectRatio",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.cocAspectRatio": 2.0
}
},
{
"name": "dof_maxApertureDiameter",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.maxApertureDiameter": 0.05
}
},
{
"name": "dof_filter_none",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.filter": "NONE"
}
},
{
"name": "dof_nativeResolution",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.nativeResolution": true
}
},
{
"name": "dof_rings_high",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.foregroundRingCount": 5,
"view.dof.backgroundRingCount": 5
}
},
{
"name": "dof_rings_low",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.foregroundRingCount": 3,
"view.dof.backgroundRingCount": 3
}
},
{
"name": "dof_max_coc",
"apply_presets": [
"base"
],
"rendering": {
"view.dof.enabled": true,
"view.dof.maxForegroundCOC": 16,
"view.dof.maxBackgroundCOC": 16
}
},
{
"name": "fog_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true
}
},
{
"name": "fog_distance",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.distance": 10.0
}
},
{
"name": "fog_cutOffDistance",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.cutOffDistance": 100.0
}
},
{
"name": "fog_maximumOpacity",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.maximumOpacity": 0.5
}
},
{
"name": "fog_height",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.height": 5.0
}
},
{
"name": "fog_heightFalloff",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.heightFalloff": 0.5
}
},
{
"name": "fog_density_high",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.density": 0.5
}
},
{
"name": "fog_inScatteringStart",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.inScatteringStart": 5.0
}
},
{
"name": "fog_inScatteringSize",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.inScatteringSize": 10.0
}
},
{
"name": "fog_fogColorFromIbl",
"apply_presets": [
"base"
],
"rendering": {
"view.fog.enabled": true,
"view.fog.fogColorFromIbl": true
}
},
{
"name": "vignette_basic",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true
}
},
{
"name": "vignette_midPoint",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.midPoint": 0.8
}
},
{
"name": "vignette_roundness_circle",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.roundness": 1.0
}
},
{
"name": "vignette_roundness_rect",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.roundness": 0.0
}
},
{
"name": "vignette_feather_sharp",
"apply_presets": [
"base"
],
"rendering": {
"view.vignette.enabled": true,
"view.vignette.feather": 0.1
}
},
{
"name": "cg_filmic",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "FILMIC"
}
},
{
"name": "cg_aces",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "ACES"
}
},
{
"name": "cg_agx",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "AGX"
}
},
{
"name": "cg_pbr_neutral",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.toneMapping": "PBR_NEUTRAL"
}
},
{
"name": "cg_contrast_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.contrast": 1.5
}
},
{
"name": "cg_saturation_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.saturation": 1.5
}
},
{
"name": "cg_exposure_high",
"apply_presets": [
"base"
],
"rendering": {
"view.colorGrading.exposure": 1.0
}
},
{
"name": "shadow_vsm",
"apply_presets": [
"base"
],
"rendering": {
"view.shadowType": "VSM"
}
},
{
"name": "shadow_vsm_anisotropy",
"apply_presets": [
"base"
],
"rendering": {
"view.shadowType": "VSM",
"view.vsmShadowOptions.anisotropy": 2
}
},
{
"name": "shadow_vsm_highPrecision",
"apply_presets": [
"base"
],
"rendering": {
"view.shadowType": "VSM",
"view.vsmShadowOptions.highPrecision": true
}
}
]
}
@@ -17,36 +17,35 @@
package com.google.android.filament.validation
import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Bundle
import android.text.Html
import android.util.Log
import android.view.Choreographer
import android.view.SurfaceView
import android.view.View
import android.view.WindowManager
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.Spinner
import android.widget.TextView
import com.google.android.filament.utils.KTX1Loader
import com.google.android.filament.utils.ModelViewer
import com.google.android.filament.utils.Utils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.google.android.filament.utils.KTX1Loader
import com.google.android.filament.IndirectLight
import com.google.android.filament.Skybox
import android.graphics.Color
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.nio.ByteBuffer
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.Spinner
import android.widget.AdapterView
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class MainActivity : Activity(), ValidationRunner.Callback {
@@ -62,13 +61,17 @@ class MainActivity : Activity(), ValidationRunner.Callback {
private lateinit var choreographer: Choreographer
private lateinit var modelViewer: ModelViewer
private lateinit var statusTextView: TextView
private lateinit var testResultsHeader: TextView
private lateinit var resultsContainer: LinearLayout
private lateinit var inputManager: ValidationInputManager
private var currentInput: ValidationInputManager.ValidationInput? = null
private lateinit var modeSpinner: Spinner
private lateinit var runButton: Button
private var resultManager: ValidationResultManager? = null
// UI Elements
private lateinit var runButton: Button
private lateinit var loadButton: Button
private lateinit var optionsButton: Button
private var resultManager: ValidationResultManager? = null
private var validationRunner: ValidationRunner? = null
// Frame callback
@@ -89,25 +92,53 @@ class MainActivity : Activity(), ValidationRunner.Callback {
surfaceView.holder.setFixedSize(512, 512)
statusTextView = findViewById(R.id.status_text)
modeSpinner = findViewById(R.id.mode_spinner)
runButton = findViewById(R.id.run_button)
testResultsHeader = findViewById(R.id.test_results_header)
resultsContainer = findViewById(R.id.results_container)
// Setup Spinner
val modes = arrayOf("Run Validation", "Generate Goldens")
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, modes)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
modeSpinner.adapter = adapter
runButton = findViewById(R.id.run_button)
loadButton = findViewById(R.id.load_button)
optionsButton = findViewById(R.id.options_button)
// Setup Run Button
runButton.setOnClickListener {
currentInput?.let { input ->
val generateGoldens = modeSpinner.selectedItemPosition == 1
val newInput = input.copy(generateGoldens = generateGoldens)
startValidation(newInput)
// Always use the generateGoldens flag from the intent/input
startValidation(input)
}
}
// Setup Load Button
loadButton.setOnClickListener {
showLoadDialog()
}
// Setup Options Menu Button
optionsButton.setOnClickListener { view ->
val popup = android.widget.PopupMenu(this, view)
popup.menu.add(0, 1, 0, "Generate Golden")
popup.menu.add(0, 2, 0, "Export Test")
popup.menu.add(0, 3, 0, "Export Result")
popup.menu.add(0, 4, 0, "Test ADB Info")
popup.menu.add(0, 5, 0, "Result ADB Info")
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
1 -> {
currentInput?.let { input ->
val goldenInput = input.copy(generateGoldens = true)
startValidation(goldenInput)
}
}
2 -> exportTestBundleAction()
3 -> exportTestResultsAction()
4 -> showTestAdbInfo()
5 -> showResultAdbInfo()
}
true
}
popup.show()
}
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
choreographer = Choreographer.getInstance()
@@ -120,6 +151,134 @@ class MainActivity : Activity(), ValidationRunner.Callback {
handleIntent()
}
private fun showLoadDialog() {
val exportDir = getExternalFilesDir(null) ?: filesDir
// Filter out result zips (starting with "results_") to only show test bundles
val zips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && !name.startsWith("results_")
}?.sortedByDescending { it.lastModified() } ?: emptyList()
if (zips.isEmpty()) {
AlertDialog.Builder(this)
.setTitle("Load Test")
.setMessage("No test bundles found.")
.setPositiveButton("OK", null)
.show()
return
}
val builder = AlertDialog.Builder(this)
builder.setTitle("Select Test Bundle")
val items = zips.map { it.name }.toTypedArray()
builder.setItems(items) { dialog, which ->
val selectedFile = zips[which]
loadZipBundle(selectedFile)
dialog.dismiss()
}
builder.setNegativeButton("Cancel", null)
builder.show()
}
private fun showTestAdbInfo() {
val exportDir = getExternalFilesDir(null) ?: filesDir
val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder()
message.append("Storage Path: $path<br><br>")
message.append("<b>--- PULL FROM DEVICE ---</b><br>")
if (isInternal) {
message.append("<tt>adb shell \"run-as $packageName cat files/&lt;filename&gt;\" &gt; &lt;filename&gt;</tt><br><br>")
} else {
message.append("<tt>adb pull $path/&lt;filename&gt; .</tt><br><br>")
}
message.append("<b>--- PUSH TO DEVICE ---</b><br>")
if (isInternal) {
message.append("1. <tt>adb push &lt;filename&gt; /sdcard/Download/</tt><br>")
message.append("2. <tt>adb shell \"run-as $packageName cp /sdcard/Download/&lt;filename&gt; files/\"</tt><br>")
} else {
message.append("<tt>adb push &lt;filename&gt; $path/</tt><br>")
}
message.append("<br>Note: Use underscores instead of spaces in &lt;filename&gt;.")
AlertDialog.Builder(this)
.setTitle("Test Bundle ADB Info")
.setMessage(Html.fromHtml(message.toString(), Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton("OK", null)
.show()
}
private fun showResultAdbInfo() {
val exportDir = getExternalFilesDir(null) ?: filesDir
val path = exportDir.absolutePath
val isInternal = path.startsWith(filesDir.absolutePath)
val message = StringBuilder()
message.append("<b>--- PULL RESULTS ---</b><br>")
if (isInternal) {
message.append("<tt>adb shell \"run-as $packageName cat files/&lt;filename&gt;\" &gt; &lt;filename&gt;</tt><br><br>")
} else {
message.append("<tt>adb pull $path/&lt;filename&gt; .</tt><br><br>")
}
message.append("<b>--- AVAILABLE RESULTS ---</b><br>")
val zips = exportDir.listFiles { _, name ->
name.endsWith(".zip") && name.startsWith("results_")
}?.sortedByDescending { it.lastModified() } ?: emptyList()
if (zips.isEmpty()) {
message.append("No result zips found.<br>")
} else {
zips.forEach { file ->
message.append("${file.name}<br>")
}
}
AlertDialog.Builder(this)
.setTitle("Result ADB Info")
.setMessage(Html.fromHtml(message.toString(), Html.FROM_HTML_MODE_LEGACY))
.setPositiveButton("OK", null)
.show()
}
private fun loadZipBundle(file: File) {
statusTextView.text = "Loading ${file.name}..."
CoroutineScope(Dispatchers.Main).launch {
try {
val config = inputManager.loadFromZip(file)
val baseDir = getExternalFilesDir(null) ?: filesDir
val outputDir = File(baseDir, "validation_results").apply { mkdirs() }
// Clear existing results UI and state
resultsContainer.removeAllViews()
resultManager = null
val newInput = ValidationInputManager.ValidationInput(
config = config,
outputDir = outputDir,
generateGoldens = false,
autoRun = false,
autoExport = false,
autoExportResults = false,
sourceZip = file
)
currentInput = newInput
statusTextView.text = "Loaded ${config.name}"
Log.i(TAG, "Setting header to: Test Results: ${config.name}")
testResultsHeader.text = "${config.name}"
} catch (e: Exception) {
Log.e(TAG, "Failed to load zip", e)
statusTextView.text = "Error: ${e.message}"
}
}
}
private fun createIndirectLight() {
try {
val engine = modelViewer.engine
@@ -156,12 +315,18 @@ class MainActivity : Activity(), ValidationRunner.Callback {
CoroutineScope(Dispatchers.Main).launch {
try {
val input = inputManager.resolveConfig(intent)
// Update header
Log.i(TAG, "handleIntent: Setting header to: Test Results: ${input.config.name}")
testResultsHeader.text = "${input.config.name}"
currentInput = input
// Sync spinner with intent
modeSpinner.setSelection(if (input.generateGoldens) 1 else 0)
startValidation(input)
if (input.autoRun) {
startValidation(input)
} else {
// Just show status
statusTextView.text = "Ready: ${input.config.name}"
}
} catch (e: Exception) {
Log.e(TAG, "Failed to resolve config", e)
statusTextView.text = "Error: ${e.message}"
@@ -175,6 +340,8 @@ class MainActivity : Activity(), ValidationRunner.Callback {
Log.i(TAG, "Starting validation with config: ${input.config.name}")
Log.i(TAG, "Output dir: ${input.outputDir.absolutePath}")
testResultsHeader.text = "${input.config.name}"
resultManager = ValidationResultManager(input.outputDir)
validationRunner = ValidationRunner(this, modelViewer, input.config, resultManager!!)
@@ -182,9 +349,6 @@ class MainActivity : Activity(), ValidationRunner.Callback {
validationRunner?.generateGoldens = input.generateGoldens
validationRunner?.start()
// Sync spinner in case it was called programmatically or changed implicitly
modeSpinner.setSelection(if (input.generateGoldens) 1 else 0)
} catch (e: Exception) {
Log.e(TAG, "Failed to start validation", e)
statusTextView.text = "Error: ${e.message}"
@@ -196,6 +360,12 @@ class MainActivity : Activity(), ValidationRunner.Callback {
choreographer.postFrameCallback(frameScheduler)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleIntent()
}
override fun onPause() {
super.onPause()
choreographer.removeFrameCallback(frameScheduler)
@@ -221,13 +391,30 @@ class MainActivity : Activity(), ValidationRunner.Callback {
resultContainer.orientation = LinearLayout.VERTICAL
resultContainer.setPadding(0, 10, 0, 20)
// Header
val resultView = TextView(this)
resultView.text = "${result.testName}: ${if(result.passed) "PASS" else "FAIL"} (Diff: ${result.diffMetric})"
resultView.setTextColor(if(result.passed) Color.GREEN else Color.RED)
resultView.textSize = 16f
resultView.setTypeface(null, android.graphics.Typeface.BOLD)
resultContainer.addView(resultView)
// Header Layout
val headerRow = LinearLayout(this)
headerRow.orientation = LinearLayout.HORIZONTAL
headerRow.gravity = android.view.Gravity.CENTER_VERTICAL
// Status Icon + Name
val statusView = TextView(this)
val icon = if (result.passed) "✔" else "✖"
statusView.text = "$icon ${result.testName}"
statusView.setTextColor(
if (result.passed) Color.parseColor("#4CAF50") else Color.parseColor("#F44336")
)
statusView.textSize = 12f
statusView.setTypeface(null, android.graphics.Typeface.BOLD)
headerRow.addView(statusView)
// Diff Metric (only show if it's relevant/there's a diff or we just always show it like before)
val diffView = TextView(this)
diffView.text = " (Diff: ${result.diffMetric})"
diffView.textSize = 12f
diffView.setTextColor(Color.GRAY)
headerRow.addView(diffView)
resultContainer.addView(headerRow)
// Images Row
val imagesRow = LinearLayout(this)
@@ -241,7 +428,7 @@ class MainActivity : Activity(), ValidationRunner.Callback {
val labelView = TextView(this)
labelView.text = label
labelView.textSize = 12f
labelView.textSize = 9f
container.addView(labelView)
val iv = ImageView(this)
@@ -274,7 +461,46 @@ class MainActivity : Activity(), ValidationRunner.Callback {
override fun onAllTestsFinished() {
runOnUiThread {
statusTextView.text = "All tests finished!"
Log.i(TAG, "All tests finished")
Log.i(TAG, "All tests finished " + if (currentInput?.autoExport == true) "Exporting bundle" else "x")
if (currentInput?.autoExport == true) {
exportTestBundleAction()
}
if (currentInput?.autoExportResults == true) {
exportTestResultsAction()
}
}
}
private fun exportTestBundleAction() {
currentInput?.let { input ->
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: ValidationResultManager(input.outputDir)
val zip = rm.exportTestBundle(input.config, timestamp)
if (zip != null) {
val msg = "Exported Bundle: ${zip.name}"
statusTextView.text = msg
Log.i(TAG, "Exported test bundle to ${zip.absolutePath}")
} else {
statusTextView.text = "Export Bundle failed"
Log.e(TAG, "Export Bundle failed")
}
}
}
private fun exportTestResultsAction() {
currentInput?.let { input ->
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val rm = resultManager ?: ValidationResultManager(input.outputDir)
val zip = rm.exportTestResults(input.sourceZip, timestamp)
if (zip != null) {
val msg = "Exported Results: ${zip.name}"
statusTextView.text = msg
Log.i(TAG, "Exported results to ${zip.absolutePath}")
} else {
statusTextView.text = "Export Results failed"
Log.e(TAG, "Export Results failed")
}
}
}
@@ -301,50 +527,3 @@ class MainActivity : Activity(), ValidationRunner.Callback {
}
}
}
/*
* Scripts for reference:
*
* generate_goldens.sh:
* --------------------
* #!/bin/bash
* set -e
*
* # Config path (on device)
* CONFIG_PATH=$1
* if [ -z "$CONFIG_PATH" ]; then
* echo "Usage: $0 <device_config_path>"
* echo "Example: $0 /sdcard/Android/data/com.google.android.filament.validation/files/default_test.json"
* exit 1
* fi
*
* echo "Starting Golden Generation for $CONFIG_PATH..."
* adb shell am force-stop com.google.android.filament.validation
* adb shell am start -n com.google.android.filament.validation/.MainActivity \
* -e test_config "$CONFIG_PATH" \
* --ez generate_goldens true
*
* echo "Check device or logcat for progress."
* echo "adb logcat -s FilamentValidation:I ValidationRunner:I"
* echo "To pull results: ./samples/sample-render-validation/pull_goldens.sh"
*
* pull_goldens.sh:
* ----------------
* #!/bin/bash
* set -e
*
* # Default destination is local golden directory relative to script
* SCRIPT_DIR=$(cd $(dirname $0); pwd)
* DEST_DIR=${1:-"$SCRIPT_DIR/golden"}
*
* echo "Pulling goldens to $DEST_DIR..."
* mkdir -p "$DEST_DIR"
*
* # Path on device
* DEVICE_GOLDEN_DIR="/storage/emulated/0/Android/data/com.google.android.filament.validation/files/golden/."
*
* adb pull "$DEVICE_GOLDEN_DIR" "$DEST_DIR"
*
* echo "Done."
* ls -l "$DEST_DIR"
*/
@@ -26,6 +26,7 @@ import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.zip.ZipFile
/**
* Handles the retrieval and preparation of test configuration and assets.
@@ -42,7 +43,11 @@ class ValidationInputManager(private val context: Context) {
data class ValidationInput(
val config: RenderTestConfig,
val outputDir: File,
val generateGoldens: Boolean
val generateGoldens: Boolean,
val autoRun: Boolean = false,
val autoExport: Boolean = false,
val autoExportResults: Boolean = false,
val sourceZip: File? = null
)
/**
@@ -53,22 +58,111 @@ class ValidationInputManager(private val context: Context) {
val testConfigPath = intent.getStringExtra("test_config")
val urlConfig = intent.getStringExtra("url_config")
val urlModelsBase = intent.getStringExtra("url_models_base")
val zipPath = intent.getStringExtra("zip_path")
val generateGoldens = intent.getBooleanExtra("generate_goldens", false)
val autoRun = intent.getBooleanExtra("auto_run", false)
val autoExport = intent.getBooleanExtra("auto_export", false)
val autoExportResults = intent.getBooleanExtra("auto_export_results", false)
val outputPath = intent.getStringExtra("output_path")
val outputDir = if (outputPath != null) {
File(outputPath).apply { mkdirs() }
Log.i(TAG, "Resolving config with outputPath: $outputPath")
val baseDir = context.getExternalFilesDir(null) ?: context.filesDir
Log.i(TAG, "Base directory for resolution: ${baseDir.absolutePath}")
val outputDir = if (!outputPath.isNullOrBlank()) {
val file = File(outputPath)
val resolved = if (file.isAbsolute) {
file
} else {
File(baseDir, outputPath)
}
// Critical check: if resolved is root or very short, it's likely wrong
if (resolved.absolutePath == "/" || resolved.parent == null) {
Log.w(TAG, "Resolved outputDir is root ($resolved), defaulting to app-specific dir")
File(baseDir, "validation_results")
} else {
resolved
}
} else {
File(context.getExternalFilesDir(null), "validation_results").apply { mkdirs() }
File(baseDir, "validation_results")
}
if (!outputDir.exists() && !outputDir.mkdirs()) {
Log.e(TAG, "Failed to create outputDir: ${outputDir.absolutePath}")
}
Log.i(TAG, "Final outputDir: ${outputDir.absolutePath}")
val sourceZipFile = if (zipPath != null) {
val file = File(zipPath)
if (file.isAbsolute) {
file
} else {
File(baseDir, zipPath)
}
} else {
null
}
val config = when {
sourceZipFile != null && sourceZipFile.exists() -> loadFromZip(sourceZipFile)
urlConfig != null -> downloadConfig(urlConfig, urlModelsBase)
testConfigPath != null -> ConfigParser.parseFromPath(testConfigPath)
else -> extractDefaultAssets()
}
return@withContext ValidationInput(config, outputDir, generateGoldens)
return@withContext ValidationInput(config, outputDir, generateGoldens, autoRun, autoExport, autoExportResults, sourceZipFile)
}
suspend fun loadFromZip(zipFile: File): RenderTestConfig {
Log.i(TAG, "Unzipping validation bundle: ${zipFile.absolutePath}")
// Use a unique cache dir based on timestamp or just overwrite a common one
// Overwriting is safer to avoid filling up disk, but we must ensure we don't conflict with current run
val baseCacheDir = context.externalCacheDir ?: context.cacheDir
val cacheDir = File(baseCacheDir, "unzipped_validation")
if (cacheDir.exists()) cacheDir.deleteRecursively()
cacheDir.mkdirs()
Log.i(TAG, "Using cacheDir: ${cacheDir.absolutePath}")
ZipFile(zipFile).use { zip ->
val entries = zip.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
val entryFile = File(cacheDir, entry.name)
// specific check to avoid zip slip vulnerability (though low risk here)
if (!entryFile.canonicalPath.startsWith(cacheDir.canonicalPath)) {
throw SecurityException("Zip entry is outside of the target dir: ${entry.name}")
}
if (entry.isDirectory) {
entryFile.mkdirs()
} else {
entryFile.parentFile?.mkdirs()
zip.getInputStream(entry).use { input ->
FileOutputStream(entryFile).use { output ->
input.copyTo(output)
}
}
}
}
}
// Find config.json
// We look for a file ending in .json within the unzipped structure
// Exclude results.json if it happened to be there
val jsonFiles = cacheDir.walkTopDown()
.filter { it.isFile && it.extension == "json" && it.name != "results.json" }
.toList()
if (jsonFiles.isEmpty()) throw IllegalStateException("No config.json found in zip")
// Prefer one named config.json or take the first one
val configFile = jsonFiles.find { it.name == "config.json" } ?: jsonFiles.first()
Log.i(TAG, "Parsed config from ${configFile.absolutePath}")
return ConfigParser.parseFromPath(configFile.absolutePath)
}
private suspend fun extractDefaultAssets(): RenderTestConfig {
@@ -90,9 +184,9 @@ class ValidationInputManager(private val context: Context) {
// Copy DamagedHelmet.glb
val modelsDir = File(filesDir, "models")
modelsDir.mkdirs()
val modelOut = File(modelsDir, "DamagedHelmet.glb")
val modelOut = File(modelsDir, "helmet.glb")
assetManager.open("DamagedHelmet.glb").use { input ->
assetManager.open("models/helmet.glb").use { input ->
FileOutputStream(modelOut).use { output ->
input.copyTo(output)
}
@@ -67,22 +67,201 @@ class ValidationResultManager(private val outputDir: File) {
fun finalizeResults(): File? {
// Write results JSON
writeResultsJson()
return null
}
/**
* Exports a zip containing:
* - results.json
* - input test bundle (as nested zip), if provided
* - diff images (if any failure)
*/
fun exportTestResults(sourceZip: File?, timestamp: String): File? {
// Safe parent dir resolution
val parentDir = outputDir.canonicalFile.parentFile ?: outputDir.parentFile
if (parentDir == null) return null
val resultZipName = "results_$timestamp"
val zipFile = File(parentDir, "$resultZipName.zip")
Log.i(TAG, "Exporting results to ${zipFile.absolutePath}")
// Zip results
val zipFile = File(outputDir, "results.zip")
try {
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
outputDir.walkTopDown().filter { it.isFile && it.name != "results.zip" }.forEach { file ->
val entryName = file.relativeTo(outputDir).path
zos.putNextEntry(ZipEntry(entryName))
file.inputStream().use { it.copyTo(zos) }
// 1. Add results.json
val resultsJson = File(outputDir, "results.json")
if (resultsJson.exists()) {
zos.putNextEntry(ZipEntry("results.json"))
resultsJson.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
// 2. Add source zip if exists
if (sourceZip != null && sourceZip.exists()) {
zos.putNextEntry(ZipEntry(sourceZip.name))
sourceZip.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
// 3. Add diff images (any file ending in _diff.png in outputDir)
outputDir.listFiles { _, name -> name.endsWith("_diff.png") }?.forEach { diffFile ->
zos.putNextEntry(ZipEntry(diffFile.name))
diffFile.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
Log.i(TAG, "Zipped results to ${zipFile.absolutePath}")
Log.i(TAG, "Exported results to ${zipFile.absolutePath}")
return zipFile
} catch (e: Exception) {
Log.e(TAG, "Failed to zip results", e)
Log.e(TAG, "Failed to export results", e)
return null
}
}
/**
* Exports a zip bundle containing:
* - Modified config.json (with updated name and relative paths)
* - Models (in models/ subdirectory)
* - Golden images (in goldens/ subdirectory)
*
* Structure:
* test_name_TIMESTAMP/
* config.json
* models/
* model.glb
* goldens/
* test_result.png
*/
fun exportTestBundle(config: RenderTestConfig, timestamp: String): File? {
Log.i(TAG, "Starting exportTestBundle for ${config.name} at $timestamp")
Log.i(TAG, "OutputDir: ${outputDir.absolutePath}")
val parentDir = outputDir.canonicalFile.parentFile ?: outputDir.parentFile
if (parentDir == null) {
Log.e(TAG, "OutputDir parent is null: ${outputDir.absolutePath}")
return null
}
Log.i(TAG, "Using parentDir for export: ${parentDir.absolutePath}")
val testNameWithTimestamp = "${config.name}_$timestamp"
val exportNameNoSpaces = testNameWithTimestamp.replace(" ", "_")
val exportDir = File(parentDir, "export_temp_$timestamp")
Log.i(TAG, "Creating export temp dir: ${exportDir.absolutePath}")
if (exportDir.exists()) exportDir.deleteRecursively()
if (!exportDir.mkdirs()) {
Log.e(TAG, "Failed to create export dir: ${exportDir.absolutePath}")
return null
}
val rootDir = File(exportDir, exportNameNoSpaces)
rootDir.mkdirs()
val modelsDir = File(rootDir, "models")
modelsDir.mkdirs()
val goldensDir = File(rootDir, "goldens")
goldensDir.mkdirs()
try {
// 1. Copy Models and update config map
val newModelsMap = mutableMapOf<String, String>()
Log.i(TAG, "Copying models...")
for ((modelName, modelPath) in config.models) {
val sourceFile = File(modelPath)
if (sourceFile.exists()) {
val destFile = File(modelsDir, sourceFile.name)
Log.d(TAG, "Copying model $modelName: $modelPath -> ${destFile.name}")
sourceFile.copyTo(destFile, overwrite = true)
// Use relative path for the new config
newModelsMap[modelName] = "models/${sourceFile.name}"
} else {
Log.w(TAG, "Model file not found: $modelPath")
}
}
// 2. Copy Golden Images
// We assume goldens are in outputDir with .png extension
Log.i(TAG, "Copying goldens from ${outputDir.absolutePath}...")
outputDir.listFiles { _, name -> name.endsWith(".png") }?.forEach { file ->
Log.d(TAG, "Copying golden: ${file.name}")
file.copyTo(File(goldensDir, file.name), overwrite = true)
}
// 3. Create modified config JSON
Log.i(TAG, "Creating config.json...")
val newConfigJson = JSONObject()
newConfigJson.put("name", testNameWithTimestamp) // Keep spaces in JSON name
// Reconstruct backends
val backendsArray = JSONArray()
config.backends.forEach { backendsArray.put(it) }
newConfigJson.put("backends", backendsArray)
// Reconstruct models
val modelsJson = JSONObject()
for ((k, v) in newModelsMap) {
modelsJson.put(k, v)
}
newConfigJson.put("models", modelsJson)
// Reconstruct tests
val testsArray = JSONArray()
for (test in config.tests) {
val testJson = JSONObject()
testJson.put("name", test.name)
if (test.description != null) testJson.put("description", test.description)
// Models for this test (set of strings)
val testModelsArray = JSONArray()
test.models.forEach { testModelsArray.put(it) }
testJson.put("models", testModelsArray)
// Rendering settings
testJson.put("rendering", test.rendering)
// Tolerance
if (test.tolerance != null) {
testJson.put("tolerance", test.tolerance)
}
// Backends (optional override)
val testBackends = JSONArray()
test.backends.forEach { testBackends.put(it) }
testJson.put("backends", testBackends)
testsArray.put(testJson)
}
newConfigJson.put("tests", testsArray)
// Write config.json
File(rootDir, "config.json").writeText(newConfigJson.toString(4))
// 4. Zip it
val zipFile = File(parentDir, "$exportNameNoSpaces.zip")
Log.i(TAG, "Zipping to ${zipFile.absolutePath}...")
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
rootDir.walkTopDown().forEach { file ->
if (file.isFile) {
val entryName = file.relativeTo(exportDir).path
zos.putNextEntry(ZipEntry(entryName))
file.inputStream().use { it.copyTo(zos) }
zos.closeEntry()
}
}
}
// Cleanup temp dir
exportDir.deleteRecursively()
Log.i(TAG, "Exported test bundle to ${zipFile.absolutePath}")
return zipFile
} catch (e: Exception) {
Log.e(TAG, "Failed to export test bundle", e)
exportDir.deleteRecursively()
return null
}
}
@@ -41,18 +41,13 @@ class ValidationRunner(
private var currentTestConfig: TestConfig? = null
private var currentModelName: String? = null
private var loadStartFence: com.google.android.filament.Fence? = null
private var loadStartTime = 0L
private var frameCounter = 0
enum class State {
IDLE,
LOADING_MODEL,
WAITING_FOR_FENCE,
WAITING_FOR_RESOURCES,
WARMUP,
RUNNING_TEST,
COMPARING
RUNNING_TEST
}
interface Callback {
@@ -93,8 +88,6 @@ class ValidationRunner(
nextModel()
return
}
currentState = State.LOADING_MODEL
callback?.onStatusChanged("Loading $modelName for ${currentTestConfig?.name}")
// Load model on main thread (required by ModelViewer)
@@ -110,51 +103,24 @@ class ValidationRunner(
Log.i("ValidationRunner", "Loading GLB buffer... (${bytes.size} bytes)")
val buffer = ByteBuffer.wrap(bytes)
modelViewer.loadModelGlb(buffer)
Log.i("ValidationRunner", "Model loaded. initializing fence.")
Log.i("ValidationRunner", "Model loaded.")
modelViewer.transformToUnitCube()
loadStartFence = modelViewer.engine.createFence()
loadStartTime = System.nanoTime()
currentState = State.WAITING_FOR_FENCE
frameCounter = 0 // Reset for fence timeout tracking
Log.i("ValidationRunner", "State set to WAITING_FOR_FENCE")
currentState = State.WAITING_FOR_RESOURCES
frameCounter = 0
Log.i("ValidationRunner", "State set to WAITING_FOR_RESOURCES")
} catch (e: Exception) {
Log.e("ValidationRunner", "Failed to load $path", e)
nextModel()
Log.e("ValidationRunner", "Failed to load $path", e)
nextModel()
}
}
fun onFrame(frameTimeNanos: Long) {
if (frameCounter % 60 == 0) {
Log.i("ValidationRunner", "onFrame: $currentState (frame: $frameCounter)")
Log.i("ValidationRunner", "onFrame: $currentState (frame: $frameCounter)")
}
when (currentState) {
State.IDLE -> {}
State.WAITING_FOR_FENCE -> {
frameCounter++
if (frameCounter > 600) {
Log.w("ValidationRunner", "Fence timed out after 600 frames! Forcing proceed.")
modelViewer.engine.destroyFence(loadStartFence!!)
loadStartFence = null
currentState = State.WAITING_FOR_RESOURCES
return
}
loadStartFence?.let { fence ->
if (fence.wait(com.google.android.filament.Fence.Mode.FLUSH, 0) ==
com.google.android.filament.Fence.FenceStatus.CONDITION_SATISFIED) {
modelViewer.engine.destroyFence(fence)
loadStartFence = null
// Compile materials (simplified)
modelViewer.scene.forEach { entity ->
// ... existing material compilation logic ...
}
currentState = State.WAITING_FOR_RESOURCES
}
}
}
State.WAITING_FOR_RESOURCES -> {
val progress = modelViewer.progress
if (progress >= 1.0f) {
@@ -166,12 +132,12 @@ class ValidationRunner(
State.WARMUP -> {
frameCounter++
if (frameCounter > 5) { // 5 frames warmup
startAutomation()
startAutomation()
}
}
State.RUNNING_TEST -> {
// Log.i("ValidationRunner", "Running test...")
currentEngine?.let { engine ->
// Log.i("ValidationRunner", "Running test...")
currentEngine?.let { engine ->
val content = AutomationEngine.ViewerContent()
content.view = modelViewer.view
content.renderer = modelViewer.renderer
@@ -186,13 +152,11 @@ class ValidationRunner(
if (engine.shouldClose()) {
Log.i("ValidationRunner", "Finishing test (frames: $frameCounter)")
// Test finished (for this spec)
currentState = State.COMPARING
currentState = State.IDLE
captureAndCompare()
}
}
}
State.COMPARING -> {} // Busy
State.LOADING_MODEL -> {}
}
}
@@ -233,7 +197,36 @@ class ValidationRunner(
// Golden path
val modelFile = File(config.models.get(modelName)!!)
val goldenFile = modelFile.parentFile!!.parentFile!!.resolve("golden/${testFullName}.png")
val modelParent = modelFile.parentFile!!
// Search for golden in:
// 1. ../golden/ (standard structure)
// 2. ../goldens/ (exported structure, sibling of models)
// 3. ./goldens/ (backup)
val searchPaths = mutableListOf<File>()
modelParent.parentFile?.let {
searchPaths.add(it.resolve("golden"))
searchPaths.add(it.resolve("goldens"))
}
searchPaths.add(modelParent.resolve("goldens"))
var goldenFile: File? = null
for (path in searchPaths) {
val f = path.resolve("${testFullName}.png")
if (f.exists()) {
goldenFile = f
break
}
}
if (goldenFile != null) {
Log.i("ValidationRunner", "Found golden at ${goldenFile.absolutePath}")
} else {
Log.w("ValidationRunner", "Golden not found for $testFullName. Searched in: ${searchPaths.joinToString { it.absolutePath }}")
// Fallback to old behavior for reference if everything else fails
goldenFile = modelParent.parentFile?.resolve("golden/${testFullName}.png") ?: File("nonexistent")
}
Thread {
try {
@@ -245,14 +238,15 @@ class ValidationRunner(
var diffMetric = 0f
if (generateGoldens) {
goldenFile.parentFile?.mkdirs()
FileOutputStream(goldenFile).use { out ->
val targetGolden = goldenFile ?: modelParent.parentFile?.resolve("golden/${testFullName}.png") ?: File(modelParent, "golden/${testFullName}.png")
targetGolden.parentFile?.mkdirs()
FileOutputStream(targetGolden).use { out ->
flipped.compress(Bitmap.CompressFormat.PNG, 100, out)
}
passed = true // Generating goldens always passes if successful
callback?.onStatusChanged("Golden generated")
} else {
if (goldenFile.exists()) {
if (goldenFile != null && goldenFile.exists()) {
val golden = android.graphics.BitmapFactory.decodeFile(goldenFile.absolutePath)
if (golden != null) {
callback?.onImageResult("Golden", golden)
@@ -273,7 +267,7 @@ class ValidationRunner(
callback?.onStatusChanged("Failed to load golden")
}
} else {
Log.w("ValidationRunner", "Golden not found: ${goldenFile.absolutePath}")
Log.w("ValidationRunner", "Golden not found: ${goldenFile?.absolutePath}")
callback?.onStatusChanged("Golden not found")
}
}
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -10,6 +10,8 @@
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintWidth_percent="0.6"
android:layout_marginTop="32dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
@@ -20,10 +22,83 @@
android:layout_height="match_parent" />
</FrameLayout>
<LinearLayout
android:id="@+id/controls_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingTop="20dp"
android:paddingBottom="0dp"
app:layout_constraintTop_toBottomOf="@id/surface_container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Initializing..."
android:textSize="12sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingBottom="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/run_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/Widget.Material3.Button"
android:text="Run Test" />
<com.google.android.material.button.MaterialButton
android:id="@+id/load_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.TonalButton"
android:text="Load Test"
android:layout_marginStart="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/options_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.IconButton.Outlined"
app:icon="@android:drawable/ic_menu_more"
android:contentDescription="More Options"
android:layout_marginStart="8dp"/>
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/test_results_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Test Results"
android:gravity="center"
android:textSize="14sp"
android:paddingTop="0dp"
android:paddingBottom="8dp" />
</LinearLayout>
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/surface_container"
app:layout_constraintTop_toBottomOf="@id/controls_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
@@ -32,41 +107,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/status_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Initializing..."
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<Spinner
android:id="@+id/mode_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/run_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Run" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Test Results"
android:textSize="18sp"
android:paddingTop="20dp"
android:paddingBottom="10dp" />
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingBottom="20dp">
<LinearLayout
android:id="@+id/results_container"
@@ -74,7 +117,6 @@
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</ScrollView>
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
@@ -1,7 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'filament-plugin'
id 'com.google.android.filament-tools'
}
project.ext.isSample = true
+5
View File
@@ -1,3 +1,8 @@
// Filament tools plugin
pluginManagement {
includeBuild 'gradle-plugin'
}
// Libraries
include ':filament-android'
include ':filamat-android'
+6 -6
View File
@@ -235,9 +235,9 @@
"license": "MIT"
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -429,9 +429,9 @@
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"requires": {
"brace-expansion": "^1.1.7"
}
+2 -2
View File
@@ -181,7 +181,7 @@ important for <code>matc</code> (material compiler).</p>
}
dependencies {
implementation 'com.google.android.filament:filament-android:1.69.2'
implementation 'com.google.android.filament:filament-android:1.69.5'
}
</code></pre>
<p>Here are all the libraries available in the group <code>com.google.android.filament</code>:</p>
@@ -195,7 +195,7 @@ dependencies {
</div>
<h3 id="ios"><a class="header" href="#ios">iOS</a></h3>
<p>iOS projects can use CocoaPods to install the latest release:</p>
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.69.2'
<pre><code class="language-shell">pod 'Filament', '~&gt; 1.69.5'
</code></pre>
<h2 id="documentation"><a class="header" href="#documentation">Documentation</a></h2>
<ul>
+23 -1
View File
@@ -225,9 +225,10 @@ into <strong>branch</strong> of <code>filament-assets</code>. This branch is pai
<code>filament</code> repo.</p>
<p>As an example, imagine I am working on a PR, and I've uploaded my change, which is in a
branch called <code>my-pr-branch</code>, to <code>filament</code>. This PR requires updating the golden. We would do
it in the following fashion</p>
it in the following fashion on a macOS machine:</p>
<h3 id="using-a-script-to-update-the-golden-repo"><a class="header" href="#using-a-script-to-update-the-golden-repo">Using a script to update the golden repo</a></h3>
<ul>
<li>Make sure you've completed the steps in 'Setting up python'</li>
<li>Run interactive mode in the <code>update_golden.py</code> script.
<pre><code>python3 test/renderdiff/src/update_golden.py
</code></pre>
@@ -263,6 +264,27 @@ branch of the golden repo (i.e. <code>my-pr-branch-golden</code>).</li>
<li>If the PR is merged, then there is another workflow that will merge <code>my-pr-branch-golden</code>
to the <code>main</code> branch of the golden repo.</li>
</ul>
<h3 id="automated-update-via-commit-message"><a class="header" href="#automated-update-via-commit-message">Automated update via commit message</a></h3>
<p>Alternatively, if you are confident in your changes and want the CI to handle the update
for you, you can use the following tag in your commit message:</p>
<ul>
<li>In the commit message of your working branch on <code>filament</code>, add the following line:
<pre><code>RDIFF_ACCEPT_NEW_GOLDENS
</code></pre>
</li>
</ul>
<p>This has the following effects:</p>
<ul>
<li>The presubmit test <code>test-renderdiff</code> will be bypassed (it will not perform rendering or
comparison).</li>
<li>When the PR is merged, the postsubmit CI will automatically:
<ol>
<li>Build Filament and generate the new images.</li>
<li>Upload them to a temporary branch in <code>filament-assets</code>.</li>
<li>Merge that branch into <code>main</code>.</li>
</ol>
</li>
</ul>
<h2 id="viewing-test-results"><a class="header" href="#viewing-test-results">Viewing test results</a></h2>
<p>We provide a viewer for looking at the result of a test run. The viewer is a webapp that can
be used by pointing your browser to a localhost port. If you input the viewer with a PR or a
+199 -199
View File
@@ -504,9 +504,9 @@ D_{GGX}(h,\alpha) = \frac{\aa}{\pi ( (\NoH)^2 (\aa - 1) + 1)^2}
\end{equation}$$
</p><p>
The GLSL implementation of the NDF, shown in <a href="#listing_speculard">listing&nbsp;1</a>, is simple and efficient.
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-built_in">float</span> <span class="hljs-title">D_GGX</span>(<span class="hljs-params"><span class="hljs-built_in">float</span> NoH, <span class="hljs-built_in">float</span> roughness</span>)</span> {</span>
<span class="line"> <span class="hljs-built_in">float</span> a = NoH * roughness;</span>
<span class="line"> <span class="hljs-built_in">float</span> k = roughness / (<span class="hljs-number">1.0</span> - NoH * NoH + a * a);</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-type">float</span> <span class="hljs-title">D_GGX</span><span class="hljs-params">(<span class="hljs-type">float</span> NoH, <span class="hljs-type">float</span> roughness)</span> </span>{</span>
<span class="line"> <span class="hljs-type">float</span> a = NoH * roughness;</span>
<span class="line"> <span class="hljs-type">float</span> k = roughness / (<span class="hljs-number">1.0</span> - NoH * NoH + a * a);</span>
<span class="line"> <span class="hljs-keyword">return</span> k * k * (<span class="hljs-number">1.0</span> / PI);</span>
<span class="line">}</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_speculard">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;1:</b> Implementation of the specular D term in GLSL</div></center>
<p>
@@ -590,10 +590,10 @@ V(v,l,\alpha) = \frac{0.5}{\NoL \sqrt{(\NoV)^2 (1 - \aa) + \aa} + \NoV \sqrt{(\N
\end{equation}$$
</p><p>
The GLSL implementation of the visibility term, shown in <a href="#listing_specularv">listing&nbsp;3</a>, is a bit more expensive than we would like since it requires two <code>sqrt</code> operations.
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-keyword">float</span> <span class="hljs-title">V_SmithGGXCorrelated</span><span class="hljs-params">(<span class="hljs-keyword">float</span> NoV, <span class="hljs-keyword">float</span> NoL, <span class="hljs-keyword">float</span> roughness)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">float</span> a2 = roughness * roughness;</span>
<span class="line"> <span class="hljs-keyword">float</span> GGXV = NoL * <span class="hljs-built_in">sqrt</span>(NoV * NoV * (<span class="hljs-number">1.0</span> - a2) + a2);</span>
<span class="line"> <span class="hljs-keyword">float</span> GGXL = NoV * <span class="hljs-built_in">sqrt</span>(NoL * NoL * (<span class="hljs-number">1.0</span> - a2) + a2);</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-type">float</span> <span class="hljs-title">V_SmithGGXCorrelated</span><span class="hljs-params">(<span class="hljs-type">float</span> NoV, <span class="hljs-type">float</span> NoL, <span class="hljs-type">float</span> roughness)</span> </span>{</span>
<span class="line"> <span class="hljs-type">float</span> a2 = roughness * roughness;</span>
<span class="line"> <span class="hljs-type">float</span> GGXV = NoL * <span class="hljs-built_in">sqrt</span>(NoV * NoV * (<span class="hljs-number">1.0</span> - a2) + a2);</span>
<span class="line"> <span class="hljs-type">float</span> GGXL = NoV * <span class="hljs-built_in">sqrt</span>(NoL * NoL * (<span class="hljs-number">1.0</span> - a2) + a2);</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-number">0.5</span> / (GGXV + GGXL);</span>
<span class="line">}</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_specularv">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;3:</b> Implementation of the specular V term in GLSL</div></center>
<p>
@@ -604,10 +604,10 @@ V(v,l,\alpha) = \frac{0.5}{\NoL (\NoV (1 - \alpha) + \alpha) + \NoV (\NoL (1 - \
\end{equation}$$
</p><p>
This approximation is mathematically wrong but saves two square root operations and is good enough for real-time mobile applications, as shown in <a href="#listing_approximatedspecularv">listing&nbsp;4</a>.
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-built_in">float</span> <span class="hljs-title">V_SmithGGXCorrelatedFast</span>(<span class="hljs-params"><span class="hljs-built_in">float</span> NoV, <span class="hljs-built_in">float</span> NoL, <span class="hljs-built_in">float</span> roughness</span>)</span> {</span>
<span class="line"> <span class="hljs-built_in">float</span> a = roughness;</span>
<span class="line"> <span class="hljs-built_in">float</span> GGXV = NoL * (NoV * (<span class="hljs-number">1.0</span> - a) + a);</span>
<span class="line"> <span class="hljs-built_in">float</span> GGXL = NoV * (NoL * (<span class="hljs-number">1.0</span> - a) + a);</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-type">float</span> <span class="hljs-title">V_SmithGGXCorrelatedFast</span><span class="hljs-params">(<span class="hljs-type">float</span> NoV, <span class="hljs-type">float</span> NoL, <span class="hljs-type">float</span> roughness)</span> </span>{</span>
<span class="line"> <span class="hljs-type">float</span> a = roughness;</span>
<span class="line"> <span class="hljs-type">float</span> GGXV = NoL * (NoV * (<span class="hljs-number">1.0</span> - a) + a);</span>
<span class="line"> <span class="hljs-type">float</span> GGXL = NoV * (NoL * (<span class="hljs-number">1.0</span> - a) + a);</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-number">0.5</span> / (GGXV + GGXL);</span>
<span class="line">}</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_approximatedspecularv">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;4:</b> Implementation of the approximated specular V term in GLSL</div></center>
<p>
@@ -659,7 +659,7 @@ $$\begin{equation}
\end{equation}$$
</p><p>
In practice, the diffuse reflectance \(\sigma\) is multiplied later, as shown in <a href="#listing_diffusebrdf">listing&nbsp;8</a>.
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-built_in">float</span> <span class="hljs-title">Fd_Lambert</span>(<span class="hljs-params"></span>)</span> {</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-built_in">float</span> <span class="hljs-title">Fd_Lambert</span>()</span> {</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-number">1.0</span> / PI;</span>
<span class="line">}</span>
<span class="line"></span>
@@ -680,14 +680,14 @@ Where:
$$\begin{equation}
\fGrazing=0.5 + 2 \cdot \alpha cos^2(\theta_d)
\end{equation}$$
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-keyword">float</span> <span class="hljs-title">F_Schlick</span><span class="hljs-params">(<span class="hljs-keyword">float</span> u, <span class="hljs-keyword">float</span> f0, <span class="hljs-keyword">float</span> f90)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">return</span> f0 + (f90 - f0) * <span class="hljs-built_in">pow</span>(<span class="hljs-number">1.0</span> - u, <span class="hljs-number">5.0</span>);</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-type">float</span> <span class="hljs-title function_">F_Schlick</span><span class="hljs-params">(<span class="hljs-type">float</span> u, <span class="hljs-type">float</span> f0, <span class="hljs-type">float</span> f90)</span> {</span>
<span class="line"> <span class="hljs-keyword">return</span> f0 + (f90 - f0) * pow(<span class="hljs-number">1.0</span> - u, <span class="hljs-number">5.0</span>);</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-function"><span class="hljs-keyword">float</span> <span class="hljs-title">Fd_Burley</span><span class="hljs-params">(<span class="hljs-keyword">float</span> NoV, <span class="hljs-keyword">float</span> NoL, <span class="hljs-keyword">float</span> LoH, <span class="hljs-keyword">float</span> roughness)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">float</span> f90 = <span class="hljs-number">0.5</span> + <span class="hljs-number">2.0</span> * roughness * LoH * LoH;</span>
<span class="line"> <span class="hljs-keyword">float</span> lightScatter = F_Schlick(NoL, <span class="hljs-number">1.0</span>, f90);</span>
<span class="line"> <span class="hljs-keyword">float</span> viewScatter = F_Schlick(NoV, <span class="hljs-number">1.0</span>, f90);</span>
<span class="line"><span class="hljs-type">float</span> <span class="hljs-title function_">Fd_Burley</span><span class="hljs-params">(<span class="hljs-type">float</span> NoV, <span class="hljs-type">float</span> NoL, <span class="hljs-type">float</span> LoH, <span class="hljs-type">float</span> roughness)</span> {</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">f90</span> <span class="hljs-operator">=</span> <span class="hljs-number">0.5</span> + <span class="hljs-number">2.0</span> * roughness * LoH * LoH;</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">lightScatter</span> <span class="hljs-operator">=</span> F_Schlick(NoL, <span class="hljs-number">1.0</span>, f90);</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">viewScatter</span> <span class="hljs-operator">=</span> F_Schlick(NoV, <span class="hljs-number">1.0</span>, f90);</span>
<span class="line"> <span class="hljs-keyword">return</span> lightScatter * viewScatter * (<span class="hljs-number">1.0</span> / PI);</span>
<span class="line">}</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_diffusebrdf">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;8:</b> Implementation of the diffuse Disney BRDF in GLSL</div></center>
<p>
@@ -704,47 +704,47 @@ We could allow artists/developers to choose the Disney diffuse BRDF depending on
<strong class="asterisk">Diffuse term</strong>: a Lambertian diffuse model.
</p><p>
The full GLSL implementation of the standard model is shown in <a href="#listing_glslbrdf">listing&nbsp;9</a>.
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-type">float</span> D_GGX(<span class="hljs-type">float</span> NoH, <span class="hljs-type">float</span> a) {</span>
<span class="line"> <span class="hljs-type">float</span> a2 = a * a;</span>
<span class="line"> <span class="hljs-type">float</span> f = (NoH * a2 - NoH) * NoH + <span class="hljs-number">1.0</span>;</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-type">float</span> <span class="hljs-title function_">D_GGX</span><span class="hljs-params">(<span class="hljs-type">float</span> NoH, <span class="hljs-type">float</span> a)</span> {</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">a2</span> <span class="hljs-operator">=</span> a * a;</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">f</span> <span class="hljs-operator">=</span> (NoH * a2 - NoH) * NoH + <span class="hljs-number">1.0</span>;</span>
<span class="line"> <span class="hljs-keyword">return</span> a2 / (PI * f * f);</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-type">vec3</span> F_Schlick(<span class="hljs-type">float</span> u, <span class="hljs-type">vec3</span> f0) {</span>
<span class="line"> <span class="hljs-keyword">return</span> f0 + (<span class="hljs-type">vec3</span>(<span class="hljs-number">1.0</span>) - f0) * <span class="hljs-built_in">pow</span>(<span class="hljs-number">1.0</span> - u, <span class="hljs-number">5.0</span>);</span>
<span class="line">vec3 <span class="hljs-title function_">F_Schlick</span><span class="hljs-params">(<span class="hljs-type">float</span> u, vec3 f0)</span> {</span>
<span class="line"> <span class="hljs-keyword">return</span> f0 + (vec3(<span class="hljs-number">1.0</span>) - f0) * pow(<span class="hljs-number">1.0</span> - u, <span class="hljs-number">5.0</span>);</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-type">float</span> V_SmithGGXCorrelated(<span class="hljs-type">float</span> NoV, <span class="hljs-type">float</span> NoL, <span class="hljs-type">float</span> a) {</span>
<span class="line"> <span class="hljs-type">float</span> a2 = a * a;</span>
<span class="line"> <span class="hljs-type">float</span> GGXL = NoV * <span class="hljs-built_in">sqrt</span>((-NoL * a2 + NoL) * NoL + a2);</span>
<span class="line"> <span class="hljs-type">float</span> GGXV = NoL * <span class="hljs-built_in">sqrt</span>((-NoV * a2 + NoV) * NoV + a2);</span>
<span class="line"><span class="hljs-type">float</span> <span class="hljs-title function_">V_SmithGGXCorrelated</span><span class="hljs-params">(<span class="hljs-type">float</span> NoV, <span class="hljs-type">float</span> NoL, <span class="hljs-type">float</span> a)</span> {</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">a2</span> <span class="hljs-operator">=</span> a * a;</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">GGXL</span> <span class="hljs-operator">=</span> NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">GGXV</span> <span class="hljs-operator">=</span> NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-number">0.5</span> / (GGXV + GGXL);</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-type">float</span> Fd_Lambert() {</span>
<span class="line"><span class="hljs-type">float</span> <span class="hljs-title function_">Fd_Lambert</span><span class="hljs-params">()</span> {</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-number">1.0</span> / PI;</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-type">void</span> BRDF(...) {</span>
<span class="line"> <span class="hljs-type">vec3</span> h = <span class="hljs-built_in">normalize</span>(v + l);</span>
<span class="line"><span class="hljs-keyword">void</span> <span class="hljs-title function_">BRDF</span><span class="hljs-params">(...)</span> {</span>
<span class="line"> <span class="hljs-type">vec3</span> <span class="hljs-variable">h</span> <span class="hljs-operator">=</span> normalize(v + l);</span>
<span class="line"></span>
<span class="line"> <span class="hljs-type">float</span> NoV = <span class="hljs-built_in">abs</span>(<span class="hljs-built_in">dot</span>(n, v)) + <span class="hljs-number">1e-5</span>;</span>
<span class="line"> <span class="hljs-type">float</span> NoL = <span class="hljs-built_in">clamp</span>(<span class="hljs-built_in">dot</span>(n, l), <span class="hljs-number">0.0</span>, <span class="hljs-number">1.0</span>);</span>
<span class="line"> <span class="hljs-type">float</span> NoH = <span class="hljs-built_in">clamp</span>(<span class="hljs-built_in">dot</span>(n, h), <span class="hljs-number">0.0</span>, <span class="hljs-number">1.0</span>);</span>
<span class="line"> <span class="hljs-type">float</span> LoH = <span class="hljs-built_in">clamp</span>(<span class="hljs-built_in">dot</span>(l, h), <span class="hljs-number">0.0</span>, <span class="hljs-number">1.0</span>);</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">NoV</span> <span class="hljs-operator">=</span> abs(dot(n, v)) + <span class="hljs-number">1e-5</span>;</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">NoL</span> <span class="hljs-operator">=</span> clamp(dot(n, l), <span class="hljs-number">0.0</span>, <span class="hljs-number">1.0</span>);</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">NoH</span> <span class="hljs-operator">=</span> clamp(dot(n, h), <span class="hljs-number">0.0</span>, <span class="hljs-number">1.0</span>);</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">LoH</span> <span class="hljs-operator">=</span> clamp(dot(l, h), <span class="hljs-number">0.0</span>, <span class="hljs-number">1.0</span>);</span>
<span class="line"></span>
<span class="line"> <span class="hljs-comment">// perceptually linear roughness to roughness (see parameterization)</span></span>
<span class="line"> <span class="hljs-type">float</span> roughness = perceptualRoughness * perceptualRoughness;</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">roughness</span> <span class="hljs-operator">=</span> perceptualRoughness * perceptualRoughness;</span>
<span class="line"></span>
<span class="line"> <span class="hljs-type">float</span> D = D_GGX(NoH, roughness);</span>
<span class="line"> <span class="hljs-type">vec3</span> F = F_Schlick(LoH, f0);</span>
<span class="line"> <span class="hljs-type">float</span> V = V_SmithGGXCorrelated(NoV, NoL, roughness);</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">D</span> <span class="hljs-operator">=</span> D_GGX(NoH, roughness);</span>
<span class="line"> <span class="hljs-type">vec3</span> <span class="hljs-variable">F</span> <span class="hljs-operator">=</span> F_Schlick(LoH, f0);</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">V</span> <span class="hljs-operator">=</span> V_SmithGGXCorrelated(NoV, NoL, roughness);</span>
<span class="line"></span>
<span class="line"> <span class="hljs-comment">// specular BRDF</span></span>
<span class="line"> <span class="hljs-type">vec3</span> Fr = (D * V) * F;</span>
<span class="line"> <span class="hljs-type">vec3</span> <span class="hljs-variable">Fr</span> <span class="hljs-operator">=</span> (D * V) * F;</span>
<span class="line"></span>
<span class="line"> <span class="hljs-comment">// diffuse BRDF</span></span>
<span class="line"> <span class="hljs-type">vec3</span> Fd = diffuseColor * Fd_Lambert();</span>
<span class="line"> <span class="hljs-type">vec3</span> <span class="hljs-variable">Fd</span> <span class="hljs-operator">=</span> diffuseColor * Fd_Lambert();</span>
<span class="line"></span>
<span class="line"> <span class="hljs-comment">// apply lighting...</span></span>
<span class="line">}</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_glslbrdf">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;9:</b> Evaluation of the BRDF in GLSL</div></center>
@@ -965,7 +965,7 @@ $$\begin{equation}
\end{equation}$$
</p><p>
<a href="#listing_fnormal">Listing&nbsp;12</a> shows how \(\fNormal\) is computed for both dielectric and metallic materials. It shows that the color of the specular reflectance is derived from the base color in the metallic case.
</p><pre class="listing tilde"><code><span class="line">vec3 f0 = 0.16 <span class="hljs-emphasis">* reflectance *</span> reflectance <span class="hljs-emphasis">* (1.0 - metallic) + baseColor *</span> metallic;</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_fnormal">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;12:</b> Computing \(\fNormal\) for dielectric and metallic materials in GLSL</div></center>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-symbol">vec3</span> <span class="hljs-built_in">f0</span> = <span class="hljs-number">0</span>.<span class="hljs-number">16</span> * reflectance * reflectance * (<span class="hljs-number">1</span>.<span class="hljs-number">0</span> - metallic) + baseColor * metallic<span class="hljs-comment">;</span></span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_fnormal">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;12:</b> Computing \(\fNormal\) for dielectric and metallic materials in GLSL</div></center>
<a class="target" name="roughnessremappingandclamping">&nbsp;</a><a class="target" name="materialsystem/parameterization/remapping/roughnessremappingandclamping">&nbsp;</a><a class="target" name="toc4.8.3.3">&nbsp;</a><h4 id="roughness-remapping-and-clamping"><a class="header" href="#roughness-remapping-and-clamping">Roughness remapping and clamping</a></h4>
<p>
<p>The roughness set by the user, called <code>perceptualRoughness</code> here, is remapped to a perceptually linear range using the following formulation:</p>
@@ -1054,7 +1054,7 @@ V(l,h) = \frac{1}{4(\LoH)^2}
This masking-shadowing function is not physically based, as shown in [<a href="#citation-heitz14">Heitz14</a>], but its simplicity makes it desirable for real-time rendering.
</p><p>
In summary, our clear coat BRDF is a Cook-Torrance specular microfacet model, with a GGX normal distribution function, a Kelemen visibility function, and a Schlick Fresnel function. <a href="#listing_kelemen">Listing&nbsp;13</a> shows how trivial the GLSL implementation is.
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-built_in">float</span> <span class="hljs-title">V_Kelemen</span>(<span class="hljs-params"><span class="hljs-built_in">float</span> LoH</span>)</span> {</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-type">float</span> <span class="hljs-title">V_Kelemen</span><span class="hljs-params">(<span class="hljs-type">float</span> LoH)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-number">0.25</span> / (LoH * LoH);</span>
<span class="line">}</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_kelemen">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;13:</b> Implementation of the Kelemen visibility term in GLSL</div></center>
<p>
@@ -1097,18 +1097,18 @@ The clear coat roughness parameter is remapped and clamped in a similar way to t
<center><div class="image" style=""><a href="../images/material_clear_coat2.png" target="_blank"><img class="markdeep" src="../images/material_clear_coat2.png" /></a><center><span class="imagecaption"><a class="target" name="figure_clearcoatroughness">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;26:</b> Clear coat roughness varying from 0.0 (left) to 1.0 (right) with metallic set to 1.0, roughness to 0.8 and clear coat to 1.0</span></center></div></center>
</p><p>
<a href="#listing_clearcoatbrdf">Listing&nbsp;14</a> shows the GLSL implementation of the clear coat material model after remapping, parameterization and integration in the standard surface response.
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">BRDF</span>(<span class="hljs-params">...</span>)</span> {</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">BRDF</span><span class="hljs-params">(...)</span> </span>{</span>
<span class="line"> <span class="hljs-comment">// compute Fd and Fr from standard model</span></span>
<span class="line"></span>
<span class="line"> <span class="hljs-comment">// remapping and linearization of clear coat roughness</span></span>
<span class="line"> clearCoatPerceptualRoughness = clamp(clearCoatPerceptualRoughness, <span class="hljs-number">0.089</span>, <span class="hljs-number">1.0</span>);</span>
<span class="line"> clearCoatPerceptualRoughness = <span class="hljs-built_in">clamp</span>(clearCoatPerceptualRoughness, <span class="hljs-number">0.089</span>, <span class="hljs-number">1.0</span>);</span>
<span class="line"> clearCoatRoughness = clearCoatPerceptualRoughness * clearCoatPerceptualRoughness;</span>
<span class="line"></span>
<span class="line"> <span class="hljs-comment">// clear coat BRDF</span></span>
<span class="line"> <span class="hljs-built_in">float</span> Dc = D_GGX(clearCoatRoughness, NoH);</span>
<span class="line"> <span class="hljs-built_in">float</span> Vc = V_Kelemen(clearCoatRoughness, LoH);</span>
<span class="line"> <span class="hljs-built_in">float</span> Fc = F_Schlick(<span class="hljs-number">0.04</span>, LoH) * clearCoat; <span class="hljs-comment">// clear coat strength</span></span>
<span class="line"> <span class="hljs-built_in">float</span> Frc = (Dc * Vc) * Fc;</span>
<span class="line"> <span class="hljs-type">float</span> Dc = <span class="hljs-built_in">D_GGX</span>(clearCoatRoughness, NoH);</span>
<span class="line"> <span class="hljs-type">float</span> Vc = <span class="hljs-built_in">V_Kelemen</span>(clearCoatRoughness, LoH);</span>
<span class="line"> <span class="hljs-type">float</span> Fc = <span class="hljs-built_in">F_Schlick</span>(<span class="hljs-number">0.04</span>, LoH) * clearCoat; <span class="hljs-comment">// clear coat strength</span></span>
<span class="line"> <span class="hljs-type">float</span> Frc = (Dc * Vc) * Fc;</span>
<span class="line"></span>
<span class="line"> <span class="hljs-comment">// account for energy loss in the base layer</span></span>
<span class="line"> <span class="hljs-keyword">return</span> color * ((Fd + Fr * (<span class="hljs-number">1.0</span> - Fc)) * (<span class="hljs-number">1.0</span> - Fc) + Frc);</span>
@@ -1294,14 +1294,14 @@ f_{r}(v,h,\alpha) = \frac{D_{velvet}(v,h,\alpha)}{4(\NoL + \NoV - (\NoL)(\NoV))}
\end{equation}$$
</p><p>
The implementation of the velvet NDF is presented in <a href="#listing_clothbrdf">listing&nbsp;17</a>, optimized to properly fit in half float formats and to avoid computing a costly cotangent, relying instead on trigonometric identities. Note that we removed the Fresnel component from this BRDF.
</p><pre class="listing tilde"><code><span class="line">float D_Ashikhmin(float roughness, float NoH) {</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-type">float</span> <span class="hljs-title">D_Ashikhmin</span><span class="hljs-params">(<span class="hljs-type">float</span> roughness, <span class="hljs-type">float</span> NoH)</span> </span>{</span>
<span class="line"> <span class="hljs-comment">// Ashikhmin 2007, &quot;Distribution-based BRDFs&quot;</span></span>
<span class="line"> float a<span class="hljs-number">2</span> = roughness * roughness;</span>
<span class="line"> float <span class="hljs-keyword">cos</span><span class="hljs-number">2</span>h = NoH * NoH;</span>
<span class="line"> float <span class="hljs-keyword">sin</span><span class="hljs-number">2</span>h = <span class="hljs-keyword">max</span>(<span class="hljs-number">1.0</span> - <span class="hljs-keyword">cos</span><span class="hljs-number">2</span>h, <span class="hljs-number">0.0078125</span>); <span class="hljs-comment">// 2^(-14/2), so sin2h^2 &gt; 0 in fp16</span></span>
<span class="line"> float <span class="hljs-keyword">sin</span><span class="hljs-number">4</span>h = <span class="hljs-keyword">sin</span><span class="hljs-number">2</span>h * <span class="hljs-keyword">sin</span><span class="hljs-number">2</span>h;</span>
<span class="line"> float cot<span class="hljs-number">2</span> = -<span class="hljs-keyword">cos</span><span class="hljs-number">2</span>h / (a<span class="hljs-number">2</span> * <span class="hljs-keyword">sin</span><span class="hljs-number">2</span>h);</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-number">1.0</span> / (PI * (<span class="hljs-number">4.0</span> * a<span class="hljs-number">2</span> + <span class="hljs-number">1.0</span>) * <span class="hljs-keyword">sin</span><span class="hljs-number">4</span>h) * (<span class="hljs-number">4.0</span> * exp(cot<span class="hljs-number">2</span>) + <span class="hljs-keyword">sin</span><span class="hljs-number">4</span>h);</span>
<span class="line"> <span class="hljs-type">float</span> a2 = roughness * roughness;</span>
<span class="line"> <span class="hljs-type">float</span> cos2h = NoH * NoH;</span>
<span class="line"> <span class="hljs-type">float</span> sin2h = <span class="hljs-built_in">max</span>(<span class="hljs-number">1.0</span> - cos2h, <span class="hljs-number">0.0078125</span>); <span class="hljs-comment">// 2^(-14/2), so sin2h^2 &gt; 0 in fp16</span></span>
<span class="line"> <span class="hljs-type">float</span> sin4h = sin2h * sin2h;</span>
<span class="line"> <span class="hljs-type">float</span> cot2 = -cos2h / (a2 * sin2h);</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-number">1.0</span> / (PI * (<span class="hljs-number">4.0</span> * a2 + <span class="hljs-number">1.0</span>) * sin4h) * (<span class="hljs-number">4.0</span> * <span class="hljs-built_in">exp</span>(cot2) + sin4h);</span>
<span class="line">}</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_clothbrdf">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;17:</b> Implementation of Ashikhmin's velvet NDF in GLSL</div></center>
<p>
<p>In [<a href="#citation-estevez17">Estevez17</a>] Estevez and Kulla propose a different NDF (called the “Charlie” sheen) that is based on an exponentiated sinusoidal instead of an inverted Gaussian. This NDF is appealing for several reasons: its parameterization feels more natural and intuitive, it provides a softer appearance and, as shown in equation (\ref{charlieNDF}), its implementation is simpler:</p>
@@ -1312,11 +1312,11 @@ D(m) = \frac{(2 + \frac{1}{\alpha}) sin(\theta)^{\frac{1}{\alpha}}}{2 \pi}
</p><p>
[<a href="#citation-estevez17">Estevez17</a>] also presents a new shadowing term that we omit here because of its cost. We instead rely on the visibility term from [<a href="#citation-neubelt13">Neubelt13</a>] (shown in equation \(\ref{clothSpecularBRDF}\) above).
The implementation of this NDF is presented in <a href="#listing_clothcharliebrdf">listing&nbsp;18</a>, optimized to properly fit in half float formats.
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-keyword">float</span> <span class="hljs-title">D_Charlie</span><span class="hljs-params">(<span class="hljs-keyword">float</span> roughness, <span class="hljs-keyword">float</span> NoH)</span> </span>{</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-type">float</span> <span class="hljs-title">D_Charlie</span><span class="hljs-params">(<span class="hljs-type">float</span> roughness, <span class="hljs-type">float</span> NoH)</span> </span>{</span>
<span class="line"> <span class="hljs-comment">// Estevez and Kulla 2017, &quot;Production Friendly Microfacet Sheen BRDF&quot;</span></span>
<span class="line"> <span class="hljs-keyword">float</span> invAlpha = <span class="hljs-number">1.0</span> / roughness;</span>
<span class="line"> <span class="hljs-keyword">float</span> cos2h = NoH * NoH;</span>
<span class="line"> <span class="hljs-keyword">float</span> sin2h = max(<span class="hljs-number">1.0</span> - cos2h, <span class="hljs-number">0.0078125</span>); <span class="hljs-comment">// 2^(-14/2), so sin2h^2 &gt; 0 in fp16</span></span>
<span class="line"> <span class="hljs-type">float</span> invAlpha = <span class="hljs-number">1.0</span> / roughness;</span>
<span class="line"> <span class="hljs-type">float</span> cos2h = NoH * NoH;</span>
<span class="line"> <span class="hljs-type">float</span> sin2h = <span class="hljs-built_in">max</span>(<span class="hljs-number">1.0</span> - cos2h, <span class="hljs-number">0.0078125</span>); <span class="hljs-comment">// 2^(-14/2), so sin2h^2 &gt; 0 in fp16</span></span>
<span class="line"> <span class="hljs-keyword">return</span> (<span class="hljs-number">2.0</span> + invAlpha) * <span class="hljs-built_in">pow</span>(sin2h, invAlpha * <span class="hljs-number">0.5</span>) / (<span class="hljs-number">2.0</span> * PI);</span>
<span class="line">}</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_clothcharliebrdf">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;18:</b> Implementation of the &ldquo;Charlie&rdquo; NDF in GLSL</div></center>
<a class="target" name="sheencolor">&nbsp;</a><a class="target" name="materialsystem/clothmodel/clothspecularbrdf/sheencolor">&nbsp;</a><a class="target" name="toc4.12.1.1">&nbsp;</a><h4 id="sheen-color"><a class="header" href="#sheen-color">Sheen color</a></h4>
@@ -1745,21 +1745,21 @@ The photometric attenuation function can be easily implemented in GLSL by adding
<span class="line">}</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_glslphotometricpunctuallight">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;22:</b> Implementation of attenuation from photometric profiles in GLSL</div></center>
<p>
<p>The light intensity is computed CPU-side (<a href="#listing_photometriclightintensity">listing 23</a>) and depends on whether the photometric profile is used as a mask.</p>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-keyword">float</span> multiplier;</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-type">float</span> multiplier;</span>
<span class="line"><span class="hljs-comment">// Photometric profile used as a mask</span></span>
<span class="line"><span class="hljs-keyword">if</span> (photometricLight.isMasked()) {</span>
<span class="line"><span class="hljs-keyword">if</span> (photometricLight.<span class="hljs-built_in">isMasked</span>()) {</span>
<span class="line"> <span class="hljs-comment">// The desired intensity is set by the artist</span></span>
<span class="line"> <span class="hljs-comment">// The integrated intensity comes from a Monte-Carlo</span></span>
<span class="line"> <span class="hljs-comment">// integration over the unit sphere around the luminaire</span></span>
<span class="line"> multiplier = photometricLight.getDesiredIntensity() /</span>
<span class="line"> photometricLight.getIntegratedIntensity();</span>
<span class="line"> multiplier = photometricLight.<span class="hljs-built_in">getDesiredIntensity</span>() /</span>
<span class="line"> photometricLight.<span class="hljs-built_in">getIntegratedIntensity</span>();</span>
<span class="line">} <span class="hljs-keyword">else</span> {</span>
<span class="line"> <span class="hljs-comment">// Multiplier provided for convenience, set to 1.0 by default</span></span>
<span class="line"> multiplier = photometricLight.getMultiplier();</span>
<span class="line"> multiplier = photometricLight.<span class="hljs-built_in">getMultiplier</span>();</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-comment">// The max intensity in cd comes from the IES profile</span></span>
<span class="line"><span class="hljs-keyword">float</span> lightIntensity = photometricLight.getMaxIntensity() * multiplier;</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_photometriclightintensity">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;23:</b> Computing the intensity of a photometric light on the CPU</div></center>
<span class="line"><span class="hljs-type">float</span> lightIntensity = photometricLight.<span class="hljs-built_in">getMaxIntensity</span>() * multiplier;</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_photometriclightintensity">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;23:</b> Computing the intensity of a photometric light on the CPU</div></center>
<p>
<div class="endnote"><a class="target" name="endnote-xarrowintensity">&nbsp;</a><sup>4</sup> The XArrow profile declares a luminous intensity of 1,750 lm but a Monte-Carlo integration shows an intensity of only 350 lm.
</div>
@@ -2058,19 +2058,19 @@ In practice only 4 or 9 coefficients (i.e.: 2 or 3 bands) are enough for \(\cosT
<center><div class="image" style=""><a href="../images/ibl/ibl_irradiance_sh2.png" target="_blank"><img class="markdeep" src="../images/ibl/ibl_irradiance_sh2.png" style="max-width:100%;" /></a><center><span class="imagecaption"><a class="target" name="figure_iblsh2">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;52:</b> 2 bands (4 coefficients)</span></center></div></center>
</p><p>
In practice we pre-convolve \(\Lt\) with \(\cosTheta\) and pre-scale these coefficients by the basis scaling factors \(K_l^m\) so that the reconstruction code is as simple as possible in the shader:
</p><pre class="listing tilde"><code><span class="line">vec3 <span class="hljs-function"><span class="hljs-title">irradianceSH</span>(<span class="hljs-params">vec3 n</span>)</span> {</span>
<span class="line"> <span class="hljs-comment">// uniform vec3 sphericalHarmonics[9]</span></span>
<span class="line"> <span class="hljs-comment">// We can use only the first 2 bands for better performance</span></span>
<span class="line"> <span class="hljs-keyword">return</span></span>
<span class="line"> sphericalHarmonics[<span class="hljs-number">0</span>]</span>
<span class="line"> + sphericalHarmonics[<span class="hljs-number">1</span>] * (n.y)</span>
<span class="line"> + sphericalHarmonics[<span class="hljs-number">2</span>] * (n.z)</span>
<span class="line"> + sphericalHarmonics[<span class="hljs-number">3</span>] * (n.x)</span>
<span class="line"> + sphericalHarmonics[<span class="hljs-number">4</span>] * (n.y * n.x)</span>
<span class="line"> + sphericalHarmonics[<span class="hljs-number">5</span>] * (n.y * n.z)</span>
<span class="line"> + sphericalHarmonics[<span class="hljs-number">6</span>] * (<span class="hljs-number">3.0</span> * n.z * n.z - <span class="hljs-number">1.0</span>)</span>
<span class="line"> + sphericalHarmonics[<span class="hljs-number">7</span>] * (n.z * n.x)</span>
<span class="line"> + sphericalHarmonics[<span class="hljs-number">8</span>] * (n.x * n.x - n.y * n.y);</span>
</p><pre class="listing tilde"><code><span class="line">vec3 irradianceSH(vec3 n) {</span>
<span class="line"> // uniform vec3 sphericalHarmonics<span class="hljs-selector-attr">[9]</span></span>
<span class="line"> // We can <span class="hljs-selector-tag">use</span> only the first <span class="hljs-number">2</span> bands for better performance</span>
<span class="line"> return</span>
<span class="line"> sphericalHarmonics<span class="hljs-selector-attr">[0]</span></span>
<span class="line"> + sphericalHarmonics<span class="hljs-selector-attr">[1]</span> * (n<span class="hljs-selector-class">.y</span>)</span>
<span class="line"> + sphericalHarmonics<span class="hljs-selector-attr">[2]</span> * (n<span class="hljs-selector-class">.z</span>)</span>
<span class="line"> + sphericalHarmonics<span class="hljs-selector-attr">[3]</span> * (n<span class="hljs-selector-class">.x</span>)</span>
<span class="line"> + sphericalHarmonics<span class="hljs-selector-attr">[4]</span> * (n<span class="hljs-selector-class">.y</span> * n<span class="hljs-selector-class">.x</span>)</span>
<span class="line"> + sphericalHarmonics<span class="hljs-selector-attr">[5]</span> * (n<span class="hljs-selector-class">.y</span> * n<span class="hljs-selector-class">.z</span>)</span>
<span class="line"> + sphericalHarmonics<span class="hljs-selector-attr">[6]</span> * (<span class="hljs-number">3.0</span> * n<span class="hljs-selector-class">.z</span> * n<span class="hljs-selector-class">.z</span> - <span class="hljs-number">1.0</span>)</span>
<span class="line"> + sphericalHarmonics<span class="hljs-selector-attr">[7]</span> * (n<span class="hljs-selector-class">.z</span> * n<span class="hljs-selector-class">.x</span>)</span>
<span class="line"> + sphericalHarmonics<span class="hljs-selector-attr">[8]</span> * (n<span class="hljs-selector-class">.x</span> * n<span class="hljs-selector-class">.x</span> - n<span class="hljs-selector-class">.y</span> * n<span class="hljs-selector-class">.y</span>);</span>
<span class="line">}</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_irradiancesh">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;26:</b> GLSL code to reconstruct the irradiance from the pre-scaled SH</div></center>
<p>
<p>Note that with 2 bands, the computation above becomes a single (4 \times 4) matrix-by-vector multiply.</p>
@@ -2443,7 +2443,7 @@ LD(n, \alpha) &= \frac{\sum_i^N V(l_i, n,
$$
</p><p>
These two new \(DFG\) terms simply need to replace the ones used in the implementation shown in section <a href="#toc9.5">9.5</a>:
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-keyword">float</span> Fc = <span class="hljs-built_in">pow</span>(<span class="hljs-number">1</span> - VoH, <span class="hljs-number">5.0f</span>);</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-type">float</span> Fc = <span class="hljs-built_in">pow</span>(<span class="hljs-number">1</span> - VoH, <span class="hljs-number">5.0f</span>);</span>
<span class="line">r.x += Gv * Fc;</span>
<span class="line">r.y += Gv;</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_multiscatteriblpreintegration">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;29:</b> C++ implementation of the \(L_{DFG}\) term for multiscattering</div></center>
<p>
@@ -2507,11 +2507,11 @@ using an environment made of colored vertical stripes (skybox hidden).</span></c
<p>
<p>When sampling the IBL, the clear coat layer is calculated as a second specular lobe. This specular lobe is oriented along the view direction since we cannot reasonably integrate over the hemisphere. <a href="#listing_clearcoatibl">Listing 31</a> demonstrates this approximation in practice. It also shows the energy conservation step. It is important to note that this second specular lobe is computed exactly the same way as the main specular lobe, using the same DFG approximation.</p>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-comment">// clearCoat_NoV == shading_NoV if the clear coat layer doesn&#x27;t have its own normal map</span></span>
<span class="line"><span class="hljs-built_in">float</span> Fc = F_Schlick(<span class="hljs-number">0.04</span>, <span class="hljs-number">1.0</span>, clearCoat_NoV) * clearCoat;</span>
<span class="line"><span class="hljs-type">float</span> Fc = <span class="hljs-built_in">F_Schlick</span>(<span class="hljs-number">0.04</span>, <span class="hljs-number">1.0</span>, clearCoat_NoV) * clearCoat;</span>
<span class="line"><span class="hljs-comment">// base layer attenuation for energy compensation</span></span>
<span class="line">iblDiffuse *= <span class="hljs-number">1.0</span> - Fc;</span>
<span class="line">iblSpecular *= sq(<span class="hljs-number">1.0</span> - Fc);</span>
<span class="line">iblSpecular += specularIBL(r, clearCoatPerceptualRoughness) * Fc;</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_clearcoatibl">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;31:</b> GLSL implementation of the clear coat specular lobe for image-based lighting</div></center>
<span class="line">iblSpecular *= <span class="hljs-built_in">sq</span>(<span class="hljs-number">1.0</span> - Fc);</span>
<span class="line">iblSpecular += <span class="hljs-built_in">specularIBL</span>(r, clearCoatPerceptualRoughness) * Fc;</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_clearcoatibl">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;31:</b> GLSL implementation of the clear coat specular lobe for image-based lighting</div></center>
<a class="target" name="anisotropy">&nbsp;</a><a class="target" name="lighting/imagebasedlights/anisotropy">&nbsp;</a><a class="target" name="toc5.3.6">&nbsp;</a><h3 id="anisotropy"><a class="header" href="#anisotropy">Anisotropy </a></h3>
<p>
<p>[<a href="#citation-mcauley15">McAuley15</a>] describes a technique called “bent reflection vector”, based [<a href="#citation-revie12">Revie12</a>]. The bent reflection vector is a rough approximation of anisotropic lighting but the alternative is to use importance sampling. This approximation is sufficiently cheap to compute and provides good results, as shown in <a href="#figure_anisotropicibl1">figure 59</a> and <a href="#figure_anisotropicibl2">figure 60</a>.</p>
@@ -2550,17 +2550,17 @@ The DG term is generated using uniform sampling as recommended in [<a href="#cit
<center><div class="image" style=""><a href="../images/ibl/dfg_cloth.png" target="_blank"><img class="markdeep" src="../images/ibl/dfg_cloth.png" /></a><center><span class="imagecaption"><a class="target" name="figure_dfgclothlut">&nbsp;</a><b style="font-style:normal;">Figure&nbsp;62:</b> DFG LUT with a 3rd channel encoding the DG term of the cloth BRDF</span></center></div></center>
</p><p>
The remainder of the image-based lighting implementation follows the same steps as the implementation of regular lights, including the optional subsurface scattering term and its wrap diffuse component. Just as with the clear coat IBL implementation, we cannot integrate over the hemisphere and use the view direction as the dominant light direction to compute the wrap diffuse component.
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-built_in">float</span> diffuse = Fd_Lambert() * ambientOcclusion;</span>
<span class="line"><span class="hljs-meta">#<span class="hljs-meta-keyword">if</span> defined(SHADING_MODEL_CLOTH)</span></span>
<span class="line"><span class="hljs-meta">#<span class="hljs-meta-keyword">if</span> defined(MATERIAL_HAS_SUBSURFACE_COLOR)</span></span>
<span class="line">diffuse *= saturate((NoV + <span class="hljs-number">0.5</span>) / <span class="hljs-number">2.25</span>);</span>
<span class="line"><span class="hljs-meta">#<span class="hljs-meta-keyword">endif</span></span></span>
<span class="line"><span class="hljs-meta">#<span class="hljs-meta-keyword">endif</span></span></span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-type">float</span> diffuse = <span class="hljs-built_in">Fd_Lambert</span>() * ambientOcclusion;</span>
<span class="line"><span class="hljs-meta">#<span class="hljs-keyword">if</span> defined(SHADING_MODEL_CLOTH)</span></span>
<span class="line"><span class="hljs-meta">#<span class="hljs-keyword">if</span> defined(MATERIAL_HAS_SUBSURFACE_COLOR)</span></span>
<span class="line">diffuse *= <span class="hljs-built_in">saturate</span>((NoV + <span class="hljs-number">0.5</span>) / <span class="hljs-number">2.25</span>);</span>
<span class="line"><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span></span>
<span class="line"><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span></span>
<span class="line"></span>
<span class="line">vec3 indirectDiffuse = irradianceIBL(n) * diffuse;</span>
<span class="line"><span class="hljs-meta">#<span class="hljs-meta-keyword">if</span> defined(SHADING_MODEL_CLOTH) &amp;&amp; defined(MATERIAL_HAS_SUBSURFACE_COLOR)</span></span>
<span class="line">indirectDiffuse *= saturate(subsurfaceColor + NoV);</span>
<span class="line"><span class="hljs-meta">#<span class="hljs-meta-keyword">endif</span></span></span>
<span class="line">vec3 indirectDiffuse = <span class="hljs-built_in">irradianceIBL</span>(n) * diffuse;</span>
<span class="line"><span class="hljs-meta">#<span class="hljs-keyword">if</span> defined(SHADING_MODEL_CLOTH) &amp;&amp; defined(MATERIAL_HAS_SUBSURFACE_COLOR)</span></span>
<span class="line">indirectDiffuse *= <span class="hljs-built_in">saturate</span>(subsurfaceColor + NoV);</span>
<span class="line"><span class="hljs-meta">#<span class="hljs-keyword">endif</span></span></span>
<span class="line"></span>
<span class="line">vec3 ibl = diffuseColor * indirectDiffuse + indirectSpecular * specularColor;</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_clothapprox">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;34:</b> GLSL implementation of the DFG approximation for the cloth NDF</div></center>
<p>
@@ -2982,20 +2982,20 @@ L_{max} &= 2^{EV_{100}} \times 1.2
<span class="line"><span class="hljs-comment">// aperture in f-stops</span></span>
<span class="line"><span class="hljs-comment">// shutterSpeed in seconds</span></span>
<span class="line"><span class="hljs-comment">// sensitivity in ISO</span></span>
<span class="line"><span class="hljs-function"><span class="hljs-keyword">float</span> <span class="hljs-title">exposureSettings</span><span class="hljs-params">(<span class="hljs-keyword">float</span> aperture, <span class="hljs-keyword">float</span> shutterSpeed, <span class="hljs-keyword">float</span> sensitivity)</span> </span>{</span>
<span class="line"><span class="hljs-type">float</span> <span class="hljs-title function_">exposureSettings</span><span class="hljs-params">(<span class="hljs-type">float</span> aperture, <span class="hljs-type">float</span> shutterSpeed, <span class="hljs-type">float</span> sensitivity)</span> {</span>
<span class="line"> <span class="hljs-keyword">return</span> log2((aperture * aperture) / shutterSpeed * <span class="hljs-number">100.0</span> / sensitivity);</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-comment">// Computes the exposure normalization factor from</span></span>
<span class="line"><span class="hljs-comment">// the camera&#x27;s EV100</span></span>
<span class="line"><span class="hljs-function"><span class="hljs-keyword">float</span> <span class="hljs-title">exposure</span><span class="hljs-params">(<span class="hljs-keyword">float</span> ev100)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-number">1.0</span> / (<span class="hljs-built_in">pow</span>(<span class="hljs-number">2.0</span>, ev100) * <span class="hljs-number">1.2</span>);</span>
<span class="line"><span class="hljs-type">float</span> <span class="hljs-title function_">exposure</span><span class="hljs-params">(<span class="hljs-type">float</span> ev100)</span> {</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-number">1.0</span> / (pow(<span class="hljs-number">2.0</span>, ev100) * <span class="hljs-number">1.2</span>);</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-keyword">float</span> ev100 = exposureSettings(aperture, shutterSpeed, sensitivity);</span>
<span class="line"><span class="hljs-keyword">float</span> exposure = exposure(ev100);</span>
<span class="line"><span class="hljs-type">float</span> <span class="hljs-variable">ev100</span> <span class="hljs-operator">=</span> exposureSettings(aperture, shutterSpeed, sensitivity);</span>
<span class="line"><span class="hljs-type">float</span> <span class="hljs-variable">exposure</span> <span class="hljs-operator">=</span> exposure(ev100);</span>
<span class="line"></span>
<span class="line">vec4 color = evaluateLighting();</span>
<span class="line"><span class="hljs-type">vec4</span> <span class="hljs-variable">color</span> <span class="hljs-operator">=</span> evaluateLighting();</span>
<span class="line">color.rgb *= exposure;</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_fragmentexposure">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;42:</b> Implementation of exposure in GLSL</div></center>
<p>
<p>In practice the exposure factor can be pre-computed on the CPU to save shader instructions.</p>
@@ -3466,9 +3466,9 @@ Our implementation is presented in <a href="#listing_specularcolorimpl">listing&
<span class="line"><span class="hljs-comment">// Data source:</span></span>
<span class="line"><span class="hljs-comment">// http://cvrl.ioo.ucl.ac.uk/cmfs.htm</span></span>
<span class="line"><span class="hljs-comment">// http://cvrl.ioo.ucl.ac.uk/database/text/cmfs/ciexyz31.htm</span></span>
<span class="line"><span class="hljs-keyword">const</span> <span class="hljs-keyword">size_t</span> CIE_XYZ_START = <span class="hljs-number">360</span>;</span>
<span class="line"><span class="hljs-keyword">const</span> <span class="hljs-keyword">size_t</span> CIE_XYZ_COUNT = <span class="hljs-number">471</span>;</span>
<span class="line"><span class="hljs-keyword">const</span> float3 CIE_XYZ[CIE_XYZ_COUNT] = { ... };</span>
<span class="line"><span class="hljs-type">const</span> <span class="hljs-type">size_t</span> CIE_XYZ_START = <span class="hljs-number">360</span>;</span>
<span class="line"><span class="hljs-type">const</span> <span class="hljs-type">size_t</span> CIE_XYZ_COUNT = <span class="hljs-number">471</span>;</span>
<span class="line"><span class="hljs-type">const</span> float3 CIE_XYZ[CIE_XYZ_COUNT] = { ... };</span>
<span class="line"></span>
<span class="line"><span class="hljs-comment">// CIE Standard Illuminant D65 relative spectral power distribution,</span></span>
<span class="line"><span class="hljs-comment">// from 300nm to 830, at 5nm intervals</span></span>
@@ -3476,51 +3476,51 @@ Our implementation is presented in <a href="#listing_specularcolorimpl">listing&
<span class="line"><span class="hljs-comment">// Data source:</span></span>
<span class="line"><span class="hljs-comment">// https://en.wikipedia.org/wiki/Illuminant_D65</span></span>
<span class="line"><span class="hljs-comment">// https://cielab.xyz/pdf/CIE_sel_colorimetric_tables.xls</span></span>
<span class="line"><span class="hljs-keyword">const</span> <span class="hljs-keyword">size_t</span> CIE_D65_INTERVAL = <span class="hljs-number">5</span>;</span>
<span class="line"><span class="hljs-keyword">const</span> <span class="hljs-keyword">size_t</span> CIE_D65_START = <span class="hljs-number">300</span>;</span>
<span class="line"><span class="hljs-keyword">const</span> <span class="hljs-keyword">size_t</span> CIE_D65_END = <span class="hljs-number">830</span>;</span>
<span class="line"><span class="hljs-keyword">const</span> <span class="hljs-keyword">size_t</span> CIE_D65_COUNT = <span class="hljs-number">107</span>;</span>
<span class="line"><span class="hljs-keyword">const</span> <span class="hljs-keyword">float</span> CIE_D65[CIE_D65_COUNT] = { ... };</span>
<span class="line"><span class="hljs-type">const</span> <span class="hljs-type">size_t</span> CIE_D65_INTERVAL = <span class="hljs-number">5</span>;</span>
<span class="line"><span class="hljs-type">const</span> <span class="hljs-type">size_t</span> CIE_D65_START = <span class="hljs-number">300</span>;</span>
<span class="line"><span class="hljs-type">const</span> <span class="hljs-type">size_t</span> CIE_D65_END = <span class="hljs-number">830</span>;</span>
<span class="line"><span class="hljs-type">const</span> <span class="hljs-type">size_t</span> CIE_D65_COUNT = <span class="hljs-number">107</span>;</span>
<span class="line"><span class="hljs-type">const</span> <span class="hljs-type">float</span> CIE_D65[CIE_D65_COUNT] = { ... };</span>
<span class="line"></span>
<span class="line"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Sample</span> {</span></span>
<span class="line"> <span class="hljs-keyword">float</span> w = <span class="hljs-number">0.0f</span>; <span class="hljs-comment">// wavelength</span></span>
<span class="line"> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">complex</span>&lt;<span class="hljs-keyword">float</span>&gt; ior; <span class="hljs-comment">// complex IOR, n + ik</span></span>
<span class="line"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Sample</span> {</span>
<span class="line"> <span class="hljs-type">float</span> w = <span class="hljs-number">0.0f</span>; <span class="hljs-comment">// wavelength</span></span>
<span class="line"> std::complex&lt;<span class="hljs-type">float</span>&gt; ior; <span class="hljs-comment">// complex IOR, n + ik</span></span>
<span class="line">};</span>
<span class="line"></span>
<span class="line"><span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">float</span> <span class="hljs-title">illuminantD65</span><span class="hljs-params">(<span class="hljs-keyword">float</span> w)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">auto</span> i0 = <span class="hljs-keyword">size_t</span>((w - CIE_D65_START) / CIE_D65_INTERVAL);</span>
<span class="line"> uint2 indexBounds{i0, <span class="hljs-built_in">std</span>::min(i0 + <span class="hljs-number">1</span>, CIE_D65_END)};</span>
<span class="line"><span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">float</span> <span class="hljs-title">illuminantD65</span><span class="hljs-params">(<span class="hljs-type">float</span> w)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">auto</span> i0 = <span class="hljs-built_in">size_t</span>((w - CIE_D65_START) / CIE_D65_INTERVAL);</span>
<span class="line"> uint2 indexBounds{i0, std::<span class="hljs-built_in">min</span>(i0 + <span class="hljs-number">1</span>, CIE_D65_END)};</span>
<span class="line"></span>
<span class="line"> float2 wavelengthBounds = CIE_D65_START + float2{indexBounds} * CIE_D65_INTERVAL;</span>
<span class="line"> <span class="hljs-keyword">float</span> t = (w - wavelengthBounds.x) / (wavelengthBounds.y - wavelengthBounds.x);</span>
<span class="line"> <span class="hljs-keyword">return</span> lerp(CIE_D65[indexBounds.x], CIE_D65[indexBounds.y], t);</span>
<span class="line"> <span class="hljs-type">float</span> t = (w - wavelengthBounds.x) / (wavelengthBounds.y - wavelengthBounds.x);</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-built_in">lerp</span>(CIE_D65[indexBounds.x], CIE_D65[indexBounds.y], t);</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-comment">// For std::lower_bound</span></span>
<span class="line"><span class="hljs-keyword">bool</span> <span class="hljs-keyword">operator</span>&lt;(<span class="hljs-keyword">const</span> Sample&amp; lhs, <span class="hljs-keyword">const</span> Sample&amp; rhs) {</span>
<span class="line"><span class="hljs-type">bool</span> <span class="hljs-keyword">operator</span>&lt;(<span class="hljs-type">const</span> Sample&amp; lhs, <span class="hljs-type">const</span> Sample&amp; rhs) {</span>
<span class="line"> <span class="hljs-keyword">return</span> lhs.w &lt; rhs.w;</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-comment">// The wavelength w must be between 360nm and 830nm</span></span>
<span class="line"><span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">complex</span>&lt;<span class="hljs-keyword">float</span>&gt; <span class="hljs-title">findSample</span><span class="hljs-params">(<span class="hljs-keyword">const</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">vector</span>&lt;sample&gt;&amp; samples, <span class="hljs-keyword">float</span> w)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">auto</span> i1 = <span class="hljs-built_in">std</span>::lower_bound(</span>
<span class="line"> samples.begin(), samples.end(), Sample{w, <span class="hljs-number">0.0f</span> + <span class="hljs-number">0.0</span><span class="hljs-keyword">if</span>});</span>
<span class="line"><span class="hljs-function"><span class="hljs-type">static</span> std::complex&lt;<span class="hljs-type">float</span>&gt; <span class="hljs-title">findSample</span><span class="hljs-params">(<span class="hljs-type">const</span> std::vector&lt;sample&gt;&amp; samples, <span class="hljs-type">float</span> w)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">auto</span> i1 = std::<span class="hljs-built_in">lower_bound</span>(</span>
<span class="line"> samples.<span class="hljs-built_in">begin</span>(), samples.<span class="hljs-built_in">end</span>(), Sample{w, <span class="hljs-number">0.0f</span> + <span class="hljs-number">0.0</span><span class="hljs-keyword">if</span>});</span>
<span class="line"> <span class="hljs-keyword">auto</span> i0 = i1 - <span class="hljs-number">1</span>;</span>
<span class="line"></span>
<span class="line"> <span class="hljs-comment">// Interpolate the complex IORs</span></span>
<span class="line"> <span class="hljs-keyword">float</span> t = (w - i0-&gt;w) / (i1-&gt;w - i0-&gt;w);</span>
<span class="line"> <span class="hljs-keyword">float</span> n = lerp(i0-&gt;ior.real(), i1-&gt;ior.real(), t);</span>
<span class="line"> <span class="hljs-keyword">float</span> k = lerp(i0-&gt;ior.imag(), i1-&gt;ior.imag(), t);</span>
<span class="line"> <span class="hljs-type">float</span> t = (w - i0-&gt;w) / (i1-&gt;w - i0-&gt;w);</span>
<span class="line"> <span class="hljs-type">float</span> n = <span class="hljs-built_in">lerp</span>(i0-&gt;ior.<span class="hljs-built_in">real</span>(), i1-&gt;ior.<span class="hljs-built_in">real</span>(), t);</span>
<span class="line"> <span class="hljs-type">float</span> k = <span class="hljs-built_in">lerp</span>(i0-&gt;ior.<span class="hljs-built_in">imag</span>(), i1-&gt;ior.<span class="hljs-built_in">imag</span>(), t);</span>
<span class="line"> <span class="hljs-keyword">return</span> { n, k };</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">float</span> <span class="hljs-title">fresnel</span><span class="hljs-params">(<span class="hljs-keyword">const</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">complex</span>&lt;<span class="hljs-keyword">float</span>&gt;&amp; sample)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">return</span> (((sample - (<span class="hljs-number">1.0f</span> + <span class="hljs-number">0</span><span class="hljs-keyword">if</span>)) * (<span class="hljs-built_in">std</span>::conj(sample) - (<span class="hljs-number">1.0f</span> + <span class="hljs-number">0</span><span class="hljs-keyword">if</span>))) /</span>
<span class="line"> ((sample + (<span class="hljs-number">1.0f</span> + <span class="hljs-number">0</span><span class="hljs-keyword">if</span>)) * (<span class="hljs-built_in">std</span>::conj(sample) + (<span class="hljs-number">1.0f</span> + <span class="hljs-number">0</span><span class="hljs-keyword">if</span>)))).real();</span>
<span class="line"><span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">float</span> <span class="hljs-title">fresnel</span><span class="hljs-params">(<span class="hljs-type">const</span> std::complex&lt;<span class="hljs-type">float</span>&gt;&amp; sample)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">return</span> (((sample - (<span class="hljs-number">1.0f</span> + <span class="hljs-number">0</span><span class="hljs-keyword">if</span>)) * (std::<span class="hljs-built_in">conj</span>(sample) - (<span class="hljs-number">1.0f</span> + <span class="hljs-number">0</span><span class="hljs-keyword">if</span>))) /</span>
<span class="line"> ((sample + (<span class="hljs-number">1.0f</span> + <span class="hljs-number">0</span><span class="hljs-keyword">if</span>)) * (std::<span class="hljs-built_in">conj</span>(sample) + (<span class="hljs-number">1.0f</span> + <span class="hljs-number">0</span><span class="hljs-keyword">if</span>)))).<span class="hljs-built_in">real</span>();</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-function"><span class="hljs-keyword">static</span> float3 <span class="hljs-title">XYZ_to_sRGB</span><span class="hljs-params">(<span class="hljs-keyword">const</span> float3&amp; v)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">const</span> mat3f XYZ_sRGB{</span>
<span class="line"><span class="hljs-function"><span class="hljs-type">static</span> float3 <span class="hljs-title">XYZ_to_sRGB</span><span class="hljs-params">(<span class="hljs-type">const</span> float3&amp; v)</span> </span>{</span>
<span class="line"> <span class="hljs-type">const</span> mat3f XYZ_sRGB{</span>
<span class="line"> <span class="hljs-number">3.2404542f</span>, <span class="hljs-number">-0.9692660f</span>, <span class="hljs-number">0.0556434f</span>,</span>
<span class="line"> <span class="hljs-number">-1.5371385f</span>, <span class="hljs-number">1.8760108f</span>, <span class="hljs-number">-0.2040259f</span>,</span>
<span class="line"> <span class="hljs-number">-0.4985314f</span>, <span class="hljs-number">0.0415560f</span>, <span class="hljs-number">1.0572252f</span></span>
@@ -3529,21 +3529,21 @@ Our implementation is presented in <a href="#listing_specularcolorimpl">listing&
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-comment">// Outputs a linear sRGB color</span></span>
<span class="line"><span class="hljs-function"><span class="hljs-keyword">static</span> float3 <span class="hljs-title">computeColor</span><span class="hljs-params">(<span class="hljs-keyword">const</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">vector</span>&lt;sample&gt;&amp; samples)</span> </span>{</span>
<span class="line"><span class="hljs-function"><span class="hljs-type">static</span> float3 <span class="hljs-title">computeColor</span><span class="hljs-params">(<span class="hljs-type">const</span> std::vector&lt;sample&gt;&amp; samples)</span> </span>{</span>
<span class="line"> float3 xyz{<span class="hljs-number">0.0f</span>};</span>
<span class="line"> <span class="hljs-keyword">float</span> y = <span class="hljs-number">0.0f</span>;</span>
<span class="line"> <span class="hljs-type">float</span> y = <span class="hljs-number">0.0f</span>;</span>
<span class="line"></span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">size_t</span> i = <span class="hljs-number">0</span>; i &lt; CIE_XYZ_COUNT; i++) {</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-type">size_t</span> i = <span class="hljs-number">0</span>; i &lt; CIE_XYZ_COUNT; i++) {</span>
<span class="line"> <span class="hljs-comment">// Current wavelength</span></span>
<span class="line"> <span class="hljs-keyword">float</span> w = CIE_XYZ_START + i;</span>
<span class="line"> <span class="hljs-type">float</span> w = CIE_XYZ_START + i;</span>
<span class="line"></span>
<span class="line"> <span class="hljs-comment">// Find most appropriate CIE XYZ sample for the wavelength</span></span>
<span class="line"> <span class="hljs-keyword">auto</span> sample = findSample(samples, w);</span>
<span class="line"> <span class="hljs-keyword">auto</span> sample = <span class="hljs-built_in">findSample</span>(samples, w);</span>
<span class="line"> <span class="hljs-comment">// Compute Fresnel reflectance at normal incidence</span></span>
<span class="line"> <span class="hljs-keyword">float</span> f0 = fresnel(sample);</span>
<span class="line"> <span class="hljs-type">float</span> f0 = <span class="hljs-built_in">fresnel</span>(sample);</span>
<span class="line"></span>
<span class="line"> <span class="hljs-comment">// We need to multiply by the spectral power distribution of the illuminant</span></span>
<span class="line"> <span class="hljs-keyword">float</span> d65 = illuminantD65(w);</span>
<span class="line"> <span class="hljs-type">float</span> d65 = <span class="hljs-built_in">illuminantD65</span>(w);</span>
<span class="line"></span>
<span class="line"> xyz += f0 * CIE_XYZ[i] * d65;</span>
<span class="line"> y += CIE_XYZ[i].y * d65;</span>
@@ -3552,10 +3552,10 @@ Our implementation is presented in <a href="#listing_specularcolorimpl">listing&
<span class="line"> <span class="hljs-comment">// Normalize so that 100% reflectance at every wavelength yields Y=1</span></span>
<span class="line"> xyz /= y;</span>
<span class="line"></span>
<span class="line"> float3 linear = XYZ_to_sRGB(xyz);</span>
<span class="line"> float3 linear = <span class="hljs-built_in">XYZ_to_sRGB</span>(xyz);</span>
<span class="line"></span>
<span class="line"> <span class="hljs-comment">// Normalize out-of-gamut values</span></span>
<span class="line"> <span class="hljs-keyword">if</span> (any(greaterThan(linear, float3{<span class="hljs-number">1.0f</span>}))) linear *= <span class="hljs-number">1.0f</span> / max(linear);</span>
<span class="line"> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">any</span>(<span class="hljs-built_in">greaterThan</span>(linear, float3{<span class="hljs-number">1.0f</span>}))) linear *= <span class="hljs-number">1.0f</span> / <span class="hljs-built_in">max</span>(linear);</span>
<span class="line"></span>
<span class="line"> <span class="hljs-keyword">return</span> linear;</span>
<span class="line">}</span></code></pre><center><div class="listingcaption tilde"><a class="target" name="listing_specularcolorimpl">&nbsp;</a><b style="font-style:normal;">Listing&nbsp;46:</b> C++ implementation to compute the base color of a metallic surface from spectral data</div></center>
@@ -3735,47 +3735,47 @@ l &= \{ cos\phi sin\theta,sin\phi sin\theta,cos\theta \}
<pre class="listing tilde"><code><span class="line">vec2f hammersley(uint i, <span class="hljs-built_in">float</span> numSamples) {</span>
<span class="line"> uint bits = i;</span>
<span class="line"> bits = (bits &lt;&lt; <span class="hljs-string">16) | (bits &gt;&gt; 16</span>);</span>
<span class="line"> bits = ((bits &amp; 0x55555555) &lt;&lt; <span class="hljs-string">1) | ((bits &amp; 0xAAAAAAAA) &gt;&gt; 1</span>);</span>
<span class="line"> bits = ((bits &amp; 0x33333333) &lt;&lt; <span class="hljs-string">2) | ((bits &amp; 0xCCCCCCCC) &gt;&gt; 2</span>);</span>
<span class="line"> bits = ((bits &amp; 0x0F0F0F0F) &lt;&lt; <span class="hljs-string">4) | ((bits &amp; 0xF0F0F0F0) &gt;&gt; 4</span>);</span>
<span class="line"> bits = ((bits &amp; 0x00FF00FF) &lt;&lt; <span class="hljs-string">8) | ((bits &amp; 0xFF00FF00) &gt;&gt; 8</span>);</span>
<span class="line"> <span class="hljs-built_in">return</span> vec2f(i / numSamples, bits / exp2(32));</span>
<span class="line"> bits = ((bits &amp; <span class="hljs-number">0</span>x55555555) &lt;&lt; <span class="hljs-number">1</span>) | ((bits &amp; <span class="hljs-number">0</span>xAAAAAAAA) &gt;&gt; <span class="hljs-number">1</span>);</span>
<span class="line"> bits = ((bits &amp; <span class="hljs-number">0</span>x33333333) &lt;&lt; <span class="hljs-number">2</span>) | ((bits &amp; <span class="hljs-number">0</span>xCCCCCCCC) &gt;&gt; <span class="hljs-number">2</span>);</span>
<span class="line"> bits = ((bits &amp; <span class="hljs-number">0</span>x0F0F0F0F) &lt;&lt; <span class="hljs-number">4</span>) | ((bits &amp; <span class="hljs-number">0</span>xF0F0F0F0) &gt;&gt; <span class="hljs-number">4</span>);</span>
<span class="line"> bits = ((bits &amp; <span class="hljs-number">0</span>x00FF00FF) &lt;&lt; <span class="hljs-number">8</span>) | ((bits &amp; <span class="hljs-number">0</span>xFF00FF00) &gt;&gt; <span class="hljs-number">8</span>);</span>
<span class="line"> return vec2f(i / numSamples, bits / exp2(<span class="hljs-number">32</span>));</span>
<span class="line">}</span></code></pre><center><div class="listingcaption tilde">C++ implementation of a Hammersley sequence generator</div></center>
<a class="target" name="precomputinglforimage-basedlighting">&nbsp;</a><a class="target" name="annex/precomputinglforimage-basedlighting">&nbsp;</a><a class="target" name="toc9.5">&nbsp;</a><h2 id="precomputing-l-for-image-based-lighting"><a class="header" href="#precomputing-l-for-image-based-lighting">Precomputing L for image-based lighting</a></h2>
<p>
<p>The term ( L_{DFG} ) is only dependent on ( \NoV ). Below, the normal is arbitrarily set to ( n=\left[0, 0, 1\right] ) and (v) is chosen to satisfy ( \NoV ). The vector ( h_i ) is the ( D_{GGX}(\alpha) ) important direction sample (i).</p>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-type">float</span> GDFG(<span class="hljs-type">float</span> NoV, <span class="hljs-type">float</span> NoL, <span class="hljs-type">float</span> a) {</span>
<span class="line"> <span class="hljs-type">float</span> a2 = a * a;</span>
<span class="line"> <span class="hljs-type">float</span> GGXL = NoV * <span class="hljs-built_in">sqrt</span>((-NoL * a2 + NoL) * NoL + a2);</span>
<span class="line"> <span class="hljs-type">float</span> GGXV = NoL * <span class="hljs-built_in">sqrt</span>((-NoV * a2 + NoV) * NoV + a2);</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-type">float</span> <span class="hljs-title function_">GDFG</span><span class="hljs-params">(<span class="hljs-type">float</span> NoV, <span class="hljs-type">float</span> NoL, <span class="hljs-type">float</span> a)</span> {</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">a2</span> <span class="hljs-operator">=</span> a * a;</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">GGXL</span> <span class="hljs-operator">=</span> NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">GGXV</span> <span class="hljs-operator">=</span> NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);</span>
<span class="line"> <span class="hljs-keyword">return</span> (<span class="hljs-number">2</span> * NoL) / (GGXV + GGXL);</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line">float2 DFG(<span class="hljs-type">float</span> NoV, <span class="hljs-type">float</span> a) {</span>
<span class="line">float2 <span class="hljs-title function_">DFG</span><span class="hljs-params">(<span class="hljs-type">float</span> NoV, <span class="hljs-type">float</span> a)</span> {</span>
<span class="line"> float3 V;</span>
<span class="line"> V.x = <span class="hljs-built_in">sqrt</span>(<span class="hljs-number">1.0</span>f - NoV*NoV);</span>
<span class="line"> V.y = <span class="hljs-number">0.0</span>f;</span>
<span class="line"> V.x = sqrt(<span class="hljs-number">1.0f</span> - NoV*NoV);</span>
<span class="line"> V.y = <span class="hljs-number">0.0f</span>;</span>
<span class="line"> V.z = NoV;</span>
<span class="line"></span>
<span class="line"> float2 r = <span class="hljs-number">0.0</span>f;</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-type">uint</span> i = <span class="hljs-number">0</span>; i &lt; sampleCount; i++) {</span>
<span class="line"> float2 Xi = hammersley(i, sampleCount);</span>
<span class="line"> float3 H = importanceSampleGGX(Xi, a, N);</span>
<span class="line"> float3 L = <span class="hljs-number">2.0</span>f * <span class="hljs-built_in">dot</span>(V, H) * H - V;</span>
<span class="line"> <span class="hljs-type">float2</span> <span class="hljs-variable">r</span> <span class="hljs-operator">=</span> <span class="hljs-number">0.0f</span>;</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-type">uint</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i &lt; sampleCount; i++) {</span>
<span class="line"> <span class="hljs-type">float2</span> <span class="hljs-variable">Xi</span> <span class="hljs-operator">=</span> hammersley(i, sampleCount);</span>
<span class="line"> <span class="hljs-type">float3</span> <span class="hljs-variable">H</span> <span class="hljs-operator">=</span> importanceSampleGGX(Xi, a, N);</span>
<span class="line"> <span class="hljs-type">float3</span> <span class="hljs-variable">L</span> <span class="hljs-operator">=</span> <span class="hljs-number">2.0f</span> * dot(V, H) * H - V;</span>
<span class="line"></span>
<span class="line"> <span class="hljs-type">float</span> VoH = saturate(<span class="hljs-built_in">dot</span>(V, H));</span>
<span class="line"> <span class="hljs-type">float</span> NoL = saturate(L.z);</span>
<span class="line"> <span class="hljs-type">float</span> NoH = saturate(H.z);</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">VoH</span> <span class="hljs-operator">=</span> saturate(dot(V, H));</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">NoL</span> <span class="hljs-operator">=</span> saturate(L.z);</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">NoH</span> <span class="hljs-operator">=</span> saturate(H.z);</span>
<span class="line"></span>
<span class="line"> <span class="hljs-keyword">if</span> (NoL &gt; <span class="hljs-number">0.0</span>f) {</span>
<span class="line"> <span class="hljs-type">float</span> G = GDFG(NoV, NoL, a);</span>
<span class="line"> <span class="hljs-type">float</span> Gv = G * VoH / NoH;</span>
<span class="line"> <span class="hljs-type">float</span> Fc = <span class="hljs-built_in">pow</span>(<span class="hljs-number">1</span> - VoH, <span class="hljs-number">5.0</span>f);</span>
<span class="line"> <span class="hljs-keyword">if</span> (NoL &gt; <span class="hljs-number">0.0f</span>) {</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">G</span> <span class="hljs-operator">=</span> GDFG(NoV, NoL, a);</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">Gv</span> <span class="hljs-operator">=</span> G * VoH / NoH;</span>
<span class="line"> <span class="hljs-type">float</span> <span class="hljs-variable">Fc</span> <span class="hljs-operator">=</span> pow(<span class="hljs-number">1</span> - VoH, <span class="hljs-number">5.0f</span>);</span>
<span class="line"> r.x += Gv * (<span class="hljs-number">1</span> - Fc);</span>
<span class="line"> r.y += Gv * Fc;</span>
<span class="line"> }</span>
<span class="line"> }</span>
<span class="line"> <span class="hljs-keyword">return</span> r * (<span class="hljs-number">1.0</span>f / sampleCount);</span>
<span class="line"> <span class="hljs-keyword">return</span> r * (<span class="hljs-number">1.0f</span> / sampleCount);</span>
<span class="line">}</span></code></pre><center><div class="listingcaption tilde">C++ implementation of the \( L_{DFG} \) term</div></center>
<a class="target" name="sphericalharmonics">&nbsp;</a><a class="target" name="annex/sphericalharmonics">&nbsp;</a><a class="target" name="toc9.6">&nbsp;</a><h2 id="spherical-harmonics"><a class="header" href="#spherical-harmonics">Spherical Harmonics</a></h2>
<p>
@@ -3851,56 +3851,56 @@ sin(m \phi + \phi) &= sin(m \phi) cos(\phi) + cos(m \phi) sin(\phi) \Leftrightar
\end{align*}$$
</p><p>
<a href="#listing_nonnormalizedshbasis">Listing&nbsp;47</a> shows the C++ code to compute the non-normalized SH basis \(\frac{y^m_l(s)}{\sqrt{2} K^m_l}\):
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">inline</span> <span class="hljs-keyword">size_t</span> <span class="hljs-title">SHindex</span><span class="hljs-params">(<span class="hljs-keyword">ssize_t</span> m, <span class="hljs-keyword">size_t</span> l)</span> </span>{</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-keyword">inline</span> <span class="hljs-type">size_t</span> <span class="hljs-title">SHindex</span><span class="hljs-params">(<span class="hljs-type">ssize_t</span> m, <span class="hljs-type">size_t</span> l)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">return</span> l * (l + <span class="hljs-number">1</span>) + m;</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">computeShBasis</span><span class="hljs-params">(</span>
<span class="line"> <span class="hljs-keyword">double</span>* <span class="hljs-keyword">const</span> SHb,</span>
<span class="line"> <span class="hljs-keyword">size_t</span> numBands,</span>
<span class="line"> <span class="hljs-keyword">const</span> vec3&amp; s)</span></span>
<span class="line"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">computeShBasis</span><span class="hljs-params">(</span>
<span class="line"> <span class="hljs-type">double</span>* <span class="hljs-type">const</span> SHb,</span>
<span class="line"> <span class="hljs-type">size_t</span> numBands,</span>
<span class="line"> <span class="hljs-type">const</span> vec3&amp; s)</span></span>
<span class="line"></span>{</span>
<span class="line"> <span class="hljs-comment">// handle m=0 separately, since it produces only one coefficient</span></span>
<span class="line"> <span class="hljs-keyword">double</span> Pml_2 = <span class="hljs-number">0</span>;</span>
<span class="line"> <span class="hljs-keyword">double</span> Pml_1 = <span class="hljs-number">1</span>;</span>
<span class="line"> <span class="hljs-type">double</span> Pml_2 = <span class="hljs-number">0</span>;</span>
<span class="line"> <span class="hljs-type">double</span> Pml_1 = <span class="hljs-number">1</span>;</span>
<span class="line"> SHb[<span class="hljs-number">0</span>] = Pml_1;</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">ssize_t</span> l = <span class="hljs-number">1</span>; l &lt; numBands; l++) {</span>
<span class="line"> <span class="hljs-keyword">double</span> Pml = ((<span class="hljs-number">2</span> * l - <span class="hljs-number">1</span>) * Pml_1 * s.z - (l - <span class="hljs-number">1</span>) * Pml_2) / l;</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-type">ssize_t</span> l = <span class="hljs-number">1</span>; l &lt; numBands; l++) {</span>
<span class="line"> <span class="hljs-type">double</span> Pml = ((<span class="hljs-number">2</span> * l - <span class="hljs-number">1</span>) * Pml_1 * s.z - (l - <span class="hljs-number">1</span>) * Pml_2) / l;</span>
<span class="line"> Pml_2 = Pml_1;</span>
<span class="line"> Pml_1 = Pml;</span>
<span class="line"> SHb[SHindex(<span class="hljs-number">0</span>, l)] = Pml;</span>
<span class="line"> SHb[<span class="hljs-built_in">SHindex</span>(<span class="hljs-number">0</span>, l)] = Pml;</span>
<span class="line"> }</span>
<span class="line"> <span class="hljs-keyword">double</span> Pmm = <span class="hljs-number">1</span>;</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">ssize_t</span> m = <span class="hljs-number">1</span>; m &lt; numBands ; m++) {</span>
<span class="line"> <span class="hljs-type">double</span> Pmm = <span class="hljs-number">1</span>;</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-type">ssize_t</span> m = <span class="hljs-number">1</span>; m &lt; numBands ; m++) {</span>
<span class="line"> Pmm = (<span class="hljs-number">1</span> - <span class="hljs-number">2</span> * m) * Pmm;</span>
<span class="line"> <span class="hljs-keyword">double</span> Pml_2 = Pmm;</span>
<span class="line"> <span class="hljs-keyword">double</span> Pml_1 = (<span class="hljs-number">2</span> * m + <span class="hljs-number">1</span>)*Pmm*s.z;</span>
<span class="line"> <span class="hljs-type">double</span> Pml_2 = Pmm;</span>
<span class="line"> <span class="hljs-type">double</span> Pml_1 = (<span class="hljs-number">2</span> * m + <span class="hljs-number">1</span>)*Pmm*s.z;</span>
<span class="line"> <span class="hljs-comment">// l == m</span></span>
<span class="line"> SHb[SHindex(-m, m)] = Pml_2;</span>
<span class="line"> SHb[SHindex( m, m)] = Pml_2;</span>
<span class="line"> SHb[<span class="hljs-built_in">SHindex</span>(-m, m)] = Pml_2;</span>
<span class="line"> SHb[<span class="hljs-built_in">SHindex</span>( m, m)] = Pml_2;</span>
<span class="line"> <span class="hljs-keyword">if</span> (m + <span class="hljs-number">1</span> &lt; numBands) {</span>
<span class="line"> <span class="hljs-comment">// l == m+1</span></span>
<span class="line"> SHb[SHindex(-m, m + <span class="hljs-number">1</span>)] = Pml_1;</span>
<span class="line"> SHb[SHindex( m, m + <span class="hljs-number">1</span>)] = Pml_1;</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">ssize_t</span> l = m + <span class="hljs-number">2</span>; l &lt; numBands; l++) {</span>
<span class="line"> <span class="hljs-keyword">double</span> Pml = ((<span class="hljs-number">2</span> * l - <span class="hljs-number">1</span>) * Pml_1 * s.z - (l + m - <span class="hljs-number">1</span>) * Pml_2)</span>
<span class="line"> SHb[<span class="hljs-built_in">SHindex</span>(-m, m + <span class="hljs-number">1</span>)] = Pml_1;</span>
<span class="line"> SHb[<span class="hljs-built_in">SHindex</span>( m, m + <span class="hljs-number">1</span>)] = Pml_1;</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-type">ssize_t</span> l = m + <span class="hljs-number">2</span>; l &lt; numBands; l++) {</span>
<span class="line"> <span class="hljs-type">double</span> Pml = ((<span class="hljs-number">2</span> * l - <span class="hljs-number">1</span>) * Pml_1 * s.z - (l + m - <span class="hljs-number">1</span>) * Pml_2)</span>
<span class="line"> / (l - m);</span>
<span class="line"> Pml_2 = Pml_1;</span>
<span class="line"> Pml_1 = Pml;</span>
<span class="line"> SHb[SHindex(-m, l)] = Pml;</span>
<span class="line"> SHb[SHindex( m, l)] = Pml;</span>
<span class="line"> SHb[<span class="hljs-built_in">SHindex</span>(-m, l)] = Pml;</span>
<span class="line"> SHb[<span class="hljs-built_in">SHindex</span>( m, l)] = Pml;</span>
<span class="line"> }</span>
<span class="line"> }</span>
<span class="line"> }</span>
<span class="line"> <span class="hljs-keyword">double</span> Cm = s.x;</span>
<span class="line"> <span class="hljs-keyword">double</span> Sm = s.y;</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">ssize_t</span> m = <span class="hljs-number">1</span>; m &lt;= numBands ; m++) {</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">ssize_t</span> l = m; l &lt; numBands ; l++) {</span>
<span class="line"> SHb[SHindex(-m, l)] *= Sm;</span>
<span class="line"> SHb[SHindex( m, l)] *= Cm;</span>
<span class="line"> <span class="hljs-type">double</span> Cm = s.x;</span>
<span class="line"> <span class="hljs-type">double</span> Sm = s.y;</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-type">ssize_t</span> m = <span class="hljs-number">1</span>; m &lt;= numBands ; m++) {</span>
<span class="line"> <span class="hljs-keyword">for</span> (<span class="hljs-type">ssize_t</span> l = m; l &lt; numBands ; l++) {</span>
<span class="line"> SHb[<span class="hljs-built_in">SHindex</span>(-m, l)] *= Sm;</span>
<span class="line"> SHb[<span class="hljs-built_in">SHindex</span>( m, l)] *= Cm;</span>
<span class="line"> }</span>
<span class="line"> <span class="hljs-keyword">double</span> Cm1 = Cm * s.x - Sm * s.y;</span>
<span class="line"> <span class="hljs-keyword">double</span> Sm1 = Sm * s.x + Cm * s.y;</span>
<span class="line"> <span class="hljs-type">double</span> Cm1 = Cm * s.x - Sm * s.y;</span>
<span class="line"> <span class="hljs-type">double</span> Sm1 = Sm * s.x + Cm * s.y;</span>
<span class="line"> Cm = Cm1;</span>
<span class="line"> Sm = Sm1;</span>
<span class="line"> }</span>
@@ -3979,10 +3979,10 @@ $$\begin{equation}
\end{equation}$$
</p><p>
Here is the C++ code to compute \(\hat{C}_l\):
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">double</span> <span class="hljs-title">factorial</span><span class="hljs-params">(<span class="hljs-keyword">size_t</span> n, <span class="hljs-keyword">size_t</span> d = <span class="hljs-number">1</span>)</span></span>;</span>
</p><pre class="listing tilde"><code><span class="line"><span class="hljs-function"><span class="hljs-type">static</span> <span class="hljs-type">double</span> <span class="hljs-title">factorial</span><span class="hljs-params">(<span class="hljs-type">size_t</span> n, <span class="hljs-type">size_t</span> d = <span class="hljs-number">1</span>)</span></span>;</span>
<span class="line"></span>
<span class="line"><span class="hljs-comment">// &lt; cos(theta) &gt; SH coefficients pre-multiplied by 1 / K(0,l)</span></span>
<span class="line"><span class="hljs-function"><span class="hljs-keyword">double</span> <span class="hljs-title">computeTruncatedCosSh</span><span class="hljs-params">(<span class="hljs-keyword">size_t</span> l)</span> </span>{</span>
<span class="line"><span class="hljs-function"><span class="hljs-type">double</span> <span class="hljs-title">computeTruncatedCosSh</span><span class="hljs-params">(<span class="hljs-type">size_t</span> l)</span> </span>{</span>
<span class="line"> <span class="hljs-keyword">if</span> (l == <span class="hljs-number">0</span>) {</span>
<span class="line"> <span class="hljs-keyword">return</span> M_PI;</span>
<span class="line"> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (l == <span class="hljs-number">1</span>) {</span>
@@ -3990,17 +3990,17 @@ Here is the C++ code to compute \(\hat{C}_l\):
<span class="line"> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (l &amp; <span class="hljs-number">1</span>) {</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;</span>
<span class="line"> }</span>
<span class="line"> <span class="hljs-keyword">const</span> <span class="hljs-keyword">size_t</span> l_2 = l / <span class="hljs-number">2</span>;</span>
<span class="line"> <span class="hljs-keyword">double</span> A0 = ((l_2 &amp; <span class="hljs-number">1</span>) ? <span class="hljs-number">1.0</span> : <span class="hljs-number">-1.0</span>) / ((l + <span class="hljs-number">2</span>) * (l - <span class="hljs-number">1</span>));</span>
<span class="line"> <span class="hljs-keyword">double</span> A1 = factorial(l, l_2) / (factorial(l_2) * (<span class="hljs-number">1</span> &lt;&lt; l));</span>
<span class="line"> <span class="hljs-type">const</span> <span class="hljs-type">size_t</span> l_2 = l / <span class="hljs-number">2</span>;</span>
<span class="line"> <span class="hljs-type">double</span> A0 = ((l_2 &amp; <span class="hljs-number">1</span>) ? <span class="hljs-number">1.0</span> : <span class="hljs-number">-1.0</span>) / ((l + <span class="hljs-number">2</span>) * (l - <span class="hljs-number">1</span>));</span>
<span class="line"> <span class="hljs-type">double</span> A1 = <span class="hljs-built_in">factorial</span>(l, l_2) / (<span class="hljs-built_in">factorial</span>(l_2) * (<span class="hljs-number">1</span> &lt;&lt; l));</span>
<span class="line"> <span class="hljs-keyword">return</span> <span class="hljs-number">2</span> * M_PI * A0 * A1;</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hljs-comment">// returns n! / d!</span></span>
<span class="line"><span class="hljs-function"><span class="hljs-keyword">double</span> <span class="hljs-title">factorial</span><span class="hljs-params">(<span class="hljs-keyword">size_t</span> n, <span class="hljs-keyword">size_t</span> d )</span> </span>{</span>
<span class="line"> d = <span class="hljs-built_in">std</span>::max(<span class="hljs-keyword">size_t</span>(<span class="hljs-number">1</span>), d);</span>
<span class="line"> n = <span class="hljs-built_in">std</span>::max(<span class="hljs-keyword">size_t</span>(<span class="hljs-number">1</span>), n);</span>
<span class="line"> <span class="hljs-keyword">double</span> r = <span class="hljs-number">1.0</span>;</span>
<span class="line"><span class="hljs-function"><span class="hljs-type">double</span> <span class="hljs-title">factorial</span><span class="hljs-params">(<span class="hljs-type">size_t</span> n, <span class="hljs-type">size_t</span> d )</span> </span>{</span>
<span class="line"> d = std::<span class="hljs-built_in">max</span>(<span class="hljs-built_in">size_t</span>(<span class="hljs-number">1</span>), d);</span>
<span class="line"> n = std::<span class="hljs-built_in">max</span>(<span class="hljs-built_in">size_t</span>(<span class="hljs-number">1</span>), n);</span>
<span class="line"> <span class="hljs-type">double</span> r = <span class="hljs-number">1.0</span>;</span>
<span class="line"> <span class="hljs-keyword">if</span> (n == d) {</span>
<span class="line"> <span class="hljs-comment">// intentionally left blank</span></span>
<span class="line"> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (n &gt; d) {</span>
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+22 -6
View File
@@ -16,12 +16,13 @@ class SimulatedSkybox {
this.ozone = 0.0;
this.msFactors = [0.1, 0.5, 0.0];
this.contrast = 1.0;
this.nightColor = [0.0, 0.0003, 0.00075];
this.nightColor = [0.0, 3.0e-9, 7.5e-9];
this.shimmerControl = [0.0, 20.0, 0.1];
this.cloudControl = [0.0, 0.1, 8000.0, 0.0];
this.cloudControl2 = [0.0, 0.0, 0.0, 0.0];
this.waterControl = [50.0, 1.0, 1.0, 4.0]; // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
this.starControl = [0.001, 1.0, 350.0, 0.01]; // x=Density, y=Enabled, z=Frequency, w=PixelScale
this.starIntensity = 1.0;
this.focalLength = 24.0;
this.height = 1000.0;
this.planetRadius = 6360.0;
@@ -43,7 +44,7 @@ class SimulatedSkybox {
// Milky Way Parameters
// x=Intensity, y=Saturation, z=Unused
this.milkyWayControl = [1.0, 1.0, 0.07];
this.milkyWayControl = [1.0, 1.2, 0.05];
this.milkyWayEnabled = true;
this.milkyWayRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity by default
this.initEntity();
@@ -453,6 +454,11 @@ class SimulatedSkybox {
this.updateCoefficients();
}
setStarIntensity(intensity) {
this.starIntensity = Math.max(0.0, intensity);
this.updateCoefficients();
}
setMoonPosition(direction) {
// normalize
const len = Math.hypot(direction[0], direction[1], direction[2]);
@@ -500,6 +506,13 @@ class SimulatedSkybox {
}
}
setExposure(exposure) {
this.exposure = exposure;
this.updateCoefficients();
}
updateCoefficients() {
if (!this.materialInstance) {
console.warn("updateCoefficients called before material loaded");
@@ -585,6 +598,7 @@ class SimulatedSkybox {
this.materialInstance.setFloat4Parameter('cloudControl2', new Float32Array(this.cloudControl2));
this.materialInstance.setFloat4Parameter('waterControl', new Float32Array(this.waterControl));
this.materialInstance.setFloat4Parameter('starControl', new Float32Array(this.starControl));
this.materialInstance.setFloatParameter('starIntensity', this.starIntensity);
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
@@ -615,12 +629,13 @@ class SimulatedSkybox {
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
// Scale Milky Way by Sun Intensity (Pre-exposed) to match dynamic range
// Calibration: 1.0 User Intensity ~ 0.025 Lux Relative (approx 2.5% of Sun Pixel Value at Day)
// At Sunny 16 (Sun ~ 2.6), this gives ~0.065 pixel brightness, which is visible but dim.
// Scale Milky Way by Sun Intensity
// Calibration: 1.0 User Intensity = 1.5e-3 cd/m^2 (Nits) approx.
// Target: 1.5e-3 Nits. SunIntensity = 100,000.
// Scale = 1.5e-3 / 1.0e5 = 1.5e-8.
const mwIntensity = this.milkyWayEnabled ? this.milkyWayControl[0] : 0.0;
const mwUniform = [
mwIntensity * this.sunIntensity * 0.025,
mwIntensity * this.sunIntensity * 1.5e-8,
this.milkyWayControl[1],
this.milkyWayControl[2]
];
@@ -634,6 +649,7 @@ class SimulatedSkybox {
const moonHaloUpload = [...this.moonHalo];
moonHaloUpload[2] *= moonRadConv;
this.materialInstance.setFloat4Parameter('sunHalo2', new Float32Array(moonHaloUpload));
this.materialInstance.setFloatParameter('exposure', this.exposure !== undefined ? this.exposure : 1.0);
// Solar Eclipse (CPU Calculation)
const sunRadius = Math.acos(this.sunHalo[0]);
Binary file not shown.
+26 -5
View File
@@ -218,6 +218,7 @@ class App {
const exposure = this.getExposure();
const preExposedIntensity = this.params.sunIntensity * exposure;
this.skybox.setSunIntensity(preExposedIntensity);
this.skybox.setExposure(exposure); // Update Skybox Exposure Uniform
// Moon Exposure
if (this.mParams) {
@@ -389,7 +390,7 @@ class App {
};
mwFolder.add(this.mwParams, 'enabled').name('Enabled').onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 5.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 100.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'saturation', 0.0, 2.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'blackPoint', 0.0, 0.5).name('Black Point').onChange(updateMW);
mwFolder.add(this.mwParams, 'siderealTime', 0.0, 24.0).name('Sidereal Time').listen().onChange(updateMW);
@@ -464,18 +465,23 @@ class App {
const starFolder = gui.addFolder('Stars');
this.sParams = {
enabled: true,
density: 0.001
density: 0.001,
intensity: 0.0 // 2^0 = 1.0
};
// Initialize defaults (Density 0.001, Enabled True)
sky.setStarControl(0.001, true);
sky.setStarIntensity(Math.pow(2.0, 0.0));
const updateStars = () => {
sky.setStarControl(this.sParams.density, this.sParams.enabled);
sky.setStarIntensity(Math.pow(2.0, this.sParams.intensity));
};
starFolder.add(this.sParams, 'enabled').name('Enabled').onChange(updateStars);
starFolder.add(this.sParams, 'density', 0.0, 0.01, 0.0001).name('Density').onChange(updateStars);
starFolder.add(this.sParams, 'intensity', 0.0, 24.0).name('Intensity (Exp)').onChange(updateStars);
starFolder.close();
this.updateStars = updateStars;
const artFolder = gui.addFolder('Artistic');
// Set Horizon Glow default to 0.0
@@ -489,7 +495,7 @@ class App {
artFolder.add(sky.msFactors, 2, 0.0, 1.0).name('Horizon Glow').onChange(v => sky.setHorizonGlow(v));
artFolder.add(sky, 'contrast', 0.1, 2.0).onChange(v => sky.setContrast(v));
artFolder.addColor(sky, 'nightColor').onChange(v => sky.setNightColor(v));
const shmFolder = artFolder.addFolder('Shimmer');
// Set Shimmer Strength default to 0.0
@@ -502,7 +508,7 @@ class App {
const camFolder = gui.addFolder('Camera');
camFolder.add(this.params, 'focalLength', 8.0, 300.0).name('Focal Length').onChange(() => this.updateCameraProjection());
camFolder.add(this.params, 'aperture', 1.4, 32.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 1.0, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 0.05, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'iso', 50.0, 3200.0).onChange(() => this.updateCameraExposure());
const bloomFolder = camFolder.addFolder('Bloom');
@@ -692,13 +698,15 @@ class App {
const s = this.sParams;
const b = this.bParams;
const m = this.mParams;
const mw = this.mwParams;
const sk = this.skybox;
return {
p: { a: p.aperture, ss: p.shutterSpeed, i: p.iso, st: p.sunTheta, sp: p.sunPhi, fl: p.focalLength, si: p.sunIntensity },
c: { v: c.volumetrics, co: c.coverage, d: c.density, h: c.height, s: c.speed, e: c.evolution },
w: { dt: w.derivativeTrick, st: w.strength, s: w.speed, o: w.octaves },
s: { e: s.enabled, d: s.density },
s: { e: s.enabled, d: s.density, i: s.intensity },
mw: { e: mw.enabled, i: mw.intensity, s: mw.saturation, bp: mw.blackPoint, st: mw.siderealTime, l: mw.latitude },
b: { e: b.enabled, lf: b.lensFlare },
m: { e: m.enabled, az: m.azimuth, h: m.height, r: m.radius, i: m.intensity },
cm: { t: this.camState.theta, p: this.camState.phi },
@@ -723,6 +731,7 @@ class App {
const c = state.c;
const w = state.w;
const s = state.s;
const mw = state.mw;
const b = state.b;
const m = state.m;
const k = state.k;
@@ -757,6 +766,18 @@ class App {
if (s) {
if (s.e !== undefined) this.sParams.enabled = s.e;
if (s.d !== undefined) this.sParams.density = s.d;
if (s.i !== undefined) this.sParams.intensity = s.i;
if (this.updateStars) this.updateStars();
}
if (mw) {
if (mw.e !== undefined) this.mwParams.enabled = mw.e;
if (mw.i !== undefined) this.mwParams.intensity = mw.i;
if (mw.s !== undefined) this.mwParams.saturation = mw.s;
if (mw.bp !== undefined) this.mwParams.blackPoint = mw.bp;
if (mw.st !== undefined) this.mwParams.siderealTime = mw.st;
if (mw.l !== undefined) this.mwParams.latitude = mw.l;
if (this.updateMW) this.updateMW();
}
if (b) {
+48 -7
View File
@@ -96,10 +96,20 @@ material {
name : starControl, // x=Density, y=Enabled, z=Frequency, w=PixelScale
precision : high
},
{
type : float,
name : starIntensity,
precision : high
},
{
type : sampler2d,
name : moonTexture
},
{
type : float,
name : exposure,
precision : high
},
{
type : sampler2d,
name : moonNormal
@@ -162,9 +172,9 @@ fragment {
// --- CONFIGURATION ---
// Stars
#define STAR_GLOBAL_INTENSITY 150.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.5 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 4.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_GLOBAL_INTENSITY 100.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.01 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 15.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_FADE_SUN_ELV_HIGH 0.10 // Sun elevation (sin) where stars are fully hidden [0.0 - 0.5]
#define STAR_FADE_SUN_ELV_LOW -0.20 // Sun elevation (sin) where stars are fully visible [-0.5 - 0.0]
#define STAR_CLOUD_OCCLUSION 0.1 // Visibility when covered by clouds [0.0 - 1.0]
@@ -961,7 +971,11 @@ fragment {
highp vec4 sunHalo, highp vec4 cloudControl, highp vec4 cloudControl2,
highp vec4 shimmerControl, highp vec4 waterControl,
highp vec3 L2, highp float sunIntensity2, highp vec4 sunHalo2,
sampler2D moonTex, sampler2D moonNormal) {
sampler2D moonTex, sampler2D moonNormal,
highp vec3 nightColor,
highp mat3 milkyWayRotation,
highp vec3 milkyWayControl,
sampler2D milkyWayTexture) {
// Project to plane y=0
highp float t = WATER_PLANE_HEIGHT / min(V.y, -0.0002); // Reduced clamp to minimize "wall" artifact
@@ -1057,7 +1071,7 @@ fragment {
highp float rHorizonMask = 1.0 - smoothstep(0.0, REFLECTION_HORIZON_FADE, R.y);
if (rHorizonMask > 0.0) {
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask;
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask * materialParams.exposure;
}
// Sun Disk Reflection
@@ -1088,6 +1102,29 @@ fragment {
// Apply clouds to reflection
reflection = mix(reflection, reflCloudLayer, reflCloudDensity);
// Add Milky Way to Reflection
// Calculate Fade based on Sun Elevation
highp float sunElvSin = L.y;
highp float mwFade = smoothstep(STAR_FADE_SUN_ELV_HIGH, STAR_FADE_SUN_ELV_LOW, sunElvSin);
if (mwFade > 0.0) {
highp vec3 mwColor = getMilkyWay(R, milkyWayRotation, milkyWayTexture, milkyWayControl);
// Apply Atmosphere Transmittance (approximate)
mwColor *= outTransmittance;
// Apply Fades
mwColor *= mwFade;
mwColor *= (1.0 - reflMoonOcclusion); // Occlude by Moon
mwColor *= (1.0 - reflCloudDensity); // Occlude by Clouds
reflection += mwColor;
}
// Add Night Color to Reflection
reflection += nightColor;
// Fresnel
highp float F0 = WATER_FRESNEL_F0; // Water
highp float cosTheta = clamp(dot(-V, N_water), 0.0, 1.0);
@@ -1187,7 +1224,7 @@ fragment {
// 7. Stars
// Add stars before clouds (clouds cover stars)
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl);
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl) * materialParams.exposure * materialParams.starIntensity;
// 7b. Milky Way
// Add Milky Way behind stars (conceptually) but handled similarly
@@ -1232,7 +1269,11 @@ fragment {
materialParams.shimmerControl,
materialParams.waterControl,
L2, materialParams.sunIntensity2, materialParams.sunHalo2,
materialParams_moonTexture, materialParams_moonNormal);
materialParams_moonTexture, materialParams_moonNormal,
materialParams.nightColor,
materialParams.milkyWayRotation,
materialParams.milkyWayControl,
materialParams_milkyWayTexture);
color = applyDynamicToneMapping(color, L, materialParams.contrast);
}
+22 -6
View File
@@ -16,12 +16,13 @@ class SimulatedSkybox {
this.ozone = 0.0;
this.msFactors = [0.1, 0.5, 0.0];
this.contrast = 1.0;
this.nightColor = [0.0, 0.0003, 0.00075];
this.nightColor = [0.0, 3.0e-9, 7.5e-9];
this.shimmerControl = [0.0, 20.0, 0.1];
this.cloudControl = [0.0, 0.1, 8000.0, 0.0];
this.cloudControl2 = [0.0, 0.0, 0.0, 0.0];
this.waterControl = [50.0, 1.0, 1.0, 4.0]; // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
this.starControl = [0.001, 1.0, 350.0, 0.01]; // x=Density, y=Enabled, z=Frequency, w=PixelScale
this.starIntensity = 1.0;
this.focalLength = 24.0;
this.height = 1000.0;
this.planetRadius = 6360.0;
@@ -43,7 +44,7 @@ class SimulatedSkybox {
// Milky Way Parameters
// x=Intensity, y=Saturation, z=Unused
this.milkyWayControl = [1.0, 1.0, 0.07];
this.milkyWayControl = [1.0, 1.2, 0.05];
this.milkyWayEnabled = true;
this.milkyWayRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity by default
this.initEntity();
@@ -453,6 +454,11 @@ class SimulatedSkybox {
this.updateCoefficients();
}
setStarIntensity(intensity) {
this.starIntensity = Math.max(0.0, intensity);
this.updateCoefficients();
}
setMoonPosition(direction) {
// normalize
const len = Math.hypot(direction[0], direction[1], direction[2]);
@@ -500,6 +506,13 @@ class SimulatedSkybox {
}
}
setExposure(exposure) {
this.exposure = exposure;
this.updateCoefficients();
}
updateCoefficients() {
if (!this.materialInstance) {
console.warn("updateCoefficients called before material loaded");
@@ -585,6 +598,7 @@ class SimulatedSkybox {
this.materialInstance.setFloat4Parameter('cloudControl2', new Float32Array(this.cloudControl2));
this.materialInstance.setFloat4Parameter('waterControl', new Float32Array(this.waterControl));
this.materialInstance.setFloat4Parameter('starControl', new Float32Array(this.starControl));
this.materialInstance.setFloatParameter('starIntensity', this.starIntensity);
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
@@ -615,12 +629,13 @@ class SimulatedSkybox {
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
// Scale Milky Way by Sun Intensity (Pre-exposed) to match dynamic range
// Calibration: 1.0 User Intensity ~ 0.025 Lux Relative (approx 2.5% of Sun Pixel Value at Day)
// At Sunny 16 (Sun ~ 2.6), this gives ~0.065 pixel brightness, which is visible but dim.
// Scale Milky Way by Sun Intensity
// Calibration: 1.0 User Intensity = 1.5e-3 cd/m^2 (Nits) approx.
// Target: 1.5e-3 Nits. SunIntensity = 100,000.
// Scale = 1.5e-3 / 1.0e5 = 1.5e-8.
const mwIntensity = this.milkyWayEnabled ? this.milkyWayControl[0] : 0.0;
const mwUniform = [
mwIntensity * this.sunIntensity * 0.025,
mwIntensity * this.sunIntensity * 1.5e-8,
this.milkyWayControl[1],
this.milkyWayControl[2]
];
@@ -634,6 +649,7 @@ class SimulatedSkybox {
const moonHaloUpload = [...this.moonHalo];
moonHaloUpload[2] *= moonRadConv;
this.materialInstance.setFloat4Parameter('sunHalo2', new Float32Array(moonHaloUpload));
this.materialInstance.setFloatParameter('exposure', this.exposure !== undefined ? this.exposure : 1.0);
// Solar Eclipse (CPU Calculation)
const sunRadius = Math.acos(this.sunHalo[0]);
+26 -5
View File
@@ -218,6 +218,7 @@ class App {
const exposure = this.getExposure();
const preExposedIntensity = this.params.sunIntensity * exposure;
this.skybox.setSunIntensity(preExposedIntensity);
this.skybox.setExposure(exposure); // Update Skybox Exposure Uniform
// Moon Exposure
if (this.mParams) {
@@ -389,7 +390,7 @@ class App {
};
mwFolder.add(this.mwParams, 'enabled').name('Enabled').onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 5.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'intensity', 0.0, 100.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'saturation', 0.0, 2.0).onChange(updateMW);
mwFolder.add(this.mwParams, 'blackPoint', 0.0, 0.5).name('Black Point').onChange(updateMW);
mwFolder.add(this.mwParams, 'siderealTime', 0.0, 24.0).name('Sidereal Time').listen().onChange(updateMW);
@@ -464,18 +465,23 @@ class App {
const starFolder = gui.addFolder('Stars');
this.sParams = {
enabled: true,
density: 0.001
density: 0.001,
intensity: 0.0 // 2^0 = 1.0
};
// Initialize defaults (Density 0.001, Enabled True)
sky.setStarControl(0.001, true);
sky.setStarIntensity(Math.pow(2.0, 0.0));
const updateStars = () => {
sky.setStarControl(this.sParams.density, this.sParams.enabled);
sky.setStarIntensity(Math.pow(2.0, this.sParams.intensity));
};
starFolder.add(this.sParams, 'enabled').name('Enabled').onChange(updateStars);
starFolder.add(this.sParams, 'density', 0.0, 0.01, 0.0001).name('Density').onChange(updateStars);
starFolder.add(this.sParams, 'intensity', 0.0, 24.0).name('Intensity (Exp)').onChange(updateStars);
starFolder.close();
this.updateStars = updateStars;
const artFolder = gui.addFolder('Artistic');
// Set Horizon Glow default to 0.0
@@ -489,7 +495,7 @@ class App {
artFolder.add(sky.msFactors, 2, 0.0, 1.0).name('Horizon Glow').onChange(v => sky.setHorizonGlow(v));
artFolder.add(sky, 'contrast', 0.1, 2.0).onChange(v => sky.setContrast(v));
artFolder.addColor(sky, 'nightColor').onChange(v => sky.setNightColor(v));
const shmFolder = artFolder.addFolder('Shimmer');
// Set Shimmer Strength default to 0.0
@@ -502,7 +508,7 @@ class App {
const camFolder = gui.addFolder('Camera');
camFolder.add(this.params, 'focalLength', 8.0, 300.0).name('Focal Length').onChange(() => this.updateCameraProjection());
camFolder.add(this.params, 'aperture', 1.4, 32.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 1.0, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'shutterSpeed', 0.05, 1000.0).onChange(() => this.updateCameraExposure());
camFolder.add(this.params, 'iso', 50.0, 3200.0).onChange(() => this.updateCameraExposure());
const bloomFolder = camFolder.addFolder('Bloom');
@@ -692,13 +698,15 @@ class App {
const s = this.sParams;
const b = this.bParams;
const m = this.mParams;
const mw = this.mwParams;
const sk = this.skybox;
return {
p: { a: p.aperture, ss: p.shutterSpeed, i: p.iso, st: p.sunTheta, sp: p.sunPhi, fl: p.focalLength, si: p.sunIntensity },
c: { v: c.volumetrics, co: c.coverage, d: c.density, h: c.height, s: c.speed, e: c.evolution },
w: { dt: w.derivativeTrick, st: w.strength, s: w.speed, o: w.octaves },
s: { e: s.enabled, d: s.density },
s: { e: s.enabled, d: s.density, i: s.intensity },
mw: { e: mw.enabled, i: mw.intensity, s: mw.saturation, bp: mw.blackPoint, st: mw.siderealTime, l: mw.latitude },
b: { e: b.enabled, lf: b.lensFlare },
m: { e: m.enabled, az: m.azimuth, h: m.height, r: m.radius, i: m.intensity },
cm: { t: this.camState.theta, p: this.camState.phi },
@@ -723,6 +731,7 @@ class App {
const c = state.c;
const w = state.w;
const s = state.s;
const mw = state.mw;
const b = state.b;
const m = state.m;
const k = state.k;
@@ -757,6 +766,18 @@ class App {
if (s) {
if (s.e !== undefined) this.sParams.enabled = s.e;
if (s.d !== undefined) this.sParams.density = s.d;
if (s.i !== undefined) this.sParams.intensity = s.i;
if (this.updateStars) this.updateStars();
}
if (mw) {
if (mw.e !== undefined) this.mwParams.enabled = mw.e;
if (mw.i !== undefined) this.mwParams.intensity = mw.i;
if (mw.s !== undefined) this.mwParams.saturation = mw.s;
if (mw.bp !== undefined) this.mwParams.blackPoint = mw.bp;
if (mw.st !== undefined) this.mwParams.siderealTime = mw.st;
if (mw.l !== undefined) this.mwParams.latitude = mw.l;
if (this.updateMW) this.updateMW();
}
if (b) {
+48 -7
View File
@@ -96,10 +96,20 @@ material {
name : starControl, // x=Density, y=Enabled, z=Frequency, w=PixelScale
precision : high
},
{
type : float,
name : starIntensity,
precision : high
},
{
type : sampler2d,
name : moonTexture
},
{
type : float,
name : exposure,
precision : high
},
{
type : sampler2d,
name : moonNormal
@@ -162,9 +172,9 @@ fragment {
// --- CONFIGURATION ---
// Stars
#define STAR_GLOBAL_INTENSITY 150.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.5 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 4.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_GLOBAL_INTENSITY 100.0 // Master brightness multiplier [0.0 - 500.0]
#define STAR_BRIGHTNESS_BASE 0.01 // Minimum random brightness [0.0 - 1.0]
#define STAR_BRIGHTNESS_VAR 15.0 // Random brightness variance range [0.0 - 10.0]
#define STAR_FADE_SUN_ELV_HIGH 0.10 // Sun elevation (sin) where stars are fully hidden [0.0 - 0.5]
#define STAR_FADE_SUN_ELV_LOW -0.20 // Sun elevation (sin) where stars are fully visible [-0.5 - 0.0]
#define STAR_CLOUD_OCCLUSION 0.1 // Visibility when covered by clouds [0.0 - 1.0]
@@ -961,7 +971,11 @@ fragment {
highp vec4 sunHalo, highp vec4 cloudControl, highp vec4 cloudControl2,
highp vec4 shimmerControl, highp vec4 waterControl,
highp vec3 L2, highp float sunIntensity2, highp vec4 sunHalo2,
sampler2D moonTex, sampler2D moonNormal) {
sampler2D moonTex, sampler2D moonNormal,
highp vec3 nightColor,
highp mat3 milkyWayRotation,
highp vec3 milkyWayControl,
sampler2D milkyWayTexture) {
// Project to plane y=0
highp float t = WATER_PLANE_HEIGHT / min(V.y, -0.0002); // Reduced clamp to minimize "wall" artifact
@@ -1057,7 +1071,7 @@ fragment {
highp float rHorizonMask = 1.0 - smoothstep(0.0, REFLECTION_HORIZON_FADE, R.y);
if (rHorizonMask > 0.0) {
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask;
reflection += getStarLayer(R, L, reflCloudDensity, outTransmittance, materialParams.starControl) * rHorizonMask * materialParams.exposure;
}
// Sun Disk Reflection
@@ -1088,6 +1102,29 @@ fragment {
// Apply clouds to reflection
reflection = mix(reflection, reflCloudLayer, reflCloudDensity);
// Add Milky Way to Reflection
// Calculate Fade based on Sun Elevation
highp float sunElvSin = L.y;
highp float mwFade = smoothstep(STAR_FADE_SUN_ELV_HIGH, STAR_FADE_SUN_ELV_LOW, sunElvSin);
if (mwFade > 0.0) {
highp vec3 mwColor = getMilkyWay(R, milkyWayRotation, milkyWayTexture, milkyWayControl);
// Apply Atmosphere Transmittance (approximate)
mwColor *= outTransmittance;
// Apply Fades
mwColor *= mwFade;
mwColor *= (1.0 - reflMoonOcclusion); // Occlude by Moon
mwColor *= (1.0 - reflCloudDensity); // Occlude by Clouds
reflection += mwColor;
}
// Add Night Color to Reflection
reflection += nightColor;
// Fresnel
highp float F0 = WATER_FRESNEL_F0; // Water
highp float cosTheta = clamp(dot(-V, N_water), 0.0, 1.0);
@@ -1187,7 +1224,7 @@ fragment {
// 7. Stars
// Add stars before clouds (clouds cover stars)
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl);
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl) * materialParams.exposure * materialParams.starIntensity;
// 7b. Milky Way
// Add Milky Way behind stars (conceptually) but handled similarly
@@ -1232,7 +1269,11 @@ fragment {
materialParams.shimmerControl,
materialParams.waterControl,
L2, materialParams.sunIntensity2, materialParams.sunHalo2,
materialParams_moonTexture, materialParams_moonNormal);
materialParams_moonTexture, materialParams_moonNormal,
materialParams.nightColor,
materialParams.milkyWayRotation,
materialParams.milkyWayControl,
materialParams_milkyWayTexture);
color = applyDynamicToneMapping(color, L, materialParams.contrast);
}
+2
View File
@@ -93,6 +93,7 @@ set(SRCS
src/MorphTargetBuffer.cpp
src/PostProcessManager.cpp
src/ProgramSpecialization.cpp
src/LocalProgramCache.cpp
src/RenderPass.cpp
src/RenderPrimitive.cpp
src/RenderTarget.cpp
@@ -186,6 +187,7 @@ set(PRIVATE_HDRS
src/MaterialCache.h
src/MaterialDefinition.h
src/MaterialParser.h
src/LocalProgramCache.h
src/MaterialInstanceManager.h
src/PIDController.h
src/PostProcessManager.h
+153 -152
View File
@@ -564,169 +564,170 @@ endif()
# ==================================================================================================
# Test
# ==================================================================================================
option(INSTALL_BACKEND_TEST "Install the backend test library so it can be consumed on iOS" OFF)
if (FILAMENT_BUILD_TESTING)
option(INSTALL_BACKEND_TEST "Install the backend test library so it can be consumed on iOS" OFF)
if (APPLE OR LINUX)
set(BACKEND_TEST_SRC
test/BackendTest.cpp
test/ShaderGenerator.cpp
test/TrianglePrimitive.cpp
test/Arguments.cpp
test/ImageExpectations.cpp
test/Lifetimes.cpp
test/PlatformRunner.cpp
test/Shader.cpp
test/SharedShaders.cpp
test/Skip.cpp
test/test_FeedbackLoops.cpp
test/test_Blit.cpp
test/test_MissingRequiredAttributes.cpp
test/test_ReadPixels.cpp
test/test_ReadTexture.cpp
test/test_BufferUpdates.cpp
test/test_Callbacks.cpp
test/test_JobQueue.cpp
test/test_MemoryMappedBuffer.cpp
test/test_MsaaSwapChain.cpp
test/test_MRT.cpp
test/test_PushConstants.cpp
test/test_LoadImage.cpp
test/test_StencilBuffer.cpp
test/test_Scissor.cpp
test/test_MipLevels.cpp
test/test_Handles.cpp
test/test_CircularBuffer.cpp
test/test_CommandBufferQueue.cpp
test/test_Template.cpp
)
if (FILAMENT_SUPPORTS_WEBGPU)
list(APPEND BACKEND_TEST_SRC
test/test_WebGPUAsyncTaskCounter.cpp
test/test_WebGPUComposeSwizzle.cpp)
endif()
if (APPLE)
# Metal-specific tests
list(APPEND BACKEND_TEST_SRC
test/test_MetalBlitter.mm
if (APPLE OR LINUX)
set(BACKEND_TEST_SRC
test/BackendTest.cpp
test/ShaderGenerator.cpp
test/TrianglePrimitive.cpp
test/Arguments.cpp
test/ImageExpectations.cpp
test/Lifetimes.cpp
test/PlatformRunner.cpp
test/Shader.cpp
test/SharedShaders.cpp
test/Skip.cpp
test/test_Autoresolve.cpp
test/test_FeedbackLoops.cpp
test/test_Blit.cpp
test/test_MissingRequiredAttributes.cpp
test/test_ReadPixels.cpp
test/test_ReadTexture.cpp
test/test_BufferUpdates.cpp
test/test_Callbacks.cpp
test/test_JobQueue.cpp
test/test_MemoryMappedBuffer.cpp
test/test_MsaaSwapChain.cpp
test/test_MRT.cpp
test/test_PushConstants.cpp
test/test_LoadImage.cpp
test/test_StencilBuffer.cpp
test/test_Scissor.cpp
test/test_MipLevels.cpp
test/test_Handles.cpp
test/test_CircularBuffer.cpp
test/test_CommandBufferQueue.cpp
test/test_Template.cpp
)
endif()
set(BACKEND_TEST_LIBS
absl::str_format
backend
getopt
gtest
imageio
filamat
SPIRV
spirv-cross-glsl)
# Create input/output directories for test result images.
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/actual_images)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/diff_images)
file(COPY test/expected_images DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images)
endif()
# TODO: Disabling IOS test due to breakage wrt glslang update
if (APPLE AND NOT IOS)
# TODO: we should expand this test to Linux and other platforms.
list(APPEND BACKEND_TEST_SRC
test/test_RenderExternalImage.cpp)
add_library(backend_test STATIC ${BACKEND_TEST_SRC})
target_link_libraries(backend_test PUBLIC ${BACKEND_TEST_LIBS})
target_compile_options(backend_test PRIVATE "-fobjc-arc")
set(BACKEND_TEST_DEPS
OSDependent
SPIRV
SPIRV-Tools
SPIRV-Tools-opt
backend_test
if (FILAMENT_SUPPORTS_WEBGPU)
list(APPEND BACKEND_TEST_SRC
test/test_WebGPUAsyncTaskCounter.cpp
test/test_WebGPUComposeSwizzle.cpp)
endif()
if (APPLE)
# Metal-specific tests
list(APPEND BACKEND_TEST_SRC
test/test_MetalBlitter.mm
)
endif()
set(BACKEND_TEST_LIBS
absl::str_format
backend
getopt
gtest
glslang
spirv-cross-core
spirv-cross-glsl
spirv-cross-msl)
if (NOT IOS)
target_link_libraries(backend_test PRIVATE image imageio)
list(APPEND BACKEND_TEST_DEPS image)
endif()
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test PRIVATE webgpu_dawn dawncpp_headers)
list(APPEND BACKEND_TEST_DEPS webgpu_dawn dawncpp_headers)
imageio
filamat
SPIRV
spirv-cross-glsl)
# Create input/output directories for test result images.
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/actual_images)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/images/diff_images)
file(COPY test/expected_images DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/images)
endif()
set(BACKEND_TEST_COMBINED_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libbackendtest_combined.a")
combine_static_libs(backend_test "${BACKEND_TEST_COMBINED_OUTPUT}" "${BACKEND_TEST_DEPS}")
# TODO: Disabling IOS test due to breakage wrt glslang update
if (APPLE AND NOT IOS)
# TODO: we should expand this test to Linux and other platforms.
list(APPEND BACKEND_TEST_SRC
test/test_RenderExternalImage.cpp)
add_library(backend_test STATIC ${BACKEND_TEST_SRC})
target_link_libraries(backend_test PUBLIC ${BACKEND_TEST_LIBS})
target_compile_options(backend_test PRIVATE "-fobjc-arc")
set(BACKEND_TEST_LIB_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}backend_test${CMAKE_STATIC_LIBRARY_SUFFIX})
set(BACKEND_TEST_DEPS
OSDependent
SPIRV
SPIRV-Tools
SPIRV-Tools-opt
backend_test
getopt
gtest
glslang
spirv-cross-core
spirv-cross-glsl
spirv-cross-msl)
if (INSTALL_BACKEND_TEST)
install(FILES "${BACKEND_TEST_COMBINED_OUTPUT}" DESTINATION lib/${DIST_DIR} RENAME ${BACKEND_TEST_LIB_NAME})
install(FILES test/PlatformRunner.h DESTINATION include/backend_test)
if (NOT IOS)
target_link_libraries(backend_test PRIVATE image imageio)
list(APPEND BACKEND_TEST_DEPS image)
endif()
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test PRIVATE webgpu_dawn dawncpp_headers)
list(APPEND BACKEND_TEST_DEPS webgpu_dawn dawncpp_headers)
endif()
set(BACKEND_TEST_COMBINED_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libbackendtest_combined.a")
combine_static_libs(backend_test "${BACKEND_TEST_COMBINED_OUTPUT}" "${BACKEND_TEST_DEPS}")
set(BACKEND_TEST_LIB_NAME ${CMAKE_STATIC_LIBRARY_PREFIX}backend_test${CMAKE_STATIC_LIBRARY_SUFFIX})
if (INSTALL_BACKEND_TEST)
install(FILES "${BACKEND_TEST_COMBINED_OUTPUT}" DESTINATION lib/${DIST_DIR} RENAME ${BACKEND_TEST_LIB_NAME})
install(FILES test/PlatformRunner.h DESTINATION include/backend_test)
endif()
set_target_properties(backend_test PROPERTIES FOLDER Tests)
if (APPLE AND NOT IOS)
add_executable(backend_test_mac test/mac_runner.mm)
target_link_libraries(backend_test_mac PRIVATE "-framework Metal -framework AppKit -framework QuartzCore")
# Because each test case is a separate file, the -force_load flag is necessary to prevent the
# linker from removing "unused" symbols.
target_link_libraries(backend_test_mac PRIVATE -force_load backend_test)
set_target_properties(backend_test_mac PROPERTIES FOLDER Tests)
# This is needed after XCode 15.3
set_target_properties(backend_test_mac PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE)
set_target_properties(backend_test_mac PROPERTIES INSTALL_RPATH /usr/local/lib)
endif()
endif()
set_target_properties(backend_test PROPERTIES FOLDER Tests)
if (LINUX)
add_executable(backend_test_linux test/linux_runner.cpp ${BACKEND_TEST_SRC})
target_link_libraries(backend_test_linux PRIVATE ${BACKEND_TEST_LIBS})
set_target_properties(backend_test_linux PROPERTIES FOLDER Tests)
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test_linux PRIVATE webgpu_dawn dawncpp_headers)
endif()
endif()
# ==================================================================================================
# Compute tests
#
#if (NOT IOS AND NOT WEBGL)
#
#add_executable(compute_test
# test/ComputeTest.cpp
# test/Arguments.cpp
# test/test_ComputeBasic.cpp
# )
#
#target_link_libraries(compute_test PRIVATE
# backend
# getopt
# gtest
# )
#
#set_target_properties(compute_test PROPERTIES FOLDER Tests)
#
#endif()
# ==================================================================================================
# Metal utils tests
if (APPLE AND NOT IOS)
add_executable(backend_test_mac test/mac_runner.mm)
target_link_libraries(backend_test_mac PRIVATE "-framework Metal -framework AppKit -framework QuartzCore")
# Because each test case is a separate file, the -force_load flag is necessary to prevent the
# linker from removing "unused" symbols.
target_link_libraries(backend_test_mac PRIVATE -force_load backend_test)
set_target_properties(backend_test_mac PROPERTIES FOLDER Tests)
add_executable(metal_utils_test test/MetalTest.mm)
# This is needed after XCode 15.3
set_target_properties(backend_test_mac PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE)
set_target_properties(backend_test_mac PROPERTIES INSTALL_RPATH /usr/local/lib)
target_compile_options(metal_utils_test PRIVATE "-fobjc-arc")
target_link_libraries(metal_utils_test PRIVATE
backend
getopt
gtest
)
set_target_properties(metal_utils_test PROPERTIES FOLDER Tests)
endif()
endif()
if (LINUX)
add_executable(backend_test_linux test/linux_runner.cpp ${BACKEND_TEST_SRC})
target_link_libraries(backend_test_linux PRIVATE ${BACKEND_TEST_LIBS})
set_target_properties(backend_test_linux PROPERTIES FOLDER Tests)
if (FILAMENT_SUPPORTS_WEBGPU)
target_link_libraries(backend_test_linux PRIVATE webgpu_dawn dawncpp_headers)
endif()
endif()
# ==================================================================================================
# Compute tests
#
#if (NOT IOS AND NOT WEBGL)
#
#add_executable(compute_test
# test/ComputeTest.cpp
# test/Arguments.cpp
# test/test_ComputeBasic.cpp
# )
#
#target_link_libraries(compute_test PRIVATE
# backend
# getopt
# gtest
# )
#
#set_target_properties(compute_test PROPERTIES FOLDER Tests)
#
#endif()
# ==================================================================================================
# Metal utils tests
if (APPLE AND NOT IOS)
add_executable(metal_utils_test test/MetalTest.mm)
target_compile_options(metal_utils_test PRIVATE "-fobjc-arc")
target_link_libraries(metal_utils_test PRIVATE
backend
getopt
gtest
)
set_target_properties(metal_utils_test PROPERTIES FOLDER Tests)
endif()
endif()
+20 -2
View File
@@ -25,6 +25,11 @@
#include <mutex>
#ifdef __ANDROID__
#include <pthread.h>
#endif
namespace filament {
using namespace utils;
@@ -105,8 +110,21 @@ JNIEnv* VirtualMachineEnv::getEnvironmentSlow() {
FILAMENT_CHECK_PRECONDITION(mVirtualMachine)
<< "JNI_OnLoad() has not been called";
#if defined(__ANDROID__)
jint const result = mVirtualMachine->AttachCurrentThread(&mJniEnv, nullptr);
#ifdef __ANDROID__
JavaVMAttachArgs args;
args.version = JNI_VERSION_1_6;
args.group = nullptr;
char threadName[16]; // pthread_getname_np returns at most 16 bytes
if (__builtin_available(android 26, *)) {
if (pthread_getname_np(pthread_self(), threadName, sizeof(threadName)) == 0) {
args.name = threadName;
} else {
args.name = nullptr;
}
} else {
args.name = nullptr;
}
jint const result = mVirtualMachine->AttachCurrentThread(&mJniEnv, &args);
#else
jint const result = mVirtualMachine->AttachCurrentThread(reinterpret_cast<void**>(&mJniEnv), nullptr);
#endif
+3 -3
View File
@@ -1092,7 +1092,7 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint
// a multisampled sidecar texture and do a resolve automatically.
if (samples > 1 && texture->samples == 1) {
auto& sidecar = texture->msaaSidecar;
if (!sidecar) {
if (!sidecar || sidecar.sampleCount != samples) {
sidecar = createMultisampledTexture(mtlTexture.pixelFormat, texture->width,
texture->height, samples);
}
@@ -1123,7 +1123,7 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint
// a multisampled sidecar texture and do a resolve automatically.
if (samples > 1 && texture->samples == 1) {
auto& sidecar = texture->msaaSidecar;
if (!sidecar) {
if (!sidecar || sidecar.sampleCount != samples) {
sidecar = createMultisampledTexture(mtlTexture.pixelFormat, texture->width,
texture->height, samples);
}
@@ -1155,7 +1155,7 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint
// a multisampled sidecar texture and do a resolve automatically.
if (samples > 1 && texture->samples == 1) {
auto& sidecar = texture->msaaSidecar;
if (!sidecar) {
if (!sidecar || sidecar.sampleCount != samples) {
sidecar = createMultisampledTexture(mtlTexture.pixelFormat, texture->width,
texture->height, samples);
}
+1 -1
View File
@@ -91,7 +91,7 @@ public:
GLenum getIndicesType() const noexcept {
return indicesType;
}
} gl;
};
static bool queryOpenGLVersion(GLint* major, GLint* minor) noexcept;
@@ -120,33 +120,27 @@ bool PlatformEGL::isOpenGL() const noexcept {
PlatformEGL::ExternalImageEGL::~ExternalImageEGL() = default;
Driver* PlatformEGL::createDriver(void* sharedContext, const DriverConfig& driverConfig) {
static constexpr int kMaxNumEGLDevices = 32;
mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
assert_invariant(mEGLDisplay != EGL_NO_DISPLAY);
EGLint major, minor;
EGLBoolean initialized = false;
EGLBoolean initialized = eglInitialize(mEGLDisplay, &major, &minor);
PFNEGLQUERYDEVICESEXTPROC const eglQueryDevicesEXT =
PFNEGLQUERYDEVICESEXTPROC(eglGetProcAddress("eglQueryDevicesEXT"));
PFNEGLGETPLATFORMDISPLAYEXTPROC const getPlatformDisplay =
PFNEGLGETPLATFORMDISPLAYEXTPROC(eglGetProcAddress("eglGetPlatformDisplay"));
if (eglQueryDevicesEXT != nullptr && getPlatformDisplay != nullptr) {
EGLint numDevices = 0;
EGLDeviceEXT eglDevices[kMaxNumEGLDevices];
if (eglQueryDevicesEXT(kMaxNumEGLDevices, eglDevices, &numDevices)) {
for (int i = 0; i < numDevices && !initialized; ++i) {
mEGLDisplay = getPlatformDisplay(EGL_PLATFORM_DEVICE_EXT, eglDevices[i], nullptr);
if (!initialized) {
EGLDeviceEXT eglDevice;
EGLint numDevices;
PFNEGLQUERYDEVICESEXTPROC const eglQueryDevicesEXT =
PFNEGLQUERYDEVICESEXTPROC(eglGetProcAddress("eglQueryDevicesEXT"));
if (eglQueryDevicesEXT != nullptr) {
eglQueryDevicesEXT(1, &eglDevice, &numDevices);
if(auto* getPlatformDisplay = reinterpret_cast<PFNEGLGETPLATFORMDISPLAYEXTPROC>(
eglGetProcAddress("eglGetPlatformDisplay"))) {
mEGLDisplay = getPlatformDisplay(EGL_PLATFORM_DEVICE_EXT, eglDevice, nullptr);
initialized = eglInitialize(mEGLDisplay, &major, &minor);
}
}
}
if (!initialized) {
mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
assert_invariant(mEGLDisplay != EGL_NO_DISPLAY);
initialized = eglInitialize(mEGLDisplay, &major, &minor);
}
if (UTILS_UNLIKELY(!initialized)) {
LOG(ERROR) << "eglInitialize failed";
return nullptr;
@@ -712,10 +706,11 @@ bool PlatformEGL::setExternalImage(ExternalImageHandleRef externalImage,
// -----------------------------------------------------------------------------------------------
void PlatformEGL::initializeGlExtensions() noexcept {
// We're guaranteed to be on an ES platform, since we're using EGL
const char* const extensions = (const char*)glGetString(GL_EXTENSIONS);
GLUtils::unordered_string_set const glExtensions = GLUtils::split(extensions);
ext.gl.OES_EGL_image_external_essl3 = glExtensions.has("GL_OES_EGL_image_external_essl3");
if (extensions) {
GLUtils::unordered_string_set const glExtensions = GLUtils::split(extensions);
ext.gl.OES_EGL_image_external_essl3 = glExtensions.has("GL_OES_EGL_image_external_essl3");
}
}
EGLContext PlatformEGL::getContextForType(ContextType const type) const noexcept {
@@ -29,6 +29,12 @@ using namespace bluevk;
namespace filament::backend {
std::shared_ptr<VulkanCmdFence> VulkanCmdFence::completed() noexcept {
auto cmdFence = std::make_shared<VulkanCmdFence>(VK_NULL_HANDLE);
cmdFence->mStatus = VK_SUCCESS;
return cmdFence;
}
FenceStatus VulkanCmdFence::wait(VkDevice device, uint64_t const timeout,
std::chrono::steady_clock::time_point const until) {
@@ -40,6 +40,13 @@ struct VulkanCmdFence {
explicit VulkanCmdFence(VkFence fence) : mFence(fence) { }
~VulkanCmdFence() = default;
// Creates a VulkanCmdFence with its status set to VK_SUCCESS. It holds
// a null handle; it is assumed that any user of this object will avoid
// using the fence handle directly if getStatus() returns VK_SUCCESS, as
// in that case, it's likely the fence is being reused for other passes,
// and is not in the expected state anyway.
static std::shared_ptr<VulkanCmdFence> completed() noexcept;
void setStatus(VkResult const value) {
std::lock_guard const l(mLock);
mStatus = value;
+5 -1
View File
@@ -277,7 +277,11 @@ private:
fvkmemory::resource_ptr<VulkanSemaphore> mLastSubmit;
VkFence mLastFence = VK_NULL_HANDLE;
std::shared_ptr<VulkanCmdFence> mLastFenceStatus;
// Start out with a completed fence, because if no commands have
// been queued or submited, then by definition, all pending work
// is complete.
std::shared_ptr<VulkanCmdFence> mLastFenceStatus =
VulkanCmdFence::completed();
VkPipelineStageFlags mInjectedDependencyWaitStage = 0;
};
+3 -2
View File
@@ -36,6 +36,7 @@ VK_DEFINE_HANDLE(VmaPool)
namespace filament::backend {
struct VulkanCommandBuffer;
struct VulkanRenderPass;
struct VulkanRenderTarget;
struct VulkanSwapChain;
struct VulkanTexture;
@@ -58,11 +59,11 @@ struct VulkanAttachment {
VkImageSubresourceRange getSubresourceRange() const;
};
struct VulkanRenderPass {
struct VulkanRenderPassContext {
// Between the begin and end command render pass we cache the command buffer
VulkanCommandBuffer* commandBuffer;
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget;
VkRenderPass renderPass;
fvkmemory::resource_ptr<VulkanRenderPass> renderPass;
RenderPassParams params;
int currentSubpass;
};
@@ -355,35 +355,8 @@ void VulkanDescriptorSetCache::updateBuffer(fvkmemory::resource_ptr<VulkanDescri
}
void VulkanDescriptorSetCache::updateSampler(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
uint8_t binding, fvkmemory::resource_ptr<VulkanTexture> texture, VkSampler sampler,
VkDescriptorSetLayout externalSamplerLayout) noexcept {
// We have to update a bound set for two use cases
// - streaming API (a changing feed of AHardwareBuffer)
// - external samplers - potential changing of dataspace per-frame
// TODO: Fix the stream flow case base on the above comment!!
if (set->isAnExternalSamplerBound) {
auto layout = set->getLayout();
// Build a new descriptor set from the new layout
VkDescriptorSetLayout const genLayout = set->boundLayout;
VkDescriptorSet const newSet = getVkSet(layout->count, genLayout);
Bitmask const ubo = layout->bitmask.ubo | layout->bitmask.dynamicUbo;
Bitmask samplers = layout->bitmask.sampler;
samplers.unset(binding);
// Each bitmask denotes a binding index, and separated into two stages - vertex and buffer
// We fold the two stages into just the lower half of the bits to denote a combined set of
// bindings.
Bitmask const copyBindings = foldBitsInHalf(ubo | samplers);
VkDescriptorSet const srcSet = set->getVkSet();
copySet(srcSet, newSet, copyBindings);
set->addNewSet(newSet,
[this, layoutCount = layout->count, genLayout, newSet](VulkanDescriptorSet*) {
this->manualRecycle(layoutCount, genLayout, newSet);
});
}
uint8_t binding, fvkmemory::resource_ptr<VulkanTexture> texture,
VkSampler sampler, VkDescriptorSetLayout externalSamplerLayout) noexcept {
VkDescriptorSet const vkset = set->getVkSet();
VkImageSubresourceRange range = texture->getPrimaryViewRange();
VkImageViewType const expectedType = texture->getViewType();
@@ -432,6 +405,28 @@ fvkmemory::resource_ptr<VulkanDescriptorSet> VulkanDescriptorSetCache::createSet
return set;
}
void VulkanDescriptorSetCache::cloneSet(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
fvkutils::SamplerBitmask samplerMask) noexcept {
auto const& layout = set->getLayout();
// Build a new descriptor set from layout
VkDescriptorSetLayout const genLayout = set->boundLayout;
VkDescriptorSet const newSet = getVkSet(layout->count, genLayout);
// Each bitmask denotes a binding index, and separated into two stages - vertex and buffer
// We fold the two stages into just the lower half of the bits to denote a combined set of
// bindings.
Bitmask const ubo = layout->bitmask.ubo | layout->bitmask.dynamicUbo;
// Don't copy the samplers in the mask
Bitmask samplers = layout->bitmask.sampler ^ samplerMask;
Bitmask const copyBindings = foldBitsInHalf(ubo | samplers);
VkDescriptorSet const srcSet = set->getVkSet();
copySet(srcSet, newSet, copyBindings);
set->addNewSet(newSet,
[this, layoutCount = layout->count, genLayout, newSet](
VulkanDescriptorSet*) { this->manualRecycle(layoutCount, genLayout, newSet); });
}
VkDescriptorSet VulkanDescriptorSetCache::getVkSet(DescriptorCount const& count,
VkDescriptorSetLayout vklayout) {
return mDescriptorPool->obtainSet(count, vklayout);
@@ -76,6 +76,11 @@ public:
fvkmemory::resource_ptr<VulkanDescriptorSet> createSet(Handle<HwDescriptorSet> handle,
fvkmemory::resource_ptr<VulkanDescriptorSetLayout> layout);
// Create and set as current a new VkDescriptorSet using the `set` currently bound layout and
// copy all the bindings and ignoring the samplers bindings in the `samplerMask`.
void cloneSet(fvkmemory::resource_ptr<VulkanDescriptorSet> set,
fvkutils::SamplerBitmask samplerMask) noexcept;
// This method is meant to be used with external samplers
VkDescriptorSet getVkSet(DescriptorCount const& count, VkDescriptorSetLayout vklayout);
+19 -14
View File
@@ -1913,9 +1913,6 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
auto rt = resource_ptr<VulkanRenderTarget>::cast(&mResourceManager, rth);
VulkanCommandBuffer* commandBuffer = rt->isProtected() ?
&mCommands.getProtected() : &mCommands.get();
// Filament has the expectation that the contents of the swap chain are not preserved on the
// first render pass. Note however that its contents are often preserved on subsequent render
// passes, due to multiple views.
@@ -1930,6 +1927,11 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
}
}
// Note that this needs to come after the acquireNextswapchainImage() above because that path
// might flush the current command buffer.
VulkanCommandBuffer* commandBuffer =
rt->isProtected() ? &mCommands.getProtected() : &mCommands.get();
// Note that retrieving the extent must come after the acquireNextSwapchainImage() above;
// otherwise it might be 0.
VkExtent2D const extent = rt->getExtent();
@@ -1975,17 +1977,19 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
rpkey.initialDepthLayout = currentDepthLayout;
rpkey.subpassMask = uint8_t(params.subpassMask);
VkRenderPass renderPass = mFramebufferCache.getRenderPass(rpkey);
fvkmemory::resource_ptr<VulkanRenderPass> renderPass =
mFramebufferCache.getRenderPass(rpkey, &mResourceManager);
mPipelineCache.bindRenderPass(renderPass, 0);
// Create the VkFramebuffer or fetch it from cache.
VulkanFboCache::FboKey fbkey = rt->getFboKey();
fbkey.renderPass = renderPass;
fbkey.renderPass = renderPass->getVkRenderPass();
fbkey.layers = 1;
rt->emitBarriersBeginRenderPass(*commandBuffer);
VkFramebuffer vkfb = mFramebufferCache.getFramebuffer(fbkey);
fvkmemory::resource_ptr<VulkanFramebuffer> vkfb =
mFramebufferCache.getFramebuffer(fbkey, &mResourceManager, rt);
// Assign a label to the framebuffer for debugging purposes.
#if FVK_ENABLED(FVK_DEBUG_GROUP_MARKERS | FVK_DEBUG_DEBUG_UTILS)
@@ -1998,12 +2002,14 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
// The current command buffer now has references to the render target and its attachments.
commandBuffer->acquire(rt);
commandBuffer->acquire(renderPass);
commandBuffer->acquire(vkfb);
// Populate the structures required for vkCmdBeginRenderPass.
VkRenderPassBeginInfo renderPassInfo {
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
.renderPass = renderPass,
.framebuffer = vkfb,
.renderPass = renderPass->getVkRenderPass(),
.framebuffer = vkfb->getVkFramebuffer(),
// The renderArea field constrains the LoadOp, but scissoring does not.
// Therefore, we do not set the scissor rect here, we only need it in draw().
@@ -2058,7 +2064,7 @@ void VulkanDriver::beginRenderPass(Handle<HwRenderTarget> rth, const RenderPassP
mCurrentRenderPass = {
.commandBuffer = commandBuffer,
.renderTarget = rt,
.renderPass = renderPassInfo.renderPass,
.renderPass = renderPass,
.params = params,
.currentSubpass = 0,
};
@@ -2078,8 +2084,7 @@ void VulkanDriver::endRenderPass(int) {
rt->emitBarriersEndRenderPass(*mCurrentRenderPass.commandBuffer);
mCurrentRenderPass.renderTarget = {};
mCurrentRenderPass.renderPass = VK_NULL_HANDLE;
mCurrentRenderPass.renderPass = {};
mCurrentRenderPass.commandBuffer = nullptr;
}
@@ -2244,7 +2249,7 @@ void VulkanDriver::resolve(
Handle<HwTexture> src, uint8_t dstLevel, uint8_t dstLayer) {
FVK_SYSTRACE_SCOPE();
FILAMENT_CHECK_PRECONDITION(mCurrentRenderPass.renderPass == VK_NULL_HANDLE)
FILAMENT_CHECK_PRECONDITION(!mCurrentRenderPass.renderPass)
<< "resolve() cannot be invoked inside a render pass.";
auto srcTexture = resource_ptr<VulkanTexture>::cast(&mResourceManager, src);
@@ -2287,7 +2292,7 @@ void VulkanDriver::blit(
math::uint2 size) {
FVK_SYSTRACE_SCOPE();
FILAMENT_CHECK_PRECONDITION(mCurrentRenderPass.renderPass == VK_NULL_HANDLE)
FILAMENT_CHECK_PRECONDITION(!mCurrentRenderPass.renderPass)
<< "blit() cannot be invoked inside a render pass.";
auto srcTexture = resource_ptr<VulkanTexture>::cast(&mResourceManager, src);
@@ -2329,7 +2334,7 @@ void VulkanDriver::blitDEPRECATED(TargetBufferFlags buffers,
// Note: blitDEPRECATED is only used for Renderer::copyFrame()
FILAMENT_CHECK_PRECONDITION(mCurrentRenderPass.renderPass == VK_NULL_HANDLE)
FILAMENT_CHECK_PRECONDITION(!mCurrentRenderPass.renderPass)
<< "blitDEPRECATED() cannot be invoked inside a render pass.";
FILAMENT_CHECK_PRECONDITION(buffers == TargetBufferFlags::COLOR0)
+1 -1
View File
@@ -139,7 +139,7 @@ private:
resource_ptr<VulkanSwapChain> mCurrentSwapChain;
resource_ptr<VulkanRenderTarget> mDefaultRenderTarget;
VulkanRenderPass mCurrentRenderPass = {};
VulkanRenderPassContext mCurrentRenderPass = {};
VmaAllocator mAllocator = VK_NULL_HANDLE;
VkDebugReportCallbackEXT mDebugCallback = VK_NULL_HANDLE;
@@ -94,6 +94,9 @@ void VulkanExternalImageManager::updateSetAndLayout(
fvkmemory::resource_ptr<VulkanDescriptorSetLayout> const& layout = set->getLayout();
set->boundLayout = mDescriptorSetLayoutCache->getVkLayout(layout->bitmask,
actualExternalSamplers, outSamplers);
mDescriptorSetCache->cloneSet(set, actualExternalSamplers);
// Update the external samplers in the set
for (auto& [binding, sampler, image]: samplerAndBindings) {
// We cannot call updateSamplerForExternalSamplerSet because some samplers are non NULL
+18 -18
View File
@@ -17,6 +17,7 @@
#include "VulkanFboCache.h"
#include "VulkanConstants.h"
#include "VulkanHandles.h"
#include "vulkan/utils/Image.h"
#include <utils/Panic.h>
@@ -69,9 +70,11 @@ VulkanFboCache::~VulkanFboCache() {
<< "Please explicitly call terminate() while the VkDevice is still alive.";
}
VkFramebuffer VulkanFboCache::getFramebuffer(FboKey const& config) noexcept {
fvkmemory::resource_ptr<VulkanFramebuffer> VulkanFboCache::getFramebuffer(FboKey const& config,
fvkmemory::ResourceManager* resManager,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget) noexcept {
FboMap::iterator iter = mFramebufferCache.find(config);
if (UTILS_LIKELY(iter != mFramebufferCache.end() && iter->second.handle != VK_NULL_HANDLE)) {
if (UTILS_LIKELY(iter != mFramebufferCache.end())) {
iter.value().timestamp = mCurrentTime;
return iter->second.handle;
}
@@ -117,13 +120,17 @@ VkFramebuffer VulkanFboCache::getFramebuffer(FboKey const& config) noexcept {
VkResult error = vkCreateFramebuffer(mDevice, &info, VKALLOC, &framebuffer);
FILAMENT_CHECK_POSTCONDITION(error == VK_SUCCESS) << "Unable to create framebuffer."
<< " error=" << static_cast<int32_t>(error);
mFramebufferCache[config] = {framebuffer, mCurrentTime};
return framebuffer;
fvkmemory::resource_ptr<VulkanFramebuffer> fbh =
fvkmemory::resource_ptr<VulkanFramebuffer>::construct(resManager, mDevice, framebuffer,
renderTarget);
mFramebufferCache[config] = { fbh, mCurrentTime };
return fbh;
}
VkRenderPass VulkanFboCache::getRenderPass(RenderPassKey const& config) noexcept {
fvkmemory::resource_ptr<VulkanRenderPass> VulkanFboCache::getRenderPass(
RenderPassKey const& config, fvkmemory::ResourceManager* resManager) noexcept {
auto iter = mRenderPassCache.find(config);
if (UTILS_LIKELY(iter != mRenderPassCache.end() && iter->second.handle != VK_NULL_HANDLE)) {
if (UTILS_LIKELY(iter != mRenderPassCache.end())) {
iter.value().timestamp = mCurrentTime;
return iter->second.handle;
}
@@ -326,7 +333,9 @@ VkRenderPass VulkanFboCache::getRenderPass(RenderPassKey const& config) noexcept
VkResult error = vkCreateRenderPass(mDevice, &renderPassInfo, VKALLOC, &renderPass);
FILAMENT_CHECK_POSTCONDITION(error == VK_SUCCESS) << "Unable to create render pass."
<< " error=" << error;
mRenderPassCache[config] = {renderPass, mCurrentTime};
fvkmemory::resource_ptr<VulkanRenderPass> rph =
fvkmemory::resource_ptr<VulkanRenderPass>::construct(resManager, mDevice, renderPass);
mRenderPassCache[config] = {rph, mCurrentTime};
#if FVK_ENABLED(FVK_DEBUG_FBO_CACHE)
FVK_LOGD << "Created render pass " << renderPass << " with ";
@@ -343,13 +352,12 @@ VkRenderPass VulkanFboCache::getRenderPass(RenderPassKey const& config) noexcept
<< "colorAttachmentCount[0] = " << subpasses[0].colorAttachmentCount;
#endif
return renderPass;
return rph;
}
void VulkanFboCache::resetFramebuffers() noexcept {
for (const auto& pair: mFramebufferCache) {
mRenderPassRefCount[pair.first.renderPass]--;
vkDestroyFramebuffer(mDevice, pair.second.handle, VKALLOC);
}
mFramebufferCache.clear();
}
@@ -357,9 +365,6 @@ void VulkanFboCache::resetFramebuffers() noexcept {
void VulkanFboCache::terminate() noexcept {
resetFramebuffers();
for (const auto& pair: mRenderPassCache) {
vkDestroyRenderPass(mDevice, pair.second.handle, VKALLOC);
}
mRenderPassRefCount.clear();
mRenderPassCache.clear();
}
@@ -380,8 +385,6 @@ void VulkanFboCache::gc() noexcept {
const FboVal fbo = iter->second;
if (fbo.timestamp < evictTime && fbo.handle) {
mRenderPassRefCount[iter->first.renderPass]--;
vkDestroyFramebuffer(mDevice, fbo.handle, VKALLOC);
iter.value().handle = VK_NULL_HANDLE;
// erase(iterator) returns the iterator to the next element.
iter = mFramebufferCache.erase(iter);
@@ -391,11 +394,8 @@ void VulkanFboCache::gc() noexcept {
}
for (RenderPassMap::iterator iter = mRenderPassCache.begin(); iter != mRenderPassCache.end(); ) {
const VkRenderPass handle = iter->second.handle;
const VkRenderPass handle = iter->second.handle->getVkRenderPass();
if (iter->second.timestamp < evictTime && handle && mRenderPassRefCount[handle] == 0) {
vkDestroyRenderPass(mDevice, handle, VKALLOC);
iter.value().handle = VK_NULL_HANDLE;
// erase(iterator) returns the iterator to the next element.
iter = mRenderPassCache.erase(iter);
mRenderPassRefCount.erase(handle);
+13 -4
View File
@@ -18,6 +18,9 @@
#define TNT_FILAMENT_BACKEND_VULKANFBOCACHE_H
#include "VulkanContext.h"
#include "vulkan/memory/Resource.h"
#include "vulkan/memory/ResourceManager.h"
#include "vulkan/memory/ResourcePointer.h"
#include <utils/Hash.h>
@@ -27,6 +30,9 @@
namespace filament::backend {
struct VulkanFramebuffer;
struct VulkanRenderPass;
// Simple manager for VkFramebuffer and VkRenderPass objects.
//
// Note that a VkFramebuffer is just a binding between a render pass and a set of image views. So,
@@ -57,7 +63,7 @@ public:
uint8_t padding[2];
};
struct RenderPassVal {
VkRenderPass handle;
fvkmemory::resource_ptr<VulkanRenderPass> handle;
uint32_t timestamp;
};
static_assert(0 == MRT::MAX_SUPPORTED_RENDER_TARGET_COUNT % 8);
@@ -83,7 +89,7 @@ public:
VkImageView depth; // 8 bytes
};
struct FboVal {
VkFramebuffer handle;
fvkmemory::resource_ptr<VulkanFramebuffer> handle;
uint32_t timestamp;
};
static_assert(sizeof(VkRenderPass) == 8, "VkRenderPass has unexpected size.");
@@ -98,10 +104,13 @@ public:
~VulkanFboCache();
// Retrieves or creates a VkFramebuffer handle.
VkFramebuffer getFramebuffer(FboKey const& config) noexcept;
fvkmemory::resource_ptr<VulkanFramebuffer> getFramebuffer(FboKey const& config,
fvkmemory::ResourceManager* resManager,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget) noexcept;
// Retrieves or creates a VkRenderPass handle.
VkRenderPass getRenderPass(RenderPassKey const& config) noexcept;
fvkmemory::resource_ptr<VulkanRenderPass> getRenderPass(
RenderPassKey const& config, fvkmemory::ResourceManager* resManager) noexcept;
// Evicts old unused Vulkan objects. Call this once per frame.
void gc() noexcept;
+18 -1
View File
@@ -565,7 +565,7 @@ void VulkanRenderTarget::transformViewportToPlatform(VkViewport* bounds) const {
flipVertically(bounds, getExtent().height);
}
uint8_t VulkanRenderTarget::getColorTargetCount(const VulkanRenderPass& pass) const {
uint8_t VulkanRenderTarget::getColorTargetCount(const VulkanRenderPassContext& pass) const {
if (!mOffscreen) {
return 1;
}
@@ -715,4 +715,21 @@ VulkanRenderPrimitive::VulkanRenderPrimitive(PrimitiveType pt,
vertexBuffer(vb),
indexBuffer(ib) {}
VulkanFramebuffer::VulkanFramebuffer(VkDevice device, VkFramebuffer framebuffer,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget)
: mDevice(device),
mFramebuffer(framebuffer),
mRenderTarget(renderTarget) {}
VulkanFramebuffer::~VulkanFramebuffer() {
vkDestroyFramebuffer(mDevice, mFramebuffer, VKALLOC);
}
VulkanRenderPass::VulkanRenderPass(VkDevice device, VkRenderPass renderPass)
: mDevice(device), mRenderPass(renderPass) {}
VulkanRenderPass::~VulkanRenderPass() {
vkDestroyRenderPass(mDevice, mRenderPass, VKALLOC);
}
} // namespace filament::backend
+36 -3
View File
@@ -195,7 +195,7 @@ public:
}
// The current layout used by the descriptor set. This one will match the bindings, including
// external samplers data.
// external samplers data.
// This will not necessarilly be the same as `mLayout`.
VkDescriptorSetLayout boundLayout = VK_NULL_HANDLE;
@@ -203,7 +203,7 @@ public:
fvkutils::UniformBufferBitmask const& dynamicUboMask;
uint8_t const uniqueDynamicUboCount;
// Flag to indicate if the current layout needs to be recreated or not.
// This should only set to `true` when a external sampler image is bound to the descriptor set.
bool isLayoutDirty = false;
@@ -419,7 +419,7 @@ struct VulkanRenderTarget : private HwRenderTarget, fvkmemory::Resource {
return mInfo->fbkey.samples;
}
uint8_t getColorTargetCount(VulkanRenderPass const& pass) const;
uint8_t getColorTargetCount(VulkanRenderPassContext const& pass) const;
inline bool hasDepth() const { return mInfo->depthIndex != Auxiliary::UNDEFINED_INDEX; }
@@ -585,6 +585,39 @@ struct VulkanRenderPrimitive : public HwRenderPrimitive, fvkmemory::Resource {
fvkmemory::resource_ptr<VulkanIndexBuffer> indexBuffer;
};
struct VulkanFramebuffer : public fvkmemory::Resource {
VulkanFramebuffer(VkDevice device,
VkFramebuffer framebuffer,
fvkmemory::resource_ptr<VulkanRenderTarget> renderTarget);
~VulkanFramebuffer();
inline VkFramebuffer getVkFramebuffer() const noexcept {
return mFramebuffer;
}
private:
VkDevice mDevice;
VkFramebuffer mFramebuffer;
// We need to keep a reference to the renderTarget because the key of the framebuffer in the
// cache has references to the image views that are derived from the textures of the render
// target.
fvkmemory::resource_ptr<VulkanRenderTarget> mRenderTarget;
};
struct VulkanRenderPass : public fvkmemory::Resource {
VulkanRenderPass(VkDevice device, VkRenderPass renderPass);
~VulkanRenderPass();
inline VkRenderPass getVkRenderPass() const noexcept {
return mRenderPass;
}
private:
VkDevice mDevice;
VkRenderPass mRenderPass;
};
} // namespace filament::backend
#endif // TNT_FILAMENT_BACKEND_VULKANHANDLES_H
@@ -459,8 +459,10 @@ void VulkanPipelineCache::bindStencilState(StencilState const& stencilState) noe
mPipelineRequirements.stencilState = stencilState;
}
void VulkanPipelineCache::bindRenderPass(VkRenderPass renderPass, int subpassIndex) noexcept {
mPipelineRequirements.renderPass = renderPass;
void VulkanPipelineCache::bindRenderPass(
fvkmemory::resource_ptr<VulkanRenderPass> renderPass,
int subpassIndex) noexcept {
mPipelineRequirements.renderPass = renderPass->getVkRenderPass();
mPipelineRequirements.subpassIndex = subpassIndex;
}
@@ -123,7 +123,8 @@ public:
void bindProgram(fvkmemory::resource_ptr<VulkanProgram> program) noexcept;
void bindRasterState(RasterState const& rasterState) noexcept;
void bindStencilState(StencilState const& stencilState) noexcept;
void bindRenderPass(VkRenderPass renderPass, int subpassIndex) noexcept;
void bindRenderPass(fvkmemory::resource_ptr<VulkanRenderPass> renderPass,
int subpassIndex) noexcept;
void bindPrimitiveTopology(VkPrimitiveTopology topology) noexcept;
void bindVertexArray(VkVertexInputAttributeDescription const* attribDesc,
VkVertexInputBindingDescription const* bufferDesc, uint8_t count);
@@ -42,6 +42,8 @@ template ResourceType getTypeEnum<VulkanSync>() noexcept;
template ResourceType getTypeEnum<VulkanMemoryMappedBuffer>() noexcept;
template ResourceType getTypeEnum<VulkanSemaphore>() noexcept;
template ResourceType getTypeEnum<VulkanStream>() noexcept;
template ResourceType getTypeEnum<VulkanFramebuffer>() noexcept;
template ResourceType getTypeEnum<VulkanRenderPass>() noexcept;
template<typename D>
ResourceType getTypeEnum() noexcept {
@@ -108,6 +110,12 @@ ResourceType getTypeEnum() noexcept {
if constexpr (std::is_same_v<D, VulkanStream>) {
return ResourceType::STREAM;
}
if constexpr (std::is_same_v<D, VulkanFramebuffer>) {
return ResourceType::FRAMEBUFFER;
}
if constexpr (std::is_same_v<D, VulkanRenderPass>) {
return ResourceType::RENDER_PASS;
}
return ResourceType::UNDEFINED_TYPE;
}
@@ -155,6 +163,10 @@ std::string_view getTypeStr(ResourceType type) {
return "Semaphore";
case ResourceType::STREAM:
return "VulkanStream";
case ResourceType::FRAMEBUFFER:
return "Framebuffer";
case ResourceType::RENDER_PASS:
return "RenderPass";
case ResourceType::UNDEFINED_TYPE:
return "";
}
@@ -56,7 +56,9 @@ enum class ResourceType : uint8_t {
MEMORY_MAPPED_BUFFER = 18,
SEMAPHORE = 19,
STREAM = 20,
UNDEFINED_TYPE = 21, // Must be the last enum because we use it for iterating over the enums.
FRAMEBUFFER = 21,
RENDER_PASS = 22,
UNDEFINED_TYPE = 23, // Must be the last enum because we use it for iterating over the enums.
};
template<typename D>
@@ -126,6 +126,12 @@ void ResourceManager::destroyWithType(ResourceType type, HandleId id) {
case ResourceType::STREAM:
destruct<VulkanStream>(Handle<VulkanStream>(id));
break;
case ResourceType::FRAMEBUFFER:
destruct<VulkanFramebuffer>(Handle<VulkanFramebuffer>(id));
break;
case ResourceType::RENDER_PASS:
destruct<VulkanRenderPass>(Handle<VulkanRenderPass>(id));
break;
case ResourceType::UNDEFINED_TYPE:
break;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

+120
View File
@@ -0,0 +1,120 @@
/*
* Copyright (C) 2025 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.
*/
#include "BackendTest.h"
#include "Shader.h"
#include "SharedShaders.h"
#include "Skip.h"
#include "TrianglePrimitive.h"
#include <backend/PixelBufferDescriptor.h>
#include <utils/FixedCapacityVector.h>
namespace test {
using namespace filament;
using namespace filament::backend;
using namespace filament::math;
TEST_F(BackendTest, AutoresolveDifferingSampleCounts) {
SKIP_IF(SkipEnvironment(OperatingSystem::CI, Backend::OPENGL), "see b/486954356");
SKIP_IF(SkipEnvironment(OperatingSystem::CI, Backend::VULKAN), "see b/486954356");
auto& api = getDriverApi();
constexpr int kRenderTargetSize = 512;
auto swapChain = addCleanup(createSwapChain());
api.makeCurrent(swapChain, swapChain);
Shader shader = SharedShaders::makeShader(api, *mCleanup,
{
.mVertexType = VertexShaderType::Simple,
.mFragmentType = FragmentShaderType::SolidColored,
.mUniformType = ShaderUniformType::Simple,
});
TrianglePrimitive triangle(api);
PipelineState ps = getColorWritePipelineState();
shader.addProgramToPipelineState(ps);
auto ubuffer = addCleanup(api.createBufferObject(sizeof(SimpleMaterialParams),
BufferObjectBinding::UNIFORM, BufferUsage::STATIC));
shader.bindUniform<SimpleMaterialParams>(api, ubuffer);
// Create a texture with sample count = 1.
Handle<HwTexture> texture = addCleanup(api.createTexture(SamplerType::SAMPLER_2D, 1,
TextureFormat::RGBA8, 1, kRenderTargetSize, kRenderTargetSize, 1,
TextureUsage::COLOR_ATTACHMENT | TextureUsage::SAMPLEABLE));
// First render pass: render a red triangle to a RenderTarget with 8 samples.
{
Handle<HwRenderTarget> renderTarget =
addCleanup(api.createRenderTarget(TargetBufferFlags::COLOR, kRenderTargetSize,
kRenderTargetSize, 8, 1, { { texture } }, {}, {}));
shader.uploadUniform(api, ubuffer,
SimpleMaterialParams{
.color = float4(1, 0, 0, 1),
});
RenderPassParams params = getClearColorRenderPass(float4(0));
params.viewport = { 0, 0, kRenderTargetSize, kRenderTargetSize };
RenderFrame frame(api);
api.beginRenderPass(renderTarget, params);
ps.primitiveType = PrimitiveType::TRIANGLES;
ps.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(ps);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
api.commit(swapChain);
}
// Second render pass: render a green triangle to a RenderTarget with 4 samples, attached to
// the same texture.
{
Handle<HwRenderTarget> renderTarget =
addCleanup(api.createRenderTarget(TargetBufferFlags::COLOR, kRenderTargetSize,
kRenderTargetSize, 4, 1, { { texture } }, {}, {}));
shader.uploadUniform(api, ubuffer,
SimpleMaterialParams{
.color = float4(0, 1, 0, 1),
});
RenderPassParams params = getClearColorRenderPass(float4(0));
params.viewport = { 0, 0, kRenderTargetSize, kRenderTargetSize };
RenderFrame frame(api);
api.beginRenderPass(renderTarget, params);
ps.primitiveType = PrimitiveType::TRIANGLES;
ps.vertexBufferInfo = triangle.getVertexBufferInfo();
api.bindPipeline(ps);
api.bindRenderPrimitive(triangle.getRenderPrimitive());
api.draw2(0, 3, 1);
api.endRenderPass();
EXPECT_IMAGE(renderTarget, ScreenshotParams(kRenderTargetSize, kRenderTargetSize,
"AutoresolveDifferingSampleCounts", 1048576));
api.commit(swapChain);
}
}
} // namespace test
+1 -1
View File
@@ -372,7 +372,7 @@ public:
} vsm;
/**
* Light bulb radius used for soft shadows. Currently this is only used when DPCF or PCSS is
* Light bulb radius used for soft shadows. Currently, this is only used when DPCF or PCSS is
* enabled. (2cm by default).
*/
float shadowBulbRadius = 0.02f;
+1 -1
View File
@@ -735,7 +735,7 @@ struct VsmShadowOptions {
bool highPrecision = false;
/**
* VSM minimum variance scale, must be positive.
* @deprecated has no effect.
*/
float minVarianceScale = 0.5f;
+265
View File
@@ -0,0 +1,265 @@
/*
* 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.
*/
#include "LocalProgramCache.h"
#include "MaterialParser.h"
#include "details/Engine.h"
#include "details/Material.h"
#include <backend/DriverApiForward.h>
namespace filament {
using namespace backend;
using namespace utils;
LocalProgramCache::LocalProgramCache(LocalProgramCache const& other)
: mMaterial(other.mMaterial),
mCachedPrograms(other.mCachedPrograms.size()),
mSpecializationConstants((other.mMaterial != nullptr)
? other.mMaterial->getEngine()
.getMaterialCache()
.getSpecializationConstantsInternPool()
.acquire(other.mSpecializationConstants)
: SpecializationConstants()) {}
LocalProgramCache& LocalProgramCache::operator=(LocalProgramCache const& other) {
assert_invariant(mMaterial == nullptr);
assert_invariant(mCachedPrograms.empty());
assert_invariant(mSpecializationConstants.empty());
mMaterial = other.mMaterial;
if (mMaterial != nullptr) {
mCachedPrograms = FixedCapacityVector<Handle<HwProgram>>(other.mCachedPrograms.size());
mSpecializationConstants = other.mMaterial->getEngine()
.getMaterialCache()
.getSpecializationConstantsInternPool()
.acquire(other.mSpecializationConstants);
}
return *this;
}
void LocalProgramCache::initializeForMaterial(FEngine& engine, FMaterial const& material,
utils::FixedCapacityVector<backend::Program::SpecializationConstant>
specializationConstants) {
assert_invariant(mMaterial == nullptr);
assert_invariant(mCachedPrograms.empty());
assert_invariant(mSpecializationConstants.empty());
mMaterial = &material;
mSpecializationConstants =
engine.getMaterialCache().getSpecializationConstantsInternPool().acquire(
std::move(specializationConstants));
size_t cachedProgramsSize;
switch (material.getMaterialDomain()) {
case filament::MaterialDomain::SURFACE:
cachedProgramsSize = 1 << VARIANT_BITS;
break;
case filament::MaterialDomain::POST_PROCESS:
cachedProgramsSize = 1 << POST_PROCESS_VARIANT_BITS;
break;
case filament::MaterialDomain::COMPUTE:
cachedProgramsSize = 1;
break;
}
mCachedPrograms = FixedCapacityVector<Handle<HwProgram>>(cachedProgramsSize);
material.getDefinition().acquirePrograms(engine, mCachedPrograms.as_slice(),
material.getMaterialParser(), mSpecializationConstants, material.isDefaultMaterial());
}
void LocalProgramCache::initializeForMaterialInstance(FEngine& engine, FMaterial const& material) {
assert_invariant(mMaterial == nullptr);
assert_invariant(mCachedPrograms.empty());
assert_invariant(mSpecializationConstants.empty());
mMaterial = &material;
LocalProgramCache const& programs = material.getPrograms();
mSpecializationConstants =
engine.getMaterialCache().getSpecializationConstantsInternPool().acquire(
programs.getSpecializationConstants());
mCachedPrograms =
FixedCapacityVector<Handle<HwProgram>>(material.getPrograms().mCachedPrograms.size());
}
Handle<HwProgram> LocalProgramCache::prepareProgramSlow(DriverApi& driver, Variant const variant,
CompilerPriorityQueue const priorityQueue) const noexcept {
assert_invariant(mMaterial != nullptr);
FEngine& engine = mMaterial->getEngine();
if (mMaterial->isSharedVariant(variant)) {
FMaterial const* defaultMaterial = engine.getDefaultMaterial();
assert_invariant(defaultMaterial);
LocalProgramCache const& defaultPrograms = defaultMaterial->getPrograms();
Handle<HwProgram> program = defaultPrograms.mCachedPrograms[variant.key];
if (program) {
return mCachedPrograms[variant.key] = program;
}
return mCachedPrograms[variant.key] =
defaultPrograms.prepareProgram(driver, variant, priorityQueue);
}
return mCachedPrograms[variant.key] = mMaterial->getDefinition().prepareProgram(engine, driver,
mMaterial->getMaterialParser(), getProgramSpecialization(variant), priorityQueue);
}
ProgramSpecialization LocalProgramCache::getProgramSpecialization(Variant variant) const noexcept {
assert_invariant(mMaterial != nullptr);
return ProgramSpecialization {
.materialCrc32 = mMaterial->getMaterialParser().getCrc32(),
.variant = variant,
.specializationConstants = mSpecializationConstants,
};
}
void LocalProgramCache::terminate(FEngine& engine) {
assert_invariant(mMaterial != nullptr);
mMaterial->getDefinition().releasePrograms(engine, mCachedPrograms.as_slice(),
mMaterial->getMaterialParser(), mSpecializationConstants,
mMaterial->isDefaultMaterial());
engine.getMaterialCache().releaseMaterial(engine, mMaterial->getDefinition());
engine.getMaterialCache().getSpecializationConstantsInternPool().release(
mSpecializationConstants);
}
void LocalProgramCache::clear(FEngine& engine) {
assert_invariant(mMaterial != nullptr);
mMaterial->getDefinition().releasePrograms(engine, mCachedPrograms.as_slice(),
mMaterial->getMaterialParser(), mSpecializationConstants,
mMaterial->isDefaultMaterial());
}
Variant LocalProgramCache::filterVariantForGetProgram(Variant variant) const noexcept {
if (UTILS_UNLIKELY(mMaterial->getEngine().features.material.enable_fog_as_postprocess)) {
// if the fog as post-process feature is enabled, we need to proceed "as-if" the material
// didn't have the FOG variant bit.
if (mMaterial->getMaterialDomain() == MaterialDomain::SURFACE) {
BlendingMode const blendingMode = mMaterial->getBlendingMode();
bool const hasScreenSpaceRefraction =
mMaterial->getRefractionMode() == RefractionMode::SCREEN_SPACE;
bool const isBlendingCommand = !hasScreenSpaceRefraction &&
(blendingMode != BlendingMode::OPAQUE && blendingMode != BlendingMode::MASKED);
if (!isBlendingCommand) {
variant.setFog(false);
}
}
}
return variant;
}
Program::SpecializationConstant LocalProgramCache::getConstantImpl(uint32_t id) const noexcept {
return mSpecializationConstants[id];
}
Program::SpecializationConstant LocalProgramCache::getConstantImpl(
std::string_view name) const noexcept {
assert_invariant(mMaterial != nullptr);
MaterialDefinition const& definition = mMaterial->getDefinition();
auto it = definition.specializationConstantsNameToIndex.find(name);
if (it != definition.specializationConstantsNameToIndex.cend()) {
return getConstantImpl(it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS);
}
std::string name_cstring(name);
PANIC_PRECONDITION("No such constant exists: %s", name_cstring.c_str());
return {};
}
void LocalProgramCache::setConstants(
std::initializer_list<std::pair<uint32_t, Program::SpecializationConstant>>
constants) noexcept {
assert_invariant(mMaterial != nullptr);
auto newSpecializationConstants =
FixedCapacityVector<Program::SpecializationConstant>(mSpecializationConstants);
bool hasChanged = false;
for (const auto& [id, value] : constants) {
if (newSpecializationConstants[id] != value) {
newSpecializationConstants[id] = value;
hasChanged = true;
}
}
if (hasChanged) {
setConstantsImpl(std::move(newSpecializationConstants));
}
}
void LocalProgramCache::setConstants(
std::initializer_list<std::pair<std::string_view, Program::SpecializationConstant>>
constants) noexcept {
assert_invariant(mMaterial != nullptr);
auto newSpecializationConstants =
FixedCapacityVector<Program::SpecializationConstant>(mSpecializationConstants);
bool hasChanged = false;
for (const auto& [name, value] : constants) {
MaterialDefinition const& definition = mMaterial->getDefinition();
auto it = definition.specializationConstantsNameToIndex.find(name);
if (it != definition.specializationConstantsNameToIndex.cend()) {
uint32_t id = it->second + CONFIG_MAX_RESERVED_SPEC_CONSTANTS;
if (newSpecializationConstants[id] != value) {
newSpecializationConstants[id] = value;
hasChanged = true;
}
}
}
if (hasChanged) {
setConstantsImpl(std::move(newSpecializationConstants));
}
}
void LocalProgramCache::setConstantsImpl(
FixedCapacityVector<Program::SpecializationConstant> constants) noexcept {
FEngine& engine = mMaterial->getEngine();
auto& internPool = engine.getMaterialCache().getSpecializationConstantsInternPool();
MaterialParser const& materialParser = mMaterial->getMaterialParser();
MaterialDefinition const& definition = mMaterial->getDefinition();
const bool isDefaultMaterial = mMaterial->isDefaultMaterial();
// Release old resources...
definition.releasePrograms(engine, mCachedPrograms.as_slice(), materialParser,
mSpecializationConstants, isDefaultMaterial);
internPool.release(mSpecializationConstants);
// Then acquire new ones.
mSpecializationConstants = internPool.acquire(std::move(constants));
definition.acquirePrograms(engine, mCachedPrograms.as_slice(), materialParser,
mSpecializationConstants, isDefaultMaterial);
}
template int32_t LocalProgramCache::getConstant<int32_t>(uint32_t id) const noexcept;
template float LocalProgramCache::getConstant<float>(uint32_t id) const noexcept;
template bool LocalProgramCache::getConstant<bool>(uint32_t id) const noexcept;
template int32_t LocalProgramCache::getConstant<int32_t>(std::string_view name) const noexcept;
template float LocalProgramCache::getConstant<float>(std::string_view name) const noexcept;
template bool LocalProgramCache::getConstant<bool>(std::string_view name) const noexcept;
} // namespace filament
+144
View File
@@ -0,0 +1,144 @@
/*
* 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.
*/
#ifndef TNT_FILAMENT_LOCALPROGRAMCACHE_H
#define TNT_FILAMENT_LOCALPROGRAMCACHE_H
#include "MaterialDefinition.h"
#include <backend/Handle.h>
#include <private/filament/Variant.h>
#include <backend/DriverEnums.h>
#include <backend/DriverApiForward.h>
#include <backend/Program.h>
#include <utility>
namespace filament {
class FMaterial;
// L0 cache for material programs. Manages recompiling them on-demand; owned by Material and
// MaterialInstance.
class LocalProgramCache {
template<typename T>
using is_supported_constant_parameter_t =
std::enable_if_t<std::is_same_v<int32_t, T> || std::is_same_v<float, T> ||
std::is_same_v<bool, T>>;
public:
using Programs = utils::Slice<const backend::Handle<backend::HwProgram>>;
using SpecializationConstants = utils::Slice<const backend::Program::SpecializationConstant>;
LocalProgramCache() = default;
LocalProgramCache(LocalProgramCache const& other);
LocalProgramCache& operator=(LocalProgramCache const& other);
// Initialize for use in a Material.
void initializeForMaterial(FEngine& engine, FMaterial const& material,
utils::FixedCapacityVector<backend::Program::SpecializationConstant>
specializationConstants);
// Initialize for use in a MaterialInstance. Copies the set of spec constants currently in use
// from its Material.
void initializeForMaterialInstance(FEngine& engine, FMaterial const& material);
bool isInitialized() const noexcept { return mMaterial != nullptr; }
// prepareProgram creates the program for the material's given variant at the backend level.
// Must be called outside of backend render pass.
// Must be called before getProgram() below.
backend::Handle<backend::HwProgram> prepareProgram(backend::DriverApi& driver,
Variant const variant,
backend::CompilerPriorityQueue const priorityQueue) const noexcept {
backend::Handle<backend::HwProgram> program = mCachedPrograms[variant.key];
if (UTILS_LIKELY(program)) {
return program;
}
return prepareProgramSlow(driver, variant, priorityQueue);
}
// getProgram returns the backend program for the material's given variant.
// Must be called after prepareProgram().
[[nodiscard]]
backend::Handle<backend::HwProgram> getProgram(Variant variant) const noexcept {
variant = filterVariantForGetProgram(variant);
backend::Handle<backend::HwProgram> program = mCachedPrograms[variant.key];
assert_invariant(program);
return program;
}
SpecializationConstants getSpecializationConstants() const noexcept {
return mSpecializationConstants;
}
Programs getPrograms() const noexcept { return mCachedPrograms.as_slice(); }
// Free all engine resources associated with this instance.
void terminate(FEngine& engine);
// Clear all cached programs. Used primarily by matdbg to "sever" a Material's connection to the
// global material cache.
void clear(FEngine& engine);
// Get constant by ID.
template<typename T, typename = is_supported_constant_parameter_t<T>>
T getConstant(uint32_t id) const noexcept {
return std::get<T>(getConstantImpl(id));
}
// Get constant by name.
template<typename T, typename = is_supported_constant_parameter_t<T>>
T getConstant(std::string_view name) const noexcept {
return std::get<T>(getConstantImpl(name));
}
// Set constants by ID.
void setConstants(
std::initializer_list<std::pair<uint32_t, backend::Program::SpecializationConstant>>
constants) noexcept;
// Set constants by name.
void setConstants(std::initializer_list<
std::pair<std::string_view, backend::Program::SpecializationConstant>>
constants) noexcept;
private:
backend::Handle<backend::HwProgram> prepareProgramSlow(backend::DriverApi& driver,
Variant const variant,
backend::CompilerPriorityQueue const priorityQueue) const noexcept;
ProgramSpecialization getProgramSpecialization(Variant variant) const noexcept;
Variant filterVariantForGetProgram(Variant const variant) const noexcept;
void setConstantsImpl(utils::FixedCapacityVector<backend::Program::SpecializationConstant>
constants) noexcept;
backend::Program::SpecializationConstant getConstantImpl(uint32_t id) const noexcept;
backend::Program::SpecializationConstant getConstantImpl(std::string_view name) const noexcept;
FMaterial const* mMaterial = nullptr;
mutable utils::FixedCapacityVector<backend::Handle<backend::HwProgram>> mCachedPrograms;
SpecializationConstants mSpecializationConstants;
};
} // namespace filament
#endif // TNT_FILAMENT_LOCALPROGRAMCACHE_H
+17 -7
View File
@@ -28,27 +28,37 @@ namespace filament {
size_t MaterialCache::MaterialKey::Hash::operator()(
filament::MaterialCache::MaterialKey const& key) const noexcept {
uint32_t crc;
if (key.parser->getMaterialCrc32(&crc)) {
return size_t(crc);
}
return size_t(key.parser->computeCrc32());
return size_t(key.parser->getCrc32());
}
bool MaterialCache::MaterialKey::operator==(MaterialKey const& rhs) const noexcept {
return parser == rhs.parser;
}
MaterialCache::MaterialCache()
: mDefinitions("MaterialCache::mDefinitions", 0),
mPrograms("MaterialCache::mPrograms", 0) {}
MaterialCache::~MaterialCache() {
assert_invariant(mDefinitions.empty());
assert_invariant(mPrograms.empty());
assert_invariant(mSpecializationConstantsInternPool.empty());
}
void MaterialCache::terminate(FEngine& engine) {
mPrograms.clearLruCache([&engine](backend::Handle<backend::HwProgram>& program) {
engine.getDriverApi().destroyProgram(program);
});
mDefinitions.clearLruCache([&engine](MaterialDefinition& definition) {
definition.terminate(engine);
});
}
MaterialDefinition* UTILS_NULLABLE MaterialCache::acquireMaterial(FEngine& engine,
const void* UTILS_NONNULL data, size_t size) noexcept {
std::unique_ptr<MaterialParser> parser = MaterialDefinition::createParser(engine.getBackend(),
engine.getShaderLanguage(), data, size);
bool const checkCrc32 = engine.features.material.check_crc32_after_loading;
std::unique_ptr<MaterialParser> parser =
MaterialDefinition::createParser(engine, data, size, checkCrc32);
assert_invariant(parser);
// The `key` must be constructed using parser.get() before parser is moved into the lambda
+5
View File
@@ -56,8 +56,13 @@ public:
using ProgramCache =
utils::RefCountedMap<ProgramSpecialization, backend::Handle<backend::HwProgram>>;
MaterialCache();
~MaterialCache();
// All reference-counted resources should be freed by the time MaterialCache is destructed, but
// the LRU cache needs to be explicitly freed in addition.
void terminate(FEngine& engine);
SpecializationConstantInternPool& getSpecializationConstantsInternPool() {
return mSpecializationConstantsInternPool;
}
+41 -38
View File
@@ -162,11 +162,30 @@ void releaseProgramsImpl(FEngine& engine, utils::Slice<Handle<HwProgram>> progra
} // namespace
std::unique_ptr<MaterialParser> MaterialDefinition::createParser(Backend const backend,
FixedCapacityVector<ShaderLanguage> languages, const void* data, size_t size) {
std::unique_ptr<MaterialParser> MaterialDefinition::createParser(FEngine const& engine,
const void* data, size_t size, bool checkCrc32) {
auto const& languages = engine.getShaderLanguage();
auto const backend = engine.getBackend();
// unique_ptr so we don't leak MaterialParser on failures below
auto materialParser = std::make_unique<MaterialParser>(languages, data, size);
// Try checking CRC32 value for the package and skip if it's unavailable.
if (checkCrc32) {
uint32_t parsedCrc32 = 0;
bool const foundParsedCrc = materialParser->getMaterialCrc32(&parsedCrc32);
uint32_t const expectedCrc32 = materialParser->computeCrc32();
if (foundParsedCrc && parsedCrc32 != expectedCrc32) {
CString name;
materialParser->getName(&name);
LOG(ERROR) << "The material '" << name.c_str_safe()
<< "' is corrupted: crc32_expected=" << expectedCrc32
<< ", crc32_parsed=" << parsedCrc32;
return nullptr;
}
}
MaterialParser::ParseResult const materialResult = materialParser->parse();
CString name;
@@ -174,7 +193,7 @@ std::unique_ptr<MaterialParser> MaterialDefinition::createParser(Backend const b
if (UTILS_UNLIKELY(materialResult == MaterialParser::ParseResult::ERROR_MISSING_BACKEND)) {
CString languageNames;
for (auto it = languages.begin(); it != languages.end(); ++it) {
languageNames.append(CString{shaderLanguageToString(*it)});
languageNames.append(CString{ shaderLanguageToString(*it) });
if (std::next(it) != languages.end()) {
languageNames.append(", ");
}
@@ -182,8 +201,9 @@ std::unique_ptr<MaterialParser> MaterialDefinition::createParser(Backend const b
FILAMENT_CHECK_POSTCONDITION(
materialResult != MaterialParser::ParseResult::ERROR_MISSING_BACKEND)
<< "the material " << name.c_str_safe() << " was not built for any of the " << to_string(backend)
<< " backend's supported shader languages (" << languageNames.c_str() << ")\n";
<< "the material " << name.c_str_safe() << " was not built for any of the "
<< to_string(backend) << " backend's supported shader languages ("
<< languageNames.c_str() << ")\n";
}
if (backend == Backend::NOOP) {
@@ -206,23 +226,6 @@ std::unique_ptr<MaterialParser> MaterialDefinition::createParser(Backend const b
std::unique_ptr<MaterialDefinition> MaterialDefinition::create(FEngine& engine,
std::unique_ptr<MaterialParser> parser) {
// Try checking CRC32 value for the package and skip if it's unavailable.
if (downcast(engine).features.material.check_crc32_after_loading) {
uint32_t parsedCrc32 = 0;
parser->getMaterialCrc32(&parsedCrc32);
uint32_t const expectedCrc32 = parser->computeCrc32();
if (parsedCrc32 != expectedCrc32) {
CString name;
parser->getName(&name);
LOG(ERROR) << "The material '" << name.c_str_safe()
<< "' is corrupted: crc32_expected=" << expectedCrc32
<< ", crc32_parsed=" << parsedCrc32;
return nullptr;
}
}
uint32_t v = 0;
parser->getShaderModels(&v);
bitset32 shaderModels;
@@ -271,8 +274,8 @@ std::unique_ptr<MaterialDefinition> MaterialDefinition::create(FEngine& engine,
void MaterialDefinition::terminate(FEngine& engine) {
DriverApi& driver = engine.getDriverApi();
perViewDescriptorSetLayout.terminate(engine.getDescriptorSetLayoutFactory(), driver);
perViewDescriptorSetLayoutVsm.terminate(engine.getDescriptorSetLayoutFactory(), driver);
perViewDescriptorSetLayoutPcf.terminate(engine.getDescriptorSetLayoutFactory(), driver);
perViewDescriptorSetLayoutS2d.terminate(engine.getDescriptorSetLayoutFactory(), driver);
descriptorSetLayout.terminate(engine.getDescriptorSetLayoutFactory(), driver);
}
@@ -607,23 +610,23 @@ void MaterialDefinition::processDescriptorSets(FEngine& engine) {
refractionMode == RefractionMode::SCREEN_SPACE;
bool const hasFog = !(variantFilterMask & UserVariantFilterMask(UserVariantFilterBit::FOG));
this->perViewDescriptorSetLayoutDescription = descriptor_sets::getPerViewDescriptorSetLayout(
this->perViewDescriptorSetLayoutPcfDescription = descriptor_sets::getPerViewDescriptorSetLayout(
materialDomain, isLit, isSSR, hasFog, false);
this->perViewDescriptorSetLayoutVsmDescription = descriptor_sets::getPerViewDescriptorSetLayout(
this->perViewDescriptorSetLayoutS2dDescription = descriptor_sets::getPerViewDescriptorSetLayout(
materialDomain, isLit, isSSR, hasFog, true);
// set the labels
this->descriptorSetLayoutDescription.label = CString{ name }.append("_perMat");
this->perViewDescriptorSetLayoutDescription.label = CString{ name }.append("_perView");
this->perViewDescriptorSetLayoutVsmDescription.label = CString{ name }.append("_perViewVsm");
this->perViewDescriptorSetLayoutPcfDescription.label = CString{ name }.append("_perView");
this->perViewDescriptorSetLayoutS2dDescription.label = CString{ name }.append("_perViewVsm");
// get the PER_RENDERABLE and PER_VIEW descriptor binding info
for (auto&& [bindingPoint, dsl] : {
std::pair{ DescriptorSetBindingPoints::PER_RENDERABLE,
descriptor_sets::getPerRenderableLayout() },
std::pair{ DescriptorSetBindingPoints::PER_VIEW,
this->perViewDescriptorSetLayoutDescription }}) {
this->perViewDescriptorSetLayoutPcfDescription }}) {
Program::DescriptorBindingsInfo& descriptors = programDescriptorBindings[+bindingPoint];
descriptors.reserve(dsl.descriptors.size());
for (auto const& entry: dsl.descriptors) {
@@ -636,17 +639,17 @@ void MaterialDefinition::processDescriptorSets(FEngine& engine) {
descriptorSetLayoutFactory, driver,
this->descriptorSetLayoutDescription };
this->perViewDescriptorSetLayout = {
this->perViewDescriptorSetLayoutPcf = {
descriptorSetLayoutFactory, driver,
this->perViewDescriptorSetLayoutDescription };
this->perViewDescriptorSetLayoutPcfDescription };
this->perViewDescriptorSetLayoutVsm = {
this->perViewDescriptorSetLayoutS2d = {
descriptorSetLayoutFactory, driver,
this->perViewDescriptorSetLayoutVsmDescription };
this->perViewDescriptorSetLayoutS2dDescription };
}
backend::DescriptorSetLayout const& MaterialDefinition::getPerViewDescriptorSetLayoutDescription(
Variant const variant, bool const useVsmDescriptorSetLayout) const noexcept {
Variant const variant, bool const useS2dDescriptorSetLayout) const noexcept {
if (materialDomain == MaterialDomain::SURFACE) {
if (Variant::isValidDepthVariant(variant)) {
// Use the layout description used to create the per view depth variant layout.
@@ -657,10 +660,10 @@ backend::DescriptorSetLayout const& MaterialDefinition::getPerViewDescriptorSetL
return descriptor_sets::getSsrVariantLayout();
}
}
if (useVsmDescriptorSetLayout) {
return perViewDescriptorSetLayoutVsmDescription;
if (useS2dDescriptorSetLayout) {
return perViewDescriptorSetLayoutS2dDescription;
}
return perViewDescriptorSetLayoutDescription;
return perViewDescriptorSetLayoutPcfDescription;
}
Handle<HwProgram> MaterialDefinition::compileProgram(
@@ -689,7 +692,7 @@ Handle<HwProgram> MaterialDefinition::compileProgram(
pb.descriptorLayout(+DescriptorSetBindingPoints::PER_VIEW,
getPerViewDescriptorSetLayoutDescription(
specialization.variant,
Variant::isVSMVariant(specialization.variant)));
Variant::isShadowSampler2DVariant(specialization.variant)));
pb.descriptorLayout(+DescriptorSetBindingPoints::PER_RENDERABLE,
descriptor_sets::getPerRenderableLayout());
pb.descriptorLayout(
+7 -8
View File
@@ -94,17 +94,17 @@ struct MaterialDefinition {
backend::ShaderModel const sm, bool isStereoSupported) const noexcept;
backend::DescriptorSetLayout const& getPerViewDescriptorSetLayoutDescription(
Variant const variant, bool useVsmDescriptorSetLayout) const noexcept;
Variant const variant, bool useS2dDescriptorSetLayout) const noexcept;
// Keep track of the definitions of the descriptor set layouts, as these
// may be used by some backends in parallel compilation of programs.
backend::DescriptorSetLayout perViewDescriptorSetLayoutDescription;
backend::DescriptorSetLayout perViewDescriptorSetLayoutVsmDescription;
backend::DescriptorSetLayout perViewDescriptorSetLayoutPcfDescription;
backend::DescriptorSetLayout perViewDescriptorSetLayoutS2dDescription;
backend::DescriptorSetLayout descriptorSetLayoutDescription;
// try to order by frequency of use
filament::DescriptorSetLayout perViewDescriptorSetLayout;
filament::DescriptorSetLayout perViewDescriptorSetLayoutVsm;
filament::DescriptorSetLayout perViewDescriptorSetLayoutPcf;
filament::DescriptorSetLayout perViewDescriptorSetLayoutS2d;
filament::DescriptorSetLayout descriptorSetLayout;
backend::Program::DescriptorSetInfo programDescriptorBindings;
@@ -165,9 +165,8 @@ private:
friend class MaterialCache;
friend class FMaterial; // for onEditCallback
static std::unique_ptr<MaterialParser> createParser(backend::Backend const backend,
utils::FixedCapacityVector<backend::ShaderLanguage> languages,
const void* UTILS_NONNULL data, size_t size);
static std::unique_ptr<MaterialParser> createParser(FEngine const& engine,
const void* UTILS_NONNULL data, size_t size, bool checkCrc32);
static std::unique_ptr<MaterialDefinition> create(FEngine& engine,
std::unique_ptr<MaterialParser> parser);
+38 -67
View File
@@ -19,90 +19,61 @@
#include "details/Engine.h"
#include "details/Material.h"
#include "details/MaterialInstance.h"
#include <utils/debug.h>
#include <iterator>
#include <utility>
#include <stdint.h>
#include <cstdint>
namespace filament {
using Record = MaterialInstanceManager::Record;
using namespace utils;
std::pair<FMaterialInstance*, int32_t> Record::getInstance() {
if (mAvailable < mInstances.size()) {
auto index = mAvailable++;
return { mInstances[index], index };
}
assert_invariant(mAvailable == mInstances.size());
auto& name = mMaterial->getName();
FMaterialInstance* inst = mMaterial->createInstance(name.c_str_safe());
mInstances.push_back(inst);
return { inst, mAvailable++ };
}
MaterialInstanceManager::MaterialInstanceManager() noexcept = default;
FMaterialInstance* Record::getInstance(int32_t const fixedInstanceindex) const {
assert_invariant(fixedInstanceindex >= 0 && fixedInstanceindex < int32_t(mInstances.size()));
return mInstances[fixedInstanceindex];
}
// Defined in cpp to avoid inlining
Record::Record(Record const& rhs) noexcept = default;
Record& Record::operator=(Record const& rhs) noexcept = default;
Record::Record(Record&& rhs) noexcept = default;
Record& Record::operator=(Record&& rhs) noexcept = default;
void Record::terminate(FEngine& engine) {
std::for_each(mInstances.begin(), mInstances.end(),
[&engine](auto instance) { engine.destroy(instance); });
}
MaterialInstanceManager::MaterialInstanceManager() noexcept {}
MaterialInstanceManager::MaterialInstanceManager(
MaterialInstanceManager const& rhs) noexcept = default;
MaterialInstanceManager::MaterialInstanceManager(MaterialInstanceManager&& rhs) noexcept = default;
MaterialInstanceManager& MaterialInstanceManager::operator=(
MaterialInstanceManager const& rhs) noexcept = default;
MaterialInstanceManager& MaterialInstanceManager::operator=(
MaterialInstanceManager&& rhs) noexcept = default;
MaterialInstanceManager& MaterialInstanceManager::operator=(MaterialInstanceManager&& rhs) noexcept = default;
MaterialInstanceManager::~MaterialInstanceManager() = default;
void MaterialInstanceManager::terminate(FEngine& engine) {
std::for_each(mMaterials.begin(), mMaterials.end(), [&engine](auto& record) {
record.terminate(engine);
});
for (auto const& [key, instance] : mMaterialInstances) {
engine.destroy(instance);
}
mMaterialInstances.clear();
for (auto const& [material, pool] : mAnonymousMaterialInstances) {
for (auto instance : pool.instances) {
engine.destroy(instance);
}
}
mAnonymousMaterialInstances.clear();
}
Record& MaterialInstanceManager::getRecord(FMaterial const* const ma) const {
auto itr = std::find_if(mMaterials.begin(), mMaterials.end(), [ma](auto& record) {
return ma == record.mMaterial;
});
if (itr == mMaterials.end()) {
mMaterials.emplace_back(ma);
itr = std::prev(mMaterials.end());
void MaterialInstanceManager::reset() {
for (auto& [material, pool] : mAnonymousMaterialInstances) {
pool.highWaterMark = 0;
}
return *itr;
}
FMaterialInstance* MaterialInstanceManager::getMaterialInstance(FMaterial const* ma, uint32_t tag) const {
Key const key{ma, tag};
auto it = mMaterialInstances.find(key);
if (it != mMaterialInstances.end()) {
return it->second;
}
FMaterialInstance* const instance = ma->createInstance(ma->getName().c_str_safe());
mMaterialInstances.emplace(key, instance);
return instance;
}
FMaterialInstance* MaterialInstanceManager::getMaterialInstance(FMaterial const* ma) const {
auto [inst, index] = getRecord(ma).getInstance();
return inst;
}
AnonymousPool& pool = mAnonymousMaterialInstances[ma];
if (pool.highWaterMark < pool.instances.size()) {
return pool.instances[pool.highWaterMark++];
}
FMaterialInstance* MaterialInstanceManager::getMaterialInstance(FMaterial const* ma,
int32_t const fixedIndex) const {
return getRecord(ma).getInstance(fixedIndex);
FMaterialInstance* const instance = ma->createInstance(ma->getName().c_str_safe());
pool.instances.push_back(instance);
pool.highWaterMark++;
return instance;
}
std::pair<FMaterialInstance*, int32_t> MaterialInstanceManager::getFixedMaterialInstance(
FMaterial const* ma) {
return getRecord(ma).getInstance();
}
} // namespace filament
+56 -60
View File
@@ -16,10 +16,13 @@
#pragma once
#include <utils/bitset.h>
#include <cstddef>
#include <cstdint>
#include <unordered_map>
#include <vector>
#include <utils/Hash.h>
namespace filament {
class FMaterial;
@@ -30,37 +33,10 @@ class FEngine;
// re-use instances across frames.
class MaterialInstanceManager {
public:
class Record {
public:
Record(FMaterial const* material)
: mMaterial(material),
mAvailable(0) {}
~Record() = default;
Record(Record const& rhs) noexcept;
Record& operator=(Record const& rhs) noexcept;
Record(Record&& rhs) noexcept;
Record& operator=(Record&& rhs) noexcept;
void terminate(FEngine& engine);
void reset() { mAvailable = 0; }
std::pair<FMaterialInstance*, int32_t> getInstance();
FMaterialInstance* getInstance(int32_t fixedInstanceindex) const;
private:
FMaterial const* mMaterial = nullptr;
std::vector<FMaterialInstance*> mInstances;
uint32_t mAvailable;
friend class MaterialInstanceManager;
};
constexpr static int32_t INVALID_FIXED_INDEX = -1;
MaterialInstanceManager() noexcept;
MaterialInstanceManager(MaterialInstanceManager const& rhs) noexcept;
MaterialInstanceManager(MaterialInstanceManager const& rhs) = delete;
MaterialInstanceManager(MaterialInstanceManager&& rhs) noexcept;
MaterialInstanceManager& operator=(MaterialInstanceManager const& rhs) noexcept;
MaterialInstanceManager& operator=(MaterialInstanceManager const& rhs) = delete;
MaterialInstanceManager& operator=(MaterialInstanceManager&& rhs) noexcept;
~MaterialInstanceManager();
@@ -71,40 +47,60 @@ public:
*/
void terminate(FEngine& engine);
/*
* This returns a material instance given a material. The implementation will try to find an
* available instance in the cache. If one is not found, then a new instance will be created and
* added to the cache.
/**
* Resets the anonymous material instances cache.
*/
void reset();
/**
* This returns a material instance given a material and a tag.
*
* If the material instance doesn't exist in the cache, it is created and cached.
*
* @param ma FMaterial to get a MaterialInstance for
* @param tag A unique tag identifying the MaterialInstance
* @return A FMaterialInstance pointer
*/
FMaterialInstance* getMaterialInstance(FMaterial const* ma, uint32_t tag) const;
/**
* This returns a material instance given a material from a cache.
*
* If the material instance doesn't exist in the cache, it is created and cached.
*
* It is permissible to call the method several times, in which case a different MaterialInstance will be returned.
* It is guaranteed to be different from MaterialInstances returned with a tag.
*
* @param ma FMaterial to get a MaterialInstance for
* @return A FMaterialInstance pointer
*/
FMaterialInstance* getMaterialInstance(FMaterial const* ma) const;
/*
* This returns a material instance given a material and an index. The `fixedIndex` should be
* a value returned by getiFixedMaterialInstance.
*/
FMaterialInstance* getMaterialInstance(FMaterial const* ma, int32_t const fixedIndex) const;
/*
* This returns a material instance and an index given a material. This is needed for the
* case when two framegraph passes need to refer to the same material instance.
* The returned index can be used with `getFixedMaterialInstance` to get a specific instance
* of a material (and not a random entry in the record cache).
*/
std::pair<FMaterialInstance*, int32_t> getFixedMaterialInstance(FMaterial const* ma);
/*
* Marks all of the material instances as unused. Typically, you'd call this at the beginning of
* a frame.
*/
void reset() {
std::for_each(mMaterials.begin(), mMaterials.end(), [](auto& record) { record.reset(); });
}
private:
Record& getRecord(FMaterial const* material) const;
struct Key {
FMaterial const* material;
uint32_t tag;
bool operator==(Key const& rhs) const noexcept {
return material == rhs.material && tag == rhs.tag;
}
};
mutable std::vector<Record> mMaterials;
struct Hasher {
std::size_t operator()(Key const& key) const noexcept {
std::size_t seed = 0;
utils::hash::combine(seed, key.material);
utils::hash::combine(seed, key.tag);
return seed;
}
};
struct AnonymousPool {
std::vector<FMaterialInstance*> instances;
uint32_t highWaterMark = 0;
};
mutable std::unordered_map<Key, FMaterialInstance*, Hasher> mMaterialInstances;
mutable std::unordered_map<FMaterial const*, AnonymousPool> mAnonymousMaterialInstances;
};
} // namespace filament
+32 -33
View File
@@ -44,6 +44,7 @@
#include <array>
#include <atomic>
#include <mutex>
#include <optional>
#include <tuple>
#include <utility>
@@ -120,12 +121,10 @@ bool MaterialParser::operator==(MaterialParser const& rhs) const noexcept {
if (mImpl.mManagedBuffer.size() != rhs.mImpl.mManagedBuffer.size()) {
return false;
}
std::optional<uint32_t> lhsCrc32 = getPrecomputedCrc32();
if (lhsCrc32) {
std::optional<uint32_t> rhsCrc32 = rhs.getPrecomputedCrc32();
if (rhsCrc32 && *lhsCrc32 != *rhsCrc32) {
return false;
}
uint32_t const lhsCrc32 = getCrc32();
uint32_t const rhsCrc32 = rhs.getCrc32();
if (lhsCrc32 != rhsCrc32) {
return false;
}
return !memcmp(mImpl.mManagedBuffer.data(), rhs.mImpl.mManagedBuffer.data(),
mImpl.mManagedBuffer.size());
@@ -187,12 +186,34 @@ MaterialParser::ParseResult MaterialParser::parse() noexcept {
return ParseResult::SUCCESS;
}
uint32_t MaterialParser::computeCrc32() const noexcept {
uint32_t crc32 = mCrc32.load(std::memory_order_relaxed);
if (crc32) {
return crc32;
uint32_t MaterialParser::getCrc32() const noexcept {
if (mCrc32Cached.load(std::memory_order_relaxed)) {
return mCrc32;
}
std::lock_guard<utils::Mutex> lock(mCrc32CachedLock);
// If we enter this section after another thread has passed through it, we need to check whether
// crc32 has been cached or not. This is the slow path that will happen hopefully just once.
if (mCrc32Cached.load(std::memory_order_relaxed)) {
return mCrc32;
}
// First check whether the compiled material contains the crc32 already.
if (uint32_t parsedCrc32 = 0; getMaterialCrc32(&parsedCrc32)) {
mCrc32 = parsedCrc32;
mCrc32Cached.store(true);
return parsedCrc32;
}
// No crc32 found, so we compute it.
mCrc32 = computeCrc32();
mCrc32Cached.store(true);
return mCrc32;
}
uint32_t MaterialParser::computeCrc32() const noexcept {
const size_t size = mImpl.mManagedBuffer.size();
const void* const UTILS_NONNULL payload = mImpl.mManagedBuffer.data();
@@ -202,29 +223,7 @@ uint32_t MaterialParser::computeCrc32() const noexcept {
std::vector<uint32_t> crc32Table;
utils::hash::crc32GenerateTable(crc32Table);
crc32 = utils::hash::crc32Update(0, payload, originalSize, crc32Table);
mCrc32.store(crc32, std::memory_order_relaxed);
return crc32;
}
std::optional<uint32_t> MaterialParser::getPrecomputedCrc32() const noexcept {
uint32_t cachedCrc32 = mCrc32.load(std::memory_order_relaxed);
if (cachedCrc32) {
return cachedCrc32;
}
uint32_t parsedCrc32;
if (getMaterialCrc32(&parsedCrc32)) {
return parsedCrc32;
}
return std::nullopt;
}
uint32_t MaterialParser::getCrc32() const noexcept {
std::optional<uint32_t> crc32 = getPrecomputedCrc32();
if (crc32) {
return *crc32;
}
return computeCrc32();
return utils::hash::crc32Update(0, payload, originalSize, crc32Table);
}
ShaderLanguage MaterialParser::getShaderLanguage() const noexcept {
+10 -8
View File
@@ -30,6 +30,7 @@
#include <utils/CString.h>
#include <utils/FixedCapacityVector.h>
#include <utils/Mutex.h>
#include <array>
#include <optional>
@@ -70,13 +71,13 @@ public:
ParseResult parse() noexcept;
// Compute the CRC32 of the material or return the cached value.
uint32_t computeCrc32() const noexcept;
// Return the cached computed CRC32 or the CRC32 built into the material file if one exists.
std::optional<uint32_t> getPrecomputedCrc32() const noexcept;
// Return the CRC32 of the material.
// Return the CRC32 of the material. This will use a cached value if it is available.
uint32_t getCrc32() const noexcept;
// Return the CRC32 of the material. This will *always* compute the crc32 from the full compiled
// binary content (i.e. this is the slow path).
uint32_t computeCrc32() const noexcept;
backend::ShaderLanguage getShaderLanguage() const noexcept;
// Accessors
@@ -196,9 +197,10 @@ private:
filaflat::ChunkContainer& getChunkContainer() noexcept;
filaflat::ChunkContainer const& getChunkContainer() const noexcept;
MaterialParserDetails mImpl;
// 0 == not cached. This technically means that a file with a CRC32 of 0 will never be cached,
// but this is unlikely, and keeping it a 32-bit value guarantees that it will be lockless.
mutable std::atomic<uint32_t> mCrc32 = 0;
mutable uint32_t mCrc32 = 0;
mutable std::atomic<bool> mCrc32Cached;
mutable utils::Mutex mCrc32CachedLock;
};
struct ChunkUniformInterfaceBlock {
+41 -64
View File
@@ -225,7 +225,6 @@ const PostProcessManager::JitterSequence<32>
PostProcessManager::PostProcessManager(FEngine& engine) noexcept
: mEngine(engine),
mFixedMaterialInstanceIndex {},
mWorkaroundSplitEasu(false),
mWorkaroundAllowReadOnlyAncillaryFeedbackLoop(false) {
// don't use Engine here, it's not fully initialized yet
@@ -455,7 +454,6 @@ Handle<HwTexture> PostProcessManager::getZeroTextureArray() const {
void PostProcessManager::resetForRender() {
mMaterialInstanceManager.reset();
mFixedMaterialInstanceIndex = {};
}
void PostProcessManager::unbindAllDescriptorSets(DriverApi& driver) noexcept {
@@ -802,7 +800,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::ssr(FrameGraph& fg,
// use our special SSR variant, it can only be applied to object that have
// the SCREEN_SPACE ReflectionMode.
passBuilder.variant(Variant{ Variant::SPECIAL_SSR });
passBuilder.variant(Variant{ Variant::SPECIAL_SSR_VARIANT });
// generate all our drawing commands, except blended objects.
passBuilder.commandTypeFlags(RenderPass::CommandTypeFlags::SCREEN_SPACE_REFLECTIONS);
@@ -1000,13 +998,10 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::screenSpaceAmbientOcclusion(
auto& material = getPostProcessMaterial(materialName);
FMaterial* ma = material.getMaterial(mEngine, driver);
{
FMaterial::SpecializationConstantsBuilder maConstants =
ma->getSpecializationConstantsBuilder();
maConstants.set("useVisibilityBitmasks", options.gtao.useVisibilityBitmasks);
maConstants.set("linearThickness", options.gtao.linearThickness);
ma->setSpecializationConstants(std::move(maConstants));
}
ma->getPrograms().setConstants({
{ "useVisibilityBitmasks", options.gtao.useVisibilityBitmasks },
{ "linearThickness", options.gtao.linearThickness },
});
ma = material.getMaterial(mEngine, driver);
FMaterialInstance* const mi = getMaterialInstance(ma);
@@ -2552,11 +2547,7 @@ void PostProcessManager::colorGradingSubpass(DriverApi& driver,
auto const& material = getPostProcessMaterial("colorGradingAsSubpass");
FMaterial const* const ma = material.getMaterial(mEngine, driver, variant);
// the UBO has been set and committed in colorGradingPrepareSubpass()
int32_t const fixedIndex = colorGradingConfig.translucent
? mFixedMaterialInstanceIndex.colorGradingTranslucent
: mFixedMaterialInstanceIndex.colorGradingOpaque;
FMaterialInstance const* mi = mMaterialInstanceManager.getMaterialInstance(ma, fixedIndex);
FMaterialInstance const* mi = mMaterialInstanceManager.getMaterialInstance(ma, colorGradingConfig.translucent);
mi->use(driver);
auto const pipeline = getPipelineState(ma, variant);
driver.nextSubpass();
@@ -2567,8 +2558,7 @@ void PostProcessManager::colorGradingSubpass(DriverApi& driver,
void PostProcessManager::customResolvePrepareSubpass(DriverApi& driver, CustomResolveOp const op) noexcept {
auto const& material = getPostProcessMaterial("customResolveAsSubpass");
auto const ma = material.getMaterial(mEngine, driver, PostProcessVariant::OPAQUE);
auto [mi, fixedIndex] = mMaterialInstanceManager.getFixedMaterialInstance(ma);
mFixedMaterialInstanceIndex.customResolve = fixedIndex;
auto* const mi = mMaterialInstanceManager.getMaterialInstance(ma, 0);
mi->setParameter("direction", op == CustomResolveOp::COMPRESS ? 1.0f : -1.0f),
mi->commit(driver, getUboManager());
}
@@ -2580,8 +2570,7 @@ void PostProcessManager::customResolveSubpass(DriverApi& driver) noexcept {
auto const& material = getPostProcessMaterial("customResolveAsSubpass");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
// the UBO has been set and committed in customResolvePrepareSubpass()
FMaterialInstance const* mi = mMaterialInstanceManager.getMaterialInstance(ma,
mFixedMaterialInstanceIndex.customResolve);
FMaterialInstance const* mi = mMaterialInstanceManager.getMaterialInstance(ma, 0);
mi->use(driver);
auto const pipeline = getPipelineState(ma);
@@ -2620,9 +2609,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::customResolveUncompressPass(
void PostProcessManager::clearAncillaryBuffersPrepare(DriverApi& driver,
Variant::type_t variant) noexcept {
auto const& material = getPostProcessMaterial("clearDepth");
auto ma = material.getMaterial(mEngine, driver, variant);
auto [mi, fixedIndex] = mMaterialInstanceManager.getFixedMaterialInstance(ma);
mFixedMaterialInstanceIndex.clearDepth = fixedIndex;
auto const ma = material.getMaterial(mEngine, driver, variant);
auto const mi = mMaterialInstanceManager.getMaterialInstance(ma, 0);
mi->commit(driver, getUboManager());
}
@@ -2640,8 +2628,7 @@ void PostProcessManager::clearAncillaryBuffers(DriverApi& driver,
FMaterial const* const ma = material.getMaterial(mEngine, driver, variant);
// the UBO has been set and committed in clearAncillaryBuffersPrepare()
FMaterialInstance const* const mi = mMaterialInstanceManager.getMaterialInstance(ma,
mFixedMaterialInstanceIndex.clearDepth);
FMaterialInstance const* const mi = mMaterialInstanceManager.getMaterialInstance(ma, 0);
mi->use(driver);
auto pipeline = getPipelineState(ma, variant);
@@ -2927,20 +2914,18 @@ void PostProcessManager::configureTemporalAntiAliasingMaterial(backend::DriverAp
TemporalAntiAliasingOptions const& taaOptions) noexcept {
FMaterial* const ma = getPostProcessMaterial("taa").getMaterial(mEngine, driver);
FMaterial::SpecializationConstantsBuilder maConstants = ma->getSpecializationConstantsBuilder();
maConstants.set("upscaling", taaOptions.upscaling > 1.0f);
maConstants.set("historyReprojection", taaOptions.historyReprojection);
maConstants.set("filterHistory", taaOptions.filterHistory);
maConstants.set("filterInput", taaOptions.filterInput);
maConstants.set("useYCoCg", taaOptions.useYCoCg);
maConstants.set("hdr", taaOptions.hdr);
maConstants.set("preventFlickering", taaOptions.preventFlickering);
maConstants.set("boxType", int32_t(taaOptions.boxType));
maConstants.set("boxClipping", int32_t(taaOptions.boxClipping));
maConstants.set("varianceGamma", taaOptions.varianceGamma);
ma->setSpecializationConstants(std::move(maConstants));
ma->getPrograms().setConstants({
{ "upscaling", taaOptions.upscaling > 1.0f },
{ "historyReprojection", taaOptions.historyReprojection },
{ "filterHistory", taaOptions.filterHistory },
{ "filterInput", taaOptions.filterInput },
{ "useYCoCg", taaOptions.useYCoCg },
{ "hdr", taaOptions.hdr },
{ "preventFlickering", taaOptions.preventFlickering },
{ "boxType", int32_t(taaOptions.boxType) },
{ "boxClipping", int32_t(taaOptions.boxClipping) },
{ "varianceGamma", taaOptions.varianceGamma },
});
}
FMaterialInstance* PostProcessManager::configureColorGradingMaterial(backend::DriverApi& driver,
@@ -2948,23 +2933,16 @@ FMaterialInstance* PostProcessManager::configureColorGradingMaterial(backend::Dr
ColorGradingConfig const& colorGradingConfig, VignetteOptions const& vignetteOptions,
uint32_t const width, uint32_t const height) noexcept {
FMaterial* ma = material.getMaterial(mEngine, driver);
{
FMaterial::SpecializationConstantsBuilder maConstants =
ma->getSpecializationConstantsBuilder();
maConstants.set("isOneDimensional", colorGrading->isOneDimensional());
maConstants.set("isLDR", colorGrading->isLDR());
ma->setSpecializationConstants(std::move(maConstants));
}
ma->getPrograms().setConstants({
{ "isOneDimensional", colorGrading->isOneDimensional() },
{ "isLDR", colorGrading->isLDR() },
});
PostProcessVariant const variant = colorGradingConfig.translucent
? PostProcessVariant::TRANSLUCENT
: PostProcessVariant::OPAQUE;
ma = material.getMaterial(mEngine, driver, variant);
FMaterialInstance* mi = nullptr;
int32_t& fixedIndex = colorGradingConfig.translucent
? mFixedMaterialInstanceIndex.colorGradingTranslucent
: mFixedMaterialInstanceIndex.colorGradingOpaque;
std::tie(mi, fixedIndex) = mMaterialInstanceManager.getFixedMaterialInstance(ma);
FMaterialInstance* mi = mMaterialInstanceManager.getMaterialInstance(ma, colorGradingConfig.translucent);
const SamplerParams params = SamplerParams{
.filterMag = SamplerMagFilter::LINEAR,
@@ -3694,8 +3672,12 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::resolve(FrameGraph& fg,
}
// The Metal / Vulkan backends currently don't support depth/stencil resolve.
if (isDepthFormat(inDesc.format) && (!mDepthStencilResolveSupported)) {
return resolveDepth(fg, outputBufferName, input, outDesc);
// TODO: Stencil resolve is actually *not* supported. Trying to resolve a stencil texture will
// trigger an assert on debug builds. We need to investigate how this can be accomplished
// through shaders or some other manipulation.
if ((isDepthFormat(inDesc.format) || isStencilFormat(inDesc.format)) &&
(!mDepthStencilResolveSupported)) {
return resolveDepthWithShader(fg, outputBufferName, input, outDesc);
}
outDesc.width = inDesc.width;
@@ -3710,9 +3692,6 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::resolve(FrameGraph& fg,
auto const& ppResolve = fg.addPass<ResolveData>("resolve",
[&](FrameGraph::Builder& builder, auto& data) {
// we currently don't support stencil resolve.
assert_invariant(!isStencilFormat(inDesc.format));
data.input = builder.read(input, FrameGraphTexture::Usage::BLIT_SRC);
data.output = builder.createTexture(outputBufferName, outDesc);
data.output = builder.write(data.output, FrameGraphTexture::Usage::BLIT_DST);
@@ -3737,7 +3716,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::resolve(FrameGraph& fg,
return ppResolve->output;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::resolveDepth(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> PostProcessManager::resolveDepthWithShader(FrameGraph& fg,
utils::StaticString outputBufferName, FrameGraphId<FrameGraphTexture> const input,
FrameGraphTexture::Descriptor outDesc) noexcept {
@@ -3762,11 +3741,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::resolveDepth(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> output;
};
auto const& ppResolve = fg.addPass<ResolveData>("resolveDepth",
auto const& ppResolve = fg.addPass<ResolveData>("resolveDepthWithShader",
[&](FrameGraph::Builder& builder, auto& data) {
// we currently don't support stencil resolve
assert_invariant(!isStencilFormat(inDesc.format));
data.input = builder.sample(input);
data.output = builder.createTexture(outputBufferName, outDesc);
data.output = builder.write(data.output, FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
@@ -3775,6 +3751,9 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::resolveDepth(FrameGraph& fg,
.clearFlags = TargetBufferFlags::DEPTH });
},
[=, this](FrameGraphResources const& resources, auto const& data, DriverApi& driver) {
// we currently don't support stencil resolve
assert_invariant(!isStencilFormat(inDesc.format));
bindPostProcessDescriptorSet(driver);
bindPerRenderableDescriptorSet(driver);
auto const& input = resources.getTexture(data.input);
@@ -3823,15 +3802,13 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::vsmMipmapPass(FrameGraph& fg
auto const& inDesc = resources.getDescriptor(data.in);
auto width = inDesc.width;
assert_invariant(width == inDesc.height);
int const dim = width >> (level + 1);
uint32_t const dim = std::max(1u, width >> (level + 1));
auto& material = getPostProcessMaterial("vsmMipmap");
FMaterial const* const ma = material.getMaterial(mEngine, driver);
// When generating shadow map mip levels, we want to preserve the 1 texel border.
// (note clearing never respects the scissor in Filament)
auto const pipeline = getPipelineState(ma);
backend::Viewport const scissor = { 1u, 1u, dim - 2u, dim - 2u };
backend::Viewport const scissor = { 0, 0, dim, dim };
FMaterialInstance* const mi = getMaterialInstance(ma);
mi->setParameter("color", in, SamplerParams{

Some files were not shown because too many files have changed in this diff Show More