Visitar URL original
fix: Correct anisotropic GGX and reflection roughness by drakeo338 · Pull Request #9530 · playcanvas/engine · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ export default /* glsl */`
// Anisotropic GGX
float calcLightSpecular(float gloss, vec3 worldNormal, vec3 viewDir, vec3 h, vec3 lightDirNorm, mat3 tbn) {
float PI = 3.141592653589793;
float roughness = max((1.0 - gloss) * (1.0 - gloss), 0.001);
float alphaRoughness = roughness * roughness;
float alphaRoughness = max((1.0 - gloss) * (1.0 - gloss), 0.001);
float anisotropy = dAnisotropy;
vec2 direction = dAnisotropyRotation;

Expand Down
2 changes: 1 addition & 1 deletion src/scene/shader-lib/glsl/chunks/lit/frag/reflDirAniso.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default /* glsl */`
void getReflDir(vec3 worldNormal, vec3 viewDir, float gloss, mat3 tbn) {
float roughness = sqrt(1.0 - min(gloss, 1.0));
float roughness = 1.0 - min(gloss, 1.0);

vec2 direction = dAnisotropyRotation;
vec3 anisotropicT = normalize(tbn * vec3(direction, 0.0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ export default /* wgsl */`
// Anisotropic GGX
fn calcLightSpecular(gloss: f32, worldNormal: vec3f, viewDir: vec3f, h: vec3f, lightDirNorm: vec3f, tbn: mat3x3f) -> f32 {
let PI: f32 = 3.141592653589793;
let roughness: f32 = max((1.0 - gloss) * (1.0 - gloss), 0.001);
let alphaRoughness: f32 = roughness * roughness;
let alphaRoughness: f32 = max((1.0 - gloss) * (1.0 - gloss), 0.001);
let anisotropy: f32 = dAnisotropy;
let direction: vec2f = dAnisotropyRotation;

Expand Down
2 changes: 1 addition & 1 deletion src/scene/shader-lib/wgsl/chunks/lit/frag/reflDirAniso.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default /* wgsl */`
fn getReflDir(worldNormal: vec3f, viewDir: vec3f, gloss: f32, tbn: mat3x3f) {
let roughness: f32 = sqrt(1.0 - min(gloss, 1.0));
let roughness: f32 = 1.0 - min(gloss, 1.0);

let direction: vec2f = dAnisotropyRotation;
let anisotropicT: vec3f = normalize(tbn * vec3f(direction, 0.0));
Expand Down
72 changes: 72 additions & 0 deletions test/scene/shader-lib/anisotropy-roughness.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { expect } from 'chai';

import lightSpecularAnisoGGXGLSL from '../../../src/scene/shader-lib/glsl/chunks/lit/frag/lightSpecularAnisoGGX.js';
import reflDirAnisoGLSL from '../../../src/scene/shader-lib/glsl/chunks/lit/frag/reflDirAniso.js';
import lightSpecularAnisoGGXWGSL from '../../../src/scene/shader-lib/wgsl/chunks/lit/frag/lightSpecularAnisoGGX.js';
import reflDirAnisoWGSL from '../../../src/scene/shader-lib/wgsl/chunks/lit/frag/reflDirAniso.js';

/**
* Evaluates the scalar declarations at the start of an anisotropy chunk function (the ones
* before the anisotropy inputs are read) for a given gloss, and returns the declared values by
* name.
*
* @param {string} chunk - The GLSL or WGSL chunk source.
* @param {string} fnName - The name of the function to evaluate.
* @param {number} gloss - The gloss value passed to the function.
* @returns {Object<string, number>} The evaluated declarations.
*/
const evaluateRoughness = (chunk, fnName, gloss) => {
const start = chunk.indexOf(fnName);
const body = chunk.slice(start, chunk.indexOf('dAnisotropy', start));
const decl = /(?:float|let)\s+(\w+)(?:\s*:\s*f32)?\s*=\s*([^;\s][^;]*);/g;
const scope = { gloss };
for (const [, name, expr] of body.matchAll(decl)) {
if (name === 'PI' || name === 'anisotropy') continue;
const names = Object.keys(scope);
// eslint-disable-next-line no-new-func
const fn = new Function(...names, 'max', 'min', 'sqrt', `return ${expr};`);
scope[name] = fn(...names.map(n => scope[n]), Math.max, Math.min, Math.sqrt);
}
return scope;
};

const perceptualRoughnessValues = [0.3, 0.5, 0.7, 1.0];

describe('lightSpecularAnisoGGX', function () {

const chunks = {
glsl: lightSpecularAnisoGGXGLSL,
wgsl: lightSpecularAnisoGGXWGSL
};

for (const [language, chunk] of Object.entries(chunks)) {

// KHR_materials_anisotropy derives at / ab from alphaRoughness = perceptualRoughness^2,
// with perceptualRoughness = 1 - gloss, the same as the isotropic GGX chunk.
it(`uses alphaRoughness = (1 - gloss)^2 (${language})`, function () {
for (const perceptualRoughness of perceptualRoughnessValues) {
const { alphaRoughness } = evaluateRoughness(chunk, 'calcLightSpecular', 1 - perceptualRoughness);
expect(alphaRoughness).to.be.closeTo(perceptualRoughness * perceptualRoughness, 1e-6);
}
});
}
});

describe('reflDirAniso', function () {

const chunks = {
glsl: reflDirAnisoGLSL,
wgsl: reflDirAnisoWGSL
};

for (const [language, chunk] of Object.entries(chunks)) {

// The glTF reference bends the reflection normal using perceptualRoughness itself.
it(`bends the normal with perceptual roughness 1 - gloss (${language})`, function () {
for (const perceptualRoughness of perceptualRoughnessValues) {
const { roughness } = evaluateRoughness(chunk, 'getReflDir', 1 - perceptualRoughness);
expect(roughness).to.be.closeTo(perceptualRoughness, 1e-6);
}
});
}
});