Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use CDF for irradiance prefiltering #16010

Merged
Merged
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 @@ -432,10 +432,8 @@ export class ReflectionBlock extends ReflectionTextureBaseBlock {
#if defined(NORMAL) && defined(USESPHERICALINVERTEX)
, ${isWebGPU ? "input." : ""}${this._vEnvironmentIrradianceName}
#endif
#ifdef USESPHERICALFROMREFLECTIONMAP
#if !defined(NORMAL) || !defined(USESPHERICALINVERTEX)
#if (defined(USESPHERICALFROMREFLECTIONMAP) && (!defined(NORMAL) || !defined(USESPHERICALINVERTEX))) || (defined(USEIRRADIANCEMAP) && defined(REFLECTIONMAP_3D))
, ${this._reflectionMatrixName}
#endif
#endif
#ifdef USEIRRADIANCEMAP
, irradianceSampler // ** not handled **
Expand Down
1 change: 1 addition & 0 deletions packages/dev/core/src/Materials/PBR/pbrBaseMaterial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1720,6 +1720,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
if (reflectionTexture.irradianceTexture) {
defines.USEIRRADIANCEMAP = true;
defines.USESPHERICALFROMREFLECTIONMAP = false;
defines.USESPHERICALINVERTEX = false;
Popov72 marked this conversation as resolved.
Show resolved Hide resolved
}
// Assume using spherical polynomial if the reflection texture is a cube map
else if (reflectionTexture.isCube) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export class HDRFiltering {
generateDepthBuffer: false,
generateStencilBuffer: false,
samplingMode: Constants.TEXTURE_NEAREST_SAMPLINGMODE,
label: "HDR_Radiance_Filtering_Target",
});
this._engine.updateTextureWrappingMode(rtWrapper.texture!, Constants.TEXTURE_CLAMP_ADDRESSMODE, Constants.TEXTURE_CLAMP_ADDRESSMODE, Constants.TEXTURE_CLAMP_ADDRESSMODE);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import { Vector3 } from "../../../Maths/math";
import { ILog2 } from "../../../Maths/math.scalar.functions";
import { BaseTexture } from "../baseTexture";
import type { AbstractEngine } from "../../../Engines/abstractEngine";
import type { Effect } from "../../../Materials/effect";
import { Constants } from "../../../Engines/constants";
import { EffectWrapper, EffectRenderer } from "../../../Materials/effectRenderer";
import type { Nullable } from "../../../types";
import type { RenderTargetWrapper } from "../../../Engines/renderTargetWrapper";
import { Logger } from "../../../Misc/logger";

import { ShaderLanguage } from "core/Materials/shaderLanguage";
import { IblCdfGenerator } from "../../../Rendering/iblCdfGenerator";

/**
* Options for texture filtering
*/
interface IHDRIrradianceFilteringOptions {
/**
* Scales pixel intensity for the input HDR map.
*/
hdrScale?: number;

/**
* Quality of the filter. Should be `Constants.TEXTURE_FILTERING_QUALITY_OFFLINE` for prefiltering
*/
quality?: number;

/**
* Use the Cumulative Distribution Function (CDF) for filtering
*/
useCdf?: boolean;
}

