瀏覽代碼

Merge branch 'dev' into city

Mr.doob 2 周之前
父節點
當前提交
8e5ca7f64f
共有 75 個文件被更改,包括 1499 次插入1512 次删除
  1. 3 3
      .github/workflows/codeql-code-scanning.yml
  2. 241 0
      .github/workflows/report-e2e.yml
  3. 114 4
      build/three.core.js
  4. 0 0
      build/three.module.js
  5. 2 4
      build/three.tsl.js
  6. 0 0
      build/three.webgpu.js
  7. 0 0
      build/three.webgpu.nodes.js
  8. 1 0
      examples/files.json
  9. 1 0
      examples/jsm/inspector/extensions/tsl-graph/TSLGraphLoader.js
  10. 6 108
      examples/jsm/materials/WoodNodeMaterial.js
  11. 0 596
      examples/jsm/tsl/display/DualKawaseBloomNode.js
  12. 二進制
      examples/screenshots/webgl_loader_gltf_iridescence.jpg
  13. 二進制
      examples/screenshots/webgpu_loader_gltf_iridescence.jpg
  14. 二進制
      examples/screenshots/webgpu_materials_retroreflective.jpg
  15. 1 1
      examples/webgl_shadowmap_pointlight.html
  16. 0 1
      examples/webgpu_custom_fog.html
  17. 0 1
      examples/webgpu_generator_building.html
  18. 0 1
      examples/webgpu_generator_city.html
  19. 0 1
      examples/webgpu_lights_projector.html
  20. 0 1
      examples/webgpu_lights_spotlight.html
  21. 192 0
      examples/webgpu_materials_retroreflective.html
  22. 32 23
      examples/webgpu_postprocessing_bloom.html
  23. 5 23
      examples/webgpu_postprocessing_bloom_emissive.html
  24. 5 25
      examples/webgpu_postprocessing_bloom_selective.html
  25. 0 1
      examples/webgpu_postprocessing_sss.html
  26. 0 1
      examples/webgpu_reflection.html
  27. 0 1
      examples/webgpu_shadowmap_array.html
  28. 0 1
      examples/webgpu_shadowmap_csm.html
  29. 1 1
      examples/webgpu_shadowmap_pointlight.html
  30. 0 1
      examples/webgpu_skinning_instancing_individual.html
  31. 0 1
      manual/examples/shadows-spot-light-with-shadow-radius.html
  32. 28 24
      package-lock.json
  33. 2 4
      src/Three.TSL.js
  34. 1 0
      src/constants.js
  35. 79 4
      src/core/RenderTarget.js
  36. 99 205
      src/extras/PMREMGenerator.js
  37. 2 0
      src/materials/Material.js
  38. 32 0
      src/materials/MeshPhysicalMaterial.js
  39. 38 3
      src/materials/nodes/MeshPhysicalNodeMaterial.js
  40. 0 1
      src/nodes/TSL.js
  41. 9 0
      src/nodes/accessors/MaterialNode.js
  42. 5 3
      src/nodes/accessors/Skinning.js
  43. 30 5
      src/nodes/accessors/UniformArrayNode.js
  44. 8 0
      src/nodes/core/PropertyNode.js
  45. 6 6
      src/nodes/functions/BSDF/BRDF_GGX.js
  46. 0 52
      src/nodes/functions/BSDF/BRDF_GGX_Multiscatter.js
  47. 2 1
      src/nodes/functions/BSDF/DFGLUT.js
  48. 58 21
      src/nodes/functions/PhysicalLightingModel.js
  49. 2 50
      src/nodes/lighting/ShadowFilterNode.js
  50. 6 6
      src/nodes/lighting/ShadowNode.js
  51. 31 74
      src/nodes/pmrem/PMREMUtils.js
  52. 2 0
      src/renderers/WebGLRenderer.js
  53. 3 0
      src/renderers/common/Bindings.js
  54. 48 9
      src/renderers/common/RenderList.js
  55. 53 0
      src/renderers/common/RenderLists.js
  56. 23 1
      src/renderers/common/Renderer.js
  57. 14 3
      src/renderers/common/XRManager.js
  58. 76 144
      src/renderers/common/extras/PMREMGenerator.js
  59. 2 1
      src/renderers/shaders/DFGLUTData.js
  60. 5 4
      src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js
  61. 6 0
      src/renderers/shaders/ShaderChunk/lights_physical_fragment.glsl.js
  62. 58 50
      src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js
  63. 1 0
      src/renderers/shaders/ShaderLib.js
  64. 4 0
      src/renderers/shaders/ShaderLib/meshphysical.glsl.js
  65. 42 17
      src/renderers/webgl-fallback/WebGLBackend.js
  66. 6 0
      src/renderers/webgl/WebGLMaterials.js
  67. 1 0
      src/renderers/webgl/WebGLProgram.js
  68. 4 0
      src/renderers/webgl/WebGLPrograms.js
  69. 1 1
      src/renderers/webgl/WebGLShadowMap.js
  70. 4 4
      src/renderers/webgl/WebGLTextures.js
  71. 32 10
      src/renderers/webgpu/WebGPUBackend.js
  72. 22 0
      src/renderers/webgpu/utils/WebGPUTextureUtils.js
  73. 6 2
      src/renderers/webxr/WebXRManager.js
  74. 34 0
      test/unit/src/materials/MeshPhysicalMaterial.tests.js
  75. 10 8
      utils/generateDFGLUT.js

+ 3 - 3
.github/workflows/codeql-code-scanning.yml

@@ -33,16 +33,16 @@ jobs:
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
+      uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4
       with:
         languages: ${{ matrix.language }}
         config-file: ./.github/codeql-config.yml
         queries: security-and-quality
 
     - name: Autobuild
-      uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
+      uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
+      uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4
       with:
         category: "/language:${{matrix.language}}"

+ 241 - 0
.github/workflows/report-e2e.yml

@@ -0,0 +1,241 @@
+name: Report E2E
+
+on:
+  workflow_run:
+    workflows: ["CI"]
+    types:
+      - completed
+
+permissions:
+  contents: read
+
+jobs:
+  report-e2e:
+    name: Comment on PR
+    runs-on: ubuntu-latest
+    # This job needs "pull-requests: write" to comment on the pull request and
+    # "contents: write" to push the failure screenshots to the "e2e-screenshots"
+    # branch so they can be embedded in the comment. We can't checkout the PR
+    # code in this workflow, and we never execute anything from the artifacts.
+    # Reference:
+    # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
+    permissions:
+      contents: write
+      pull-requests: write
+    if: github.event.workflow_run.event == 'pull_request' &&
+      (github.event.workflow_run.conclusion == 'success' ||
+      github.event.workflow_run.conclusion == 'failure')
+    steps:
+      # Using actions/download-artifact doesn't work here
+      # https://github.com/actions/download-artifact/issues/60
+      - name: Download artifacts
+        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
+        id: download
+        with:
+          script: |
+            const fs = require('fs/promises');
+
+            const runId = context.payload.workflow_run.id;
+
+            const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              run_id: runId,
+            });
+
+            await fs.mkdir('screenshots', { recursive: true });
+
+            for (const artifact of artifacts) {
+
+              if (!artifact.name.startsWith('Output screenshots-') || artifact.expired) continue;
+
+              const download = await github.rest.actions.downloadArtifact({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                artifact_id: artifact.id,
+                archive_format: 'zip',
+              });
+
+              await fs.writeFile(`${artifact.id}.zip`, Buffer.from(download.data));
+
+              // -j flattens paths so a hostile archive can't write outside the directory
+              await exec.exec('unzip', ['-q', '-j', '-o', `${artifact.id}.zip`, '-d', 'screenshots']);
+
+            }
+
+            // The e2e test only writes these files for examples that failed.
+            // Only strictly named jpgs are accepted; everything else is ignored.
+            const pattern = /^([a-zA-Z0-9_-]+)-(actual|expected|diff)\.jpg$/;
+
+            const examples = new Map();
+
+            for (const file of await fs.readdir('screenshots')) {
+
+              const match = pattern.exec(file);
+              if (match === null) continue;
+
+              if (!examples.has(match[1])) examples.set(match[1], {});
+              examples.get(match[1])[match[2]] = file;
+
+            }
+
+            const names = [...examples.keys()].sort();
+
+            // Cap the embedded images so a broken build failing hundreds of
+            // examples doesn't blow up the comment. The rest are listed by name.
+            const MAX_EMBEDDED = 10;
+            const embedded = names.slice(0, MAX_EMBEDDED);
+
+            await fs.mkdir(`payload/${runId}`, { recursive: true });
+
+            for (const name of embedded) {
+
+              for (const file of Object.values(examples.get(name))) {
+
+                await fs.copyFile(`screenshots/${file}`, `payload/${runId}/${file}`);
+
+              }
+
+            }
+
+            const report = {
+              total: names.length,
+              embedded: embedded.map((name) => ({ name, ...examples.get(name) })),
+              omitted: names.slice(MAX_EMBEDDED),
+            };
+
+            await fs.writeFile('report.json', JSON.stringify(report));
+
+            core.setOutput('failed', names.length);
+
+      # Each run force-pushes a fresh orphan commit, so the branch stays a
+      # single small commit. The comment links images by commit sha, which
+      # keeps working until GitHub garbage-collects the unreachable commit.
+      - name: Publish images
+        id: publish
+        if: steps.download.outputs.failed != '0'
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+        run: |
+          cd payload
+          git init --quiet --initial-branch=e2e-screenshots
+          git config user.name "github-actions[bot]"
+          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+          git add .
+          git commit --quiet -m "E2E screenshots for run ${{ github.event.workflow_run.id }}"
+          git push --quiet --force "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:refs/heads/e2e-screenshots
+          echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
+
+      - name: Comment on PR
+        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
+        env:
+          IMAGES_SHA: ${{ steps.publish.outputs.sha }}
+        with:
+          script: |
+            const fs = require('fs/promises');
+
+            const run = context.payload.workflow_run;
+            const { owner, repo } = context.repo;
+
+            // workflow_run.pull_requests is empty for PRs from forks, so fall
+            // back to looking the PR up by its head. The head sha check also
+            // skips stale runs where the PR has received a newer push.
+            let pr = run.pull_requests.find((p) => p.head.sha === run.head_sha);
+
+            if (pr === undefined && run.head_repository !== null) {
+
+              const pulls = await github.rest.pulls.list({
+                owner,
+                repo,
+                state: 'open',
+                head: `${run.head_repository.owner.login}:${run.head_branch}`,
+                per_page: 100,
+              });
+
+              pr = pulls.data.find((p) => p.head.sha === run.head_sha);
+
+            }
+
+            if (pr === undefined) {
+
+              core.info('No open PR matches this run, skipping.');
+              return;
+
+            }
+
+            const report = JSON.parse(await fs.readFile('report.json', 'utf8'));
+
+            const marker = '<!-- e2e-report -->';
+            const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${run.id}`;
+
+            const comments = await github.paginate(github.rest.issues.listComments, {
+              owner,
+              repo,
+              issue_number: pr.number,
+              per_page: 100,
+            });
+
+            const previous = comments.find((comment) => comment.body && comment.body.includes(marker));
+
+            let body;
+
+            if (report.total > 0) {
+
+              const raw = `https://raw.githubusercontent.com/${owner}/${repo}/${process.env.IMAGES_SHA}/${run.id}`;
+              const cell = (file) => file === undefined ? '' :
+                `<a href="${raw}/${file}"><img src="${raw}/${file}" width="200"></a>`;
+
+              const rows = report.embedded.map((example) =>
+                `| \`${example.name}\` | ${cell(example.expected)} | ${cell(example.actual)} | ${cell(example.diff)} |`
+              );
+
+              const lines = [
+                marker,
+                '### 🖼️ E2E screenshot tests',
+                '',
+                `❌ **${report.total}** example(s) failed ([full artifacts](${runUrl})).`,
+                '',
+                '| Example | Expected | Actual | Diff |',
+                '|:--|:-:|:-:|:-:|',
+                ...rows,
+              ];
+
+              if (report.omitted.length > 0) {
+
+                lines.push('', `…and ${report.omitted.length} more: ${report.omitted.map((name) => `\`${name}\``).join(', ')}`);
+
+              }
+
+              body = lines.join('\n');
+
+            } else {
+
+              // Only turn a previous failure report green; don't comment on
+              // pull requests that never failed.
+              if (previous === undefined) return;
+
+              // The run can also fail on lint or unit tests. Only report
+              // success if the e2e jobs themselves all passed.
+              const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
+                owner,
+                repo,
+                run_id: run.id,
+                filter: 'latest',
+              });
+
+              const e2eJobs = jobs.filter((job) => job.name.startsWith('E2E testing'));
+              if (e2eJobs.length === 0 || !e2eJobs.every((job) => job.conclusion === 'success')) return;
+
+              body = `${marker}\n### 🖼️ E2E screenshot tests\n\n✅ All examples render correctly again ([run](${runUrl})).`;
+
+            }
+
+            if (previous === undefined) {
+
+              await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
+
+            } else {
+
+              await github.rest.issues.updateComment({ owner, repo, comment_id: previous.id, body });
+
+            }

+ 114 - 4
build/three.core.js

@@ -75,6 +75,7 @@ const PCFShadowMap = 1;
  *
  * @type {number}
  * @constant
+ * @deprecated since r186. Use `PCFShadowMap` instead.
  */
 const PCFSoftShadowMap = 2;
 