/**
* Filters HDR maps to get correct renderings of PBR reflections
*/
export class HDRIrradianceFiltering {
private _engine: AbstractEngine;
private _effectRenderer: EffectRenderer;
private _effectWrapper: EffectWrapper;
private _cdfGenerator: IblCdfGenerator;

/**
* Quality switch for prefiltering. Should be set to `Constants.TEXTURE_FILTERING_QUALITY_OFFLINE` unless
* you care about baking speed.
*/
public quality: number = Constants.TEXTURE_FILTERING_QUALITY_OFFLINE;

/**
* Scales pixel intensity for the input HDR map.
*/
public hdrScale: number = 1;

/**
* Use the Cumulative Distribution Function (CDF) for filtering
*/
public useCdf: boolean = false;

/**
* Instantiates HDR filter for irradiance map
*
* @param engine Thin engine
* @param options Options
*/
constructor(engine: AbstractEngine, options: IHDRIrradianceFilteringOptions = {}) {
// pass
this._engine = engine;
this.hdrScale = options.hdrScale || this.hdrScale;
this.quality = options.quality || this.quality;
this.useCdf = options.useCdf || this.useCdf;
}

private _createRenderTarget(size: number): RenderTargetWrapper {
let textureType = Constants.TEXTURETYPE_UNSIGNED_BYTE;
if (this._engine.getCaps().textureHalfFloatRender) {
textureType = Constants.TEXTURETYPE_HALF_FLOAT;
} else if (this._engine.getCaps().textureFloatRender) {
textureType = Constants.TEXTURETYPE_FLOAT;
}

const rtWrapper = this._engine.createRenderTargetCubeTexture(size, {
format: Constants.TEXTUREFORMAT_RGBA,
type: textureType,
createMipMaps: false,
generateMipMaps: false,
generateDepthBuffer: false,
generateStencilBuffer: false,
samplingMode: Constants.TEXTURE_BILINEAR_SAMPLINGMODE,
label: "HDR_Irradiance_Filtering_Target",
});
this._engine.updateTextureWrappingMode(rtWrapper.texture!, Constants.TEXTURE_CLAMP_ADDRESSMODE, Constants.TEXTURE_CLAMP_ADDRESSMODE, Constants.TEXTURE_CLAMP_ADDRESSMODE);

return rtWrapper;
}

private _prefilterInternal(texture: BaseTexture): BaseTexture {
const width = texture.getSize().width;
const mipmapsCount = ILog2(width);

const effect = this._effectWrapper.effect;
// Choose a power of 2 size for the irradiance map.
// It can be much smaller than the original texture.
const irradianceSize = Math.max(32, 1 << ILog2(width >> 3));
const outputTexture = this._createRenderTarget(irradianceSize);
this._effectRenderer.saveStates();
this._effectRenderer.setViewport();

this._effectRenderer.applyEffectWrapper(this._effectWrapper);

const directions = [
[new Vector3(0, 0, -1), new Vector3(0, -1, 0), new Vector3(1, 0, 0)], // PositiveX
[new Vector3(0, 0, 1), new Vector3(0, -1, 0), new Vector3(-1, 0, 0)], // NegativeX
[new Vector3(1, 0, 0), new Vector3(0, 0, 1), new Vector3(0, 1, 0)], // PositiveY
[new Vector3(1, 0, 0), new Vector3(0, 0, -1), new Vector3(0, -1, 0)], // NegativeY
[new Vector3(1, 0, 0), new Vector3(0, -1, 0), new Vector3(0, 0, 1)], // PositiveZ
[new Vector3(-1, 0, 0), new Vector3(0, -1, 0), new Vector3(0, 0, -1)], // NegativeZ
];

effect.setFloat("hdrScale", this.hdrScale);
effect.setFloat2("vFilteringInfo", texture.getSize().width, mipmapsCount);
effect.setTexture("inputTexture", texture);
if (this._cdfGenerator) {
effect.setTexture("icdfTexture", this._cdfGenerator.getIcdfTexture());
}

for (let face = 0; face < 6; face++) {
effect.setVector3("up", directions[face][0]);
effect.setVector3("right", directions[face][1]);
effect.setVector3("front", directions[face][2]);

this._engine.bindFramebuffer(outputTexture, face, undefined, undefined, true);
this._effectRenderer.applyEffectWrapper(this._effectWrapper);

this._effectRenderer.draw();
}

// Cleanup
this._effectRenderer.restoreStates();
this._engine.restoreDefaultFramebuffer();
effect.setTexture("inputTexture", null);
effect.setTexture("icdfTexture", null);
const irradianceTexture = new BaseTexture(texture.getScene(), outputTexture.texture!);
irradianceTexture.name = texture.name + "_irradiance";
irradianceTexture.displayName = texture.name + "_irradiance";
irradianceTexture.gammaSpace = false;
return irradianceTexture;
}

private _createEffect(texture: BaseTexture, onCompiled?: Nullable<(effect: Effect) => void>): EffectWrapper {
const defines = [];
if (texture.gammaSpace) {
defines.push("#define GAMMA_INPUT");
}

defines.push("#define NUM_SAMPLES " + this.quality + "u"); // unsigned int

const isWebGPU = this._engine.isWebGPU;
const samplers = ["inputTexture"];
if (this._cdfGenerator) {
samplers.push("icdfTexture");
defines.push("#define IBL_CDF_FILTERING");
}
const effectWrapper = new EffectWrapper({
engine: this._engine,
name: "HDRIrradianceFiltering",
vertexShader: "hdrIrradianceFiltering",
fragmentShader: "hdrIrradianceFiltering",
samplerNames: samplers,
uniformNames: ["vSampleDirections", "vWeights", "up", "right", "front", "vFilteringInfo", "hdrScale"],
useShaderStore: true,
defines,
onCompiled: onCompiled,
shaderLanguage: isWebGPU ? ShaderLanguage.WGSL : ShaderLanguage.GLSL,
extraInitializationsAsync: async () => {
if (isWebGPU) {
await Promise.all([import("../../../ShadersWGSL/hdrIrradianceFiltering.vertex"), import("../../../ShadersWGSL/hdrIrradianceFiltering.fragment")]);
} else {
await Promise.all([import("../../../Shaders/hdrIrradianceFiltering.vertex"), import("../../../Shaders/hdrIrradianceFiltering.fragment")]);
}
},
});

return effectWrapper;
}

/**
* Get a value indicating if the filter is ready to be used
* @param texture Texture to filter
* @returns true if the filter is ready
*/
public isReady(texture: BaseTexture) {
return texture.isReady() && this._effectWrapper.effect.isReady();
}

/**
* Prefilters a cube texture to contain IBL irradiance.
* Prefiltering will be invoked at the end of next rendering pass.
* This has to be done once the map is loaded, and has not been prefiltered by a third party software.
* See http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf for more information
* @param texture Texture to filter
* @param onFinished Callback when filtering is done
* @returns Promise called when prefiltering is done
*/
public prefilter(texture: BaseTexture, onFinished: Nullable<() => void> = null): Promise<BaseTexture> {
if (!this._engine._features.allowTexturePrefiltering) {
Logger.Warn("HDR prefiltering is not available in WebGL 1., you can use real time filtering instead.");
return Promise.reject("HDR prefiltering is not available in WebGL 1., you can use real time filtering instead.");
}
let cdfGeneratedPromise: Promise<Nullable<BaseTexture>> = Promise.resolve(null);
if (this.useCdf) {
this._cdfGenerator = new IblCdfGenerator(this._engine);
this._cdfGenerator.iblSource = texture;
cdfGeneratedPromise = new Promise((resolve) => {
this._cdfGenerator.onGeneratedObservable.addOnce(() => {
resolve(null);
});
});
}

return cdfGeneratedPromise.then(() => {
return new Promise((resolve) => {
this._effectRenderer = new EffectRenderer(this._engine);
this._effectWrapper = this._createEffect(texture);
this._effectWrapper.effect.executeWhenCompiled(() => {
const irradianceTexture = this._prefilterInternal(texture);
this._effectRenderer.dispose();
this._effectWrapper.dispose();
this._cdfGenerator?.dispose();
resolve(irradianceTexture);
if (onFinished) {
onFinished();
}
});
});
});
}
}
36 changes: 33 additions & 3 deletions packages/dev/core/src/Materials/Textures/hdrCubeTexture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Tools } from "../../Misc/tools";
import { ToGammaSpace } from "../../Maths/math.constants";
import type { AbstractEngine } from "../../Engines/abstractEngine";
import { HDRFiltering } from "../../Materials/Textures/Filtering/hdrFiltering";
import { HDRIrradianceFiltering } from "../../Materials/Textures/Filtering/hdrIrradianceFiltering";
import { ToHalfFloat } from "../../Misc/textureTools";
import "../../Materials/Textures/baseTexture.polynomial";