@@ -9117,8 +9118,12 @@ class RenderTarget extends EventDispatcher {
 	 * @property {string} [colorSpace=NoColorSpace] - The texture's color space.
 	 * @property {boolean} [depthBuffer=true] - Whether to allocate a depth buffer or not.
 	 * @property {boolean} [stencilBuffer=false] - Whether to allocate a stencil buffer or not.
-	 * @property {boolean} [resolveDepthBuffer=true] - Whether to resolve the depth buffer or not.
-	 * @property {boolean} [resolveStencilBuffer=true] - Whether  to resolve the stencil buffer or not.
+	 * @property {boolean} [resolveColorBuffer=true] - Whether to resolve the color buffer or not. Only relevant for multisampled render targets.
+	 * @property {boolean} [resolveDepthBuffer=true] - Whether to resolve the depth buffer or not. Only relevant for multisampled render targets.
+	 * @property {boolean} [resolveStencilBuffer=true] - Whether to resolve the stencil buffer or not. Only relevant for multisampled render targets.
+	 * @property {boolean} [storeMultisampledColorBuffer=true] - Whether to store the multisampled color buffer or not. Setting to `false` saves memory bandwidth when the multisampled data are not needed after a render pass.
+	 * @property {boolean} [storeMultisampledDepthBuffer=true] - Whether to store the multisampled depth buffer or not. Setting to `false` saves memory bandwidth when the multisampled data are not needed after a render pass.
+	 * @property {boolean} [storeMultisampledStencilBuffer=true] - Whether to store the multisampled stencil buffer or not. Setting to `false` saves memory bandwidth when the multisampled data are not needed after a render pass.
 	 * @property {?Texture} [depthTexture=null] - Reference to a depth texture.
 	 * @property {number} [samples=0] - The MSAA samples count.
 	 * @property {number} [count=1] - Defines the number of color attachments . Must be at least `1`.
@@ -9144,8 +9149,12 @@ class RenderTarget extends EventDispatcher {
 			minFilter: LinearFilter,
 			depthBuffer: true,
 			stencilBuffer: false,
+			resolveColorBuffer: true,
 			resolveDepthBuffer: true,
 			resolveStencilBuffer: true,
+			storeMultisampledColorBuffer: true,
+			storeMultisampledDepthBuffer: true,
+			storeMultisampledStencilBuffer: true,
 			depthTexture: null,
 			samples: 0,
 			count: 1,
@@ -9252,7 +9261,28 @@ class RenderTarget extends EventDispatcher {
 		this.stencilBuffer = options.stencilBuffer;
 
 		/**
-		 * Whether to resolve the depth buffer or not.
+		 * Whether to resolve the color buffer or not. When set to `false`, the color
+		 * attachments do not receive the resolved (single-sampled) output of a render
+		 * pass and the render target's textures are left untouched. The rendered
+		 * content is then only accessible within the render pass itself.
+		 *
+		 * Only relevant for multisampled render targets.
+		 *
+		 * @type {boolean}
+		 * @default true
+		 */
+		this.resolveColorBuffer = options.resolveColorBuffer;
+
+		/**
+		 * Whether to resolve the depth buffer or not. When set to `false`, the depth
+		 * texture does not receive the resolved depth output of a render pass which
+		 * saves memory bandwidth. Use this setting when the depth data of a render
+		 * pass are not required afterwards.
+		 *
+		 * Only relevant for multisampled render targets in WebGL. WebGPU does not
+		 * support depth resolves; sampling the depth texture of a multisampled render
+		 * target accesses the multisampled data directly, see
+		 * {@link RenderTarget#storeMultisampledDepthBuffer}.
 		 *
 		 * @type {boolean}
 		 * @default true
@@ -9260,13 +9290,54 @@ class RenderTarget extends EventDispatcher {
 		this.resolveDepthBuffer = options.resolveDepthBuffer;
 
 		/**
-		 * Whether to resolve the stencil buffer or not.
+		 * Whether to resolve the stencil buffer or not. Analogous to
+		 * {@link RenderTarget#resolveDepthBuffer} but for the stencil aspect.
 		 *
 		 * @type {boolean}
 		 * @default true
 		 */
 		this.resolveStencilBuffer = options.resolveStencilBuffer;
 
+		/**
+		 * Whether to store the multisampled color buffer or not. When set to `false`,
+		 * the multisampled data are discarded at the end of a render pass, right after
+		 * they have been resolved. This saves memory bandwidth, especially on tile-based
+		 * GPUs, and is the recommended setting for render targets that are fully redrawn
+		 * each frame and whose output is only accessed via the resolved textures (e.g.
+		 * scene passes in post-processing chains).
+		 *
+		 * Must be kept `true` when the multisampled data are needed after the render
+		 * pass ends, e.g. when rendering into the target without clearing or when the
+		 * scene contains transmissive objects which require a mid-pass framebuffer copy.
+		 *
+		 * @type {boolean}
+		 * @default true
+		 */
+		this.storeMultisampledColorBuffer = options.storeMultisampledColorBuffer;
+
+		/**
+		 * Whether to store the multisampled depth buffer or not. When set to `false`,
+		 * the multisampled depth data are discarded at the end of a render pass which
+		 * saves memory bandwidth.
+		 *
+		 * Must be kept `true` in WebGPU when the depth texture of a multisampled render
+		 * target is sampled (e.g. by depth-based post-processing effects) since depth
+		 * is read directly from the multisampled data.
+		 *
+		 * @type {boolean}
+		 * @default true
+		 */
+		this.storeMultisampledDepthBuffer = options.storeMultisampledDepthBuffer;
+
+		/**
+		 * Whether to store the multisampled stencil buffer or not. Analogous to
+		 * {@link RenderTarget#storeMultisampledDepthBuffer} but for the stencil aspect.
+		 *
+		 * @type {boolean}
+		 * @default true
+		 */
+		this.storeMultisampledStencilBuffer = options.storeMultisampledStencilBuffer;
+
 		this._depthTexture = null;
 		this.depthTexture = options.depthTexture;
 
@@ -9462,9 +9533,14 @@ class RenderTarget extends EventDispatcher {
 		this.depthBuffer = source.depthBuffer;
 		this.stencilBuffer = source.stencilBuffer;
 
+		this.resolveColorBuffer = source.resolveColorBuffer;
 		this.resolveDepthBuffer = source.resolveDepthBuffer;
 		this.resolveStencilBuffer = source.resolveStencilBuffer;
 
+		this.storeMultisampledColorBuffer = source.storeMultisampledColorBuffer;
+		this.storeMultisampledDepthBuffer = source.storeMultisampledDepthBuffer;
+		this.storeMultisampledStencilBuffer = source.storeMultisampledStencilBuffer;
+
 		if ( source.depthTexture !== null ) this.depthTexture = source.depthTexture.clone();
 
 		this.samples = source.samples;
@@ -21158,6 +21234,7 @@ class Material extends EventDispatcher {
 		}
 
 		if ( this.dispersion !== undefined ) data.dispersion = this.dispersion;
+		if ( this.retroreflective !== undefined ) data.retroreflective = this.retroreflective;
 
 		if ( this.iridescence !== undefined ) data.iridescence = this.iridescence;
 		if ( this.iridescenceIOR !== undefined ) data.iridescenceIOR = this.iridescenceIOR;
@@ -21386,6 +21463,7 @@ class Material extends EventDispatcher {
 		if ( json.clearcoat !== undefined ) this.clearcoat = json.clearcoat;
 		if ( json.clearcoatRoughness !== undefined ) this.clearcoatRoughness = json.clearcoatRoughness;
 		if ( json.dispersion !== undefined ) this.dispersion = json.dispersion;
+		if ( json.retroreflective !== undefined ) this.retroreflective = json.retroreflective;
 		if ( json.iridescence !== undefined ) this.iridescence = json.iridescence;
 		if ( json.iridescenceIOR !== undefined ) this.iridescenceIOR = json.iridescenceIOR;
 		if ( json.iridescenceThicknessRange !== undefined ) this.iridescenceThicknessRange = json.iridescenceThicknessRange;
@@ -38761,6 +38839,8 @@ class MeshStandardMaterial extends Material {
  * transparent materials are less reflective. Physically-based transmission provides a more
  * realistic option for thin, transparent surfaces like glass.
  * - Advanced reflectivity: More flexible reflectivity for non-metallic materials.
+ * - Retroreflection: Redirects specular light back toward the light source for
+ * safety materials like road markings and reflective tape.
  * - Sheen: Can be used for representing cloth and fabric materials.
  *
  * As a result of these complex shading features, `MeshPhysicalMaterial` has a
@@ -39096,6 +39176,7 @@ class MeshPhysicalMaterial extends MeshStandardMaterial {
 		this._clearcoat = 0;
 		this._dispersion = 0;
 		this._iridescence = 0;
+		this._retroreflective = 0;
 		this._sheen = 0.0;
 		this._transmission = 0;
 
@@ -39203,6 +39284,33 @@ class MeshPhysicalMaterial extends MeshStandardMaterial {
 
 	}
 
+	/**
+	 * The strength of retroreflection, from `0.0` to `1.0`. A value of `1.0`
+	 * evaluates the material's microfacet reflection with the view direction
+	 * reflected about the surface normal, redirecting the specular lobe back
+	 * toward the light source.
+	 *
+	 * @type {number}
+	 * @default 0
+	 */
+	get retroreflective() {
+
+		return this._retroreflective;
+
+	}
+
+	set retroreflective( value ) {
+
+		if ( this._retroreflective > 0 !== value > 0 ) {
+
+			this.version ++;
+
+		}
+
+		this._retroreflective = value;
+
+	}
+
 	/**
 	 * The intensity of the sheen layer, from `0.0` to `1.0`.
 	 *
@@ -39288,6 +39396,8 @@ class MeshPhysicalMaterial extends MeshStandardMaterial {
 		this.iridescenceThicknessRange = [ ...source.iridescenceThicknessRange ];
 		this.iridescenceThicknessMap = source.iridescenceThicknessMap;
 
+		this.retroreflective = source.retroreflective;
+
 		this.sheen = source.sheen;
 		this.sheenColor.copy( source.sheenColor );
 		this.sheenColorMap = source.sheenColorMap;

File diff suppressed because it is too large
+ 0 - 0
build/three.module.js


File diff suppressed because it is too large
+ 2 - 4
build/three.tsl.js


File diff suppressed because it is too large
+ 0 - 0
build/three.webgpu.js


File diff suppressed because it is too large
+ 0 - 0
build/three.webgpu.nodes.js


+ 1 - 0
examples/files.json

@@ -396,6 +396,7 @@
 		"webgpu_materials_envmaps",
 		"webgpu_materials_lightmap",
 		"webgpu_materials_matcap",
+		"webgpu_materials_retroreflective",
 		"webgpu_materials_sss",
 		"webgpu_materials_texture_html",
 		"webgpu_materials_texture_manualmipmap",

+ 1 - 0
examples/jsm/inspector/extensions/tsl-graph/TSLGraphLoader.js

@@ -4,6 +4,7 @@ import * as THREE from 'three';
 import * as TSL from 'three/tsl';
 
 const _library = {
+	'three': { ...THREE },
 	'three/tsl': { ...TSL }
 };
 

+ 6 - 108
examples/jsm/materials/WoodNodeMaterial.js

@@ -99,112 +99,10 @@ const softLightMix = TSL.Fn( ( [ t, col1, col2 ] ) => {
 
 } );
 
-const noiseFbm = TSL.Fn( ( [ p, detail, roughness, lacunarity, useNormalize ] ) => {
+// single-octave normalized noise — equivalent to fbm with detail = 1
 
-	const fscale = TSL.float( 1.0 ).toVar();
-	const amp = TSL.float( 1.0 ).toVar();
-	const maxamp = TSL.float( 0.0 ).toVar();
-	const sum = TSL.float( 0.0 ).toVar();
-
-	const iterations = detail.floor();
-
-	TSL.Loop( iterations, () => {
-
-		const t = TSL.mx_noise_float( p.mul( fscale ) );
-		sum.addAssign( t.mul( amp ) );
-		maxamp.addAssign( amp );
-		amp.mulAssign( roughness );
-		fscale.mulAssign( lacunarity );
-
-	} );
-
-	const rmd = detail.sub( iterations );
-	const hasRemainder = rmd.greaterThan( 0.001 );
-
-	return TSL.select(
-		hasRemainder,
-		TSL.select(
-			useNormalize.equal( 1 ),
-			( () => {
-
-				const t = TSL.mx_noise_float( p.mul( fscale ) );
-				const sum2 = sum.add( t.mul( amp ) );
-				const maxamp2 = maxamp.add( amp );
-				const normalizedSum = sum.div( maxamp ).mul( 0.5 ).add( 0.5 );
-				const normalizedSum2 = sum2.div( maxamp2 ).mul( 0.5 ).add( 0.5 );
-				return TSL.mix( normalizedSum, normalizedSum2, rmd );
-
-			} )(),
-			( () => {
-
-				const t = TSL.mx_noise_float( p.mul( fscale ) );
-				const sum2 = sum.add( t.mul( amp ) );
-				return TSL.mix( sum, sum2, rmd );
-
-			} )()
-		),
-		TSL.select(
-			useNormalize.equal( 1 ),
-			sum.div( maxamp ).mul( 0.5 ).add( 0.5 ),
-			sum
-		)
-	);
-
-} );
-
-const noiseFbm3d = TSL.Fn( ( [ p, detail, roughness, lacunarity, useNormalize ] ) => {
-
-	const fscale = TSL.float( 1.0 ).toVar();
-
-	const amp = TSL.float( 1.0 ).toVar();
-	const maxamp = TSL.float( 0.0 ).toVar();
-	const sum = TSL.vec3( 0.0 ).toVar();
-
-	const iterations = detail.floor();
-
-	TSL.Loop( iterations, () => {
-
-		const t = TSL.mx_noise_vec3( p.mul( fscale ) );
-		sum.addAssign( t.mul( amp ) );
-		maxamp.addAssign( amp );
-		amp.mulAssign( roughness );
-		fscale.mulAssign( lacunarity );
-
-	} );
-
-	const rmd = detail.sub( iterations );
-	const hasRemainder = rmd.greaterThan( 0.001 );
-
-	return TSL.select(
-		hasRemainder,
-		TSL.select(
-			useNormalize.equal( 1 ),
-			( () => {
-
-				const t = TSL.mx_noise_vec3( p.mul( fscale ) );
-				const sum2 = sum.add( t.mul( amp ) );
-				const maxamp2 = maxamp.add( amp );
-				const normalizedSum = sum.div( maxamp ).mul( 0.5 ).add( 0.5 );
-				const normalizedSum2 = sum2.div( maxamp2 ).mul( 0.5 ).add( 0.5 );
-				return TSL.mix( normalizedSum, normalizedSum2, rmd );
-
-			} )(),
-			( () => {
-
-				const t = TSL.mx_noise_vec3( p.mul( fscale ) );
-				const sum2 = sum.add( t.mul( amp ) );
-				return TSL.mix( sum, sum2, rmd );
-
-			} )()
-		),
-		TSL.select(
-			useNormalize.equal( 1 ),
-			sum.div( maxamp ).mul( 0.5 ).add( 0.5 ),
-			sum
-		)
-	);
-
-} );
+const noise1Norm = TSL.Fn( ( [ p ] ) => TSL.mx_noise_float( p ).mul( 0.5 ).add( 0.5 ) );
+const noise3Norm = TSL.Fn( ( [ p ] ) => TSL.mx_noise_vec3( p ).mul( 0.5 ).add( 0.5 ) );
 
 const woodCenter = TSL.Fn( ( [ p, centerSize ] ) => {
 
@@ -218,7 +116,7 @@ const woodCenter = TSL.Fn( ( [ p, centerSize ] ) => {
 const spaceWarp = TSL.Fn( ( [ p, warpStrength, xyScale, zScale ] ) => {
 
 	const combinedXyz = TSL.vec3( xyScale, xyScale, zScale ).mul( p );
-	const noise = noiseFbm3d( combinedXyz.mul( 1.6 * 1.5 ), TSL.float( 1 ), TSL.float( 0.5 ), TSL.float( 2 ), TSL.int( 1 ) ).sub( 0.5 ).mul( warpStrength );
+	const noise = noise3Norm( combinedXyz.mul( 1.6 * 1.5 ) ).sub( 0.5 ).mul( warpStrength );
 	const pXy = p.mul( TSL.vec3( 1, 1, 0 ) );
 	const normalizedXy = pXy.normalize();
 	const warp = noise.mul( normalizedXy ).add( pXy );
@@ -229,7 +127,7 @@ const spaceWarp = TSL.Fn( ( [ p, warpStrength, xyScale, zScale ] ) => {
 
 const woodRings = TSL.Fn( ( [ w, ringThickness, ringBias, ringSizeVariance, ringVarianceScale, barkThickness ] ) => {
 
-	const rings = noiseFbm( w.mul( ringVarianceScale ), TSL.float( 1 ), TSL.float( 0.5 ), TSL.float( 1 ), TSL.int( 1 ) ).mul( ringSizeVariance ).add( w ).mul( ringThickness ).fract().mul( barkThickness );
+	const rings = noise1Norm( w.mul( ringVarianceScale ) ).mul( ringSizeVariance ).add( w ).mul( ringThickness ).fract().mul( barkThickness );
 
 	const sharpRings = TSL.min( mapRange( rings, 0, ringBias, 0, 1, TSL.bool( true ) ), mapRange( rings, ringBias, 1, 1, 0, TSL.bool( true ) ) );
 
@@ -246,7 +144,7 @@ const woodDetail = TSL.Fn( ( [ warp, p, y, splotchScale ] ) => {
 	const combinedXyz = TSL.vec3( radialCoords.sin(), y, radialCoords.cos().mul( p.z ) );
 	const scaled = TSL.vec3( 0.1, 1.19, 0.05 ).mul( combinedXyz );
 
-	return noiseFbm( scaled.mul( splotchScale ), TSL.float( 1 ), TSL.float( 0.5 ), TSL.float( 2 ), TSL.bool( true ) );
+	return noise1Norm( scaled.mul( splotchScale ) );
 
 } );
 

+ 0 - 596
examples/jsm/tsl/display/DualKawaseBloomNode.js

@@ -1,596 +0,0 @@
-import { HalfFloatType, RenderTarget, Vector2, TempNode, QuadMesh, NodeMaterial, RendererUtils, NodeUpdateType } from 'three/webgpu';
-import { nodeObject, Fn, float, vec2, uv, passTexture, uniform, texture, luminance, smoothstep, mix, vec4 } from 'three/tsl';
-
-const _quadMesh = /*@__PURE__*/ new QuadMesh();
-const _size = /*@__PURE__*/ new Vector2();
-
-let _rendererState;
-
-const luminosityHighPass = Fn( ( { input, threshold, smoothWidth } ) => {
-
-	const v = luminance( input.rgb );
-	const alpha = smoothstep( threshold, threshold.add( smoothWidth ), v );
-
-	return mix( vec4( 0 ), input, alpha );
-
-} );
-
-/**
- * Post processing node for creating a bloom effect.
- *
- * The bloom is produced with a Dual Kawase blur: the bright areas are
- * progressively downsampled with a 5-tap filter and then upsampled with an
- * 8-tap filter, accumulating the levels back together.
- * ```js
- * const renderPipeline = new THREE.RenderPipeline( renderer );
- *
- * const scenePass = pass( scene, camera );
- * const scenePassColor = scenePass.getTextureNode( 'output' );
- *
- * const bloomPass = dualKawaseBloom( scenePassColor );
- *
- * renderPipeline.outputNode = scenePassColor.add( bloomPass );
- * ```
- * By default, the node affects the entire image. For a selective bloom,
- * use the `emissive` material property to control which objects should
- * contribute to bloom or not. This can be achieved via MRT.
- * ```js
- * const renderPipeline = new THREE.RenderPipeline( renderer );
- *
- * const scenePass = pass( scene, camera );
- * scenePass.setMRT( mrt( {
- * 	output,
- * 	emissive
- * } ) );
- *
- * const scenePassColor = scenePass.getTextureNode( 'output' );
- * const emissivePass = scenePass.getTextureNode( 'emissive' );
- *
- * const bloomPass = dualKawaseBloom( emissivePass );
- * renderPipeline.outputNode = scenePassColor.add( bloomPass );
- * ```
- * @augments TempNode
- * @three_import import { dualKawaseBloom } from 'three/addons/tsl/display/DualKawaseBloomNode.js';
- */
-class DualKawaseBloomNode extends TempNode {
-
-	static get type() {
-
-		return 'DualKawaseBloomNode';
-
-	}
-
-	/**
-	 * Constructs a new bloom node.
-	 *
-	 * @param {Node<vec4>} inputNode - The node that represents the input of the effect.
-	 * @param {number} [strength=1] - The strength of the bloom.
-	 * @param {number} [radius=0] - The radius of the bloom.
-	 * @param {number} [threshold=0] - The luminance threshold limits which bright areas contribute to the bloom effect.
-	 */
-	constructor( inputNode, strength = 1, radius = 0, threshold = 0 ) {
-
-		super( 'vec4' );
-
-		/**
-		 * The node that represents the input of the effect.
-		 *
-		 * @type {Node<vec4>}
-		 */
-		this.inputNode = inputNode;
-
-		/**
-		 * The strength of the bloom.
-		 *
-		 * @type {UniformNode<float>}
-		 */
-		this.strength = strength.isNode ? strength : uniform( strength );
-
-		/**
-		 * The radius of the bloom. Must be in the range `[0,1]`.
-		 *
-		 * @type {UniformNode<float>}
-		 */
-		this.radius = radius.isNode ? radius : uniform( radius );
-
-		/**
-		 * The luminance threshold limits which bright areas contribute to the bloom effect.
-		 *
-		 * @type {UniformNode<float>}
-		 */
-		this.threshold = threshold.isNode ? threshold : uniform( threshold );
-
-		/**
-		 * Can be used to tweak the extracted luminance from the scene.
-		 *
-		 * @type {UniformNode<float>}
-		 */
-		this.smoothWidth = uniform( 0.01 );
-
-		/**
-		 * Scale factor for the internal render targets.
-		 *
-		 * @private
-		 * @type {number}
-		 * @default 0.5
-		 */
-		this._resolutionScale = 0.5;
-
-		/**
-		 * Can be used to inject a custom high pass filter (e.g., for anamorphic effects).
-		 *
-		 * @type {Function}
-		 */
-		this.highPassFn = luminosityHighPass;
-
-		/**
-		 * The number of downsample / upsample levels in the Dual Kawase pyramid.
-		 *
-		 * @private
-		 * @type {number}
-		 */
-		this._nMips = 6;
-
-		/**
-		 * Sample spread of the Dual Kawase filters. Kept small so each level's kernel
-		 * stays round (a wide offset makes the diamond sampling pattern visible as a
-		 * square halo); the bloom width comes from the depth of the pyramid instead.
-		 *
-		 * @private
-		 * @type {UniformNode<float>}
-		 */
-		this._offset = uniform( 3 );
-
-		/**
-		 * Per-level mixing factors, redistributed by `radius` between a tight and a wide bloom.
-		 * A linear ramp from `1.0` to `0.2` (mean `0.6`) keeps the total bloom energy constant
-		 * as `radius` shifts weight between the fine and coarse levels.
-		 *
-		 * @private
-		 * @type {Array<number>}
-		 */
-		this._bloomFactors = [];
-
-		for ( let i = 0; i < this._nMips; i ++ ) {
-
-			this._bloomFactors.push( this._nMips === 1 ? 0.6 : 1.0 - 0.8 * i / ( this._nMips - 1 ) );
-
-		}
-
-		/**
-		 * The render target for the luminance pass.
-		 *
-		 * @private
-		 * @type {RenderTarget}
-		 */
-		this._renderTargetBright = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
-		this._renderTargetBright.texture.name = 'DualKawaseBloom.bright';
-		this._renderTargetBright.texture.generateMipmaps = false;
-
-		/**
-		 * The render targets for the downsample chain.
-		 *
-		 * @private
-		 * @type {Array<RenderTarget>}
-		 */
-		this._downsampleRTs = [];
-
-		/**
-		 * The render targets for the upsample / accumulation chain.
-		 *
-		 * @private
-		 * @type {Array<RenderTarget>}
-		 */
-		this._accumRTs = [];
-
-		/**
-		 * The resolution of each pyramid level.
-		 *
-		 * @private
-		 * @type {Array<Vector2>}
-		 */
-		this._levelSizes = [];
-
-		/**
-		 * The resolution of the bright pass.
-		 *
-		 * @private
-		 * @type {Vector2}
-		 */
-		this._brightSize = new Vector2();
-
-		for ( let i = 0; i < this._nMips; i ++ ) {
-
-			const downsampleRT = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
-			downsampleRT.texture.name = 'DualKawaseBloom.down' + i;
-			downsampleRT.texture.generateMipmaps = false;
-			this._downsampleRTs.push( downsampleRT );
-
-			const accumRT = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } );
-			accumRT.texture.name = 'DualKawaseBloom.up' + i;
-			accumRT.texture.generateMipmaps = false;
-			this._accumRTs.push( accumRT );
-
-			this._levelSizes.push( new Vector2() );
-
-		}
-
-		/**
-		 * The material for the luminance pass.
-		 *
-		 * @private
-		 * @type {?NodeMaterial}
-		 */
-		this._highPassFilterMaterial = null;
-
-		/**
-		 * The material for the downsample pass.
-		 *
-		 * @private
-		 * @type {?NodeMaterial}
-		 */
-		this._downsampleMaterial = null;
-
-		/**
-		 * The material for the upsample / accumulation pass.
-		 *
-		 * @private
-		 * @type {?NodeMaterial}
-		 */
-		this._upsampleMaterial = null;
-
-		/**
-		 * The result of the effect is represented as a separate texture node.
-		 * The finest accumulation target holds the composited bloom.
-		 *
-		 * @private
-		 * @type {PassTextureNode}
-		 */
-		this._textureOutput = passTexture( this, this._accumRTs[ 0 ].texture );
-
-		/**
-		 * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders
-		 * its effect once per frame in `updateBefore()`.
-		 *
-		 * @type {string}
-		 * @default 'frame'
-		 */
-		this.updateBeforeType = NodeUpdateType.FRAME;
-
-	}
-
-	/**
-	 * Returns the result of the effect as a texture node.
-	 *
-	 * @return {PassTextureNode} A texture node that represents the result of the effect.
-	 */
-	getTextureNode() {
-
-		return this._textureOutput;
-
-	}
-
-	/**
-	 * Sets the resolution scale for the pass.
-	 * The resolution scale is a factor that is multiplied with the renderer's width and height.
-	 *
-	 * @param {number} resolutionScale - The resolution scale to set. A value of `1` means full resolution.
-	 * @return {DualKawaseBloomNode} A reference to this node.
-	 */
-	setResolutionScale( resolutionScale ) {
-
-		this._resolutionScale = resolutionScale;
-
-		return this;
-
-	}
-
-	/**
-	 * Gets the current resolution scale of the pass.
-	 *
-	 * @return {number} The current resolution scale. A value of `1` means full resolution.
-	 */
-	getResolutionScale() {
-
-		return this._resolutionScale;
-
-	}
-
-	/**
-	 * Sets the size of the effect.
-	 *
-	 * @param {number} width - The width of the effect.
-	 * @param {number} height - The height of the effect.
-	 */
-	setSize( width, height ) {
-
-		const resx = Math.max( 1, Math.floor( width * this._resolutionScale ) );
-		const resy = Math.max( 1, Math.floor( height * this._resolutionScale ) );
-
-		this._renderTargetBright.setSize( resx, resy );
-		this._brightSize.set( resx, resy );
-
-		let rx = resx;
-		let ry = resy;
-
-		for ( let i = 0; i < this._nMips; i ++ ) {
-
-			// Level 0 blurs in place at bright resolution; the rest halve.
-
-			if ( i > 0 ) {
-
-				rx = Math.max( 1, Math.floor( rx / 2 ) );
-				ry = Math.max( 1, Math.floor( ry / 2 ) );
-
-			}
-
-			this._downsampleRTs[ i ].setSize( rx, ry );
-			this._accumRTs[ i ].setSize( rx, ry );
-			this._levelSizes[ i ].set( rx, ry );
-
-		}
-
-	}
-
-	/**
-	 * This method is used to render the effect once per frame.
-	 *
-	 * @param {NodeFrame} frame - The current node frame.
-	 */
-	updateBefore( frame ) {
-
-		const { renderer } = frame;
-
-		_rendererState = RendererUtils.resetRendererState( renderer, _rendererState );
-
-		//
-
-		const size = renderer.getDrawingBufferSize( _size );
-		this.setSize( size.width, size.height );
-
-		const nMips = this._nMips;
-		const downMaterial = this._downsampleMaterial;
-		const upMaterial = this._upsampleMaterial;
-
-		// 1. Extract bright areas
-
-		renderer.setRenderTarget( this._renderTargetBright );
-		_quadMesh.material = this._highPassFilterMaterial;
-		_quadMesh.name = 'Dual Kawase Bloom [ High Pass ]';
-		_quadMesh.render( renderer );
-
-		// 2. Downsample chain ( bright -> down0 -> down1 -> ... ). The coarsest level
-		// pre-multiplies its `radius` weight so the upsample chain can seed from it.
-
-		_quadMesh.material = downMaterial;
-
-		let sourceTexture = this._renderTargetBright.texture;
-		let sourceSize = this._brightSize;
-
-		for ( let i = 0; i < nMips; i ++ ) {
-
-			const isCoarsest = ( i === nMips - 1 );
-
-			downMaterial.colorTexture.value = sourceTexture;
-			downMaterial.texelSize.value.set( 1 / sourceSize.x, 1 / sourceSize.y );
-			downMaterial.applyWeight.value = isCoarsest ? 1 : 0;
-			downMaterial.bloomFactor.value = this._bloomFactors[ i ];
-			renderer.setRenderTarget( this._downsampleRTs[ i ] );
-			_quadMesh.name = `Dual Kawase Bloom [ Downsample - ${ i } ]`;
-			_quadMesh.render( renderer );
-
-			sourceTexture = this._downsampleRTs[ i ].texture;
-			sourceSize = this._levelSizes[ i ];
-
-		}
-
-		// 3. Upsample chain with weighted accumulation ( ... -> up1 -> up0 ).
-		// Each level adds its matching downsample weighted by `radius`; the
-		// coarsest downsample seeds the chain. The finest step ( i = 0 ) lands at
-		// bright resolution and applies `strength`, so it doubles as the composite.
-
-		_quadMesh.material = upMaterial;
-
-		for ( let i = nMips - 2; i >= 0; i -- ) {
-
-			const isSeed = ( i === nMips - 2 );
-
-			upMaterial.finalFlag.value = ( i === 0 ) ? 1 : 0;
-			upMaterial.bloomFactor.value = this._bloomFactors[ i ];
-			upMaterial.prevTexture.value = isSeed ? this._downsampleRTs[ i + 1 ].texture : this._accumRTs[ i + 1 ].texture;
-			upMaterial.addTexture.value = this._downsampleRTs[ i ].texture;
-			upMaterial.texelSize.value.set( 1 / this._levelSizes[ i + 1 ].x, 1 / this._levelSizes[ i + 1 ].y );
-
-			renderer.setRenderTarget( this._accumRTs[ i ] );
-			_quadMesh.name = ( i === 0 ) ? 'Dual Kawase Bloom [ Composite ]' : `Dual Kawase Bloom [ Upsample - ${ i } ]`;
-			_quadMesh.render( renderer );
-
-		}
-
-		// restore
-
-		RendererUtils.restoreRendererState( renderer, _rendererState );
-
-	}
-
-	/**
-	 * This method is used to setup the effect's TSL code.
-	 *
-	 * @param {NodeBuilder} builder - The current node builder.
-	 * @return {PassTextureNode}
-	 */
-	setup( builder ) {
-
-		// luminosity high pass material
-
-		this._highPassFilterMaterial = this._highPassFilterMaterial || new NodeMaterial();
-		this._highPassFilterMaterial.fragmentNode = this.highPassFn( { input: this.inputNode, threshold: this.threshold, smoothWidth: this.smoothWidth } ).context( builder.getSharedContext() );
-		this._highPassFilterMaterial.name = 'DualKawaseBloom_highPass';
-		this._highPassFilterMaterial.needsUpdate = true;
-
-		// downsample material ( Dual Kawase, 5 taps )
-
-		this._downsampleMaterial = this._downsampleMaterial || this._getDownsampleMaterial( builder );
-
-		// upsample material ( Dual Kawase, 8 taps, weighted accumulation )
-
-		this._upsampleMaterial = this._upsampleMaterial || this._getUpsampleMaterial( builder );
-
-		//
-
-		return this._textureOutput;
-
-	}
-
-	/**
-	 * Frees internal resources. This method should be called
-	 * when the effect is no longer required.
-	 */
-	dispose() {
-
-		this._renderTargetBright.dispose();
-
-		for ( let i = 0; i < this._nMips; i ++ ) {
-
-			this._downsampleRTs[ i ].dispose();
-			this._accumRTs[ i ].dispose();
-
-		}
-
-		if ( this._highPassFilterMaterial !== null ) this._highPassFilterMaterial.dispose();
-		if ( this._downsampleMaterial !== null ) this._downsampleMaterial.dispose();
-		if ( this._upsampleMaterial !== null ) this._upsampleMaterial.dispose();
-
-	}
-
-	/**
-	 * Creates the Dual Kawase downsample material. Each output texel reads the
-	 * center plus four diagonal corners of the source.
-	 *
-	 * @private
-	 * @param {NodeBuilder} builder - The current node builder.
-	 * @return {NodeMaterial}
-	 */
-	_getDownsampleMaterial( builder ) {
-
-		const colorTexture = texture( null );
-		const texelSize = uniform( new Vector2() );
-		const offset = this._offset;
-
-		const applyWeight = uniform( 0 );
-		const bloomFactor = uniform( 1 );
-
-		const uvNode = uv();
-
-		const downsamplePass = Fn( () => {
-
-			const o = texelSize.mul( 0.5 ).mul( offset );
-
-			const color = colorTexture.sample( uvNode ).mul( 4.0 ).toVar();
-
-			color.addAssign( colorTexture.sample( uvNode.add( vec2( o.x.negate(), o.y.negate() ) ) ) );
-			color.addAssign( colorTexture.sample( uvNode.add( vec2( o.x, o.y.negate() ) ) ) );
-			color.addAssign( colorTexture.sample( uvNode.add( vec2( o.x.negate(), o.y ) ) ) );
-			color.addAssign( colorTexture.sample( uvNode.add( vec2( o.x, o.y ) ) ) );
-
-			// The coarsest level pre-applies its `radius` weight so it can seed the upsample chain.
-			const weight = mix( bloomFactor, float( 1.2 ).sub( bloomFactor ), this.radius );
-
-			return vec4( color.rgb.div( 8.0 ).mul( mix( float( 1.0 ), weight, applyWeight ) ), 1.0 );
-
-		} );
-
-		const material = new NodeMaterial();
-		material.fragmentNode = downsamplePass().context( builder.getSharedContext() );
-		material.name = 'DualKawaseBloom_down';
-		material.needsUpdate = true;
-
-		material.colorTexture = colorTexture;
-		material.texelSize = texelSize;
-		material.applyWeight = applyWeight;
-		material.bloomFactor = bloomFactor;
-
-		return material;
-
-	}
-
-	/**
-	 * Creates the Dual Kawase upsample material. Each output texel reads four
-	 * edge centers and four diagonal corners of the source, then accumulates the
-	 * matching downsample level weighted by `radius`.
-	 *
-	 * @private
-	 * @param {NodeBuilder} builder - The current node builder.
-	 * @return {NodeMaterial}
-	 */
-	_getUpsampleMaterial( builder ) {
-
-		const prevTexture = texture( null );
-		const addTexture = texture( null );
-		const texelSize = uniform( new Vector2() );
-		const offset = this._offset;
-
-		const finalFlag = uniform( 0 );
-		const bloomFactor = uniform( 1 );
-
-		const uvNode = uv();
-
-		const upsamplePass = Fn( () => {
-
-			const o = texelSize.mul( 0.5 ).mul( offset );
-
-			const sum = prevTexture.sample( uvNode.add( vec2( o.x.mul( - 2.0 ), 0.0 ) ) ).rgb.toVar();
-			sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x.mul( 2.0 ), 0.0 ) ) ).rgb );
-			sum.addAssign( prevTexture.sample( uvNode.add( vec2( 0.0, o.y.mul( - 2.0 ) ) ) ).rgb );
-			sum.addAssign( prevTexture.sample( uvNode.add( vec2( 0.0, o.y.mul( 2.0 ) ) ) ).rgb );
-
-			sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x.negate(), o.y ) ) ).rgb.mul( 2.0 ) );
-			sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x, o.y ) ) ).rgb.mul( 2.0 ) );
-			sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x.negate(), o.y.negate() ) ) ).rgb.mul( 2.0 ) );
-			sum.addAssign( prevTexture.sample( uvNode.add( vec2( o.x, o.y.negate() ) ) ).rgb.mul( 2.0 ) );
-
-			const blurred = sum.div( 12.0 );
-
-			// redistribute the level's contribution between a tight and a wide bloom
-			const weight = mix( bloomFactor, float( 1.2 ).sub( bloomFactor ), this.radius );
-			const added = addTexture.sample( uvNode ).rgb.mul( weight );
-
-			// keep the total intensity independent of the level count ( the factors sum to `_nMips * 0.6` )
-			const norm = 3 / ( this._nMips * 0.6 );
-			const result = blurred.add( added ).mul( mix( float( 1.0 ), this.strength.mul( norm ), finalFlag ) );
-
-			return vec4( result, 1.0 );
-
-		} );
-
-		const material = new NodeMaterial();
-		material.fragmentNode = upsamplePass().context( builder.getSharedContext() );
-		material.name = 'DualKawaseBloom_up';
-		material.needsUpdate = true;
-
-		material.prevTexture = prevTexture;
-		material.addTexture = addTexture;
-		material.texelSize = texelSize;
-		material.finalFlag = finalFlag;
-		material.bloomFactor = bloomFactor;
-
-		return material;
-
-	}
-
-}
-
-/**
- * TSL function for creating a bloom effect.
- *
- * @tsl
- * @function
- * @param {Node<vec4>} node - The node that represents the input of the effect.
- * @param {number} [strength=1] - The strength of the bloom.
- * @param {number} [radius=0] - The radius of the bloom.
- * @param {number} [threshold=0] - The luminance threshold limits which bright areas contribute to the bloom effect.
- * @returns {DualKawaseBloomNode}
- */
-export const dualKawaseBloom = ( node, strength, radius, threshold ) => new DualKawaseBloomNode( nodeObject( node ), strength, radius, threshold );
-
-export default DualKawaseBloomNode;