Expand All @@ -27,6 +28,8 @@ export class HDRCubeTexture extends BaseTexture {
private _generateHarmonics = true;
private _noMipmap: boolean;
private _prefilterOnLoad: boolean;
private _prefilterIrradianceOnLoad: boolean;
private _prefilterUsingCdf: boolean;
private _textureMatrix: Matrix;
private _size: number;
private _supersample: boolean;
Expand Down Expand Up @@ -113,6 +116,8 @@ export class HDRCubeTexture extends BaseTexture {
* @param onLoad on success callback function
* @param onError on error callback function
* @param supersample Defines if texture must be supersampled (default: false)
* @param prefilterIrradianceOnLoad Prefilters HDR texture to allow use of this texture for irradiance lighting.
* @param prefilterUsingCdf Defines if the prefiltering should be done using a CDF instead of the default approach.
*/
constructor(
url: string,
Expand All @@ -124,7 +129,9 @@ export class HDRCubeTexture extends BaseTexture {
prefilterOnLoad = false,
onLoad: Nullable<() => void> = null,
onError: Nullable<(message?: string, exception?: any) => void> = null,
supersample = false
supersample = false,
prefilterIrradianceOnLoad = false,
prefilterUsingCdf = false
) {
super(sceneOrEngine);

Expand All @@ -139,6 +146,8 @@ export class HDRCubeTexture extends BaseTexture {
this.isCube = true;
this._textureMatrix = Matrix.Identity();
this._prefilterOnLoad = prefilterOnLoad;
this._prefilterIrradianceOnLoad = prefilterIrradianceOnLoad;
this._prefilterUsingCdf = prefilterUsingCdf;
this._onLoad = () => {
this.onLoadObservable.notifyObservers(this);
if (onLoad) {
Expand Down Expand Up @@ -274,11 +283,32 @@ export class HDRCubeTexture extends BaseTexture {
return results;
};

if (engine._features.allowTexturePrefiltering && this._prefilterOnLoad) {
if (engine._features.allowTexturePrefiltering && (this._prefilterOnLoad || this._prefilterIrradianceOnLoad)) {
const previousOnLoad = this._onLoad;
const hdrFiltering = new HDRFiltering(engine);
this._onLoad = () => {
hdrFiltering.prefilter(this, previousOnLoad);
let irradiancePromise: Promise<Nullable<BaseTexture>> = Promise.resolve(null);
let radiancePromise: Promise<void> = Promise.resolve();
if (this._prefilterIrradianceOnLoad) {
const hdrIrradianceFiltering = new HDRIrradianceFiltering(engine, { useCdf: this._prefilterUsingCdf });
irradiancePromise = hdrIrradianceFiltering.prefilter(this);
}
if (this._prefilterOnLoad) {
radiancePromise = hdrFiltering.prefilter(this);
}
Promise.all([irradiancePromise, radiancePromise]).then((results) => {
const irradianceTexture = results[0] as Nullable<BaseTexture>;
if (this._prefilterIrradianceOnLoad && irradianceTexture) {
this.irradianceTexture = irradianceTexture;
const scene = this.getScene();
if (scene) {
scene.markAllMaterialsAsDirty(Constants.MATERIAL_TextureDirtyFlag);
}
}
if (previousOnLoad) {
previousOnLoad();
}
});
};
}

Expand Down
4 changes: 4 additions & 0 deletions packages/dev/core/src/Materials/Textures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ export * from "../../Shaders/hdrFiltering.vertex";
export * from "../../Shaders/hdrFiltering.fragment";
export * from "../../ShadersWGSL/hdrFiltering.vertex";
export * from "../../ShadersWGSL/hdrFiltering.fragment";
export * from "../../Shaders/hdrIrradianceFiltering.vertex";
export * from "../../Shaders/hdrIrradianceFiltering.fragment";
export * from "../../ShadersWGSL/hdrIrradianceFiltering.vertex";
export * from "../../ShadersWGSL/hdrIrradianceFiltering.fragment";
Loading