二進制
examples/screenshots/webgl_loader_gltf_iridescence.jpg


二進制
examples/screenshots/webgpu_loader_gltf_iridescence.jpg


二進制
examples/screenshots/webgpu_materials_retroreflective.jpg


+ 1 - 1
examples/webgl_shadowmap_pointlight.html

@@ -54,7 +54,7 @@
 					const light = new THREE.PointLight( color, intensity, 20 );
 					light.castShadow = true;
 					light.shadow.bias = - 0.005; // reduces self-shadowing on double-sided objects
-					light.shadow.mapSize.width = 128;
+					light.shadow.mapSize.setScalar( 128 );
 					light.shadow.radius = 10;
 
 					let geometry = new THREE.SphereGeometry( 0.3, 12, 6 );

+ 0 - 1
examples/webgpu_custom_fog.html

@@ -69,7 +69,6 @@
 				renderer.toneMapping = THREE.ACESFilmicToneMapping;
 				renderer.toneMappingExposure = 0.62;
 				renderer.shadowMap.enabled = true;
-				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 				renderer.inspector = new Inspector();
 				document.body.appendChild( renderer.domElement );
 

+ 0 - 1
examples/webgpu_generator_building.html

@@ -80,7 +80,6 @@
 				renderer.toneMapping = THREE.ACESFilmicToneMapping;
 				renderer.toneMappingExposure = 0.25;
 				renderer.shadowMap.enabled = true;
-				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 				renderer.inspector = new Inspector();
 				document.body.appendChild( renderer.domElement );
 

+ 0 - 1
examples/webgpu_generator_city.html

@@ -79,7 +79,6 @@
 				renderer.toneMapping = THREE.ACESFilmicToneMapping;
 				renderer.toneMappingExposure = 0.13;
 				renderer.shadowMap.enabled = true;
-				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 				renderer.inspector = new Inspector();
 				document.body.appendChild( renderer.domElement );
 

+ 0 - 1
examples/webgpu_lights_projector.html

@@ -68,7 +68,6 @@
 				document.body.appendChild( renderer.domElement );
 
 				renderer.shadowMap.enabled = true;
-				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 
 				renderer.toneMapping = THREE.ACESFilmicToneMapping;
 				renderer.toneMappingExposure = 1;

+ 0 - 1
examples/webgpu_lights_spotlight.html

@@ -64,7 +64,6 @@
 				renderer.toneMappingExposure = 1;
 
 				renderer.shadowMap.enabled = true;
-				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 
 				scene = new THREE.Scene();
 

+ 192 - 0
examples/webgpu_materials_retroreflective.html

@@ -0,0 +1,192 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - materials - retroreflective</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<meta property="og:title" content="three.js webgpu - materials - retroreflective">
+		<meta property="og:type" content="website">
+		<meta property="og:url" content="https://threejs.org/examples/webgpu_materials_retroreflective.html">
+		<link type="text/css" rel="stylesheet" href="example.css">
+	</head>
+	<body>
+
+		<div id="info">
+			<a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
+
+			<div class="title-wrapper">
+				<a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>Retroreflective Materials</span>
+			</div>
+
+			<small>
+				Traffic cone with a retroreflective band and a front-facing light demonstrating the <a href="https://jcgt.org/published/0015/01/04/" target="_blank" rel="noopener">Minimal Retroreflective Microfacet Model</a>.  By <a href="https://ben3d.ca" target="_blank" rel="noopener">Ben Houston</a>.
+			</small>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/webgpu": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.tsl.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three/webgpu';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+
+			let camera, scene, renderer;
+			let frontLight, bandMaterial;
+
+			const params = {
+				retroreflectivity: true,
+				retroreflective: 1.0,
+				frontLightIntensity: 5.0
+			};
+
+			init();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				const container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 4, 2.5, 6 );
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( 0x11151d );
+				scene.add( camera );
+
+				scene.add( new THREE.HemisphereLight( 0xffffff, 0x223344, 0.8 ) );
+
+			
+				frontLight = new THREE.PointLight( 0xffffff, params.frontLightIntensity, 18, 1.4 );
+				camera.add( frontLight );
+
+				createCone();
+
+				const grid = new THREE.GridHelper( 8, 16, 0x344055, 0x202838 );
+				scene.add( grid );
+
+				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( render );
+				container.appendChild( renderer.domElement );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.target.set( 0, 1.5, 0 );
+				controls.update();
+
+				const gui = new GUI();
+				gui.add( params, 'retroreflectivity' ).name( 'retroreflectivity' ).onChange( updateMaterial );
+				gui.add( params, 'retroreflective', 0, 1, 0.01 ).name( 'retroreflective' ).onChange( updateMaterial );
+				gui.add( params, 'frontLightIntensity', 0, 30, 0.1 ).name( 'front light intensity' ).onChange( updateMaterial );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+				updateMaterial();
+
+			}
+
+			function createCone() {
+
+				const cone = new THREE.Group();
+				scene.add( cone );
+
+				const orangeMaterial = new THREE.MeshPhysicalNodeMaterial( {
+					color: 0xff6a00,
+					roughness: 0.45,
+					metalness: 0.0
+				} );
+
+				bandMaterial = new THREE.MeshPhysicalNodeMaterial( {
+					color: 0xf7f0d6,
+					roughness: 0.22,
+					metalness: 0.0,
+					retroreflective: params.retroreflective
+				} );
+
+				const base = new THREE.Mesh(
+					new THREE.BoxGeometry( 2.6, 0.24, 2.6 ),
+					orangeMaterial
+				);
+				base.position.y = 0.12;
+				cone.add( base );
+
+				const bodyHeight = 3.2;
+				const bodyBottom = 0.24;
+				const bodyRadiusBottom = 1.0;
+				const bodyRadiusTop = 0.18;
+
+				const body = new THREE.Mesh(
+					new THREE.CylinderGeometry( bodyRadiusTop, bodyRadiusBottom, bodyHeight, 96, 1, false ),
+					orangeMaterial
+				);
+				body.position.y = bodyBottom + bodyHeight * 0.5;
+				cone.add( body );
+
+				const bandHeight = 0.48;
+				const bandCenter = bodyBottom + bodyHeight * 0.52;
+				const bandBottom = bandCenter - bandHeight * 0.5;
+				const bandTop = bandCenter + bandHeight * 0.5;
+				const bandRadiusBottom = radiusAtHeight( bandBottom, bodyBottom, bodyHeight, bodyRadiusBottom, bodyRadiusTop ) * 1.01;
+				const bandRadiusTop = radiusAtHeight( bandTop, bodyBottom, bodyHeight, bodyRadiusBottom, bodyRadiusTop ) * 1.01;
+
+				const band = new THREE.Mesh(
+					new THREE.CylinderGeometry( bandRadiusTop, bandRadiusBottom, bandHeight, 96, 1, true ),
+					bandMaterial
+				);
+				band.position.y = bandCenter;
+				cone.add( band );
+
+			}
+
+			function radiusAtHeight( y, bodyBottom, bodyHeight, radiusBottom, radiusTop ) {
+
+				const t = THREE.MathUtils.clamp( ( y - bodyBottom ) / bodyHeight, 0, 1 );
+				return THREE.MathUtils.lerp( radiusBottom, radiusTop, t );
+
+			}
+
+			function updateMaterial() {
+
+				bandMaterial.retroreflective = params.retroreflectivity ? params.retroreflective : 0;
+				frontLight.intensity = params.frontLightIntensity;
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			function render() {
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 32 - 23
examples/webgpu_postprocessing_bloom.html

@@ -20,7 +20,7 @@
 			</div>
 
 			<small>
-				Gaussian bloom by <a href="http://eduperiment.com" target="_blank" rel="noopener">Prashant Sharma</a> and <a href="https://clara.io" target="_blank" rel="noopener">Ben Houston</a>.<br/>
+				Bloom pass by <a href="http://eduperiment.com" target="_blank" rel="noopener">Prashant Sharma</a> and <a href="https://clara.io" target="_blank" rel="noopener">Ben Houston</a>.<br/>
 				<a href="https://blog.sketchfab.com/art-spotlight-primary-ion-drive/" target="_blank" rel="noopener">Primary Ion Drive</a>
 				by <a href="http://mjmurdock.com/" target="_blank" rel="noopener">Mike Murdock</a> is licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener">Creative Commons Attribution</a>.<br />
 			</small>
@@ -40,9 +40,8 @@
 		<script type="module">
 
 			import * as THREE from 'three/webgpu';
-			import { pass, uniform } from 'three/tsl';
+			import { pass } from 'three/tsl';
 			import { bloom } from 'three/addons/tsl/display/BloomNode.js';
-			import { dualKawaseBloom } from 'three/addons/tsl/display/DualKawaseBloomNode.js';
 
 			import { Inspector } from 'three/addons/inspector/Inspector.js';
 
@@ -54,7 +53,9 @@
 			let renderPipeline, renderer, mixer, timer;
 
 			const params = {
-				type: 'Gaussian',
+				threshold: 0,
+				strength: 1,
+				radius: 0,
 				exposure: 1
 			};
 
@@ -100,26 +101,24 @@
 
 				renderPipeline = new THREE.RenderPipeline( renderer );
 
-				const scenePass = pass( scene, camera );
-				const scenePassColor = scenePass.getTextureNode( 'output' ).toInspector( 'Color' );
-
-				const strength = uniform( 1 );
-				const radius = uniform( 0 );
-				const threshold = uniform( 0 );
+				// below optimized config is only relevant when using MSAA and also optional.
+				// store no multisampled data and only resolve color
 
-				const bloomPasses = {
-					'Gaussian': bloom( scenePassColor, strength, radius, threshold ),
-					'Dual Kawase': dualKawaseBloom( scenePassColor, strength, radius, threshold )
+				const config = {
+ 					storeMultisampledColorBuffer: false,
+					storeMultisampledDepthBuffer: false,
+					storeMultisampledStencilBuffer: false,
+					resolveColorBuffer: true,
+					resolveDepthBuffer: false,
+					resolveStencilBuffer: false
 				};
 
-				function updateBloom() {
+				const scenePass = pass( scene, camera, config );
+				const scenePassColor = scenePass.getTextureNode( 'output' ).toInspector( 'Color' );
 
-					renderPipeline.outputNode = scenePassColor.add( bloomPasses[ params.type ] );
-					renderPipeline.needsUpdate = true;
+				const bloomPass = bloom( scenePassColor ).toInspector( 'Bloom' );
 
-				}
-
-				updateBloom();
+				renderPipeline.outputNode = scenePassColor.add( bloomPass );
 
 				//
 
@@ -134,13 +133,23 @@
 
 				const bloomFolder = gui.addFolder( 'bloom' );
 
-				bloomFolder.add( params, 'type', [ 'Gaussian', 'Dual Kawase' ] ).onChange( updateBloom );
+				bloomFolder.add( params, 'threshold', 0.0, 1.0 ).onChange( function ( value ) {
+
+					bloomPass.threshold.value = value;
 
-				bloomFolder.add( threshold, 'value', 0.0, 1.0 ).name( 'threshold' );
+				} );
 
-				bloomFolder.add( strength, 'value', 0.0, 3.0 ).name( 'strength' );
+				bloomFolder.add( params, 'strength', 0.0, 3.0 ).onChange( function ( value ) {
 
-				bloomFolder.add( radius, 'value', 0.0, 1.0, 0.01 ).name( 'radius' );
+					bloomPass.strength.value = value;
+
+				} );
+
+				bloomFolder.add( params, 'radius', 0.0, 1.0, 0.01 ).onChange( function ( value ) {
+
+					bloomPass.radius.value = value;
+
+				} );
 
 				const toneMappingFolder = gui.addFolder( 'tone mapping' );
 

+ 5 - 23
examples/webgpu_postprocessing_bloom_emissive.html

@@ -36,9 +36,8 @@
 		<script type="module">
 
 			import * as THREE from 'three/webgpu';
-			import { pass, mrt, output, emissive, vec4, uniform } from 'three/tsl';
+			import { pass, mrt, output, emissive, vec4 } from 'three/tsl';
 			import { bloom } from 'three/addons/tsl/display/BloomNode.js';
-			import { dualKawaseBloom } from 'three/addons/tsl/display/DualKawaseBloomNode.js';
 
 			import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
 
@@ -119,26 +118,10 @@
 				const outputPass = scenePass.getTextureNode().toInspector( 'Color' );
 				const emissivePass = scenePass.getTextureNode( 'emissive' ).toInspector( 'Emissive' );
 
-				const params = { type: 'Gaussian' };
-
-				const strength = uniform( 2.5 );
-				const radius = uniform( 0.5 );
-
-				const bloomPasses = {
-					'Gaussian': bloom( emissivePass, strength, radius ),
-					'Dual Kawase': dualKawaseBloom( emissivePass, strength, radius )
-				};
+				const bloomPass = bloom( emissivePass, 2.5, .5 );
 
 				renderPipeline = new THREE.RenderPipeline( renderer );
-
-				function updateBloom() {
-
-					renderPipeline.outputNode = outputPass.add( bloomPasses[ params.type ] );
-					renderPipeline.needsUpdate = true;
-
-				}
-
-				updateBloom();
+				renderPipeline.outputNode = outputPass.add( bloomPass );
 
 				//
 
@@ -155,9 +138,8 @@
 				const gui = renderer.inspector.createParameters( 'Settings' );
 
 				const bloomFolder = gui.addFolder( 'Bloom' );
-				bloomFolder.add( params, 'type', [ 'Gaussian', 'Dual Kawase' ] ).onChange( updateBloom );
-				bloomFolder.add( strength, 'value', 0.0, 5.0 ).name( 'strength' );
-				bloomFolder.add( radius, 'value', 0.0, 1.0 ).name( 'radius' );
+				bloomFolder.add( bloomPass.strength, 'value', 0.0, 5.0 ).name( 'strength' );
+				bloomFolder.add( bloomPass.radius, 'value', 0.0, 1.0 ).name( 'radius' );
 
 				const toneMappingFolder = gui.addFolder( 'Tone Mapping' );
 				toneMappingFolder.add( renderer, 'toneMappingExposure', 0.1, 2 ).name( 'exposure' );

+ 5 - 25
examples/webgpu_postprocessing_bloom_selective.html

@@ -38,7 +38,6 @@
 			import * as THREE from 'three/webgpu';
 			import { pass, mrt, output, float, uniform } from 'three/tsl';
 			import { bloom } from 'three/addons/tsl/display/BloomNode.js';
-			import { dualKawaseBloom } from 'three/addons/tsl/display/DualKawaseBloomNode.js';
 
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 
@@ -97,29 +96,11 @@
 			const outputPass = scenePass.getTextureNode().toInspector( 'Color' );
 			const bloomIntensityPass = scenePass.getTextureNode( 'bloomIntensity' ).toInspector( 'Bloom Intensity' );
 
-			const params = { type: 'Gaussian' };
-			const bloomInput = outputPass.mul( bloomIntensityPass );
-
-			const strength = uniform( 1 );
-			const radius = uniform( 0 );
-			const threshold = uniform( 0 );
-
-			const bloomPasses = {
-				'Gaussian': bloom( bloomInput, strength, radius, threshold ),
-				'Dual Kawase': dualKawaseBloom( bloomInput, strength, radius, threshold )
-			};
+			const bloomPass = bloom( outputPass.mul( bloomIntensityPass ) );
 
 			const renderPipeline = new THREE.RenderPipeline( renderer );
 			renderPipeline.outputColorTransform = false;
-
-			function updateBloom() {
-
-				renderPipeline.outputNode = outputPass.add( bloomPasses[ params.type ] ).renderOutput();
-				renderPipeline.needsUpdate = true;
-
-			}
-
-			updateBloom();
+			renderPipeline.outputNode = outputPass.add( bloomPass ).renderOutput();
 
 			// controls
 
@@ -158,10 +139,9 @@
 			const gui = renderer.inspector.createParameters( 'Settings' );
 
 			const bloomFolder = gui.addFolder( 'Bloom' );
-			bloomFolder.add( params, 'type', [ 'Gaussian', 'Dual Kawase' ] ).onChange( updateBloom );
-			bloomFolder.add( threshold, 'value', 0.0, 1.0 ).name( 'threshold' );
-			bloomFolder.add( strength, 'value', 0.0, 3 ).name( 'strength' );
-			bloomFolder.add( radius, 'value', 0.0, 1.0 ).name( 'radius' );
+			bloomFolder.add( bloomPass.threshold, 'value', 0.0, 1.0 ).name( 'threshold' );
+			bloomFolder.add( bloomPass.strength, 'value', 0.0, 3 ).name( 'strength' );
+			bloomFolder.add( bloomPass.radius, 'value', 0.0, 1.0 ).name( 'radius' );
 
 			const toneMappingFolder = gui.addFolder( 'Tone Mapping' );
 			toneMappingFolder.add( renderer, 'toneMappingExposure', 0.1, 3 ).name( 'exposure' );

+ 0 - 1
examples/webgpu_postprocessing_sss.html

@@ -128,7 +128,6 @@
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.setAnimationLoop( animate );
 				renderer.shadowMap.enabled = true;
-				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 				renderer.inspector = new Inspector();
 				document.body.appendChild( renderer.domElement );
 

+ 0 - 1
examples/webgpu_reflection.html

@@ -131,7 +131,6 @@
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.setAnimationLoop( animate );
 				renderer.shadowMap.enabled = true;
-				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 				renderer.toneMapping = THREE.ACESFilmicToneMapping;
 				renderer.inspector = new Inspector();
 				document.body.appendChild( renderer.domElement );

+ 0 - 1
examples/webgpu_shadowmap_array.html

@@ -66,7 +66,6 @@
 
 				renderer.shadowMap.enabled = true;
 				renderer.shadowMap.type = THREE.BasicShadowMap;
-				// renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 
 				renderer.toneMapping = THREE.ACESFilmicToneMapping;
 				renderer.toneMappingExposure = 1.2;

+ 0 - 1
examples/webgpu_shadowmap_csm.html

@@ -100,7 +100,6 @@
 				renderer.setAnimationLoop( animate );
 
 				renderer.shadowMap.enabled = params.shadows;
-				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 
 				renderer.inspector = new Inspector();
 

+ 1 - 1
examples/webgpu_shadowmap_pointlight.html

@@ -63,7 +63,7 @@
 					const light = new THREE.PointLight( color, intensity, 20 );
 					light.castShadow = true;
 					light.shadow.bias = - 0.005; // reduces self-shadowing on double-sided objects
-					light.shadow.mapSize.width = 128;
+					light.shadow.mapSize.setScalar( 128 );
 					light.shadow.radius = 10;
 
 					let geometry = new THREE.SphereGeometry( 0.3, 12, 6 );

+ 0 - 1
examples/webgpu_skinning_instancing_individual.html

@@ -321,7 +321,6 @@
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.setAnimationLoop( animate );
 				renderer.shadowMap.enabled = true;
-				renderer.shadowMap.type = THREE.PCFSoftShadowMap;
 				renderer.toneMapping = THREE.NeutralToneMapping;
 				renderer.toneMappingExposure = 0.96;
 				renderer.inspector = new Inspector();

+ 0 - 1
manual/examples/shadows-spot-light-with-shadow-radius.html

@@ -39,7 +39,6 @@ function main() {
 	const canvas = document.querySelector( '#c' );
 	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );
 	renderer.shadowMap.enabled = true;
-	renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
 
 	const fov = 45;
 	const aspect = 2; // the canvas default

+ 28 - 24
package-lock.json

@@ -78,9 +78,9 @@
       }
     },
     "node_modules/@es-joy/jsdoccomment": {
-      "version": "0.87.0",
-      "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.87.0.tgz",
-      "integrity": "sha512-mFXZloZMzuJZXSHUmAFu/pXTk0ZJTJBluuAkrvbzidpTN8W6F2bpRFuedSH+85kbdlRLJqc+gfN+kD3JOLJK5g==",
+      "version": "0.88.0",
+      "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.88.0.tgz",
+      "integrity": "sha512-GK/HL/claLLNo5KG705auIlZMwEtmn88ofSGuLsmVZwKBqMPJhW9DiznYNq07QEqz9BPtA3LBfYImtZmhVvRAw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -391,9 +391,9 @@
       "license": "BSD-2-Clause"
     },
     "node_modules/@puppeteer/browsers": {
-      "version": "3.0.5",
-      "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.5.tgz",
-      "integrity": "sha512-xYXNuEQmHNIPWWcbL/skf2KF7seyp7c1xmKFRk3wmdFx7VwBsKVrtOLKs8ecaezsKPsWeF1YsgwIiElAscaryA==",
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.6.tgz",
+      "integrity": "sha512-B/gKoqlFkzhvzsI6jo9K1cZz9o5ypviVv/xu8CwA4grZzyVwN+XfkT+tu8T1zrauuEXv6VhS2oGX+6NL95WcKA==",
       "dev": true,
       "license": "Apache-2.0",
       "dependencies": {
@@ -407,11 +407,15 @@
         "node": ">=22.12.0"
       },
       "peerDependencies": {
-        "proxy-agent": ">=8.0.1"
+        "proxy-agent": ">=8.0.1",
+        "yauzl": "^2.10.0 || ^3.4.0"
       },
       "peerDependenciesMeta": {
         "proxy-agent": {
           "optional": true
+        },
+        "yauzl": {
+          "optional": true
         }
       }
     },
@@ -1565,13 +1569,13 @@
       }
     },
     "node_modules/eslint-plugin-jsdoc": {
-      "version": "63.0.10",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-63.0.10.tgz",
-      "integrity": "sha512-A9UIWsCquaKnit7rasXxYf12hhGIdDRjv65/RUE3AxbT6rdKBvr3MjH37g3gP+g4ipQMXuMn9slFKjO+vqNfkg==",
+      "version": "63.0.11",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-63.0.11.tgz",
+      "integrity": "sha512-QhLmqREgRJSIKg0r23ckTVaNK1lmMsTmpyY5Hv7NTCHNWTFZx/8PqycWhR0p0Lk/U5aJ6H3zAKMQ0xB2NkN0sA==",
       "dev": true,
       "license": "BSD-3-Clause",
       "dependencies": {
-        "@es-joy/jsdoccomment": "~0.87.0",
+        "@es-joy/jsdoccomment": "~0.88.0",
         "@es-joy/resolve.exports": "1.2.0",
         "are-docs-informative": "^0.0.2",
         "comment-parser": "1.4.7",
@@ -1582,7 +1586,7 @@
         "html-entities": "^2.6.0",
         "object-deep-merge": "^2.0.1",
         "parse-imports-exports": "^0.2.4",
-        "semver": "^7.8.2",
+        "semver": "^7.8.5",
         "spdx-expression-parse": "^4.0.0",
         "to-valid-identifier": "^1.0.0"
       },
@@ -2564,18 +2568,18 @@
       }
     },
     "node_modules/puppeteer": {
-      "version": "25.2.1",
-      "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.2.1.tgz",
-      "integrity": "sha512-2D5RMkQH9FRhDU57a1/jV9xWoxqZvUjaZOYjAAPdRCEY8A01V5sxzyGOMs8XiKU9fPF91SOSwNYpHRu5SD958g==",
+      "version": "25.3.0",
+      "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.3.0.tgz",
+      "integrity": "sha512-O1tx8S315aw8eI99HZ5ZNcVEzJ9+jKF//eO5UvfZ3cXJ6okZ5sX3Y50u7DJaM+ewEK4LqXP068tBhfRaWikj+g==",
       "dev": true,
       "hasInstallScript": true,
       "license": "Apache-2.0",
       "dependencies": {
-        "@puppeteer/browsers": "3.0.5",
+        "@puppeteer/browsers": "3.0.6",
         "chromium-bidi": "16.0.1",
         "devtools-protocol": "0.0.1638949",
         "lilconfig": "^3.1.3",
-        "puppeteer-core": "25.2.1",
+        "puppeteer-core": "25.3.0",
         "typed-query-selector": "^2.12.2"
       },
       "bin": {
@@ -2586,13 +2590,13 @@
       }
     },
     "node_modules/puppeteer-core": {
-      "version": "25.2.1",
-      "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.2.1.tgz",
-      "integrity": "sha512-MwEZ4FFGJ1ZLOmu/04eISxoEMKtCnHyJBRFfgpwPPSYNG6gT6Xw1laNziFSV7uwDcx3jK+ATYIo9SfOd8Uhc3w==",
+      "version": "25.3.0",
+      "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.3.0.tgz",
+      "integrity": "sha512-fm+wpUr2oigH1PXZvwgATrM2tYWHMDG8ASzTEe9uukCye4X5Ldx1K5BTHPFKITrIWvQQAQ256d1NpbEveBcKjA==",
       "dev": true,
       "license": "Apache-2.0",
       "dependencies": {
-        "@puppeteer/browsers": "3.0.5",
+        "@puppeteer/browsers": "3.0.6",
         "chromium-bidi": "16.0.1",
         "devtools-protocol": "0.0.1638949",
         "typed-query-selector": "^2.12.2",
@@ -2704,9 +2708,9 @@
       }
     },
     "node_modules/semver": {
-      "version": "7.8.2",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
-      "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+      "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
       "dev": true,
       "license": "ISC",
       "bin": {

+ 2 - 4
src/Three.TSL.js

@@ -21,7 +21,6 @@ export const NodeShaderStage = TSL.NodeShaderStage;
 export const NodeType = TSL.NodeType;
 export const NodeUpdateType = TSL.NodeUpdateType;
 export const PCFShadowFilter = TSL.PCFShadowFilter;
-export const PCFSoftShadowFilter = TSL.PCFSoftShadowFilter;
 export const PI = TSL.PI;
 export const PI2 = TSL.PI2;
 export const TWO_PI = TSL.TWO_PI;
@@ -94,7 +93,6 @@ export const blendColor = TSL.blendColor;
 export const blendDodge = TSL.blendDodge;
 export const blendOverlay = TSL.blendOverlay;
 export const blendScreen = TSL.blendScreen;
-export const blur = TSL.blur;
 export const bool = TSL.bool;
 export const buffer = TSL.buffer;
 export const bufferAttribute = TSL.bufferAttribute;
@@ -200,7 +198,6 @@ export const gain = TSL.gain;
 export const gapSize = TSL.gapSize;
 export const getConstNodeType = TSL.getConstNodeType;
 export const getCurrentStack = TSL.getCurrentStack;
-export const getDirection = TSL.getDirection;
 export const getDistanceAttenuation = TSL.getDistanceAttenuation;
 export const getGeometryRoughness = TSL.getGeometryRoughness;
 export const getNormalFromDepth = TSL.getNormalFromDepth;
@@ -303,6 +300,7 @@ export const materialPointSize = TSL.materialPointSize;
 export const materialReference = TSL.materialReference;
 export const materialReflectivity = TSL.materialReflectivity;
 export const materialRefractionRatio = TSL.materialRefractionRatio;
+export const materialRetroreflective = TSL.materialRetroreflective;
 export const materialRotation = TSL.materialRotation;
 export const materialRoughness = TSL.materialRoughness;
 export const materialSheen = TSL.materialSheen;
@@ -469,6 +467,7 @@ export const refract = TSL.refract;
 export const refractVector = TSL.refractVector;
 export const refractView = TSL.refractView;
 export const reinhardToneMapping = TSL.reinhardToneMapping;
+export const retroreflective = TSL.retroreflective;
 export const remap = TSL.remap;
 export const remapClamp = TSL.remapClamp;
 export const renderGroup = TSL.renderGroup;
@@ -562,7 +561,6 @@ export const texture3D = TSL.texture3D;
 export const textureBarrier = TSL.textureBarrier;
 export const textureBicubic = TSL.textureBicubic;
 export const textureBicubicLevel = TSL.textureBicubicLevel;
-export const textureCubeUV = TSL.textureCubeUV;
 export const textureLoad = TSL.textureLoad;
 export const textureSize = TSL.textureSize;
 export const textureLevel = TSL.textureLevel;

+ 1 - 0
src/constants.js

@@ -70,6 +70,7 @@ export const PCFShadowMap = 1;
  *
  * @type {number}
  * @constant
+ * @deprecated since r186. Use `PCFShadowMap` instead.
  */
 export const PCFSoftShadowMap = 2;
 

+ 79 - 4
src/core/RenderTarget.js

@@ -30,8 +30,12 @@ class RenderTarget extends EventDispatcher {
 	 * @property {string} [colorSpace=NoColorSpace] - The texture's color space.
 	 * @property {boolean} [depthBuffer=true] - Whether to allocate a depth buffer or not.
 	 * @property {boolean} [stencilBuffer=false] - Whether to allocate a stencil buffer or not.
-	 * @property {boolean} [resolveDepthBuffer=true] - Whether to resolve the depth buffer or not.
-	 * @property {boolean} [resolveStencilBuffer=true] - Whether  to resolve the stencil buffer or not.
+	 * @property {boolean} [resolveColorBuffer=true] - Whether to resolve the color buffer or not. Only relevant for multisampled render targets.
+	 * @property {boolean} [resolveDepthBuffer=true] - Whether to resolve the depth buffer or not. Only relevant for multisampled render targets.
+	 * @property {boolean} [resolveStencilBuffer=true] - Whether to resolve the stencil buffer or not. Only relevant for multisampled render targets.
+	 * @property {boolean} [storeMultisampledColorBuffer=true] - Whether to store the multisampled color buffer or not. Setting to `false` saves memory bandwidth when the multisampled data are not needed after a render pass.
+	 * @property {boolean} [storeMultisampledDepthBuffer=true] - Whether to store the multisampled depth buffer or not. Setting to `false` saves memory bandwidth when the multisampled data are not needed after a render pass.
+	 * @property {boolean} [storeMultisampledStencilBuffer=true] - Whether to store the multisampled stencil buffer or not. Setting to `false` saves memory bandwidth when the multisampled data are not needed after a render pass.
 	 * @property {?Texture} [depthTexture=null] - Reference to a depth texture.
 	 * @property {number} [samples=0] - The MSAA samples count.
 	 * @property {number} [count=1] - Defines the number of color attachments . Must be at least `1`.
@@ -57,8 +61,12 @@ class RenderTarget extends EventDispatcher {
 			minFilter: LinearFilter,
 			depthBuffer: true,
 			stencilBuffer: false,
+			resolveColorBuffer: true,
 			resolveDepthBuffer: true,
 			resolveStencilBuffer: true,
+			storeMultisampledColorBuffer: true,
+			storeMultisampledDepthBuffer: true,
+			storeMultisampledStencilBuffer: true,
 			depthTexture: null,
 			samples: 0,
 			count: 1,
@@ -165,7 +173,28 @@ class RenderTarget extends EventDispatcher {
 		this.stencilBuffer = options.stencilBuffer;
 
 		/**
-		 * Whether to resolve the depth buffer or not.
+		 * Whether to resolve the color buffer or not. When set to `false`, the color
+		 * attachments do not receive the resolved (single-sampled) output of a render
+		 * pass and the render target's textures are left untouched. The rendered
+		 * content is then only accessible within the render pass itself.
+		 *
+		 * Only relevant for multisampled render targets.
+		 *
+		 * @type {boolean}
+		 * @default true
+		 */
+		this.resolveColorBuffer = options.resolveColorBuffer;
+
+		/**
+		 * Whether to resolve the depth buffer or not. When set to `false`, the depth
+		 * texture does not receive the resolved depth output of a render pass which
+		 * saves memory bandwidth. Use this setting when the depth data of a render
+		 * pass are not required afterwards.
+		 *
+		 * Only relevant for multisampled render targets in WebGL. WebGPU does not
+		 * support depth resolves; sampling the depth texture of a multisampled render
+		 * target accesses the multisampled data directly, see
+		 * {@link RenderTarget#storeMultisampledDepthBuffer}.
 		 *
 		 * @type {boolean}
 		 * @default true
@@ -173,13 +202,54 @@ class RenderTarget extends EventDispatcher {
 		this.resolveDepthBuffer = options.resolveDepthBuffer;
 
 		/**
-		 * Whether to resolve the stencil buffer or not.
+		 * Whether to resolve the stencil buffer or not. Analogous to
+		 * {@link RenderTarget#resolveDepthBuffer} but for the stencil aspect.
 		 *
 		 * @type {boolean}
 		 * @default true
 		 */
 		this.resolveStencilBuffer = options.resolveStencilBuffer;
 
+		/**
+		 * Whether to store the multisampled color buffer or not. When set to `false`,
+		 * the multisampled data are discarded at the end of a render pass, right after
+		 * they have been resolved. This saves memory bandwidth, especially on tile-based
+		 * GPUs, and is the recommended setting for render targets that are fully redrawn
+		 * each frame and whose output is only accessed via the resolved textures (e.g.
+		 * scene passes in post-processing chains).
+		 *
+		 * Must be kept `true` when the multisampled data are needed after the render
+		 * pass ends, e.g. when rendering into the target without clearing or when the
+		 * scene contains transmissive objects which require a mid-pass framebuffer copy.
+		 *
+		 * @type {boolean}
+		 * @default true
+		 */
+		this.storeMultisampledColorBuffer = options.storeMultisampledColorBuffer;
+
+		/**
+		 * Whether to store the multisampled depth buffer or not. When set to `false`,
+		 * the multisampled depth data are discarded at the end of a render pass which
+		 * saves memory bandwidth.
+		 *
+		 * Must be kept `true` in WebGPU when the depth texture of a multisampled render
+		 * target is sampled (e.g. by depth-based post-processing effects) since depth
+		 * is read directly from the multisampled data.
+		 *
+		 * @type {boolean}
+		 * @default true
+		 */
+		this.storeMultisampledDepthBuffer = options.storeMultisampledDepthBuffer;
+
+		/**
+		 * Whether to store the multisampled stencil buffer or not. Analogous to
+		 * {@link RenderTarget#storeMultisampledDepthBuffer} but for the stencil aspect.
+		 *
+		 * @type {boolean}
+		 * @default true
+		 */
+		this.storeMultisampledStencilBuffer = options.storeMultisampledStencilBuffer;
+
 		this._depthTexture = null;
 		this.depthTexture = options.depthTexture;
 
@@ -375,9 +445,14 @@ class RenderTarget extends EventDispatcher {
 		this.depthBuffer = source.depthBuffer;
 		this.stencilBuffer = source.stencilBuffer;
 
+		this.resolveColorBuffer = source.resolveColorBuffer;
 		this.resolveDepthBuffer = source.resolveDepthBuffer;
 		this.resolveStencilBuffer = source.resolveStencilBuffer;
 
+		this.storeMultisampledColorBuffer = source.storeMultisampledColorBuffer;
+		this.storeMultisampledDepthBuffer = source.storeMultisampledDepthBuffer;
+		this.storeMultisampledStencilBuffer = source.storeMultisampledStencilBuffer;
+
 		if ( source.depthTexture !== null ) this.depthTexture = source.depthTexture.clone();
 
 		this.samples = source.samples;

+ 99 - 205
src/extras/PMREMGenerator.js

@@ -22,18 +22,16 @@ import { Color } from '../math/Color.js';
 import { WebGLRenderTarget } from '../renderers/WebGLRenderTarget.js';
 import { MeshBasicMaterial } from '../materials/MeshBasicMaterial.js';
 import { BoxGeometry } from '../geometries/BoxGeometry.js';
-import { error, warn } from '../utils.js';
 
 const LOD_MIN = 4;
 
-// The standard deviations (radians) associated with the extra mips.
+// The number of extra mips.
 // Used for scene blur in fromScene() method.
-const EXTRA_LOD_SIGMA = [ 0.125, 0.215, 0.35, 0.446, 0.526, 0.582 ];
+const EXTRA_LODS = 6;
 
-// The maximum length of the blur for loop. Smaller sigmas will use fewer
-// samples and exit early, but not recompile the shader.
+// The number of spiral samples per blur pass.
 // Used for scene blur in fromScene() method.
-const MAX_SAMPLES = 20;
+const BLUR_SAMPLES = 20;
 
 // GGX VNDF importance sampling configuration
 const GGX_SAMPLES = 256;
@@ -46,6 +44,7 @@ let _oldActiveMipmapLevel = 0;
 let _oldXrEnabled = false;
 
 const _origin = /*@__PURE__*/ new Vector3();
+const _direction = /*@__PURE__*/ new Vector3();
 
 /**
  * This class generates a Prefiltered, Mipmapped Radiance Environment Map
@@ -78,7 +77,6 @@ class PMREMGenerator {
 		this._lodMax = 0;
 		this._cubeSize = 0;
 		this._sizeLods = [];
-		this._sigmas = [];
 		this._lodMeshes = [];
 
 		this._backgroundBox = null;
@@ -313,7 +311,7 @@ class PMREMGenerator {
 			this._pingPongRenderTarget = _createRenderTarget( width, height, params );
 
 			const { _lodMax } = this;
-			( { lodMeshes: this._lodMeshes, sizeLods: this._sizeLods, sigmas: this._sigmas } = _createPlanes( _lodMax ) );
+			( { lodMeshes: this._lodMeshes, sizeLods: this._sizeLods } = _createPlanes( _lodMax ) );
 
 			this._blurMaterial = _getBlurShader( _lodMax, width, height );
 			this._ggxMaterial = _getGGXShader( _lodMax, width, height );
@@ -534,7 +532,7 @@ class PMREMGenerator {
 		const incrementalRoughness = Math.sqrt( targetRoughness * targetRoughness - sourceRoughness * sourceRoughness );
 
 		// Apply blur strength mapping for better quality across the roughness range
-		const blurStrength = 0.0 + targetRoughness * 1.25;
+		const blurStrength = targetRoughness * 1.25;
 		const adjustedRoughness = incrementalRoughness * blurStrength;
 
 		// Calculate viewport position based on output LOD level
@@ -564,11 +562,9 @@ class PMREMGenerator {
 	}
 
 	/**
-	 * This is a two-pass Gaussian blur for a cubemap. Normally this is done
-	 * vertically and horizontally, but this breaks down on a cube. Here we apply
-	 * the blur latitudinally (around the poles), and then longitudinally (towards
-	 * the poles) to approximate the orthogonally-separable blur. It is least
-	 * accurate at the poles, but still does a decent job.
+	 * This is a two-pass Gaussian blur for a cubemap. Each pass importance-samples
+	 * the Gaussian along a spiral kernel (Golden Angle), which distributes samples
+	 * isotropically on the sphere (no pole artifacts).
 	 *
 	 * Used for initial scene blur in fromScene() method when sigma > 0.
 	 *
@@ -577,109 +573,37 @@ class PMREMGenerator {
 	 * @param {number} lodIn
 	 * @param {number} lodOut
 	 * @param {number} sigma
-	 * @param {Vector3} [poleAxis]
 	 */
-	_blur( cubeUVRenderTarget, lodIn, lodOut, sigma, poleAxis ) {
+	_blur( cubeUVRenderTarget, lodIn, lodOut, sigma ) {
 
 		const pingPongRenderTarget = this._pingPongRenderTarget;
 
-		this._halfBlur(
-			cubeUVRenderTarget,
-			pingPongRenderTarget,
-			lodIn,
-			lodOut,
-			sigma,
-			'latitudinal',
-			poleAxis );
-
-		this._halfBlur(
-			pingPongRenderTarget,
-			cubeUVRenderTarget,
-			lodOut,
-			lodOut,
-			sigma,
-			'longitudinal',
-			poleAxis );
+		// Two passes of sigma / sqrt( 2 ) compose to a blur of sigma while squaring
+		// the effective sample count. Sigmas beyond PI are visually indistinguishable
+		// from a uniform blur, so clamp to keep the shader math finite.
+		const blurSigma = Math.min( sigma, Math.PI ) / Math.SQRT2;
+
+		this._blurPass( cubeUVRenderTarget, pingPongRenderTarget, lodIn, lodOut, blurSigma );
+		this._blurPass( pingPongRenderTarget, cubeUVRenderTarget, lodOut, lodOut, blurSigma );
 
 	}
 
-	_halfBlur( targetIn, targetOut, lodIn, lodOut, sigmaRadians, direction, poleAxis ) {
+	_blurPass( targetIn, targetOut, lodIn, lodOut, sigmaRadians ) {
 
 		const renderer = this._renderer;
 		const blurMaterial = this._blurMaterial;
 
-		if ( direction !== 'latitudinal' && direction !== 'longitudinal' ) {
-
-			error(
-				'blur direction must be either latitudinal or longitudinal!' );
-
-		}
-
-		// Number of standard deviations at which to cut off the discrete approximation.
-		const STANDARD_DEVIATIONS = 3;
-
 		const blurMesh = this._lodMeshes[ lodOut ];
 		blurMesh.material = blurMaterial;
 
 		const blurUniforms = blurMaterial.uniforms;
 
-		const pixels = this._sizeLods[ lodIn ] - 1;
-		const radiansPerPixel = isFinite( sigmaRadians ) ? Math.PI / ( 2 * pixels ) : 2 * Math.PI / ( 2 * MAX_SAMPLES - 1 );
-		const sigmaPixels = sigmaRadians / radiansPerPixel;
-		const samples = isFinite( sigmaRadians ) ? 1 + Math.floor( STANDARD_DEVIATIONS * sigmaPixels ) : MAX_SAMPLES;
-
-		if ( samples > MAX_SAMPLES ) {
-
-			warn( `sigmaRadians, ${
-				sigmaRadians}, is too large and will clip, as it requested ${
-				samples} samples when the maximum is set to ${MAX_SAMPLES}` );
-
-		}
-
-		const weights = [];
-		let sum = 0;
-
-		for ( let i = 0; i < MAX_SAMPLES; ++ i ) {
-
-			const x = i / sigmaPixels;
-			const weight = Math.exp( - x * x / 2 );
-			weights.push( weight );
-
-			if ( i === 0 ) {
-
-				sum += weight;
-
-			} else if ( i < samples ) {
-
-				sum += 2 * weight;
-
-			}
-
-		}
-
-		for ( let i = 0; i < weights.length; i ++ ) {
-
-			weights[ i ] = weights[ i ] / sum;
-
-		}
-
 		blurUniforms[ 'envMap' ].value = targetIn.texture;
-		blurUniforms[ 'samples' ].value = samples;
-		blurUniforms[ 'weights' ].value = weights;
-		blurUniforms[ 'latitudinal' ].value = direction === 'latitudinal';
-
-		if ( poleAxis ) {
-
-			blurUniforms[ 'poleAxis' ].value = poleAxis;
-
-		}
-
-		const { _lodMax } = this;
-		blurUniforms[ 'dTheta' ].value = radiansPerPixel;
-		blurUniforms[ 'mipInt' ].value = _lodMax - lodIn;
+		blurUniforms[ 'sigma' ].value = sigmaRadians;
+		blurUniforms[ 'mipInt' ].value = this._lodMax - lodIn;
 
 		const outputSize = this._sizeLods[ lodOut ];
-		const x = 3 * outputSize * ( lodOut > _lodMax - LOD_MIN ? lodOut - _lodMax + LOD_MIN : 0 );
+		const x = 3 * outputSize * ( lodOut > this._lodMax - LOD_MIN ? lodOut - this._lodMax + LOD_MIN : 0 );
 		const y = 4 * ( this._cubeSize - outputSize );
 
 		_setViewport( targetOut, x, y, 3 * outputSize, 2 * outputSize );
@@ -695,31 +619,18 @@ class PMREMGenerator {
 function _createPlanes( lodMax ) {
 
 	const sizeLods = [];
-	const sigmas = [];
 	const lodMeshes = [];
 
 	let lod = lodMax;
 
-	const totalLods = lodMax - LOD_MIN + 1 + EXTRA_LOD_SIGMA.length;
+	const totalLods = lodMax - LOD_MIN + 1 + EXTRA_LODS;
 
 	for ( let i = 0; i < totalLods; i ++ ) {
 
 		const sizeLod = Math.pow( 2, lod );
 		sizeLods.push( sizeLod );
-		let sigma = 1.0 / sizeLod;
-
-		if ( i > lodMax - LOD_MIN ) {
-
-			sigma = EXTRA_LOD_SIGMA[ i - lodMax + LOD_MIN - 1 ];
-
-		} else if ( i === 0 ) {
-
-			sigma = 0;
-
-		}
-
-		sigmas.push( sigma );
 
+		// UVs overshoot the face by one texel to bake the CubeUV border into the directions.
 		const texelSize = 1.0 / ( sizeLod - 2 );
 		const min = - texelSize;
 		const max = 1 + texelSize;
@@ -728,12 +639,9 @@ function _createPlanes( lodMax ) {
 		const cubeFaces = 6;
 		const vertices = 6;
 		const positionSize = 3;
-		const uvSize = 2;
-		const faceIndexSize = 1;
 
 		const position = new Float32Array( positionSize * vertices * cubeFaces );
-		const uv = new Float32Array( uvSize * vertices * cubeFaces );
-		const faceIndex = new Float32Array( faceIndexSize * vertices * cubeFaces );
+		const outputDirection = new Float32Array( positionSize * vertices * cubeFaces );
 
 		for ( let face = 0; face < cubeFaces; face ++ ) {
 
@@ -748,16 +656,48 @@ function _createPlanes( lodMax ) {
 				x, y + 1, 0
 			];
 			position.set( coordinates, positionSize * vertices * face );
-			uv.set( uv1, uvSize * vertices * face );
-			const fill = [ face, face, face, face, face, face ];
-			faceIndex.set( fill, faceIndexSize * vertices * face );
+
+			for ( let vertex = 0; vertex < vertices; vertex ++ ) {
+
+				const u = uv1[ vertex * 2 ] * 2 - 1;
+				const v = uv1[ vertex * 2 + 1 ] * 2 - 1;
+
+				// RH coordinate system; PMREM face-indexing convention
+				if ( face === 0 ) {
+
+					_direction.set( 1, v, u ); // pos x
+
+				} else if ( face === 1 ) {
+
+					_direction.set( - u, 1, - v ); // pos y
+
+				} else if ( face === 2 ) {
+
+					_direction.set( - u, v, 1 ); // pos z
+
+				} else if ( face === 3 ) {
+
+					_direction.set( - 1, v, - u ); // neg x
+
+				} else if ( face === 4 ) {
+
+					_direction.set( - u, - 1, v ); // neg y
+
+				} else {
+
+					_direction.set( u, v, - 1 ); // neg z
+
+				}
+
+				_direction.toArray( outputDirection, ( face * vertices + vertex ) * positionSize );
+
+			}
 
 		}
 
 		const planes = new BufferGeometry();
 		planes.setAttribute( 'position', new BufferAttribute( position, positionSize ) );
-		planes.setAttribute( 'uv', new BufferAttribute( uv, uvSize ) );
-		planes.setAttribute( 'faceIndex', new BufferAttribute( faceIndex, faceIndexSize ) );
+		planes.setAttribute( 'outputDirection', new BufferAttribute( outputDirection, positionSize ) );
 		lodMeshes.push( new Mesh( planes, null ) );
 
 		if ( lod > LOD_MIN ) {
@@ -768,7 +708,7 @@ function _createPlanes( lodMax ) {
 
 	}
 
-	return { lodMeshes, sizeLods, sigmas };
+	return { lodMeshes, sizeLods };
 
 }
 
@@ -928,14 +868,12 @@ function _getGGXShader( lodMax, width, height ) {
 
 function _getBlurShader( lodMax, width, height ) {
 
-	const weights = new Float32Array( MAX_SAMPLES );
-	const poleAxis = new Vector3( 0, 1, 0 );
 	const shaderMaterial = new ShaderMaterial( {
 
 		name: 'SphericalGaussianBlur',
 
 		defines: {
-			'n': MAX_SAMPLES,
+			'SAMPLES': BLUR_SAMPLES,
 			'CUBEUV_TEXEL_WIDTH': 1.0 / width,
 			'CUBEUV_TEXEL_HEIGHT': 1.0 / height,
 			'CUBEUV_MAX_MIP': `${lodMax}.0`,
@@ -943,75 +881,71 @@ function _getBlurShader( lodMax, width, height ) {
 
 		uniforms: {
 			'envMap': { value: null },
-			'samples': { value: 1 },
-			'weights': { value: weights },
-			'latitudinal': { value: false },
-			'dTheta': { value: 0 },
+			'sigma': { value: 0 },
 			'mipInt': { value: 0 },
-			'poleAxis': { value: poleAxis }
 		},
 
 		vertexShader: _getCommonVertexShader(),
 
 		fragmentShader: /* glsl */`
 
-			precision mediump float;
-			precision mediump int;
+			precision highp float;
+			precision highp int;
 
 			varying vec3 vOutputDirection;
 
 			uniform sampler2D envMap;
-			uniform int samples;
-			uniform float weights[ n ];
-			uniform bool latitudinal;
-			uniform float dTheta;
+			uniform float sigma;
 			uniform float mipInt;
-			uniform vec3 poleAxis;
 
 			#define ENVMAP_TYPE_CUBE_UV
 			#include <cube_uv_reflection_fragment>
 
-			vec3 getSample( float theta, vec3 axis ) {
-
-				float cosTheta = cos( theta );
-				// Rodrigues' axis-angle rotation
-				vec3 sampleDirection = vOutputDirection * cosTheta
-					+ cross( axis, vOutputDirection ) * sin( theta )
-					+ axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta );
-
-				return bilinearCubeUV( envMap, sampleDirection, mipInt );
-
-			}
+			#define PI 3.14159265359
+			#define GOLDEN_ANGLE 2.39996322973
 
 			void main() {
 
-				vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection );
-
-				if ( all( equal( axis, vec3( 0.0 ) ) ) ) {
+				if ( sigma == 0.0 ) {
 
-					axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x );
+					gl_FragColor = vec4( bilinearCubeUV( envMap, vOutputDirection, mipInt ), 1.0 );
+					return;
 
 				}
 
-				axis = normalize( axis );
+				vec3 outputDirection = normalize( vOutputDirection );
 
-				gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );
-				gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis );
+				vec3 up = abs( outputDirection.z ) < 0.999 ? vec3( 0.0, 0.0, 1.0 ) : vec3( 1.0, 0.0, 0.0 );
+				vec3 tangent = normalize( cross( up, outputDirection ) );
+				vec3 bitangent = cross( outputDirection, tangent );
 
-				for ( int i = 1; i < n; i++ ) {
+				// Truncate the kernel at three standard deviations or at the antipode.
+				float thetaMax = min( 3.0 * sigma, PI );
+				float truncation = 1.0 - exp( - 0.5 * thetaMax * thetaMax / ( sigma * sigma ) );
 
-					if ( i >= samples ) {
+				vec3 accumColor = vec3( 0.0 );
+				float accumWeight = 0.0;
 
-						break;
+				for ( int i = 0; i < SAMPLES; i ++ ) {
 
-					}
+					// Stratified inverse-CDF sampling of the Gaussian, placed on a golden-angle spiral.
+					float stratum = ( float( i ) + 0.5 ) / float( SAMPLES );
+					float theta = sigma * sqrt( - 2.0 * log( 1.0 - stratum * truncation ) );
+					float phi = float( i ) * GOLDEN_ANGLE;
 
-					float theta = dTheta * float( i );
-					gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis );
-					gl_FragColor.rgb += weights[ i ] * getSample( theta, axis );
+					vec3 offset = cos( phi ) * tangent + sin( phi ) * bitangent;
+					vec3 sampleDirection = cos( theta ) * outputDirection + sin( theta ) * offset;
+
+					// Correct the planar sample density to solid angle.
+					float weight = sin( theta ) / theta;
+
+					accumColor += weight * bilinearCubeUV( envMap, sampleDirection, mipInt );
+					accumWeight += weight;
 
 				}
 
+				gl_FragColor = vec4( accumColor / accumWeight, 1.0 );
+
 			}
 		`,
 
@@ -1112,53 +1046,13 @@ function _getCommonVertexShader() {
 		precision mediump float;
 		precision mediump int;
 
-		attribute float faceIndex;
+		attribute vec3 outputDirection;
 
 		varying vec3 vOutputDirection;
 
-		// RH coordinate system; PMREM face-indexing convention
-		vec3 getDirection( vec2 uv, float face ) {
-
-			uv = 2.0 * uv - 1.0;
-
-			vec3 direction = vec3( uv, 1.0 );
-
-			if ( face == 0.0 ) {
-
-				direction = direction.zyx; // ( 1, v, u ) pos x
-
-			} else if ( face == 1.0 ) {
-
-				direction = direction.xzy;
-				direction.xz *= -1.0; // ( -u, 1, -v ) pos y
-
-			} else if ( face == 2.0 ) {
-
-				direction.x *= -1.0; // ( -u, v, 1 ) pos z
-
-			} else if ( face == 3.0 ) {
-
-				direction = direction.zyx;
-				direction.xz *= -1.0; // ( -1, v, -u ) neg x
-
-			} else if ( face == 4.0 ) {
-
-				direction = direction.xzy;
-				direction.xy *= -1.0; // ( -u, -1, v ) neg y
-
-			} else if ( face == 5.0 ) {
-
-				direction.z *= -1.0; // ( u, v, -1 ) neg z
-
-			}
-
-			return direction;
-
-		}
-
 		void main() {
 
-			vOutputDirection = getDirection( uv, faceIndex );
+			vOutputDirection = outputDirection;
 			gl_Position = vec4( position, 1.0 );
 
 		}

+ 2 - 0
src/materials/Material.js

@@ -683,6 +683,7 @@ class Material extends EventDispatcher {
 		}
 
 		if ( this.dispersion !== undefined ) data.dispersion = this.dispersion;
+		if ( this.retroreflective !== undefined ) data.retroreflective = this.retroreflective;
 
 		if ( this.iridescence !== undefined ) data.iridescence = this.iridescence;
 		if ( this.iridescenceIOR !== undefined ) data.iridescenceIOR = this.iridescenceIOR;
@@ -911,6 +912,7 @@ class Material extends EventDispatcher {
 		if ( json.clearcoat !== undefined ) this.clearcoat = json.clearcoat;
 		if ( json.clearcoatRoughness !== undefined ) this.clearcoatRoughness = json.clearcoatRoughness;
 		if ( json.dispersion !== undefined ) this.dispersion = json.dispersion;
+		if ( json.retroreflective !== undefined ) this.retroreflective = json.retroreflective;
 		if ( json.iridescence !== undefined ) this.iridescence = json.iridescence;
 		if ( json.iridescenceIOR !== undefined ) this.iridescenceIOR = json.iridescenceIOR;
 		if ( json.iridescenceThicknessRange !== undefined ) this.iridescenceThicknessRange = json.iridescenceThicknessRange;

+ 32 - 0
src/materials/MeshPhysicalMaterial.js

@@ -19,6 +19,8 @@ import { clamp } from '../math/MathUtils.js';
  * transparent materials are less reflective. Physically-based transmission provides a more
  * realistic option for thin, transparent surfaces like glass.
  * - Advanced reflectivity: More flexible reflectivity for non-metallic materials.
+ * - Retroreflection: Redirects specular light back toward the light source for
+ * safety materials like road markings and reflective tape.
  * - Sheen: Can be used for representing cloth and fabric materials.
  *
  * As a result of these complex shading features, `MeshPhysicalMaterial` has a
@@ -354,6 +356,7 @@ class MeshPhysicalMaterial extends MeshStandardMaterial {
 		this._clearcoat = 0;
 		this._dispersion = 0;
 		this._iridescence = 0;
+		this._retroreflective = 0;
 		this._sheen = 0.0;
 		this._transmission = 0;
 
@@ -461,6 +464,33 @@ class MeshPhysicalMaterial extends MeshStandardMaterial {
 
 	}
 
+	/**
+	 * The strength of retroreflection, from `0.0` to `1.0`. A value of `1.0`
+	 * evaluates the material's microfacet reflection with the view direction
+	 * reflected about the surface normal, redirecting the specular lobe back
+	 * toward the light source.
+	 *
+	 * @type {number}
+	 * @default 0
+	 */
+	get retroreflective() {
+
+		return this._retroreflective;
+
+	}
+
+	set retroreflective( value ) {
+
+		if ( this._retroreflective > 0 !== value > 0 ) {
+
+			this.version ++;
+
+		}
+
+		this._retroreflective = value;
+
+	}
+
 	/**
 	 * The intensity of the sheen layer, from `0.0` to `1.0`.
 	 *
@@ -546,6 +576,8 @@ class MeshPhysicalMaterial extends MeshStandardMaterial {
 		this.iridescenceThicknessRange = [ ...source.iridescenceThicknessRange ];
 		this.iridescenceThicknessMap = source.iridescenceThicknessMap;
 
+		this.retroreflective = source.retroreflective;
+
 		this.sheen = source.sheen;
 		this.sheenColor.copy( source.sheenColor );
 		this.sheenColorMap = source.sheenColorMap;

+ 38 - 3
src/materials/nodes/MeshPhysicalNodeMaterial.js

@@ -1,5 +1,5 @@
-import { clearcoat, clearcoatRoughness, sheen, sheenRoughness, iridescence, iridescenceIOR, iridescenceThickness, specularColor, specularColorBlended, specularF90, diffuseColor, metalness, roughness, anisotropy, alphaT, anisotropyT, anisotropyB, ior, transmission, thickness, attenuationDistance, attenuationColor, dispersion } from '../../nodes/core/PropertyNode.js';
-import { materialClearcoat, materialClearcoatRoughness, materialClearcoatNormal, materialSheen, materialSheenRoughness, materialIridescence, materialIridescenceIOR, materialIridescenceThickness, materialSpecularIntensity, materialSpecularColor, materialAnisotropy, materialIOR, materialTransmission, materialThickness, materialAttenuationDistance, materialAttenuationColor, materialDispersion } from '../../nodes/accessors/MaterialNode.js';
+import { clearcoat, clearcoatRoughness, sheen, sheenRoughness, iridescence, iridescenceIOR, iridescenceThickness, specularColor, specularColorBlended, specularF90, diffuseColor, metalness, roughness, anisotropy, alphaT, anisotropyT, anisotropyB, ior, transmission, thickness, attenuationDistance, attenuationColor, dispersion, retroreflective } from '../../nodes/core/PropertyNode.js';
+import { materialClearcoat, materialClearcoatRoughness, materialClearcoatNormal, materialSheen, materialSheenRoughness, materialIridescence, materialIridescenceIOR, materialIridescenceThickness, materialSpecularIntensity, materialSpecularColor, materialAnisotropy, materialIOR, materialTransmission, materialThickness, materialAttenuationDistance, materialAttenuationColor, materialDispersion, materialRetroreflective } from '../../nodes/accessors/MaterialNode.js';
 import { float, vec2, vec3, If } from '../../nodes/tsl/TSLBase.js';
 import getRoughness from '../../nodes/functions/material/getRoughness.js';
 import { TBNViewMatrix } from '../../nodes/accessors/AccessorsUtils.js';
@@ -251,6 +251,19 @@ class MeshPhysicalNodeMaterial extends MeshStandardNodeMaterial {
 		 */
 		this.dispersionNode = null;
 
+		/**
+		 * The retroreflective strength of physical materials is by default inferred from the
+		 * `retroreflective` property. This node property allows to overwrite the default
+		 * and define the retroreflective strength with a node instead.
+		 *
+		 * If you don't want to overwrite the retroreflective strength but modify the existing
+		 * value instead, use {@link materialRetroreflective}.
+		 *
+		 * @type {?Node<float>}
+		 * @default null
+		 */
+		this.retroreflectiveNode = null;
+
 		/**
 		 * The anisotropy of physical materials is by default inferred from the
 		 * `anisotropy` property. This node property allows to overwrite the default
@@ -342,6 +355,18 @@ class MeshPhysicalNodeMaterial extends MeshStandardNodeMaterial {
 
 	}
 
+	/**
+	 * Whether the lighting model should use retroreflection or not.
+	 *
+	 * @type {boolean}
+	 * @default true
+	 */
+	get useRetroreflective() {
+
+		return this.retroreflective > 0 || this.retroreflectiveNode !== null;
+
+	}
+
 	/**
 	 * Setups the specular related node variables.
 	 */
@@ -363,7 +388,7 @@ class MeshPhysicalNodeMaterial extends MeshStandardNodeMaterial {
 	 */
 	setupLightingModel( /*builder*/ ) {
 
-		return new PhysicalLightingModel( this.useClearcoat, this.useSheen, this.useIridescence, this.useAnisotropy, this.useTransmission, this.useDispersion );
+		return new PhysicalLightingModel( this.useClearcoat, this.useSheen, this.useIridescence, this.useAnisotropy, this.useTransmission, this.useDispersion, this.useRetroreflective );
 
 	}
 
@@ -400,6 +425,16 @@ class MeshPhysicalNodeMaterial extends MeshStandardNodeMaterial {
 
 		}
 
+		// RETROREFLECTIVE
+
+		if ( this.useRetroreflective ) {
+
+			const retroreflectiveNode = this.retroreflectiveNode ? float( this.retroreflectiveNode ) : materialRetroreflective;
+
+			retroreflective.assign( retroreflectiveNode );
+
+		}
+
 		// IRIDESCENCE
 
 		if ( this.useIridescence ) {

+ 0 - 1
src/nodes/TSL.js

@@ -147,7 +147,6 @@ export * from './lighting/PointLightNode.js';
 
 // pmrem
 export * from './pmrem/PMREMNode.js';
-export * from './pmrem/PMREMUtils.js';
 
 // procedural
 export * from './procedural/Checker.js';

+ 9 - 0
src/nodes/accessors/MaterialNode.js

@@ -463,6 +463,7 @@ MaterialNode.LINE_WIDTH = 'linewidth';
 MaterialNode.LINE_DASH_OFFSET = 'dashOffset';
 MaterialNode.POINT_SIZE = 'size';
 MaterialNode.DISPERSION = 'dispersion';
+MaterialNode.RETROREFLECTIVE = 'retroreflective';
 MaterialNode.LIGHT_MAP = 'light';
 MaterialNode.AO = 'ao';
 
@@ -764,6 +765,14 @@ export const materialPointSize = /*@__PURE__*/ nodeImmutable( MaterialNode, Mate
  */
 export const materialDispersion = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.DISPERSION );
 
+/**
+ * TSL object that represents the retroreflective strength of the current material.
+ *
+ * @tsl
+ * @type {Node<float>}
+ */
+export const materialRetroreflective = /*@__PURE__*/ nodeImmutable( MaterialNode, MaterialNode.RETROREFLECTIVE );
+
 /**
  * TSL object that represents the light map of the current material.
  * The value is composed via `lightMapIntensity` * `lightMap.rgb`.

+ 5 - 3
src/nodes/accessors/Skinning.js

@@ -1,5 +1,5 @@
 
-import { Fn, add, uniform } from '../tsl/TSLBase.js';
+import { Fn, add, uniform, mat3 } from '../tsl/TSLBase.js';
 import { attribute } from '../core/AttributeNode.js';
 import { OnObjectUpdate } from '../utils/EventNode.js';
 import { normalLocal } from './Normal.js';
@@ -78,8 +78,10 @@ function getSkinnedNormalAndTangent( boneMatrices, normal, tangent, bindMatrix,
 
 	skinMatrix = bindMatrixInverse.mul( skinMatrix ).mul( bindMatrix );
 
-	const skinNormal = skinMatrix.transformDirection( normal ).xyz;
-	const skinTangent = skinMatrix.transformDirection( tangent ).xyz;
+	const skinMatrix3 = mat3( skinMatrix );
+
+	const skinNormal = skinMatrix3.mul( normal );
+	const skinTangent = skinMatrix3.mul( tangent );
 
 	return { skinNormal, skinTangent };
 

+ 30 - 5
src/nodes/accessors/UniformArrayNode.js

@@ -186,13 +186,38 @@ class UniformArrayNode extends BufferNode {
 
 	}
 
+	update( /*frame*/ ) {
+
+		this.updateBuffer();
+
+	}
+
 	/**
-	 * The update makes sure to correctly transfer the data from the (complex) objects
-	 * in the array to the internal, correctly padded value buffer.
+	 * Composes a user-defined update with the buffer transfer.
 	 *
-	 * @param {NodeFrame} frame - A reference to the current node frame.
+	 * @param {Function} callback - The update function.
+	 * @param {string} updateType - The update type.
+	 * @return {UniformArrayNode} A reference to this node.
 	 */
-	update( /*frame*/ ) {
+	onUpdate( callback, updateType ) {
+
+		callback = callback.bind( this );
+
+		return super.onUpdate( ( frame, self ) => {
+
+			callback( frame, self );
+
+			this.updateBuffer();
+
+		}, updateType );
+
+	}
+
+	/**
+	 * The method makes sure to correctly transfer the data from the (complex) objects
+	 * in the array to the internal, correctly padded value buffer.
+	 */
+	updateBuffer() {
 
 		const { array, value } = this;
 
@@ -315,7 +340,7 @@ class UniformArrayNode extends BufferNode {
 		this.bufferCount = length;
 		this.bufferType = paddedType;
 
-		this.update(); // initialize the buffer values
+		this.updateBuffer(); // initialize the buffer values
 
 		return super.setup( builder );
 

+ 8 - 0
src/nodes/core/PropertyNode.js

@@ -403,6 +403,14 @@ export const attenuationColor = /*@__PURE__*/ nodeImmutable( PropertyNode, 'colo
  */
 export const dispersion = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'Dispersion' );
 
+/**
+ * TSL object that represents the shader variable `Retroreflective`.
+ *
+ * @tsl
+ * @type {PropertyNode<float>}
+ */
+export const retroreflective = /*@__PURE__*/ nodeImmutable( PropertyNode, 'float', 'Retroreflective' );
+
 /**
  * TSL object that represents the shader variable `AmbientOcclusion`.
  * If no value is assigned to this property, it defaults to a placeholder value of `1.0`.

+ 6 - 6
src/nodes/functions/BSDF/BRDF_GGX.js

@@ -9,16 +9,16 @@ import { iridescence, alphaT, anisotropyT, anisotropyB } from '../../core/Proper
 import { Fn, defined } from '../../tsl/TSLBase.js';
 
 // GGX Distribution, Schlick Fresnel, GGX_SmithCorrelated Visibility
-const BRDF_GGX = /*@__PURE__*/ Fn( ( { lightDirection, f0, f90, roughness, f, normalView = NormalView, USE_IRIDESCENCE, USE_ANISOTROPY } ) => {
+const BRDF_GGX = /*@__PURE__*/ Fn( ( { lightDirection, f0, f90, roughness, f, normalView = NormalView, viewDirection = positionViewDirection, USE_IRIDESCENCE, USE_ANISOTROPY } ) => {
 
 	const alpha = roughness.pow2(); // UE4's roughness
 
-	const halfDir = lightDirection.add( positionViewDirection ).normalize();
+	const halfDir = lightDirection.add( viewDirection ).normalize();
 
 	const dotNL = normalView.dot( lightDirection ).clamp();
-	const dotNV = normalView.dot( positionViewDirection ).clamp(); // @ TODO: Move to core dotNV
+	const dotNV = normalView.dot( viewDirection ).clamp(); // @ TODO: Move to core dotNV
 	const dotNH = normalView.dot( halfDir ).clamp();
-	const dotVH = positionViewDirection.dot( halfDir ).clamp();
+	const dotVH = viewDirection.dot( halfDir ).clamp();
 
 	let F = F_Schlick( { f0, f90, dotVH } );
 	let V, D;
@@ -32,10 +32,10 @@ const BRDF_GGX = /*@__PURE__*/ Fn( ( { lightDirection, f0, f90, roughness, f, no
 	if ( defined( USE_ANISOTROPY ) ) {
 
 		const dotTL = anisotropyT.dot( lightDirection );
-		const dotTV = anisotropyT.dot( positionViewDirection );
+		const dotTV = anisotropyT.dot( viewDirection );
 		const dotTH = anisotropyT.dot( halfDir );
 		const dotBL = anisotropyB.dot( lightDirection );
-		const dotBV = anisotropyB.dot( positionViewDirection );
+		const dotBV = anisotropyB.dot( viewDirection );
 		const dotBH = anisotropyB.dot( halfDir );
 
 		V = V_GGX_SmithCorrelated_Anisotropic( { alphaT, alphaB: alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL } );

+ 0 - 52
src/nodes/functions/BSDF/BRDF_GGX_Multiscatter.js

@@ -1,52 +0,0 @@
-import BRDF_GGX from './BRDF_GGX.js';
-import DFGLUT from './DFGLUT.js';
-import { normalView } from '../../accessors/Normal.js';
-import { positionViewDirection } from '../../accessors/Position.js';
-import { EPSILON } from '../../math/MathNode.js';
-import { Fn, float } from '../../tsl/TSLBase.js';
-
-// GGX BRDF with multi-scattering energy compensation for direct lighting
-// This provides more accurate energy conservation, especially for rough materials
-// Based on "Practical Multiple Scattering Compensation for Microfacet Models"
-// https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf
-const BRDF_GGX_Multiscatter = /*@__PURE__*/ Fn( ( { lightDirection, f0, f90, roughness: _roughness, f, USE_IRIDESCENCE, USE_ANISOTROPY } ) => {
-
-	// Single-scattering BRDF (standard GGX)
-	const singleScatter = BRDF_GGX( { lightDirection, f0, f90, roughness: _roughness, f, USE_IRIDESCENCE, USE_ANISOTROPY } );
-
-	// Multi-scattering compensation
-	const dotNL = normalView.dot( lightDirection ).clamp();
-	const dotNV = normalView.dot( positionViewDirection ).clamp();
-
-	// Precomputed DFG values for view and light directions
-	const dfgV = DFGLUT( { roughness: _roughness, dotNV } );
-	const dfgL = DFGLUT( { roughness: _roughness, dotNV: dotNL } );
-
-	// Single-scattering energy for view and light
-	const FssEss_V = f0.mul( dfgV.x ).add( f90.mul( dfgV.y ) );
-	const FssEss_L = f0.mul( dfgL.x ).add( f90.mul( dfgL.y ) );
-
-	const Ess_V = dfgV.x.add( dfgV.y );
-	const Ess_L = dfgL.x.add( dfgL.y );
-
-	// Energy lost to multiple scattering
-	const Ems_V = float( 1.0 ).sub( Ess_V );
-	const Ems_L = float( 1.0 ).sub( Ess_L );
-
-	// Average Fresnel reflectance
-	const Favg = f0.add( f0.oneMinus().mul( 0.047619 ) ); // 1/21
-
-	// Multiple scattering contribution
-	// Uses geometric mean of view and light contributions for better energy distribution
-	const Fms = FssEss_V.mul( FssEss_L ).mul( Favg ).div( float( 1.0 ).sub( Ems_V.mul( Ems_L ).mul( Favg ).mul( Favg ) ).add( EPSILON ) );
-
-	// Energy compensation factor
-	const compensationFactor = Ems_V.mul( Ems_L );
-
-	const multiScatter = Fms.mul( compensationFactor );
-
-	return singleScatter.add( multiScatter );
-
-} );
-
-export default BRDF_GGX_Multiscatter;

+ 2 - 1
src/nodes/functions/BSDF/DFGLUT.js

@@ -5,7 +5,8 @@ import { DataTexture } from '../../../textures/DataTexture.js';
 import { RGFormat, HalfFloatType, LinearFilter, ClampToEdgeWrapping } from '../../../constants.js';
 
 /**
- * Precomputed DFG LUT for Image-Based Lighting
+ * Precomputed DFG LUT for physically based specular lighting, used by both
+ * image-based lighting and direct-light multi-scattering energy compensation
  * Resolution: 16x16
  * Samples: 4096 per texel
  * Format: RG16F (2 half floats per texel: scale, bias)

+ 58 - 21
src/nodes/functions/PhysicalLightingModel.js

@@ -1,6 +1,5 @@
 import BRDF_Lambert from './BSDF/BRDF_Lambert.js';
 import BRDF_GGX from './BSDF/BRDF_GGX.js';
-import BRDF_GGX_Multiscatter from './BSDF/BRDF_GGX_Multiscatter.js';
 import DFGLUT from './BSDF/DFGLUT.js';
 import EnvironmentBRDF from './BSDF/EnvironmentBRDF.js';
 import F_Schlick from './BSDF/F_Schlick.js';
@@ -8,7 +7,7 @@ import Schlick_to_F0 from './BSDF/Schlick_to_F0.js';
 import BRDF_Sheen from './BSDF/BRDF_Sheen.js';
 import { LTC_Evaluate, LTC_Uv } from './BSDF/LTC.js';
 import LightingModel from '../core/LightingModel.js';
-import { diffuseColor, diffuseContribution, specularColor, specularColorBlended, specularF90, roughness, metalness, clearcoat, clearcoatRoughness, sheen, sheenRoughness, iridescence, iridescenceIOR, iridescenceThickness, ior, thickness, transmission, attenuationDistance, attenuationColor, dispersion } from '../core/PropertyNode.js';
+import { diffuseColor, diffuseContribution, specularColor, specularColorBlended, specularF90, roughness, metalness, clearcoat, clearcoatRoughness, sheen, sheenRoughness, iridescence, iridescenceIOR, iridescenceThickness, ior, thickness, transmission, attenuationDistance, attenuationColor, dispersion, retroreflective } from '../core/PropertyNode.js';
 import { normalView, clearcoatNormalView, normalWorld } from '../accessors/Normal.js';
 import { positionViewDirection, positionView, positionWorld } from '../accessors/Position.js';
 import { Fn, float, vec2, vec3, vec4, mat3, If } from '../tsl/TSLBase.js';
@@ -190,9 +189,9 @@ const getIBLVolumeRefraction = /*@__PURE__*/ Fn( ( [ n, v, roughness, diffuseCol
 
 // XYZ to linear-sRGB color space
 const XYZ_TO_REC709 = /*@__PURE__*/ mat3(
-	3.2404542, - 0.9692660, 0.0556434,
-	- 1.5371385, 1.8760108, - 0.2040259,
-	- 0.4985314, 0.0415560, 1.0572252
+	3.2404542, - 1.5371385, - 0.4985314,
+	- 0.9692660, 1.8760108, 0.0415560,
+	0.0556434, - 0.2040259, 1.0572252
 );
 
 // Assume air interface for top
@@ -348,8 +347,9 @@ class PhysicalLightingModel extends LightingModel {
 	 * @param {boolean} [anisotropy=false] - Whether anisotropy is supported or not.
 	 * @param {boolean} [transmission=false] - Whether transmission is supported or not.
 	 * @param {boolean} [dispersion=false] - Whether dispersion is supported or not.
+	 * @param {boolean} [retroreflective=false] - Whether retroreflection is supported or not.
 	 */
-	constructor( clearcoat = false, sheen = false, iridescence = false, anisotropy = false, transmission = false, dispersion = false ) {
+	constructor( clearcoat = false, sheen = false, iridescence = false, anisotropy = false, transmission = false, dispersion = false, retroreflective = false ) {
 
 		super();
 
@@ -401,6 +401,14 @@ class PhysicalLightingModel extends LightingModel {
 		 */
 		this.dispersion = dispersion;
 
+		/**
+		 * Whether retroreflection is supported or not.
+		 *
+		 * @type {boolean}
+		 * @default false
+		 */
+		this.retroreflective = retroreflective;
+
 		/**
 		 * The clear coat radiance.
 		 *
@@ -449,14 +457,6 @@ class PhysicalLightingModel extends LightingModel {
 		 */
 		this.iridescenceFresnel = null;
 
-		/**
-		 * The iridescence F0.
-		 *
-		 * @type {?Node}
-		 * @default null
-		 */
-		this.iridescenceF0 = null;
-
 		/**
 		 * The iridescence F0 dielectric.
 		 *
@@ -523,8 +523,6 @@ class PhysicalLightingModel extends LightingModel {
 			this.iridescenceF0Dielectric = Schlick_to_F0( { f: iridescenceFresnelDielectric, f90: 1.0, dotVH: dotNVi } );
 			this.iridescenceF0Metallic = Schlick_to_F0( { f: iridescenceFresnelMetallic, f90: 1.0, dotVH: dotNVi } );
 
-			this.iridescenceF0 = mix( this.iridescenceF0Dielectric, this.iridescenceF0Metallic, metalness );
-
 		}
 
 		if ( this.transmission === true ) {
@@ -621,9 +619,39 @@ class PhysicalLightingModel extends LightingModel {
 
 		}
 
-		reflectedLight.directDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor: diffuseContribution } ) ) );
+		// Light reflected by the specular interface is not available to the diffuse layer ( glTF fresnel_mix )
+		const halfDir = lightDirection.add( positionViewDirection ).normalize();
+		const dotVH = positionViewDirection.dot( halfDir ).clamp();
+		const F = F_Schlick( { f0: specularColor, f90: specularF90, dotVH } );
+
+		reflectedLight.directDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor: diffuseContribution } ) ).mul( F.oneMinus() ) );
+
+		let specularBRDF = BRDF_GGX( { lightDirection, f0: specularColorBlended, f90: 1, roughness, f: this.iridescenceFresnel, USE_IRIDESCENCE: this.iridescence, USE_ANISOTROPY: this.anisotropy } );
+
+		if ( this.retroreflective === true ) {
+
+			// Minimal Retroreflective Microfacet Model:
+			// https://jcgt.org/published/0015/01/04/
+			const retroViewDirection = positionViewDirection.negate().reflect( normalView );
+			const retroSpecularBRDF = BRDF_GGX( { lightDirection, viewDirection: retroViewDirection, f0: specularColorBlended, f90: 1, roughness, f: this.iridescenceFresnel, USE_IRIDESCENCE: this.iridescence, USE_ANISOTROPY: this.anisotropy } );
+
+			specularBRDF = mix( specularBRDF, retroSpecularBRDF, retroreflective.clamp() );
+
+		}
+
+		// Multi-scattering energy compensation for direct lighting
+		// Based on "Practical Multiple Scattering Compensation for Microfacet Models"
+		// https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf
+		const dotNV = normalView.dot( positionViewDirection ).clamp();
+		const fab = DFGLUT( { roughness, dotNV } );
+
+		// Energy of the single-scattering lobe in a white furnace ( F0 = F90 = 1 )
+		const Ess = fab.x.add( fab.y );
+
+		// Compensate for the energy lost to multiple scattering, tinting the added term by F0 ( equation 16 )
+		const energyCompensation = specularColorBlended.mul( Ess.reciprocal().sub( 1.0 ) ).add( 1.0 );
 
-		reflectedLight.directSpecular.addAssign( irradiance.mul( BRDF_GGX_Multiscatter( { lightDirection, f0: specularColorBlended, f90: 1, roughness, f: this.iridescenceFresnel, USE_IRIDESCENCE: this.iridescence, USE_ANISOTROPY: this.anisotropy } ) ) );
+		reflectedLight.directSpecular.addAssign( irradiance.mul( specularBRDF ).mul( energyCompensation ) );
 
 	}
 
@@ -710,12 +738,20 @@ class PhysicalLightingModel extends LightingModel {
 
 		const { irradiance, reflectedLight } = builder.context;
 
-		const diffuse = irradiance.mul( BRDF_Lambert( { diffuseColor: diffuseContribution } ) ).toVar();
+		// Energy reflected by the specular lobe is not available to the diffuse layer
+		const singleScattering = vec3().toVar();
+		const multiScattering = vec3().toVar();
+
+		this.computeMultiscattering( singleScattering, multiScattering, specularF90, specularColor, this.iridescenceF0Dielectric );
+
+		const diffuse = irradiance.mul( BRDF_Lambert( { diffuseColor: diffuseContribution } ) ).mul( singleScattering.add( multiScattering ).oneMinus() ).toVar();
 
 		if ( this.sheen === true ) {
 
 			const sheenAlbedo = IBLSheenBRDF( { normal: normalView, viewDir: positionViewDirection, roughness: sheenRoughness } );
 
+			this.sheenSpecularIndirect.addAssign( irradiance.mul( sheen, sheenAlbedo, 1 / Math.PI ) );
+
 			const sheenEnergyComp = sheen.r.max( sheen.g ).max( sheen.b ).mul( sheenAlbedo ).oneMinus();
 
 			diffuse.mulAssign( sheenEnergyComp );
@@ -743,7 +779,8 @@ class PhysicalLightingModel extends LightingModel {
 					normal: normalView,
 					viewDir: positionViewDirection,
 					roughness: sheenRoughness
-				} )
+				} ),
+				1 / Math.PI
 			) );
 
 		}
@@ -865,7 +902,7 @@ class PhysicalLightingModel extends LightingModel {
 
 		if ( this.sheen === true ) {
 
-			const sheenLight = outgoingLight.add( this.sheenSpecularDirect, this.sheenSpecularIndirect.mul( 1.0 / Math.PI ) );
+			const sheenLight = outgoingLight.add( this.sheenSpecularDirect, this.sheenSpecularIndirect );
 
 			outgoingLight.assign( sheenLight );
 

+ 2 - 50
src/nodes/lighting/ShadowFilterNode.js

@@ -1,7 +1,7 @@
-import { float, vec2, vec4, If, Fn, ivec2 } from '../tsl/TSLBase.js';
+import { float, vec2, vec4, If, Fn } from '../tsl/TSLBase.js';
 import { reference } from '../accessors/ReferenceNode.js';
 import { texture } from '../accessors/TextureNode.js';
-import { mix, fract, step, max, clamp } from '../math/MathNode.js';
+import { step, max, clamp } from '../math/MathNode.js';
 import { add, sub } from '../math/OperatorNode.js';
 import { renderGroup } from '../core/UniformGroupNode.js';
 import NodeMaterial from '../../materials/nodes/NodeMaterial.js';
@@ -85,54 +85,6 @@ export const PCFShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord,
 
 } );
 
-/**
- * A shadow filtering function performing PCF soft filtering.
- *
- * @method
- * @param {Object} inputs - The input parameter object.
- * @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
- * @param {Node<vec3>} inputs.shadowCoord - The shadow coordinates.
- * @param {LightShadow} inputs.shadow - The light shadow.
- * @return {Node<float>} The filtering result.
- */
-export const PCFSoftShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, shadowCoord, shadow, depthLayer } ) => {
-
-	const mapSize = reference( 'mapSize', 'vec2', shadow ).setGroup( renderGroup );
-
-	const texelSize = vec2( 1 ).div( mapSize );
-
-	const uv = shadowCoord.xy;
-	const f = fract( uv.mul( mapSize ).add( 0.5 ) ).toConst();
-	uv.subAssign( f.sub( 0.5 ).mul( texelSize ) );
-
-	const gatherCompare = ( offset ) => {
-
-		let t = texture( depthTexture, uv ).offset( offset ).gather();
-
-		if ( depthTexture.isArrayTexture ) {
-
-			t = t.depth( depthLayer );
-
-		}
-
-		return t.compare( shadowCoord.z );
-
-	};
-
-	const c1 = gatherCompare( ivec2( - 1, 1 ) ).toConst();
-	const c2 = gatherCompare( ivec2( 1, 1 ) ).toConst();
-	const c3 = gatherCompare( ivec2( - 1, - 1 ) ).toConst();
-	const c4 = gatherCompare( ivec2( 1, - 1 ) ).toConst();
-
-	return add(
-		mix( c1.x, c2.y, f.x ).add( c1.y ).add( c2.x ).mul( f.y ),
-		mix( c1.w, c2.z, f.x ).add( c1.z ).add( c2.w ),
-		mix( c3.x, c4.y, f.x ).add( c3.y ).add( c4.x ),
-		mix( c3.w, c4.z, f.x ).add( c3.z ).add( c4.w ).mul( f.y.oneMinus() )
-	).mul( 1 / 9 );
-
-} );
-
 /**
  * A shadow filtering function performing VSM filtering.
  *

+ 6 - 6
src/nodes/lighting/ShadowNode.js

@@ -12,13 +12,13 @@ import { add } from '../math/OperatorNode.js';
 import { DepthTexture } from '../../textures/DepthTexture.js';
 import { Loop } from '../utils/LoopNode.js';
 import { screenCoordinate } from '../display/ScreenNode.js';
-import { Compatibility, GreaterEqualCompare, HalfFloatType, LessEqualCompare, LinearFilter, NearestFilter, PCFShadowMap, PCFSoftShadowMap, RGFormat, VSMShadowMap } from '../../constants.js';
+import { Compatibility, GreaterEqualCompare, HalfFloatType, LessEqualCompare, LinearFilter, NearestFilter, PCFShadowMap, RGFormat, VSMShadowMap } from '../../constants.js';
 import { renderGroup } from '../core/UniformGroupNode.js';
 import { viewZToLogarithmicDepth, perspectiveDepthToViewZ, orthographicDepthToViewZ, viewZToOrthographicDepth } from '../display/ViewportDepthNode.js';
 import { lightShadowMatrix } from '../accessors/Lights.js';
 import { resetRendererAndSceneState, restoreRendererAndSceneState } from '../../renderers/common/RendererUtils.js';
 import { getDataFromObject } from '../core/NodeUtils.js';
-import { getShadowMaterial, disposeShadowMaterial, BasicShadowFilter, PCFShadowFilter, PCFSoftShadowFilter, VSMShadowFilter } from './ShadowFilterNode.js';
+import { getShadowMaterial, disposeShadowMaterial, BasicShadowFilter, PCFShadowFilter, VSMShadowFilter } from './ShadowFilterNode.js';
 import { positionLocal } from '../accessors/Position.js';
 import { uniform } from '../core/UniformNode.js';
 import { equirectDirection } from '../utils/EquirectUV.js';
@@ -174,7 +174,7 @@ const VSMPassHorizontal = /*@__PURE__*/ Fn( ( { samples, radius, size, shadowPas
 
 } );
 
-const _shadowFilterLib = [ BasicShadowFilter, PCFShadowFilter, PCFSoftShadowFilter, VSMShadowFilter ];
+const _shadowFilterLib = [ BasicShadowFilter, PCFShadowFilter, null /* PCFSoftShadowMap, removed */, VSMShadowFilter ];
 
 //
 
@@ -423,7 +423,7 @@ class ShadowNode extends ShadowBaseNode {
 		const shadowMapType = renderer.shadowMap.type;
 		const hasTextureCompare = renderer.hasCompatibility( Compatibility.TEXTURE_COMPARE );
 
-		if ( ( shadowMapType === PCFShadowMap || shadowMapType === PCFSoftShadowMap ) && hasTextureCompare ) {
+		if ( shadowMapType === PCFShadowMap && hasTextureCompare ) {
 
 			depthTexture.minFilter = LinearFilter;
 			depthTexture.magFilter = LinearFilter;
@@ -860,13 +860,13 @@ class ShadowNode extends ShadowBaseNode {
 
 		if ( needsUpdate ) {
 
-			if ( this._cameraFrameId[ frame.camera ] === frame.frameId ) {
+			if ( this._cameraFrameId.get( frame.camera ) === frame.frameId ) {
 
 				needsUpdate = false;
 
 			}
 
-			this._cameraFrameId[ frame.camera ] = frame.frameId;
+			this._cameraFrameId.set( frame.camera, frame.frameId );
 
 		}
 

+ 31 - 74
src/nodes/pmrem/PMREMUtils.js

@@ -1,8 +1,10 @@
 import { Fn, int, uint, float, vec2, vec3, vec4, If } from '../tsl/TSLBase.js';
-import { cos, sin, abs, max, exp2, log2, clamp, fract, mix, floor, normalize, cross, dot, sqrt } from '../math/MathNode.js';
+import { cos, sin, abs, min, max, exp, exp2, log, log2, clamp, fract, mix, floor, normalize, cross, dot, sqrt } from '../math/MathNode.js';
 import { mul } from '../math/OperatorNode.js';
 import { select } from '../math/ConditionalNode.js';
-import { Loop, Break } from '../utils/LoopNode.js';
+import { Loop } from '../utils/LoopNode.js';
+
+const GOLDEN_ANGLE = 2.399963229728653;
 
 // These defines must match with PMREMGenerator
 
@@ -143,53 +145,6 @@ const roughnessToMip = /*@__PURE__*/ Fn( ( [ roughness ] ) => {
 	]
 } );
 
-// RH coordinate system; PMREM face-indexing convention
-export const getDirection = /*@__PURE__*/ Fn( ( [ uv_immutable, face ] ) => {
-
-	const uv = uv_immutable.toVar();
-	uv.assign( mul( 2.0, uv ).sub( 1.0 ) );
-	const direction = vec3( uv, 1.0 ).toVar();
-
-	If( face.equal( 0.0 ), () => {
-
-		direction.assign( direction.zyx ); // ( 1, v, u ) pos x
-
-	} ).ElseIf( face.equal( 1.0 ), () => {
-
-		direction.assign( direction.xzy );
-		direction.xz.mulAssign( - 1.0 ); // ( -u, 1, -v ) pos y
-
-	} ).ElseIf( face.equal( 2.0 ), () => {
-
-		direction.x.mulAssign( - 1.0 ); // ( -u, v, 1 ) pos z
-
-	} ).ElseIf( face.equal( 3.0 ), () => {
-
-		direction.assign( direction.zyx );
-		direction.xz.mulAssign( - 1.0 ); // ( -1, v, -u ) neg x
-
-	} ).ElseIf( face.equal( 4.0 ), () => {
-
-		direction.assign( direction.xzy );
-		direction.xy.mulAssign( - 1.0 ); // ( -u, -1, v ) neg y
-
-	} ).ElseIf( face.equal( 5.0 ), () => {
-
-		direction.z.mulAssign( - 1.0 ); // ( u, v, -1 ) neg zS
-
-	} );
-
-	return direction;
-
-} ).setLayout( {
-	name: 'getDirection',
-	type: 'vec3',
-	inputs: [
-		{ name: 'uv', type: 'vec2' },
-		{ name: 'face', type: 'float' }
-	]
-} );
-
 //
 
 export const textureCubeUV = /*@__PURE__*/ Fn( ( [ envMap, sampleDir_immutable, roughness_immutable, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP ] ) => {
@@ -241,49 +196,51 @@ const bilinearCubeUV = /*@__PURE__*/ Fn( ( [ envMap, direction_immutable, mipInt
 
 } );
 
-const getSample = /*@__PURE__*/ Fn( ( { envMap, mipInt, outputDirection, theta, axis, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) => {
-
-	const cosTheta = cos( theta );
+// Gaussian blur along a golden-angle spiral, importance-sampled by stratified
+// inverse-CDF so every sample carries equal Gaussian weight.
+export const sphericalGaussianBlur = /*@__PURE__*/ Fn( ( { SAMPLES, sigma, outputDirection, mipInt, envMap, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) => {
 
-	// Rodrigues' axis-angle rotation
-	const sampleDirection = outputDirection.mul( cosTheta )
-		.add( axis.cross( outputDirection ).mul( sin( theta ) ) )
-		.add( axis.mul( axis.dot( outputDirection ).mul( cosTheta.oneMinus() ) ) );
+	const color = vec3( 0.0 ).toVar();
 
-	return bilinearCubeUV( envMap, sampleDirection, mipInt, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP );
+	If( sigma.equal( 0.0 ), () => {
 
-} );
-
-export const blur = /*@__PURE__*/ Fn( ( { n, latitudinal, poleAxis, outputDirection, weights, samples, dTheta, mipInt, envMap, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) => {
+		color.assign( bilinearCubeUV( envMap, outputDirection, mipInt, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP ) );
 
-	const axis = vec3( select( latitudinal, poleAxis, cross( poleAxis, outputDirection ) ) ).toVar();
+	} ).Else( () => {
 
-	If( axis.equal( vec3( 0.0 ) ), () => {
+		const up = select( abs( outputDirection.z ).lessThan( 0.999 ), vec3( 0.0, 0.0, 1.0 ), vec3( 1.0, 0.0, 0.0 ) );
+		const tangent = normalize( cross( up, outputDirection ) ).toVar();
+		const bitangent = cross( outputDirection, tangent ).toVar();
 
-		axis.assign( vec3( outputDirection.z, 0.0, outputDirection.x.negate() ) );
+		// Truncate the kernel at three standard deviations or at the antipode.
+		const thetaMax = min( sigma.mul( 3.0 ), Math.PI );
+		const truncation = exp( thetaMax.mul( thetaMax ).mul( - 0.5 ).div( sigma.mul( sigma ) ) ).oneMinus().toVar();
 
-	} );
+		const accumWeight = float( 0.0 ).toVar();
 
-	axis.assign( normalize( axis ) );
+		Loop( { start: int( 0 ), end: SAMPLES }, ( { i } ) => {
 
-	const gl_FragColor = vec3().toVar();
-	gl_FragColor.addAssign( weights.element( 0 ).mul( getSample( { theta: 0.0, axis, outputDirection, mipInt, envMap, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) ) );
+			// Stratified inverse-CDF sampling of the Gaussian, placed on a golden-angle spiral.
+			const stratum = float( i ).add( 0.5 ).div( float( SAMPLES ) );
+			const theta = sigma.mul( sqrt( log( stratum.mul( truncation ).oneMinus() ).mul( - 2.0 ) ) ).toVar();
+			const phi = float( i ).mul( GOLDEN_ANGLE ).toVar();
 
-	Loop( { start: int( 1 ), end: n }, ( { i } ) => {
+			const offset = tangent.mul( cos( phi ) ).add( bitangent.mul( sin( phi ) ) );
+			const sampleDirection = outputDirection.mul( cos( theta ) ).add( offset.mul( sin( theta ) ) );
 
-		If( i.greaterThanEqual( samples ), () => {
+			// Correct the planar sample density to solid angle.
+			const weight = sin( theta ).div( theta ).toVar();
 
-			Break();
+			color.addAssign( bilinearCubeUV( envMap, sampleDirection, mipInt, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP ).mul( weight ) );
+			accumWeight.addAssign( weight );
 
 		} );
 
-		const theta = float( dTheta.mul( float( i ) ) ).toVar();
-		gl_FragColor.addAssign( weights.element( i ).mul( getSample( { theta: theta.mul( - 1.0 ), axis, outputDirection, mipInt, envMap, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) ) );
-		gl_FragColor.addAssign( weights.element( i ).mul( getSample( { theta, axis, outputDirection, mipInt, envMap, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) ) );
+		color.divAssign( accumWeight );
 
 	} );
 
-	return vec4( gl_FragColor, 1 );
+	return vec4( color, 1.0 );
 
 } );
 

+ 2 - 0
src/renderers/WebGLRenderer.js

@@ -2000,6 +2000,8 @@ class WebGLRenderer {
 					stencilBuffer: stencil,
 					resolveDepthBuffer: false,
 					resolveStencilBuffer: false,
+					storeMultisampledDepthBuffer: false,
+					storeMultisampledStencilBuffer: false,
 					colorSpace: ColorManagement.workingColorSpace,
 				} );
 

+ 3 - 0
src/renderers/common/Bindings.js

@@ -273,6 +273,9 @@ class Bindings extends DataMap {
 
 						}
 
+						const textureData = this.textures.get( binding.texture );
+						if ( textureData.bindGroups !== undefined ) textureData.bindGroups.delete( bindGroup );
+
 						binding.release();
 
 					}

+ 48 - 9
src/renderers/common/RenderList.js

@@ -192,6 +192,14 @@ class RenderList {
 		 */
 		this.occlusionQueryCount = 0;
 
+		/**
+		 * The ID of the frame the render list was last used in.
+		 *
+		 * @type {number}
+		 * @default -1
+		 */
+		this.frameId = - 1;
+
 		/**
 		 * The last object that was counted for occlusion query testing. Used to
 		 * avoid counting an object more than once when it produces multiple render
@@ -415,15 +423,7 @@ class RenderList {
 
 			if ( renderItem.id === null ) break;
 
-			renderItem.id = null;
-			renderItem.object = null;
-			renderItem.geometry = null;
-			renderItem.material = null;
-			renderItem.groupOrder = null;
-			renderItem.renderOrder = null;
-			renderItem.z = null;
-			renderItem.group = null;
-			renderItem.clippingContext = null;
+			resetRenderItem( renderItem );
 
 		}
 
@@ -431,6 +431,45 @@ class RenderList {
 
 	}
 
+	/**
+	 * This method is called when the render list has become inactive.
+	 */
+	clear() {
+
+		// Clear all references from the render items so scene objects
+		// are not retained when the render list is not used anymore.
+
+		for ( let i = 0, il = this.renderItems.length; i < il; i ++ ) {
+
+			const renderItem = this.renderItems[ i ];
+
+			if ( renderItem.id === null ) break;
+
+			resetRenderItem( renderItem );
+
+		}
+
+		this.opaque.length = 0;
+		this.transparentDoublePass.length = 0;
+		this.transparent.length = 0;
+		this.bundles.length = 0;
+
+	}
+
+}
+
+function resetRenderItem( renderItem ) {
+
+	renderItem.id = null;
+	renderItem.object = null;
+	renderItem.geometry = null;
+	renderItem.material = null;
+	renderItem.groupOrder = null;
+	renderItem.renderOrder = null;
+	renderItem.z = null;
+	renderItem.group = null;
+	renderItem.clippingContext = null;
+
 }
 
 export default RenderList;

+ 53 - 0
src/renderers/common/RenderLists.js

@@ -32,6 +32,23 @@ class RenderLists {
 		 */
 		this.lists = new ChainMap();
 
+		/**
+		 * The render lists which are currently in use. Lists are removed
+		 * as soon as they become stale.
+		 *
+		 * @private
+		 * @type {Set<RenderList>}
+		 */
+		this._activeLists = new Set();
+
+		/**
+		 * The current frame ID.
+		 *
+		 * @private
+		 * @type {number}
+		 */
+		this._frameId = - 1;
+
 	}
 
 	/**
@@ -60,10 +77,43 @@ class RenderLists {
 		_chainKeys[ 0 ] = null;
 		_chainKeys[ 1 ] = null;
 
+		//
+
+		list.frameId = this._frameId;
+		this._activeLists.add( list );
+
 		return list;
 
 	}
 
+	/**
+	 * Must be called when a new frame begins.
+	 *
+	 * @param {number} frameId - The current frame ID.
+	 */
+	update( frameId ) {
+
+		if ( frameId === this._frameId ) return;
+
+		this._frameId = frameId;
+
+		for ( const list of this._activeLists ) {
+
+			// if a render list has not been used within 10 frames, consider
+			// it as inactive and clear it
+
+			if ( frameId - list.frameId > 10 ) {
+
+				list.clear();
+
+				this._activeLists.delete( list );
+
+			}
+
+		}
+
+	}
+
 	/**
 	 * Frees all internal resources.
 	 */
@@ -71,6 +121,9 @@ class RenderLists {
 
 		this.lists = new ChainMap();
 
+		this._activeLists.clear();
+		this._frameId = - 1;
+
 	}
 
 }

+ 23 - 1
src/renderers/common/Renderer.js

@@ -30,7 +30,7 @@ import { Matrix4 } from '../../math/Matrix4.js';
 import { Vector2 } from '../../math/Vector2.js';
 import { Vector4 } from '../../math/Vector4.js';
 import { RenderTarget } from '../../core/RenderTarget.js';
-import { DoubleSide, BackSide, FrontSide, SRGBColorSpace, NoToneMapping, LinearFilter, HalfFloatType, RGBAFormat, PCFShadowMap, VSMShadowMap } from '../../constants.js';
+import { DoubleSide, BackSide, FrontSide, SRGBColorSpace, NoToneMapping, LinearFilter, HalfFloatType, RGBAFormat, PCFShadowMap, PCFSoftShadowMap, VSMShadowMap } from '../../constants.js';
 
 import { float, vec3, vec4, Fn } from '../../nodes/tsl/TSLCore.js';
 import { reference } from '../../nodes/accessors/ReferenceNode.js';
@@ -902,6 +902,14 @@ class Renderer {
 
 		if ( this._initialized === false ) await this.init();
 
+		if ( this.shadowMap.type === PCFSoftShadowMap ) {
+
+			warn( 'WebGPURenderer: PCFSoftShadowMap has been removed. Using PCFShadowMap instead.' );
+
+			this.shadowMap.type = PCFShadowMap;
+
+		}
+
 		// preserve render tree
 
 		const nodeFrame = this._nodes.nodeFrame;
@@ -1519,6 +1527,10 @@ class Renderer {
 		frameBufferTarget.multiview = outputRenderTarget !== null ? outputRenderTarget.multiview : false;
 		frameBufferTarget.useArrayDepthTexture = outputRenderTarget !== null ? outputRenderTarget.useArrayDepthTexture : false;
 		frameBufferTarget.resolveDepthBuffer = outputRenderTarget !== null ? outputRenderTarget.resolveDepthBuffer : true;
+		frameBufferTarget.resolveStencilBuffer = outputRenderTarget !== null ? outputRenderTarget.resolveStencilBuffer : true;
+		frameBufferTarget.storeMultisampledColorBuffer = outputRenderTarget !== null ? outputRenderTarget.storeMultisampledColorBuffer : true;
+		frameBufferTarget.storeMultisampledDepthBuffer = outputRenderTarget !== null ? outputRenderTarget.storeMultisampledDepthBuffer : true;
+		frameBufferTarget.storeMultisampledStencilBuffer = outputRenderTarget !== null ? outputRenderTarget.storeMultisampledStencilBuffer : true;
 		frameBufferTarget._autoAllocateDepthBuffer = outputRenderTarget !== null ? outputRenderTarget._autoAllocateDepthBuffer : false;
 
 		return frameBufferTarget;
@@ -1538,6 +1550,14 @@ class Renderer {
 
 		if ( this._isDeviceLost === true ) return;
 
+		if ( this.shadowMap.type === PCFSoftShadowMap ) {
+
+			warn( 'WebGPURenderer: PCFSoftShadowMap has been removed. Using PCFShadowMap instead.' );
+
+			this.shadowMap.type = PCFShadowMap;
+
+		}
+
 		//
 
 		const frameBufferTarget = useFrameBufferTarget ? this._getFrameBufferTarget() : null;
@@ -1686,6 +1706,8 @@ class Renderer {
 
 		}
 
+		this._renderLists.update( nodeFrame.frameId );
+
 		const renderList = this._renderLists.get( scene, camera );
 		renderList.begin();
 

+ 14 - 3
src/renderers/common/XRManager.js

@@ -953,7 +953,9 @@ class XRManager extends EventDispatcher {
 				),
 				stencilBuffer: attributes.stencil,
 				resolveDepthBuffer: false,
-				resolveStencilBuffer: false
+				resolveStencilBuffer: false,
+				storeMultisampledDepthBuffer: false,
+				storeMultisampledStencilBuffer: false
 			} );
 
 		renderTarget._autoAllocateDepthBuffer = true;
@@ -1044,7 +1046,9 @@ class XRManager extends EventDispatcher {
 				),
 				stencilBuffer: attributes.stencil,
 				resolveDepthBuffer: false,
-				resolveStencilBuffer: false
+				resolveStencilBuffer: false,
+				storeMultisampledDepthBuffer: false,
+				storeMultisampledStencilBuffer: false
 			} );
 
 		renderTarget._autoAllocateDepthBuffer = true;
@@ -1298,6 +1302,9 @@ class XRManager extends EventDispatcher {
 						samples: attributes.antialias ? 4 : 0,
 						resolveDepthBuffer: ( glProjLayer.ignoreDepthValues === false ),
 						resolveStencilBuffer: ( glProjLayer.ignoreDepthValues === false ),
+						storeMultisampledColorBuffer: false,
+						storeMultisampledDepthBuffer: ( glProjLayer.ignoreDepthValues === false ),
+						storeMultisampledStencilBuffer: ( glProjLayer.ignoreDepthValues === false ),
 						depth: this._useMultiview ? 2 : 1,
 						multiview: this._useMultiview
 					} );
@@ -1363,6 +1370,8 @@ class XRManager extends EventDispatcher {
 						stencilBuffer: renderer.stencil,
 						resolveDepthBuffer: ( glBaseLayer.ignoreDepthValues === false ),
 						resolveStencilBuffer: ( glBaseLayer.ignoreDepthValues === false ),
+						storeMultisampledDepthBuffer: ( glBaseLayer.ignoreDepthValues === false ),
+						storeMultisampledStencilBuffer: ( glBaseLayer.ignoreDepthValues === false ),
 					}
 				);
 
@@ -1716,7 +1725,9 @@ function onSessionEnd() {
 					),
 					stencilBuffer: layer.stencilBuffer,
 					resolveDepthBuffer: false,
-					resolveStencilBuffer: false
+					resolveStencilBuffer: false,
+					storeMultisampledDepthBuffer: false,
+					storeMultisampledStencilBuffer: false
 				} );
 
 			layer.renderTarget.isXRRenderTarget = false;

+ 76 - 144
src/renderers/common/extras/PMREMGenerator.js

@@ -1,12 +1,10 @@
 import NodeMaterial from '../../../materials/nodes/NodeMaterial.js';
-import { getDirection, blur, ggxConvolution } from '../../../nodes/pmrem/PMREMUtils.js';
+import { sphericalGaussianBlur, ggxConvolution } from '../../../nodes/pmrem/PMREMUtils.js';
 import { equirectUV } from '../../../nodes/utils/EquirectUV.js';
 import { uniform } from '../../../nodes/core/UniformNode.js';
-import { uniformArray } from '../../../nodes/accessors/UniformArrayNode.js';
 import { texture } from '../../../nodes/accessors/TextureNode.js';
 import { cubeTexture } from '../../../nodes/accessors/CubeTextureNode.js';
-import { float, uint, vec3 } from '../../../nodes/tsl/TSLBase.js';
-import { uv } from '../../../nodes/accessors/UV.js';
+import { float, int, uint } from '../../../nodes/tsl/TSLBase.js';
 import { attribute } from '../../../nodes/core/AttributeNode.js';
 
 import { OrthographicCamera } from '../../../cameras/OrthographicCamera.js';
@@ -30,21 +28,20 @@ import {
 	BackSide,
 	LinearSRGBColorSpace
 } from '../../../constants.js';
-import { warn, error, warnOnce } from '../../../utils.js';
+import { warnOnce } from '../../../utils.js';
 
 const LOD_MIN = 4;
 
-// The standard deviations (radians) associated with the extra mips.
+// The number of extra mips.
 // Used for scene blur in fromScene() method.
-const EXTRA_LOD_SIGMA = [ 0.125, 0.215, 0.35, 0.446, 0.526, 0.582 ];
+const EXTRA_LODS = 6;
 
-// The maximum length of the blur for loop. Smaller sigmas will use fewer
-// samples and exit early, but not recompile the shader.
+// The number of spiral samples per blur pass.
 // Used for scene blur in fromScene() method.
-const MAX_SAMPLES = 20;
+const BLUR_SAMPLES = 20;
 
 // GGX VNDF importance sampling configuration
-const GGX_SAMPLES = 512;
+const GGX_SAMPLES = 256;
 
 const _flatCamera = /*@__PURE__*/ new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
 const _cubeCamera = /*@__PURE__*/ new PerspectiveCamera( 90, 1 );
@@ -54,6 +51,7 @@ let _oldActiveCubeFace = 0;
 let _oldActiveMipmapLevel = 0;
 
 const _origin = /*@__PURE__*/ new Vector3();
+const _direction = /*@__PURE__*/ new Vector3();
 
 // maps blur materials to their uniforms dictionary
 
@@ -65,8 +63,7 @@ const _faceLib = [
 	0, 4, 2
 ];
 
-const _direction = /*@__PURE__*/ getDirection( uv(), attribute( 'faceIndex' ) ).normalize();
-const _outputDirection = /*@__PURE__*/ vec3( _direction.x, _direction.y, _direction.z );
+const _outputDirection = /*@__PURE__*/ attribute( 'outputDirection' ).normalize();
 
 /**
  * This class generates a Prefiltered, Mipmapped Radiance Environment Map
@@ -99,7 +96,6 @@ class PMREMGenerator {
 		this._lodMax = 0;
 		this._cubeSize = 0;
 		this._sizeLods = [];
-		this._sigmas = [];
 		this._lodMeshes = [];
 
 		this._blurMaterial = null;
@@ -129,7 +125,7 @@ class PMREMGenerator {
 	 * @param {number} [far=100] - The far plane distance.
 	 * @param {Object} [options={}] - The configuration options.
 	 * @param {number} [options.size=256] - The texture size of the PMREM.
-	 * @param {Vector3} [options.renderTarget=origin] - The position of the internal cube camera that renders the scene.
+	 * @param {Vector3} [options.position=origin] - The position of the internal cube camera that renders the scene.
 	 * @param {?RenderTarget} [options.renderTarget=null] - The render target to use.
 	 * @return {RenderTarget} The resulting PMREM.
 	 * @see {@link PMREMGenerator#fromScene}
@@ -433,7 +429,7 @@ class PMREMGenerator {
 			this._pingPongRenderTarget = _createRenderTarget( renderTarget.width, renderTarget.height );
 
 			const { _lodMax } = this;
-			( { lodMeshes: this._lodMeshes, sizeLods: this._sizeLods, sigmas: this._sigmas } = _createPlanes( _lodMax ) );
+			( { lodMeshes: this._lodMeshes, sizeLods: this._sizeLods } = _createPlanes( _lodMax ) );
 
 			this._blurMaterial = _getBlurShader( _lodMax, renderTarget.width, renderTarget.height );
 			this._ggxMaterial = _getGGXShader( _lodMax, renderTarget.width, renderTarget.height );
@@ -638,7 +634,7 @@ class PMREMGenerator {
 		const incrementalRoughness = Math.sqrt( targetRoughness * targetRoughness - sourceRoughness * sourceRoughness );
 
 		// Apply blur strength mapping for better quality across the roughness range
-		const blurStrength = 0.0 + targetRoughness * 1.25;
+		const blurStrength = targetRoughness * 1.25;
 		const adjustedRoughness = incrementalRoughness * blurStrength;
 
 		// Calculate viewport position based on output LOD level
@@ -670,11 +666,9 @@ class PMREMGenerator {
 	}
 
 	/**
-	 * This is a two-pass Gaussian blur for a cubemap. Normally this is done
-	 * vertically and horizontally, but this breaks down on a cube. Here we apply
-	 * the blur latitudinally (around the poles), and then longitudinally (towards
-	 * the poles) to approximate the orthogonally-separable blur. It is least
-	 * accurate at the poles, but still does a decent job.
+	 * This is a two-pass Gaussian blur for a cubemap. Each pass importance-samples
+	 * the Gaussian along a spiral kernel (Golden Angle), which distributes samples
+	 * isotropically on the sphere (no pole artifacts).
 	 *
 	 * Used for initial scene blur in fromScene() method when sigma > 0.
 	 *
@@ -683,110 +677,39 @@ class PMREMGenerator {
 	 * @param {number} lodIn - The input level-of-detail.
 	 * @param {number} lodOut - The output level-of-detail.
 	 * @param {number} sigma - The blur radius in radians.
-	 * @param {Vector3} [poleAxis] - The pole axis.
 	 */
-	_blur( cubeUVRenderTarget, lodIn, lodOut, sigma, poleAxis ) {
+	_blur( cubeUVRenderTarget, lodIn, lodOut, sigma ) {
 
 		const pingPongRenderTarget = this._pingPongRenderTarget;
 
-		this._halfBlur(
-			cubeUVRenderTarget,
-			pingPongRenderTarget,
-			lodIn,
-			lodOut,
-			sigma,
-			'latitudinal',
-			poleAxis );
-
-		this._halfBlur(
-			pingPongRenderTarget,
-			cubeUVRenderTarget,
-			lodOut,
-			lodOut,
-			sigma,
-			'longitudinal',
-			poleAxis );
+		// Two passes of sigma / sqrt( 2 ) compose to a blur of sigma while squaring
+		// the effective sample count. Sigmas beyond PI are visually indistinguishable
+		// from a uniform blur, so clamp to keep the shader math finite.
+		const blurSigma = Math.min( sigma, Math.PI ) / Math.SQRT2;
+
+		this._blurPass( cubeUVRenderTarget, pingPongRenderTarget, lodIn, lodOut, blurSigma );
+		this._blurPass( pingPongRenderTarget, cubeUVRenderTarget, lodOut, lodOut, blurSigma );
 
 	}
 
-	_halfBlur( targetIn, targetOut, lodIn, lodOut, sigmaRadians, direction, poleAxis ) {
+	_blurPass( targetIn, targetOut, lodIn, lodOut, sigmaRadians ) {
 
 		const renderer = this._renderer;
 		const blurMaterial = this._blurMaterial;
 
-		if ( direction !== 'latitudinal' && direction !== 'longitudinal' ) {
-
-			error( 'blur direction must be either latitudinal or longitudinal!' );
-
-		}
-
-		// Number of standard deviations at which to cut off the discrete approximation.
-		const STANDARD_DEVIATIONS = 3;
-
 		const blurMesh = this._lodMeshes[ lodOut ];
 		blurMesh.material = blurMaterial;
 
 		const blurUniforms = _uniformsMap.get( blurMaterial );
 
-		const pixels = this._sizeLods[ lodIn ] - 1;
-		const radiansPerPixel = isFinite( sigmaRadians ) ? Math.PI / ( 2 * pixels ) : 2 * Math.PI / ( 2 * MAX_SAMPLES - 1 );
-		const sigmaPixels = sigmaRadians / radiansPerPixel;
-		const samples = isFinite( sigmaRadians ) ? 1 + Math.floor( STANDARD_DEVIATIONS * sigmaPixels ) : MAX_SAMPLES;
-
-		if ( samples > MAX_SAMPLES ) {
-
-			warn( `sigmaRadians, ${
-				sigmaRadians}, is too large and will clip, as it requested ${
-				samples} samples when the maximum is set to ${MAX_SAMPLES}` );
-
-		}
-
-		const weights = [];
-		let sum = 0;
-
-		for ( let i = 0; i < MAX_SAMPLES; ++ i ) {
-
-			const x = i / sigmaPixels;
-			const weight = Math.exp( - x * x / 2 );
-			weights.push( weight );
-
-			if ( i === 0 ) {
-
-				sum += weight;
-
-			} else if ( i < samples ) {
-
-				sum += 2 * weight;
-
-			}
-
-		}
-
-		for ( let i = 0; i < weights.length; i ++ ) {
-
-			weights[ i ] = weights[ i ] / sum;
-
-		}
-
 		targetIn.texture.frame = ( targetIn.texture.frame || 0 ) + 1;
 
 		blurUniforms.envMap.value = targetIn.texture;
-		blurUniforms.samples.value = samples;
-		blurUniforms.weights.array = weights;
-		blurUniforms.latitudinal.value = direction === 'latitudinal' ? 1 : 0;
-
-		if ( poleAxis ) {
-
-			blurUniforms.poleAxis.value = poleAxis;
-
-		}
-
-		const { _lodMax } = this;
-		blurUniforms.dTheta.value = radiansPerPixel;
-		blurUniforms.mipInt.value = _lodMax - lodIn;
+		blurUniforms.sigma.value = sigmaRadians;
+		blurUniforms.mipInt.value = this._lodMax - lodIn;
 
 		const outputSize = this._sizeLods[ lodOut ];
-		const x = 3 * outputSize * ( lodOut > _lodMax - LOD_MIN ? lodOut - _lodMax + LOD_MIN : 0 );
+		const x = 3 * outputSize * ( lodOut > this._lodMax - LOD_MIN ? lodOut - this._lodMax + LOD_MIN : 0 );
 		const y = 4 * ( this._cubeSize - outputSize );
 
 		this._setViewport( targetOut, x, y, 3 * outputSize, 2 * outputSize );
@@ -817,31 +740,18 @@ class PMREMGenerator {
 function _createPlanes( lodMax ) {
 
 	const sizeLods = [];
-	const sigmas = [];
 	const lodMeshes = [];
 
 	let lod = lodMax;
 
-	const totalLods = lodMax - LOD_MIN + 1 + EXTRA_LOD_SIGMA.length;
+	const totalLods = lodMax - LOD_MIN + 1 + EXTRA_LODS;
 
 	for ( let i = 0; i < totalLods; i ++ ) {
 
 		const sizeLod = Math.pow( 2, lod );
 		sizeLods.push( sizeLod );
-		let sigma = 1.0 / sizeLod;
-
-		if ( i > lodMax - LOD_MIN ) {
-
-			sigma = EXTRA_LOD_SIGMA[ i - lodMax + LOD_MIN - 1 ];
-
-		} else if ( i === 0 ) {
-
-			sigma = 0;
-
-		}
-
-		sigmas.push( sigma );
 
+		// UVs overshoot the face by one texel to bake the CubeUV border into the directions.
 		const texelSize = 1.0 / ( sizeLod - 2 );
 		const min = - texelSize;
 		const max = 1 + texelSize;
@@ -850,12 +760,9 @@ function _createPlanes( lodMax ) {
 		const cubeFaces = 6;
 		const vertices = 6;
 		const positionSize = 3;
-		const uvSize = 2;
-		const faceIndexSize = 1;
 
 		const position = new Float32Array( positionSize * vertices * cubeFaces );
-		const uv = new Float32Array( uvSize * vertices * cubeFaces );
-		const faceIndex = new Float32Array( faceIndexSize * vertices * cubeFaces );
+		const outputDirection = new Float32Array( positionSize * vertices * cubeFaces );
 
 		for ( let face = 0; face < cubeFaces; face ++ ) {
 
@@ -872,16 +779,48 @@ function _createPlanes( lodMax ) {
 
 			const faceIdx = _faceLib[ face ];
 			position.set( coordinates, positionSize * vertices * faceIdx );
-			uv.set( uv1, uvSize * vertices * faceIdx );
-			const fill = [ faceIdx, faceIdx, faceIdx, faceIdx, faceIdx, faceIdx ];
-			faceIndex.set( fill, faceIndexSize * vertices * faceIdx );
+
+			for ( let vertex = 0; vertex < vertices; vertex ++ ) {
+
+				const u = uv1[ vertex * 2 ] * 2 - 1;
+				const v = uv1[ vertex * 2 + 1 ] * 2 - 1;
+
+				// RH coordinate system; PMREM face-indexing convention
+				if ( faceIdx === 0 ) {
+
+					_direction.set( 1, v, u ); // pos x
+
+				} else if ( faceIdx === 1 ) {
+
+					_direction.set( - u, 1, - v ); // pos y
+
+				} else if ( faceIdx === 2 ) {
+
+					_direction.set( - u, v, 1 ); // pos z
+
+				} else if ( faceIdx === 3 ) {
+
+					_direction.set( - 1, v, - u ); // neg x
+
+				} else if ( faceIdx === 4 ) {
+
+					_direction.set( - u, - 1, v ); // neg y
+
+				} else {
+
+					_direction.set( u, v, - 1 ); // neg z
+
+				}
+
+				_direction.toArray( outputDirection, ( faceIdx * vertices + vertex ) * positionSize );
+
+			}
 
 		}
 
 		const planes = new BufferGeometry();
 		planes.setAttribute( 'position', new BufferAttribute( position, positionSize ) );
-		planes.setAttribute( 'uv', new BufferAttribute( uv, uvSize ) );
-		planes.setAttribute( 'faceIndex', new BufferAttribute( faceIndex, faceIndexSize ) );
+		planes.setAttribute( 'outputDirection', new BufferAttribute( outputDirection, positionSize ) );
 		lodMeshes.push( new Mesh( planes, null ) );
 
 		if ( lod > LOD_MIN ) {
@@ -892,7 +831,7 @@ function _createPlanes( lodMax ) {
 
 	}
 
-	return { lodMeshes, sizeLods, sigmas };
+	return { lodMeshes, sizeLods };
 
 }
 
@@ -931,27 +870,16 @@ function _getMaterial( type ) {
 
 function _getBlurShader( lodMax, width, height ) {
 
-	const weights = uniformArray( new Array( MAX_SAMPLES ).fill( 0 ) );
-	const poleAxis = uniform( new Vector3( 0, 1, 0 ) );
-	const dTheta = uniform( 0 );
-	const n = float( MAX_SAMPLES );
-	const latitudinal = uniform( 0 ); // false, bool
-	const samples = uniform( 1 ); // int
 	const envMap = texture();
+	const sigma = uniform( 0 );
 	const mipInt = uniform( 0 ); // int
 	const CUBEUV_TEXEL_WIDTH = float( 1 / width );
 	const CUBEUV_TEXEL_HEIGHT = float( 1 / height );
 	const CUBEUV_MAX_MIP = float( lodMax );
 
 	const materialUniforms = {
-		n,
-		latitudinal,
-		weights,
-		poleAxis,
-		outputDirection: _outputDirection,
-		dTheta,
-		samples,
 		envMap,
+		sigma,
 		mipInt,
 		CUBEUV_TEXEL_WIDTH,
 		CUBEUV_TEXEL_HEIGHT,
@@ -959,7 +887,11 @@ function _getBlurShader( lodMax, width, height ) {
 	};
 
 	const material = _getMaterial( 'blur' );
-	material.fragmentNode = blur( { ...materialUniforms, latitudinal: latitudinal.equal( 1 ) } );
+	material.fragmentNode = sphericalGaussianBlur( {
+		...materialUniforms,
+		outputDirection: _outputDirection,
+		SAMPLES: int( BLUR_SAMPLES )
+	} );
 
 	_uniformsMap.set( material, materialUniforms );
 

+ 2 - 1
src/renderers/shaders/DFGLUTData.js

@@ -1,5 +1,6 @@
 /**
- * Precomputed DFG LUT for Image-Based Lighting
+ * Precomputed DFG LUT for physically based specular lighting, used by both
+ * image-based lighting and direct-light multi-scattering energy compensation
  * Resolution: 16x16
  * Samples: 4096 per texel
  * Format: RG16F (2 half floats per texel: scale, bias)

+ 5 - 4
src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js

@@ -41,13 +41,14 @@ vec3 geometryClearcoatNormal = vec3( 0.0 );
 
 	if ( material.iridescence > 0.0 ) {
 
-		material.iridescenceFresnelDielectric = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor );
-		material.iridescenceFresnelMetallic = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.diffuseColor );
+		vec3 iridescenceFresnelDielectric = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor );
+		vec3 iridescenceFresnelMetallic = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.diffuseColor );
 
-		material.iridescenceFresnel = mix( material.iridescenceFresnelDielectric, material.iridescenceFresnelMetallic, material.metalness );
+		material.iridescenceFresnel = mix( iridescenceFresnelDielectric, iridescenceFresnelMetallic, material.metalness );
 
 		// Iridescence F0 approximation
-		material.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi );
+		material.iridescenceF0Dielectric = Schlick_to_F0( iridescenceFresnelDielectric, 1.0, dotNVi );
+		material.iridescenceF0Metallic = Schlick_to_F0( iridescenceFresnelMetallic, 1.0, dotNVi );
 
 	}
 

+ 6 - 0
src/renderers/shaders/ShaderChunk/lights_physical_fragment.glsl.js

@@ -85,6 +85,12 @@ material.roughness = min( material.roughness, 1.0 );
 
 #endif
 
+#ifdef USE_RETROREFLECTIVE
+
+	material.retroreflective = retroreflective;
+
+#endif
+
 #ifdef USE_IRIDESCENCE
 
 	material.iridescence = iridescence;

+ 58 - 50
src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js

@@ -14,6 +14,10 @@ struct PhysicalMaterial {
 	float specularF90;
 	float dispersion;
 
+	#ifdef USE_RETROREFLECTIVE
+		float retroreflective;
+	#endif
+
 	#ifdef USE_CLEARCOAT
 		float clearcoat;
 		float clearcoatRoughness;
@@ -26,9 +30,8 @@ struct PhysicalMaterial {
 		float iridescenceIOR;
 		float iridescenceThickness;
 		vec3 iridescenceFresnel;
-		vec3 iridescenceF0;
-		vec3 iridescenceFresnelDielectric;
-		vec3 iridescenceFresnelMetallic;
+		vec3 iridescenceF0Dielectric;
+		vec3 iridescenceF0Metallic;
 	#endif
 
 	#ifdef USE_SHEEN
@@ -418,48 +421,6 @@ void computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const
 
 }
 
-// GGX BRDF with multi-scattering energy compensation for direct lighting
-// Based on "Practical Multiple Scattering Compensation for Microfacet Models"
-// https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf
-vec3 BRDF_GGX_Multiscatter( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) {
-
-	// Single-scattering BRDF (standard GGX)
-	vec3 singleScatter = BRDF_GGX( lightDir, viewDir, normal, material );
-
-	// Multi-scattering compensation
-	float dotNL = saturate( dot( normal, lightDir ) );
-	float dotNV = saturate( dot( normal, viewDir ) );
-
-	// Precomputed DFG values for view and light directions
-	vec2 dfgV = texture2D( dfgLUT, vec2( material.roughness, dotNV ) ).rg;
-	vec2 dfgL = texture2D( dfgLUT, vec2( material.roughness, dotNL ) ).rg;
-
-	// Single-scattering energy for view and light
-	vec3 FssEss_V = material.specularColorBlended * dfgV.x + material.specularF90 * dfgV.y;
-	vec3 FssEss_L = material.specularColorBlended * dfgL.x + material.specularF90 * dfgL.y;
-
-	float Ess_V = dfgV.x + dfgV.y;
-	float Ess_L = dfgL.x + dfgL.y;
-
-	// Energy lost to multiple scattering
-	float Ems_V = 1.0 - Ess_V;
-	float Ems_L = 1.0 - Ess_L;
-
-	// Average Fresnel reflectance
-	vec3 Favg = material.specularColorBlended + ( 1.0 - material.specularColorBlended ) * 0.047619; // 1/21
-
-	// Multiple scattering contribution
-	vec3 Fms = FssEss_V * FssEss_L * Favg / ( 1.0 - Ems_V * Ems_L * Favg + EPSILON );
-
-	// Energy compensation factor
-	float compensationFactor = Ems_V * Ems_L;
-
-	vec3 multiScatter = Fms * compensationFactor;
-
-	return singleScatter + multiScatter;
-
-}
-
 #if NUM_RECT_AREA_LIGHTS > 0
 
 	void RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {
@@ -553,19 +514,66 @@ void RE_Direct_Physical( const in IncidentLight directLight, const in vec3 geome
  
  	#endif
 
-	reflectedLight.directSpecular += irradiance * BRDF_GGX_Multiscatter( directLight.direction, geometryViewDir, geometryNormal, material );
+	vec3 specularBRDF = BRDF_GGX( directLight.direction, geometryViewDir, geometryNormal, material );
+
+	#ifdef USE_RETROREFLECTIVE
+
+		// Minimal Retroreflective Microfacet Model:
+		// https://jcgt.org/published/0015/01/04/
+		vec3 retroViewDir = reflect( - geometryViewDir, geometryNormal );
+		vec3 retroSpecularBRDF = BRDF_GGX( directLight.direction, retroViewDir, geometryNormal, material );
 
-	reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseContribution );
+		specularBRDF = mix( specularBRDF, retroSpecularBRDF, saturate( material.retroreflective ) );
+
+	#endif
+
+	// Multi-scattering energy compensation for direct lighting
+	// Based on "Practical Multiple Scattering Compensation for Microfacet Models"
+	// https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf
+	float dotNV = saturate( dot( geometryNormal, geometryViewDir ) );
+
+	vec2 fab = texture2D( dfgLUT, vec2( material.roughness, dotNV ) ).rg;
+
+	// Energy of the single-scattering lobe in a white furnace ( F0 = F90 = 1 )
+	float Ess = fab.x + fab.y;
+
+	// Compensate for the energy lost to multiple scattering, tinting the added term by F0 ( equation 16 )
+	vec3 energyCompensation = 1.0 + material.specularColorBlended * ( 1.0 / Ess - 1.0 );
+
+	reflectedLight.directSpecular += irradiance * specularBRDF * energyCompensation;
+
+	// Light reflected by the specular interface is not available to the diffuse layer ( glTF fresnel_mix )
+	vec3 halfDir = normalize( directLight.direction + geometryViewDir );
+	float dotVH = saturate( dot( geometryViewDir, halfDir ) );
+	vec3 F = F_Schlick( material.specularColor, material.specularF90, dotVH );
+
+	reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseContribution ) * ( 1.0 - F );
 }
 
 void RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {
 
-	vec3 diffuse = irradiance * BRDF_Lambert( material.diffuseContribution );
+	// Energy reflected by the specular lobe is not available to the diffuse layer
+	vec3 singleScattering = vec3( 0.0 );
+	vec3 multiScattering = vec3( 0.0 );
+
+	#ifdef USE_IRIDESCENCE
+
+		computeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceF0Dielectric, material.roughness, singleScattering, multiScattering );
+
+	#else
+
+		computeMultiscattering( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering );
+
+	#endif
+
+	vec3 diffuse = irradiance * BRDF_Lambert( material.diffuseContribution ) * ( 1.0 - singleScattering - multiScattering );
 
 	#ifdef USE_SHEEN
 
 		float sheenAlbedo = IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness );
 
+		sheenSpecularIndirect += irradiance * material.sheenColor * sheenAlbedo * RECIPROCAL_PI;
+
 		float sheenEnergyComp = 1.0 - max3( material.sheenColor ) * sheenAlbedo;
 
 		diffuse *= sheenEnergyComp;
@@ -601,8 +609,8 @@ void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradia
 
 	#ifdef USE_IRIDESCENCE
 
-		computeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnelDielectric, material.roughness, singleScatteringDielectric, multiScatteringDielectric );
-		computeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.diffuseColor, material.specularF90, material.iridescence, material.iridescenceFresnelMetallic, material.roughness, singleScatteringMetallic, multiScatteringMetallic );
+		computeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceF0Dielectric, material.roughness, singleScatteringDielectric, multiScatteringDielectric );
+		computeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.diffuseColor, material.specularF90, material.iridescence, material.iridescenceF0Metallic, material.roughness, singleScatteringMetallic, multiScatteringMetallic );
 
 	#else
 

+ 1 - 0
src/renderers/shaders/ShaderLib.js

@@ -316,6 +316,7 @@ ShaderLib.physical = {
 			clearcoatRoughnessMap: { value: null },
 			clearcoatRoughnessMapTransform: { value: /*@__PURE__*/ new Matrix3() },
 			dispersion: { value: 0 },
+			retroreflective: { value: 0 },
 			iridescence: { value: 0 },
 			iridescenceMap: { value: null },
 			iridescenceMapTransform: { value: /*@__PURE__*/ new Matrix3() },

+ 4 - 0
src/renderers/shaders/ShaderLib/meshphysical.glsl.js

@@ -99,6 +99,10 @@ uniform float opacity;
 	uniform float dispersion;
 #endif
 
+#ifdef USE_RETROREFLECTIVE
+	uniform float retroreflective;
+#endif
+
 #ifdef USE_IRIDESCENCE
 	uniform float iridescence;
 	uniform float iridescenceIOR;

+ 42 - 17
src/renderers/webgl-fallback/WebGLBackend.js

@@ -15,6 +15,8 @@ import { isTypedArray, warnOnce, warn, error } from '../../utils.js';
 import { WebGLCoordinateSystem, TimestampQuery, Compatibility } from '../../constants.js';
 import WebGLTimestampQueryPool from './utils/WebGLTimestampQueryPool.js';
 
+const _invalidationArray = [];
+
 /**
  * A backend implementation targeting WebGL 2.
  *
@@ -519,6 +521,12 @@ class WebGLBackend extends Backend {
 			renderContextData.occlusionQueryObjects = new Array( occlusionQueryCount );
 			renderContextData.occlusionQueryIndex = 0;
 
+		} else if ( renderContextData.lastOcclusionObject !== undefined ) {
+
+			// invalidate if there is a stale query
+
+			renderContextData.lastOcclusionObject = undefined;
+
 		}
 
 	}
@@ -541,7 +549,9 @@ class WebGLBackend extends Backend {
 
 		if ( occlusionQueryCount > 0 ) {
 
-			if ( occlusionQueryCount > renderContextData.occlusionQueryIndex ) {
+			const lastOcclusionObject = renderContextData.lastOcclusionObject;
+
+			if ( lastOcclusionObject && lastOcclusionObject.occlusionTest === true ) {
 
 				gl.endQuery( gl.ANY_SAMPLES_PASSED );
 
@@ -630,14 +640,14 @@ class WebGLBackend extends Backend {
 
 			const check = () => {
 
-				let completed = 0;
+				let completed = true;
 
 				// check all queries and requeue as appropriate
 				for ( let i = 0; i < currentOcclusionQueries.length; i ++ ) {
 
 					const query = currentOcclusionQueries[ i ];
 
-					if ( query === null ) continue;
+					if ( ! query ) continue;
 
 					if ( gl.getQueryParameter( query, gl.QUERY_RESULT_AVAILABLE ) ) {
 
@@ -646,13 +656,15 @@ class WebGLBackend extends Backend {
 						currentOcclusionQueries[ i ] = null;
 						gl.deleteQuery( query );
 
-						completed ++;
+					} else {
+
+						completed = false;
 
 					}
 
 				}
 
-				if ( completed < currentOcclusionQueries.length ) {
+				if ( completed === false ) {
 
 					requestAnimationFrame( check );
 
@@ -2423,8 +2435,6 @@ class WebGLBackend extends Backend {
 
 				if ( msaaFb === undefined ) {
 
-					const invalidationArray = [];
-
 					msaaFb = gl.createFramebuffer();
 
 					state.bindFramebuffer( gl.FRAMEBUFFER, msaaFb );
@@ -2439,8 +2449,6 @@ class WebGLBackend extends Backend {
 
 						gl.bindRenderbuffer( gl.RENDERBUFFER, msaaRenderbuffers[ i ] );
 
-						invalidationArray.push( gl.COLOR_ATTACHMENT0 + i );
-
 						const texture = descriptor.textures[ i ];
 						const textureData = this.get( texture );
 
@@ -2462,13 +2470,8 @@ class WebGLBackend extends Backend {
 
 						renderTargetContextData.depthRenderbuffer = depthRenderbuffer;
 
-						const depthStyle = stencilBuffer ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT;
-						invalidationArray.push( depthStyle );
-
 					}
 
-					renderTargetContextData.invalidationArray = invalidationArray;
-
 				}
 
 				currentFrameBuffer = renderTargetContextData.msaaFrameBuffer;
@@ -2718,7 +2721,7 @@ class WebGLBackend extends Backend {
 
 				const fb = renderTargetContextData.framebuffers[ renderContext.getCacheKey() ];
 
-				let mask = gl.COLOR_BUFFER_BIT;
+				let mask = renderTarget.resolveColorBuffer === false ? 0 : gl.COLOR_BUFFER_BIT;
 
 				if ( renderTarget.resolveDepthBuffer ) {
 
@@ -2796,11 +2799,33 @@ class WebGLBackend extends Backend {
 
 				if ( this._supportsInvalidateFramebuffer === true ) {
 
-					gl.invalidateFramebuffer( gl.READ_FRAMEBUFFER, renderTargetContextData.invalidationArray );
+					if ( renderTarget.storeMultisampledColorBuffer === false ) {
+
+						for ( let i = 0; i < textures.length; i ++ ) {
+
+							_invalidationArray.push( gl.COLOR_ATTACHMENT0 + i );
+
+						}
+
+					}
+
+					if ( renderTarget.depthBuffer && renderTarget.storeMultisampledDepthBuffer === false ) {
+
+						_invalidationArray.push( renderTarget.stencilBuffer ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT );
+
+					}
+
+					if ( _invalidationArray.length > 0 ) {
+
+						gl.invalidateFramebuffer( gl.READ_FRAMEBUFFER, _invalidationArray );
+
+						_invalidationArray.length = 0;
+
+					}
 
 				}
 
-			} else if ( renderTarget.resolveDepthBuffer === false && renderTargetContextData.framebuffers ) {
+			} else if ( renderTarget.storeMultisampledDepthBuffer === false && renderTargetContextData.framebuffers ) {
 
 				const fb = renderTargetContextData.framebuffers[ renderContext.getCacheKey() ];
 				state.bindFramebuffer( gl.DRAW_FRAMEBUFFER, fb );

+ 6 - 0
src/renderers/webgl/WebGLMaterials.js

@@ -483,6 +483,12 @@ function WebGLMaterials( renderer, properties ) {
 
 		}
 
+		if ( material.retroreflective > 0 ) {
+
+			uniforms.retroreflective.value = material.retroreflective;
+
+		}
+
 		if ( material.iridescence > 0 ) {
 
 			uniforms.iridescence.value = material.iridescence;

+ 1 - 0
src/renderers/webgl/WebGLProgram.js

@@ -709,6 +709,7 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
 			parameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',
 
 			parameters.dispersion ? '#define USE_DISPERSION' : '',
+			parameters.retroreflective ? '#define USE_RETROREFLECTIVE' : '',
 
 			parameters.iridescence ? '#define USE_IRIDESCENCE' : '',
 			parameters.iridescenceMap ? '#define USE_IRIDESCENCEMAP' : '',

+ 4 - 0
src/renderers/webgl/WebGLPrograms.js

@@ -140,6 +140,7 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin
 		const HAS_ANISOTROPY = material.anisotropy > 0;
 		const HAS_CLEARCOAT = material.clearcoat > 0;
 		const HAS_DISPERSION = material.dispersion > 0;
+		const HAS_RETROREFLECTIVE = material.retroreflective > 0;
 		const HAS_IRIDESCENCE = material.iridescence > 0;
 		const HAS_SHEEN = material.sheen > 0;
 		const HAS_TRANSMISSION = material.transmission > 0;
@@ -240,6 +241,7 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin
 			clearcoatRoughnessMap: HAS_CLEARCOAT_ROUGHNESSMAP,
 
 			dispersion: HAS_DISPERSION,
+			retroreflective: HAS_RETROREFLECTIVE,
 
 			iridescence: HAS_IRIDESCENCE,
 			iridescenceMap: HAS_IRIDESCENCEMAP,
@@ -532,6 +534,8 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin
 			_programLayers.enable( 18 );
 		if ( parameters.dispersion )
 			_programLayers.enable( 19 );
+		if ( parameters.retroreflective )
+			_programLayers.enable( 24 );
 		if ( parameters.batchingColor )
 			_programLayers.enable( 20 );
 		if ( parameters.gradientMap )

+ 1 - 1
src/renderers/webgl/WebGLShadowMap.js

@@ -98,7 +98,7 @@ function WebGLShadowMap( renderer, objects, capabilities ) {
 
 		if ( this.type === PCFSoftShadowMap ) {
 
-			warn( 'WebGLShadowMap: PCFSoftShadowMap has been deprecated. Using PCFShadowMap instead.' );
+			warn( 'WebGLShadowMap: PCFSoftShadowMap has been removed. Using PCFShadowMap instead.' );
 			this.type = PCFShadowMap;
 
 		}

+ 4 - 4
src/renderers/webgl/WebGLTextures.js

@@ -1065,8 +1065,6 @@ function WebGLTextures( _gl, extensions, state, properties, capabilities, utils,
 
 											}
 
-											texture.clearLayerUpdates();
-
 										} else {
 
 											state.compressedTexSubImage3D( _gl.TEXTURE_2D_ARRAY, i, 0, 0, 0, mipmap.width, mipmap.height, image.depth, glFormat, mipmap.data );
@@ -1107,6 +1105,8 @@ function WebGLTextures( _gl, extensions, state, properties, capabilities, utils,
 
 					}
 
+					if ( texture.layerUpdates.size > 0 ) texture.clearLayerUpdates();
+
 				} else {
 
 					if ( useTexStorage && allocateMemory ) {
@@ -2348,7 +2348,7 @@ function WebGLTextures( _gl, extensions, state, properties, capabilities, utils,
 
 						invalidationArrayRead.push( _gl.COLOR_ATTACHMENT0 + i );
 
-						if ( renderTarget.depthBuffer && renderTarget.resolveDepthBuffer === false ) {
+						if ( renderTarget.depthBuffer && renderTarget.storeMultisampledDepthBuffer === false ) {
 
 							invalidationArrayRead.push( depthStyle );
 							invalidationArrayDraw.push( depthStyle );
@@ -2387,7 +2387,7 @@ function WebGLTextures( _gl, extensions, state, properties, capabilities, utils,
 
 			} else {
 
-				if ( renderTarget.depthBuffer && renderTarget.resolveDepthBuffer === false && supportsInvalidateFramebuffer ) {
+				if ( renderTarget.depthBuffer && renderTarget.storeMultisampledDepthBuffer === false && supportsInvalidateFramebuffer ) {
 
 					const depthStyle = renderTarget.stencilBuffer ? _gl.DEPTH_STENCIL_ATTACHMENT : _gl.DEPTH_ATTACHMENT;
 

+ 32 - 10
src/renderers/webgpu/WebGPUBackend.js

@@ -430,7 +430,7 @@ class WebGPUBackend extends Backend {
 	 * pass descriptor even when rendering directly to screen.
 	 *
 	 * @private
-	 * @return {Object} The render pass descriptor.
+	 * @return {GPURenderPassDescriptor} The render pass descriptor.
 	 */
 	_getDefaultRenderPassDescriptor() {
 
@@ -674,7 +674,7 @@ class WebGPUBackend extends Backend {
 					if ( textureData.msaaTexture !== undefined ) {
 
 						view = textureData.msaaTexture.createView();
-						resolveTarget = textureView;
+						resolveTarget = renderTarget.resolveColorBuffer === true ? textureView : undefined;
 
 					} else {
 
@@ -822,6 +822,15 @@ class WebGPUBackend extends Backend {
 
 			renderContextData.lastOcclusionObject = null;
 
+		} else if ( renderContextData.lastOcclusionObject !== undefined ) {
+
+			// invalidate if there is a stale query
+
+			renderContextData.lastOcclusionObject = undefined;
+
+			renderContextData.occlusionQuerySet.destroy();
+			renderContextData.occlusionQuerySet = undefined;
+
 		}
 
 		let descriptor;
@@ -841,6 +850,7 @@ class WebGPUBackend extends Backend {
 		descriptor.occlusionQuerySet = occlusionQuerySet;
 
 		const depthStencilAttachment = descriptor.depthStencilAttachment;
+		const renderTarget = renderContext.renderTarget;
 
 		if ( renderContext.textures !== null ) {
 
@@ -875,7 +885,15 @@ class WebGPUBackend extends Backend {
 
 				}
 
-				colorAttachment.storeOp = GPUStoreOp.Store;
+				if ( renderContext.sampleCount > 1 && renderTarget?.storeMultisampledColorBuffer === false ) {
+
+					colorAttachment.storeOp = GPUStoreOp.Discard;
+
+				} else {
+
+					colorAttachment.storeOp = GPUStoreOp.Store;
+
+				}
 
 			}
 
@@ -900,8 +918,6 @@ class WebGPUBackend extends Backend {
 
 		//
 
-		const renderTarget = renderContext.renderTarget;
-
 		if ( renderContext.depth ) {
 
 			if ( renderContext.clearDepth ) {
@@ -915,7 +931,7 @@ class WebGPUBackend extends Backend {
 
 			}
 
-			if ( renderContext.sampleCount > 1 && renderTarget?.resolveDepthBuffer === false ) {
+			if ( renderContext.sampleCount > 1 && renderTarget?.storeMultisampledDepthBuffer === false ) {
 
 				depthStencilAttachment.depthStoreOp = GPUStoreOp.Discard;
 
@@ -940,7 +956,7 @@ class WebGPUBackend extends Backend {
 
 			}
 
-			if ( renderContext.sampleCount > 1 && renderTarget?.resolveStencilBuffer === false ) {
+			if ( renderContext.sampleCount > 1 && renderTarget?.storeMultisampledStencilBuffer === false ) {
 
 				depthStencilAttachment.stencilStoreOp = GPUStoreOp.Discard;
 
@@ -1195,7 +1211,9 @@ class WebGPUBackend extends Backend {
 
 		}
 
-		if ( occlusionQueryCount > renderContextData.occlusionQueryIndex ) {
+		const lastOcclusionObject = renderContextData.lastOcclusionObject;
+
+		if ( lastOcclusionObject && lastOcclusionObject.occlusionTest === true ) {
 
 			renderContextData.currentPass.endOcclusionQuery();
 
@@ -1364,9 +1382,11 @@ class WebGPUBackend extends Backend {
 
 			for ( let i = 0; i < currentOcclusionQueryObjects.length; i ++ ) {
 
-				if ( results[ i ] === BigInt( 0 ) ) {
+				const object = currentOcclusionQueryObjects[ i ];
+
+				if ( object !== undefined && results[ i ] === BigInt( 0 ) ) {
 
-					occluded.add( currentOcclusionQueryObjects[ i ] );
+					occluded.add( object );
 
 				}
 
@@ -2405,6 +2425,8 @@ class WebGPUBackend extends Backend {
 
 		renderContextData.currentSets = renderContextData._currentSets;
 		renderContextData.currentPass = renderContextData._currentPass;
+		renderContextData._currentPass = null;
+		renderContextData._currentSets = null;
 
 	}
 

+ 22 - 0
src/renderers/webgpu/utils/WebGPUTextureUtils.js

@@ -365,6 +365,20 @@ class WebGPUTextureUtils {
 
 		}
 
+		const renderTarget = texture.renderTarget;
+
+		// when the multisampled data are discarded, try to use a transient attachment if possible
+
+		if ( texture.isDepthTexture === true && primarySamples > 1 && GPUTextureUsage.TRANSIENT_ATTACHMENT !== undefined ) {
+
+			if ( renderTarget?.storeMultisampledDepthBuffer === false && ( renderTarget.stencilBuffer === false || renderTarget.storeMultisampledStencilBuffer === false ) ) {
+
+				usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TRANSIENT_ATTACHMENT;
+
+			}
+
+		}
+
 		const textureDescriptorGPU = new GPUTextureDescriptor();
 		textureDescriptorGPU.label = texture.name;
 		textureDescriptorGPU.size.width = width;
@@ -413,6 +427,14 @@ class WebGPUTextureUtils {
 			msaaTextureDescriptorGPU.sampleCount = samples;
 			msaaTextureDescriptorGPU.mipLevelCount = 1; // See https://www.w3.org/TR/webgpu/#texture-creation
 
+			// when the multisampled data are discarded, try to use a transient attachment if possible
+
+			if ( renderTarget?.storeMultisampledColorBuffer === false && GPUTextureUsage.TRANSIENT_ATTACHMENT !== undefined ) {
+
+				msaaTextureDescriptorGPU.usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TRANSIENT_ATTACHMENT;
+
+			}
+
 			textureData.msaaTexture = backend.device.createTexture( msaaTextureDescriptorGPU );
 
 		}

+ 6 - 2
src/renderers/webxr/WebXRManager.js

@@ -451,7 +451,9 @@ class WebXRManager extends EventDispatcher {
 							colorSpace: renderer.outputColorSpace,
 							stencilBuffer: attributes.stencil,
 							resolveDepthBuffer: ( glBaseLayer.ignoreDepthValues === false ),
-							resolveStencilBuffer: ( glBaseLayer.ignoreDepthValues === false )
+							resolveStencilBuffer: ( glBaseLayer.ignoreDepthValues === false ),
+							storeMultisampledDepthBuffer: ( glBaseLayer.ignoreDepthValues === false ),
+							storeMultisampledStencilBuffer: ( glBaseLayer.ignoreDepthValues === false )
 
 						}
 					);
@@ -496,7 +498,9 @@ class WebXRManager extends EventDispatcher {
 							colorSpace: renderer.outputColorSpace,
 							samples: attributes.antialias ? 4 : 0,
 							resolveDepthBuffer: ( glProjLayer.ignoreDepthValues === false ),
-							resolveStencilBuffer: ( glProjLayer.ignoreDepthValues === false )
+							resolveStencilBuffer: ( glProjLayer.ignoreDepthValues === false ),
+							storeMultisampledDepthBuffer: ( glProjLayer.ignoreDepthValues === false ),
+							storeMultisampledStencilBuffer: ( glProjLayer.ignoreDepthValues === false )
 						} );
 
 				}

+ 34 - 0
test/unit/src/materials/MeshPhysicalMaterial.tests.js

@@ -37,6 +37,40 @@ export default QUnit.module( 'Materials', () => {
 
 		} );
 
+		QUnit.test( 'retroreflective', ( assert ) => {
+
+			const object = new MeshPhysicalMaterial();
+			assert.strictEqual( object.retroreflective, 0, 'retroreflective defaults to 0.' );
+
+			object.retroreflective = 0.75;
+			assert.strictEqual( object.retroreflective, 0.75, 'Can set retroreflective.' );
+
+		} );
+
+		QUnit.test( 'copy copies retroreflective', ( assert ) => {
+
+			const source = new MeshPhysicalMaterial( { retroreflective: 0.5 } );
+			const object = new MeshPhysicalMaterial();
+
+			object.copy( source );
+
+			assert.strictEqual( object.retroreflective, 0.5, 'copy() preserves retroreflective.' );
+
+		} );
+
+		QUnit.test( 'fromJSON restores retroreflective', ( assert ) => {
+
+			const source = new MeshPhysicalMaterial( { retroreflective: 0.25 } );
+			const json = source.toJSON();
+			const object = new MeshPhysicalMaterial();
+
+			object.fromJSON( json );
+
+			assert.strictEqual( json.retroreflective, 0.25, 'toJSON() serializes retroreflective.' );
+			assert.strictEqual( object.retroreflective, 0.25, 'fromJSON() restores retroreflective.' );
+
+		} );
+
 		// PUBLIC
 		QUnit.test( 'isMeshPhysicalMaterial', ( assert ) => {
 

+ 10 - 8
utils/generateDFGLUT.js

@@ -1,8 +1,8 @@
 /**
  * DFG LUT Generator
  *
- * Generates a precomputed lookup table for the split-sum approximation
- * used in Image-Based Lighting.
+ * Generates a precomputed lookup table for the split-sum approximation,
+ * used by image-based lighting and direct-light multi-scattering energy compensation.
  *
  * Reference: "Real Shading in Unreal Engine 4" by Brian Karis
  */
@@ -243,7 +243,8 @@ function saveAsJavaScript( data ) {
 	}
 
 	const webgl = `/**
- * Precomputed DFG LUT for Image-Based Lighting
+ * Precomputed DFG LUT for physically based specular lighting, used by both
+ * image-based lighting and direct-light multi-scattering energy compensation
  * Resolution: ${LUT_SIZE}x${LUT_SIZE}
  * Samples: ${SAMPLE_COUNT} per texel
  * Format: RG16F (2 half floats per texel: scale, bias)
@@ -285,7 +286,8 @@ import { DataTexture } from '../../../textures/DataTexture.js';
 import { RGFormat, HalfFloatType, LinearFilter, ClampToEdgeWrapping } from '../../../constants.js';
 
 /**
- * Precomputed DFG LUT for Image-Based Lighting
+ * Precomputed DFG LUT for physically based specular lighting, used by both
+ * image-based lighting and direct-light multi-scattering energy compensation
  * Resolution: ${LUT_SIZE}x${LUT_SIZE}
  * Samples: ${SAMPLE_COUNT} per texel
  * Format: RG16F (2 half floats per texel: scale, bias)
@@ -297,7 +299,7 @@ ${rows.join( ',\n' )}
 
 let lut = null;
 
-const DFG = /*@__PURE__*/ Fn( ( { roughness, dotNV } ) => {
+const DFGLUT = /*@__PURE__*/ Fn( ( { roughness, dotNV } ) => {
 
 	if ( lut === null ) {
 
@@ -318,14 +320,14 @@ const DFG = /*@__PURE__*/ Fn( ( { roughness, dotNV } ) => {
 
 } );
 
-export default DFG;
+export default DFGLUT;
 `;
 
 	fs.writeFileSync( './src/renderers/shaders/DFGLUTData.js', webgl );
 	console.log( 'Saved WebGL version to ./src/renderers/shaders/DFGLUTData.js' );
 
-	fs.writeFileSync( './src/nodes/functions/BSDF/DFG.js', webgpu );
-	console.log( 'Saved WebGPU version to ./src/nodes/functions/BSDF/DFG.js' );
+	fs.writeFileSync( './src/nodes/functions/BSDF/DFGLUT.js', webgpu );
+	console.log( 'Saved WebGPU version to ./src/nodes/functions/BSDF/DFGLUT.js' );
 
 }
 

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

粤ICP备19079148号