diff --git a/cocos/rendering/custom/pipeline.ts b/cocos/rendering/custom/pipeline.ts index b2fb592896b..a776f433d3e 100644 --- a/cocos/rendering/custom/pipeline.ts +++ b/cocos/rendering/custom/pipeline.ts @@ -1660,7 +1660,6 @@ export interface Pipeline extends BasicPipeline { export interface PipelinePassBuilder { getConfigOrder (): number; getRenderOrder (): number; - resetCamera? (cameraConfigs: { [name: string]: any }): void; configCamera? ( camera: Readonly, pplConfigs: { readonly [name: string]: any }, diff --git a/editor/assets/default_renderpipeline/builtin-dof-pass.ts b/editor/assets/default_renderpipeline/builtin-dof-pass.ts new file mode 100644 index 00000000000..11a6ab56f42 --- /dev/null +++ b/editor/assets/default_renderpipeline/builtin-dof-pass.ts @@ -0,0 +1,321 @@ +/* + Copyright (c) 2021-2024 Xiamen Yaji Software Co., Ltd. + + https://www.cocos.com/ + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +import { + _decorator, assert, CCBoolean, CCFloat, CCInteger, + gfx, Material, renderer, rendering, Vec3, Vec4, +} from 'cc'; + +import { EDITOR } from 'cc/env'; + +import { + BuiltinPipelineSettings, +} from './builtin-pipeline-settings'; + +import { + BuiltinPipelinePassBuilder, +} from './builtin-pipeline-pass'; + +import { + CameraConfigs, + getPingPongRenderTarget, + PipelineConfigs, + PipelineContext, +} from './builtin-pipeline'; + +const { ccclass, disallowMultiple, executeInEditMode, menu, property, requireComponent, type } = _decorator; + +const { Color, LoadOp, StoreOp } = gfx; + +export interface DofPassConfigs { + enableDof: boolean; +} + +@ccclass('BuiltinDepthOfFieldPass') +@menu('Rendering/BuiltinDepthOfFieldPass') +@requireComponent(BuiltinPipelineSettings) +@disallowMultiple +@executeInEditMode +export class BuiltinDepthOfFieldPass extends BuiltinPipelinePassBuilder + implements rendering.PipelinePassBuilder { + @property({ + group: { id: 'BuiltinPass', name: 'Pass Settings', style: 'section' }, + type: CCInteger, + }) + configOrder = 0; + @property({ + group: { id: 'BuiltinPass', name: 'Pass Settings', style: 'section' }, + type: CCInteger, + }) + renderOrder = 150; + + @property + private _enableDof = false; + @property + private _material: Material | null = null; + @property + private _minRange = 0; + @property + private _maxRange = 2; + @property + private _blurRadius = 1; + @property + private _intensity = 1; + @property + private _focusPos = new Vec3(0, 0, 0); + + // DepthOfField + @property({ + group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, + type: CCBoolean, + visible: true, + }) + set dofEnable(value: boolean) { + this._enableDof = value; + if (EDITOR) { + this._parent._tryEnableEditorPreview(); + } + } + get dofEnable(): boolean { + return this._enableDof; + } + + @property({ + group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, + type: Material, + visible: true, + }) + set dofMaterial(value: Material) { + if (this._material === value) { + return; + } + this._material = value; + if (EDITOR) { + this._parent._tryEnableEditorPreview(); + } + } + get dofMaterial(): Material { + return this._material!; + } + + @property({ + group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, + type: CCFloat, + min: 0, + visible: true, + }) + set dofMinRange(value: number) { + this._minRange = value; + } + get dofMinRange(): number { + return this._minRange; + } + + @property({ + group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, + type: CCFloat, + min: 0, + visible: true, + }) + set dofMaxRange(value: number) { + this._maxRange = value; + } + get dofMaxRange(): number { + return this._maxRange; + } + + @property({ + group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, + type: CCFloat, + range: [0.0, 2, 0.01], + slide: true, + visible: true, + }) + set dofIntensity(value: number) { + this._intensity = value; + } + get dofIntensity(): number { + return this._intensity; + } + + @type(CCFloat) + @property({ + group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, + type: CCFloat, + range: [0.01, 10, 0.01], + slide: true, + visible: true, + }) + set dofBlurRadius(value: number) { + this._blurRadius = value; + } + get dofBlurRadius(): number { + return this._blurRadius; + } + + @type(Vec3) + @property({ + group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, + type: Vec3, + visible: true, + }) + set dofFocusPos(value: Vec3) { + this._focusPos = value; + } + get dofFocusPos(): Vec3 { + return this._focusPos; + } + + // PipelinePassBuilder + getConfigOrder(): number { + return this.configOrder; + } + getRenderOrder(): number { + return this.renderOrder; + } + configCamera( + camera: Readonly, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & DofPassConfigs): void { + cameraConfigs.enableDof = pplConfigs.supportDepthSample + && this._enableDof + && !!this._material; + + if (cameraConfigs.enableDof) { + // Output scene depth, this is allowed but has performance impact + cameraConfigs.enableStoreSceneDepth = true; + ++cameraConfigs.remainingPasses; + } + } + windowResize( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: Readonly, + window: renderer.RenderWindow): void { + const id = window.renderWindowId; + if (cameraConfigs.enableDof) { + ppl.addRenderTarget(`DofRadiance${id}`, + cameraConfigs.radianceFormat, + cameraConfigs.width, + cameraConfigs.height); + } + } + setup( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & Readonly, + camera: renderer.scene.Camera, + context: PipelineContext, + prevRenderPass?: rendering.BasicRenderPassBuilder): rendering.BasicRenderPassBuilder | undefined { + if (!cameraConfigs.enableDof) { + return prevRenderPass; + } + --cameraConfigs.remainingPasses; + + assert(!!this._material); + if (cameraConfigs.remainingPasses === 0) { + return this._addDepthOfFieldPasses(ppl, pplConfigs, + cameraConfigs, this._material, + camera, cameraConfigs.width, cameraConfigs.height, + context.colorName, + context.depthStencilName, + cameraConfigs.colorName); + } else { + const prefix = cameraConfigs.enableShadingScale + ? `ScaledRadiance` + : `Radiance`; + const outputRadianceName = getPingPongRenderTarget( + context.colorName, prefix, cameraConfigs.renderWindowId); + const inputRadianceName = context.colorName; + context.colorName = outputRadianceName; + return this._addDepthOfFieldPasses(ppl, pplConfigs, + cameraConfigs, this._material, + camera, cameraConfigs.width, cameraConfigs.height, + inputRadianceName, + context.depthStencilName, + outputRadianceName); + } + } + private _addDepthOfFieldPasses( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & Readonly, + dofMaterial: Material, + camera: renderer.scene.Camera, + width: number, + height: number, + inputRadiance: string, + inputDepthStencil: string, + outputRadianceName: string, + ): rendering.BasicRenderPassBuilder { + this._cocParams.x = this._minRange; + this._cocParams.y = this._maxRange; + this._cocParams.z = this._blurRadius; + this._cocParams.w = this._intensity; + this._focusPosVec4.x = this._focusPos.x; + this._focusPosVec4.y = this._focusPos.y; + this._focusPosVec4.z = this._focusPos.z; + this._cocTexSize.x = 1.0 / width; + this._cocTexSize.y = 1.0 / height; + this._cocTexSize.z = width; + this._cocTexSize.w = height; + + const id = cameraConfigs.renderWindowId; + const tempRadiance = `DofRadiance${id}`; + + // Blur Pass + const blurPass = ppl.addRenderPass(width, height, 'cc-dof-blur'); + blurPass.addRenderTarget(tempRadiance, LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); + blurPass.addTexture(inputRadiance, 'screenTex'); + blurPass.setVec4('g_platform', pplConfigs.platform); + blurPass.setVec4('blurParams', this._cocParams); + blurPass.setVec4('mainTexTexelSize', this._cocTexSize); + blurPass + .addQueue(rendering.QueueHint.OPAQUE) + .addCameraQuad(camera, dofMaterial, 0); // addCameraQuad will set camera related UBOs + // coc pass + const cocPass = ppl.addRenderPass(width, height, 'cc-dof-coc'); + cocPass.addRenderTarget(outputRadianceName, LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); + cocPass.addTexture(tempRadiance, 'colorTex'); + cocPass.addTexture(inputDepthStencil, "DepthTex"); + cocPass.addTexture(inputRadiance, "screenTex"); + cocPass.setVec4('g_platform', pplConfigs.platform); + cocPass.setMat4('proj', camera.matProj); + cocPass.setMat4('invProj', camera.matProjInv); + cocPass.setMat4('viewMatInv', camera.node.worldMatrix); + cocPass.setVec4('cocParams', this._cocParams); + cocPass.setVec4('focus', this._focusPosVec4); + cocPass + .addQueue(rendering.QueueHint.OPAQUE) + .addCameraQuad(camera, dofMaterial, 1); + + return cocPass; + } + + // Runtime members + private readonly _clearColorTransparentBlack = new Color(0, 0, 0, 0); + private readonly _cocParams = new Vec4(0, 0, 0, 0); + private readonly _focusPosVec4 = new Vec4(0, 0, 0, 1); + private readonly _cocTexSize = new Vec4(0, 0, 0, 0); +} diff --git a/editor/assets/default_renderpipeline/builtin-dof-pass.ts.meta b/editor/assets/default_renderpipeline/builtin-dof-pass.ts.meta new file mode 100644 index 00000000000..069024307b9 --- /dev/null +++ b/editor/assets/default_renderpipeline/builtin-dof-pass.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "11f3130e-c08c-47bb-a209-71d114594e6d", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/editor/assets/default_renderpipeline/builtin-pipeline-pass.ts b/editor/assets/default_renderpipeline/builtin-pipeline-pass.ts new file mode 100644 index 00000000000..d672c9ddaa1 --- /dev/null +++ b/editor/assets/default_renderpipeline/builtin-pipeline-pass.ts @@ -0,0 +1,81 @@ +/* + Copyright (c) 2021-2024 Xiamen Yaji Software Co., Ltd. + + https://www.cocos.com/ + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +import { + _decorator, + assert, + Component, + rendering, +} from 'cc'; + +import { BuiltinPipelineSettings } from './builtin-pipeline-settings'; +import { PipelineSettings2 } from './builtin-pipeline'; +import { EDITOR } from 'cc/env'; + +const { ccclass, disallowMultiple, executeInEditMode, menu, requireComponent } = _decorator; + +@ccclass('BuiltinPipelinePassBuilder') +@menu('Rendering/BuiltinPipelinePassBuilder') +@requireComponent(BuiltinPipelineSettings) +@disallowMultiple +@executeInEditMode +export class BuiltinPipelinePassBuilder extends Component + implements rendering.PipelinePassBuilder { + protected _parent!: BuiltinPipelineSettings; + protected _settings!: PipelineSettings2; + getConfigOrder(): number { + return 0; + } + getRenderOrder(): number { + return 200; + } + onEnable(): void { + this._parent = this.getComponent(BuiltinPipelineSettings)!; + this._settings = this._parent.getPipelineSettings(); + + if (!Object.prototype.hasOwnProperty.call(this._settings, '_passes')) { + Object.defineProperty(this._settings, '_passes', { + value: [], + configurable: false, + enumerable: false, + writable: true, + }); + } + + assert(this._settings._passes !== undefined); + this._settings._passes.push(this); + + if (EDITOR) { + this._parent._tryEnableEditorPreview(); + } + } + onDisable(): void { + assert(Object.prototype.hasOwnProperty.call(this._settings, '_passes')); + const passes = this._settings._passes; + assert(passes !== undefined); + const idx = passes.indexOf(this); + assert(idx >= 0); + passes.splice(idx, 1); + } +} diff --git a/editor/assets/default_renderpipeline/builtin-pipeline-pass.ts.meta b/editor/assets/default_renderpipeline/builtin-pipeline-pass.ts.meta new file mode 100644 index 00000000000..db41f540f05 --- /dev/null +++ b/editor/assets/default_renderpipeline/builtin-pipeline-pass.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "6f94083c-fc92-438b-a15b-a20ec61666c7", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/editor/assets/default_renderpipeline/builtin-pipeline-settings.ts b/editor/assets/default_renderpipeline/builtin-pipeline-settings.ts index 8c830f33f5a..c4bc43f9990 100644 --- a/editor/assets/default_renderpipeline/builtin-pipeline-settings.ts +++ b/editor/assets/default_renderpipeline/builtin-pipeline-settings.ts @@ -23,24 +23,14 @@ */ import { - _decorator, - Camera, - CCBoolean, - CCFloat, - CCInteger, - Component, - Material, - rendering, - Texture2D, - Vec3, + _decorator, Camera, CCBoolean, CCFloat, CCInteger, Component, + Material, rendering, Texture2D, } from 'cc'; import { EDITOR } from 'cc/env'; import { - PipelineSettings, - makePipelineSettings, - fillRequiredPipelineSettings, + fillRequiredPipelineSettings, makePipelineSettings, PipelineSettings, } from './builtin-pipeline-types'; const { ccclass, disallowMultiple, executeInEditMode, menu, property, requireComponent, type } = _decorator; @@ -54,6 +44,10 @@ export class BuiltinPipelineSettings extends Component { @property private readonly _settings: PipelineSettings = makePipelineSettings(); + getPipelineSettings(): PipelineSettings { + return this._settings; + } + // Enable/Disable onEnable(): void { fillRequiredPipelineSettings(this._settings); @@ -92,7 +86,7 @@ export class BuiltinPipelineSettings extends Component { this._tryEnableEditorPreview(); } } - private _tryEnableEditorPreview(): void { + public _tryEnableEditorPreview(): void { if (rendering === undefined) { return; } @@ -102,7 +96,7 @@ export class BuiltinPipelineSettings extends Component { this._disableEditorPreview(); } } - private _disableEditorPreview(): void { + public _disableEditorPreview(): void { if (rendering === undefined) { return; } @@ -176,109 +170,6 @@ export class BuiltinPipelineSettings extends Component { return this._settings.shadingScale; } - // DepthOfField - @property({ - group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, - type: CCBoolean, - visible: true, - }) - set dofEnable(value: boolean) { - this._settings.depthOfField.enabled = value; - if (EDITOR) { - this._tryEnableEditorPreview(); - } - } - get dofEnable(): boolean { - return this._settings.depthOfField.enabled; - } - - @property({ - group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, - type: Material, - visible: true, - }) - set dofMaterial(value: Material) { - if (this._settings.depthOfField.material === value) { - return; - } - this._settings.depthOfField.material = value; - if (EDITOR) { - this._tryEnableEditorPreview(); - } - } - get dofMaterial(): Material { - return this._settings.depthOfField.material!; - } - - @property({ - group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, - type: CCFloat, - min: 0, - visible: true, - }) - set dofMinRange(value: number) { - this._settings.depthOfField.minRange = value; - } - get dofMinRange(): number { - return this._settings.depthOfField.minRange; - } - - @property({ - group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, - type: CCFloat, - min: 0, - visible: true, - }) - set dofMaxRange(value: number) { - this._settings.depthOfField.maxRange = value; - } - get dofMaxRange(): number { - return this._settings.depthOfField.maxRange; - } - - @property({ - group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, - type: CCFloat, - min: 0, - max: 1, - visible: true, - }) - set dofIntensity(value: number) { - this._settings.depthOfField.intensity = value; - } - get dofIntensity(): number { - return this._settings.depthOfField.intensity; - } - - - @type(CCFloat) - @property({ - group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, - type: CCFloat, - range: [0.01, 10, 0.01], - slide: true, - visible: true, - }) - set dofBlurRadius(value: number) { - this._settings.depthOfField.blurRadius = value; - } - get dofBlurRadius(): number { - return this._settings.depthOfField.blurRadius; - } - - @type(Vec3) - @property({ - group: { id: 'DepthOfField', name: 'DepthOfField (PostProcessing)', style: 'section' }, - type: Vec3, - visible: true, - }) - set dofFocusPos(value: Vec3) { - this._settings.depthOfField.focusPos = value; - } - get dofFocusPos(): Vec3 { - return this._settings.depthOfField.focusPos; - } - // Bloom @property({ group: { id: 'Bloom', name: 'Bloom (PostProcessing)', style: 'section' }, diff --git a/editor/assets/default_renderpipeline/builtin-pipeline-types.ts b/editor/assets/default_renderpipeline/builtin-pipeline-types.ts index acfcb4724a3..7440d6588c1 100644 --- a/editor/assets/default_renderpipeline/builtin-pipeline-types.ts +++ b/editor/assets/default_renderpipeline/builtin-pipeline-types.ts @@ -28,7 +28,7 @@ * ========================= !DO NOT CHANGE THE FOLLOWING SECTION MANUALLY! ========================= */ /* eslint-disable max-len */ -import { Material, Texture2D, Vec3, gfx } from 'cc'; +import { Material, Texture2D, gfx } from 'cc'; const { SampleCount } = gfx; @@ -96,53 +96,6 @@ export function fillRequiredHBAO(value: HBAO): void { } } -export interface DepthOfField { - enabled: boolean; /* false */ - /* refcount */ material: Material | null; - minRange: number; /* 0 */ - maxRange: number; /* 50 */ - blurRadius: number; /* 1 */ - intensity: number; /* 1 */ - focusPos: Vec3; - [name: string]: unknown; -} - -export function makeDepthOfField(): DepthOfField { - return { - enabled: false, - material: null, - minRange: 0, - maxRange: 50, - blurRadius: 1, - intensity: 1, - focusPos: new Vec3(0, 0, 0), - }; -} - -export function fillRequiredDepthOfField(value: DepthOfField): void { - if (value.enabled === undefined) { - value.enabled = false; - } - if (value.material === undefined) { - value.material = null; - } - if (value.minRange === undefined) { - value.minRange = 0; - } - if (value.maxRange === undefined) { - value.maxRange = 50; - } - if (value.blurRadius === undefined) { - value.blurRadius = 1; - } - if (value.intensity === undefined) { - value.intensity = 1; - } - if (value.focusPos === undefined) { - value.focusPos = new Vec3(0, 0, 0); - } -} - export interface Bloom { enabled: boolean; /* false */ /* refcount */ material: Material | null; @@ -287,7 +240,6 @@ export interface PipelineSettings { readonly msaa: MSAA; enableShadingScale: boolean; /* false */ shadingScale: number; /* 0.5 */ - readonly depthOfField: DepthOfField; readonly bloom: Bloom; readonly toneMapping: ToneMapping; readonly colorGrading: ColorGrading; @@ -301,7 +253,6 @@ export function makePipelineSettings(): PipelineSettings { msaa: makeMSAA(), enableShadingScale: false, shadingScale: 0.5, - depthOfField: makeDepthOfField(), bloom: makeBloom(), toneMapping: makeToneMapping(), colorGrading: makeColorGrading(), @@ -322,11 +273,6 @@ export function fillRequiredPipelineSettings(value: PipelineSettings): void { if (value.shadingScale === undefined) { value.shadingScale = 0.5; } - if (!value.depthOfField) { - (value.depthOfField as DepthOfField) = makeDepthOfField(); - } else { - fillRequiredDepthOfField(value.depthOfField); - } if (!value.bloom) { (value.bloom as Bloom) = makeBloom(); } else { diff --git a/editor/assets/default_renderpipeline/builtin-pipeline.ts b/editor/assets/default_renderpipeline/builtin-pipeline.ts index d0b92895f49..d12d4ced141 100644 --- a/editor/assets/default_renderpipeline/builtin-pipeline.ts +++ b/editor/assets/default_renderpipeline/builtin-pipeline.ts @@ -22,31 +22,17 @@ THE SOFTWARE. */ -import { DEBUG, EDITOR } from 'cc/env'; import { - assert, - clamp, - geometry, - gfx, - Layers, - Material, - pipeline, - renderer, - rendering, - sys, - Vec2, - Vec3, - Vec4, - cclegacy, - PipelineEventType, - PipelineEventProcessor, - ReflectionProbeManager, - warn, + assert, cclegacy, clamp, geometry, gfx, Layers, Material, pipeline, + PipelineEventProcessor, PipelineEventType, ReflectionProbeManager, renderer, + rendering, sys, Vec2, Vec3, Vec4, warn, } from 'cc'; +import { DEBUG, EDITOR } from 'cc/env'; + import { - PipelineSettings, makePipelineSettings, + PipelineSettings, } from './builtin-pipeline-types'; const { AABB, Sphere, intersect } = geometry; @@ -87,7 +73,7 @@ function getCsmMainLightViewport( vp.height = Math.max(1, vp.height); } -class PipelineConfigs { +export class PipelineConfigs { isWeb = false; isWebGL1 = false; isWebGPU = false; @@ -137,1134 +123,1634 @@ function setupPipelineConfigs( configs.platform.w = (screenSpaceSignY * 0.5 + 0.5) << 1 | (device.capabilities.clipSpaceSignY * 0.5 + 0.5); } +export interface PipelineSettings2 extends PipelineSettings { + _passes?: rendering.PipelinePassBuilder[]; +} + const defaultSettings = makePipelineSettings(); -class CameraConfigs { +export class CameraConfigs { + settings: PipelineSettings = defaultSettings; + // Window + isMainGameWindow = false; + renderWindowId = 0; + // Camera colorName = ''; depthStencilName = ''; - enableMainLightShadowMap = false; - enableMainLightPlanarShadowMap = false; - enablePostProcess = false; + // Pipeline + enableFullPipeline = false; enableProfiler = false; + remainingPasses = 0; + // Shading Scale enableShadingScale = false; - enableMSAA = false; - enableDOF = false; - enableBloom = false; - enableColorGrading = false; - enableFXAA = false; - enableFSR = false; + shadingScale = 1.0; + nativeWidth = 1; + nativeHeight = 1; + width = 1; // Scaled width + height = 1; // Scaled height + // Radiance enableHDR = false; - enablePlanarReflectionProbe = false; - useFullPipeline = false; - singleForwardRadiancePass = false; radianceFormat = gfx.Format.RGBA8; - shadingScale = 0.5; - settings: PipelineSettings = defaultSettings; + // Tone Mapping + copyAndTonemapMaterial: Material | null = null; + // Depth + /** @en mutable */ + enableStoreSceneDepth = false; } -function setupPostProcessConfigs( - pipelineConfigs: PipelineConfigs, - settings: PipelineSettings, - cameraConfigs: CameraConfigs, -) { - cameraConfigs.enableDOF = pipelineConfigs.supportDepthSample - && settings.depthOfField.enabled - && !!settings.depthOfField.material; - - cameraConfigs.enableBloom = settings.bloom.enabled - && !!settings.bloom.material; - - cameraConfigs.enableColorGrading = settings.colorGrading.enabled - && !!settings.colorGrading.material - && !!settings.colorGrading.colorGradingMap; +const sClearColorTransparentBlack = new Color(0, 0, 0, 0); - cameraConfigs.enableFXAA = settings.fxaa.enabled - && !!settings.fxaa.material; +function sortPipelinePassBuildersByConfigOrder(passBuilders: rendering.PipelinePassBuilder[]): void { + passBuilders.sort((a, b) => { + return a.getConfigOrder() - b.getConfigOrder(); + }); +} - cameraConfigs.enablePostProcess = (cameraConfigs.enableDOF - || cameraConfigs.enableBloom - || cameraConfigs.enableColorGrading - || cameraConfigs.enableFXAA); +function sortPipelinePassBuildersByRenderOrder(passBuilders: rendering.PipelinePassBuilder[]): void { + passBuilders.sort((a, b) => { + return a.getRenderOrder() - b.getRenderOrder(); + }); } -function setupCameraConfigs( - camera: renderer.scene.Camera, - pipelineConfigs: PipelineConfigs, +function addCopyToScreenPass( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, cameraConfigs: CameraConfigs, -): void { - const window = camera.window; - const isMainGameWindow: boolean = camera.cameraUsage === CameraUsage.GAME && !!window.swapchain; - const isEditorView: boolean = camera.cameraUsage === CameraUsage.SCENE_VIEW || camera.cameraUsage === CameraUsage.PREVIEW; + input: string, +): rendering.BasicRenderPassBuilder { + assert(!!cameraConfigs.copyAndTonemapMaterial); + const pass = ppl.addRenderPass( + cameraConfigs.nativeWidth, + cameraConfigs.nativeHeight, + 'cc-tone-mapping'); + pass.addRenderTarget( + cameraConfigs.colorName, + LoadOp.CLEAR, StoreOp.STORE, + sClearColorTransparentBlack); + pass.addTexture(input, 'inputTexture'); + pass.setVec4('g_platform', pplConfigs.platform); + pass.addQueue(rendering.QueueHint.OPAQUE) + .addFullscreenQuad(cameraConfigs.copyAndTonemapMaterial, 1); + return pass; +} - cameraConfigs.colorName = window.colorName; - cameraConfigs.depthStencilName = window.depthStencilName; +export function getPingPongRenderTarget(prevName: string, prefix: string, id: number): string { + if (prevName.startsWith(prefix)) { + return `${prefix}${1 - Number(prevName.charAt(prefix.length))}_${id}`; + } else { + return `${prefix}0_${id}`; + } +} - cameraConfigs.useFullPipeline = (camera.visibility & (Layers.Enum.DEFAULT)) !== 0; +export interface PipelineContext { + colorName: string; + depthStencilName: string; +} - cameraConfigs.enableMainLightShadowMap = pipelineConfigs.shadowEnabled - && !pipelineConfigs.usePlanarShadow - && !!camera.scene - && !!camera.scene.mainLight - && camera.scene.mainLight.shadowEnabled; +class ForwardLighting { + // Active lights + private readonly lights: renderer.scene.Light[] = []; + // Active spot lights with shadows (Mutually exclusive with `lights`) + private readonly shadowEnabledSpotLights: renderer.scene.SpotLight[] = []; + + // Internal cached resources + private readonly _sphere = Sphere.create(0, 0, 0, 1); + private readonly _boundingBox = new AABB(); + private readonly _rangedDirLightBoundingBox = new AABB(0.0, 0.0, 0.0, 0.5, 0.5, 0.5); + + // ---------------------------------------------------------------- + // Interface + // ---------------------------------------------------------------- + public cullLights(scene: renderer.RenderScene, frustum: geometry.Frustum, cameraPos?: Vec3): void { + // TODO(zhouzhenglong): Make light culling native + this.lights.length = 0; + this.shadowEnabledSpotLights.length = 0; + // spot lights + for (const light of scene.spotLights) { + if (light.baked) { + continue; + } + Sphere.set(this._sphere, light.position.x, light.position.y, light.position.z, light.range); + if (intersect.sphereFrustum(this._sphere, frustum)) { + if (light.shadowEnabled) { + this.shadowEnabledSpotLights.push(light); + } else { + this.lights.push(light); + } + } + } + // sphere lights + for (const light of scene.sphereLights) { + if (light.baked) { + continue; + } + Sphere.set(this._sphere, light.position.x, light.position.y, light.position.z, light.range); + if (intersect.sphereFrustum(this._sphere, frustum)) { + this.lights.push(light); + } + } + // point lights + for (const light of scene.pointLights) { + if (light.baked) { + continue; + } + Sphere.set(this._sphere, light.position.x, light.position.y, light.position.z, light.range); + if (intersect.sphereFrustum(this._sphere, frustum)) { + this.lights.push(light); + } + } + // ranged dir lights + for (const light of scene.rangedDirLights) { + AABB.transform(this._boundingBox, this._rangedDirLightBoundingBox, light.node!.getWorldMatrix()); + if (intersect.aabbFrustum(this._boundingBox, frustum)) { + this.lights.push(light); + } + } - cameraConfigs.enableMainLightPlanarShadowMap = pipelineConfigs.shadowEnabled - && pipelineConfigs.usePlanarShadow - && !!camera.scene - && !!camera.scene.mainLight - && camera.scene.mainLight.shadowEnabled; + if (cameraPos) { + this.shadowEnabledSpotLights.sort( + (lhs, rhs) => Vec3.squaredDistance(cameraPos, lhs.position) - Vec3.squaredDistance(cameraPos, rhs.position), + ); + } + } + private _addLightQueues(camera: renderer.scene.Camera, pass: rendering.BasicRenderPassBuilder): void { + for (const light of this.lights) { + const queue = pass.addQueue(rendering.QueueHint.BLEND, 'forward-add'); + switch (light.type) { + case LightType.SPHERE: + queue.name = 'sphere-light'; + break; + case LightType.SPOT: + queue.name = 'spot-light'; + break; + case LightType.POINT: + queue.name = 'point-light'; + break; + case LightType.RANGED_DIRECTIONAL: + queue.name = 'ranged-directional-light'; + break; + default: + queue.name = 'unknown-light'; + } + queue.addScene( + camera, + rendering.SceneFlags.BLEND, + light, + ); + } + } + public addSpotlightShadowPasses( + ppl: rendering.BasicPipeline, + camera: renderer.scene.Camera, + maxNumShadowMaps: number, + ): void { + let i = 0; + for (const light of this.shadowEnabledSpotLights) { + const shadowMapSize = ppl.pipelineSceneData.shadows.size; + const shadowPass = ppl.addRenderPass(shadowMapSize.x, shadowMapSize.y, 'default'); + shadowPass.name = `SpotLightShadowPass${i}`; + shadowPass.addRenderTarget(`SpotShadowMap${i}`, LoadOp.CLEAR, StoreOp.STORE, new Color(1, 1, 1, 1)); + shadowPass.addDepthStencil(`SpotShadowDepth${i}`, LoadOp.CLEAR, StoreOp.DISCARD); + shadowPass.addQueue(rendering.QueueHint.NONE, 'shadow-caster') + .addScene(camera, rendering.SceneFlags.OPAQUE | rendering.SceneFlags.MASK | rendering.SceneFlags.SHADOW_CASTER) + .useLightFrustum(light); + ++i; + if (i >= maxNumShadowMaps) { + break; + } + } + } + public addLightQueues(pass: rendering.BasicRenderPassBuilder, + camera: renderer.scene.Camera, maxNumShadowMaps: number): void { + this._addLightQueues(camera, pass); + let i = 0; + for (const light of this.shadowEnabledSpotLights) { + // Add spot-light pass + // Save last RenderPass to the `pass` variable + // TODO(zhouzhenglong): Fix per queue addTexture + pass.addTexture(`SpotShadowMap${i}`, 'cc_spotShadowMap'); + const queue = pass.addQueue(rendering.QueueHint.BLEND, 'forward-add'); + queue.addScene(camera, rendering.SceneFlags.BLEND, light); + ++i; + if (i >= maxNumShadowMaps) { + break; + } + } + } - cameraConfigs.enablePlanarReflectionProbe = isMainGameWindow || camera.cameraUsage === CameraUsage.SCENE_VIEW; + // Notice: ForwardLighting cannot handle a lot of lights. + // If there are too many lights, the performance will be very poor. + // If many lights are needed, please implement a forward+ or deferred rendering pipeline. + public addLightPasses( + colorName: string, + depthStencilName: string, + depthStencilStoreOp: gfx.StoreOp, + id: number, // window id + width: number, + height: number, + camera: renderer.scene.Camera, + viewport: gfx.Viewport, + ppl: rendering.BasicPipeline, + pass: rendering.BasicRenderPassBuilder, + ): rendering.BasicRenderPassBuilder { + this._addLightQueues(camera, pass); + + let count = 0; + const shadowMapSize = ppl.pipelineSceneData.shadows.size; + for (const light of this.shadowEnabledSpotLights) { + const shadowPass = ppl.addRenderPass(shadowMapSize.x, shadowMapSize.y, 'default'); + shadowPass.name = 'SpotlightShadowPass'; + // Reuse csm shadow map + shadowPass.addRenderTarget(`ShadowMap${id}`, LoadOp.CLEAR, StoreOp.STORE, new Color(1, 1, 1, 1)); + shadowPass.addDepthStencil(`ShadowDepth${id}`, LoadOp.CLEAR, StoreOp.DISCARD); + shadowPass.addQueue(rendering.QueueHint.NONE, 'shadow-caster') + .addScene(camera, rendering.SceneFlags.OPAQUE | rendering.SceneFlags.MASK | rendering.SceneFlags.SHADOW_CASTER) + .useLightFrustum(light); + + // Add spot-light pass + // Save last RenderPass to the `pass` variable + ++count; + const storeOp = count === this.shadowEnabledSpotLights.length + ? depthStencilStoreOp + : StoreOp.STORE; + + pass = ppl.addRenderPass(width, height, 'default'); + pass.name = 'SpotlightWithShadowMap'; + pass.setViewport(viewport); + pass.addRenderTarget(colorName, LoadOp.LOAD); + pass.addDepthStencil(depthStencilName, LoadOp.LOAD, storeOp); + pass.addTexture(`ShadowMap${id}`, 'cc_spotShadowMap'); + const queue = pass.addQueue(rendering.QueueHint.BLEND, 'forward-add'); + queue.addScene( + camera, + rendering.SceneFlags.BLEND, + light, + ); + } + return pass; + } - cameraConfigs.enableProfiler = DEBUG && isMainGameWindow; + public isMultipleLightPassesNeeded(): boolean { + return this.shadowEnabledSpotLights.length > 0; + } +} - cameraConfigs.settings = camera.pipelineSettings - ? camera.pipelineSettings as PipelineSettings : defaultSettings; +export interface ForwardPassConfigs { + enableMainLightShadowMap: boolean; + enableMainLightPlanarShadowMap: boolean; + enablePlanarReflectionProbe: boolean; + enableMSAA: boolean; + enableSingleForwardPass: boolean; +} - setupPostProcessConfigs(pipelineConfigs, cameraConfigs.settings, cameraConfigs); +export class BuiltinForwardPassBuilder implements rendering.PipelinePassBuilder { + static ConfigOrder = 100; + static RenderOrder = 100; + getConfigOrder(): number { + return BuiltinForwardPassBuilder.ConfigOrder; + } + getRenderOrder(): number { + return BuiltinForwardPassBuilder.RenderOrder; + } + configCamera( + camera: Readonly, + pipelineConfigs: Readonly, + cameraConfigs: CameraConfigs & ForwardPassConfigs): void { + // Shadow + cameraConfigs.enableMainLightShadowMap = pipelineConfigs.shadowEnabled + && !pipelineConfigs.usePlanarShadow + && !!camera.scene + && !!camera.scene.mainLight + && camera.scene.mainLight.shadowEnabled; + + cameraConfigs.enableMainLightPlanarShadowMap = pipelineConfigs.shadowEnabled + && pipelineConfigs.usePlanarShadow + && !!camera.scene + && !!camera.scene.mainLight + && camera.scene.mainLight.shadowEnabled; + + // Reflection Probe + cameraConfigs.enablePlanarReflectionProbe = + cameraConfigs.isMainGameWindow || camera.cameraUsage === CameraUsage.SCENE_VIEW; + + // MSAA + cameraConfigs.enableMSAA = cameraConfigs.settings.msaa.enabled + && !cameraConfigs.enableStoreSceneDepth // Cannot store MS depth, resolve depth is also not cross-platform + && !pipelineConfigs.isWeb // TODO(zhouzhenglong): remove this constraint + && !pipelineConfigs.isWebGL1; + + // Forward rendering (Depend on MSAA and TBR) + cameraConfigs.enableSingleForwardPass + = pipelineConfigs.isMobile || cameraConfigs.enableMSAA; + + ++cameraConfigs.remainingPasses; + } + windowResize( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: Readonly, + window: renderer.RenderWindow, + camera: renderer.scene.Camera, + nativeWidth: number, + nativeHeight: number): void { + const ResourceFlags = rendering.ResourceFlags; + const ResourceResidency = rendering.ResourceResidency; + const id = window.renderWindowId; + const settings = cameraConfigs.settings; + + const width = cameraConfigs.enableShadingScale + ? Math.max(Math.floor(nativeWidth * cameraConfigs.shadingScale), 1) + : nativeWidth; + const height = cameraConfigs.enableShadingScale + ? Math.max(Math.floor(nativeHeight * cameraConfigs.shadingScale), 1) + : nativeHeight; + + // MsaaRadiance + if (cameraConfigs.enableMSAA) { + // Notice: We never store multisample results. + // These samples are always resolved and discarded at the end of the render pass. + // So the ResourceResidency should be MEMORYLESS. + if (cameraConfigs.enableHDR) { + ppl.addTexture(`MsaaRadiance${id}`, TextureType.TEX2D, cameraConfigs.radianceFormat, width, height, 1, 1, 1, + settings.msaa.sampleCount, ResourceFlags.COLOR_ATTACHMENT, ResourceResidency.MEMORYLESS); + } else { + ppl.addTexture(`MsaaRadiance${id}`, TextureType.TEX2D, Format.RGBA8, width, height, 1, 1, 1, + settings.msaa.sampleCount, ResourceFlags.COLOR_ATTACHMENT, ResourceResidency.MEMORYLESS); + } + ppl.addTexture(`MsaaDepthStencil${id}`, TextureType.TEX2D, Format.DEPTH_STENCIL, width, height, 1, 1, 1, + settings.msaa.sampleCount, ResourceFlags.DEPTH_STENCIL_ATTACHMENT, ResourceResidency.MEMORYLESS); + } - if (isEditorView) { - const editorSettings = rendering.getEditorPipelineSettings() as PipelineSettings | null; - if (editorSettings) { - cameraConfigs.settings = editorSettings; - setupPostProcessConfigs(pipelineConfigs, - cameraConfigs.settings, cameraConfigs); + // Mainlight ShadowMap + ppl.addRenderTarget( + `ShadowMap${id}`, + pplConfigs.shadowMapFormat, + pplConfigs.shadowMapSize.x, + pplConfigs.shadowMapSize.y, + ); + ppl.addDepthStencil( + `ShadowDepth${id}`, + Format.DEPTH_STENCIL, + pplConfigs.shadowMapSize.x, + pplConfigs.shadowMapSize.y, + ); + + // Spot-light shadow maps + if (cameraConfigs.enableSingleForwardPass) { + const count = pplConfigs.mobileMaxSpotLightShadowMaps; + for (let i = 0; i !== count; ++i) { + ppl.addRenderTarget( + `SpotShadowMap${i}`, + pplConfigs.shadowMapFormat, + pplConfigs.shadowMapSize.x, + pplConfigs.shadowMapSize.y, + ); + ppl.addDepthStencil( + `SpotShadowDepth${i}`, + Format.DEPTH_STENCIL, + pplConfigs.shadowMapSize.x, + pplConfigs.shadowMapSize.y, + ); + } } } + setup( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & ForwardPassConfigs, + camera: renderer.scene.Camera, + context: PipelineContext): rendering.BasicRenderPassBuilder | undefined { + const id = camera.window.renderWindowId; + + const scene = camera.scene!; + const mainLight = scene.mainLight; + + --cameraConfigs.remainingPasses; + assert(cameraConfigs.remainingPasses >= 0); + + // Forward Lighting (Light Culling) + this.forwardLighting.cullLights(scene, camera.frustum); + + // Main Directional light CSM Shadow Map + if (cameraConfigs.enableMainLightShadowMap) { + assert(!!mainLight); + this._addCascadedShadowMapPass(ppl, pplConfigs, id, mainLight, camera); + } - // MSAA - cameraConfigs.enableMSAA = cameraConfigs.settings.msaa.enabled - && !pipelineConfigs.isWeb // TODO(zhouzhenglong): remove this constraint - && !pipelineConfigs.isWebGL1; - - // Shading scale - cameraConfigs.shadingScale = cameraConfigs.settings.shadingScale; - cameraConfigs.enableShadingScale = cameraConfigs.settings.enableShadingScale - && cameraConfigs.shadingScale !== 1.0; - - // FSR (Depend on Shading scale) - cameraConfigs.enableFSR = cameraConfigs.settings.fsr.enabled - && !!cameraConfigs.settings.fsr.material - && cameraConfigs.enableShadingScale - && cameraConfigs.shadingScale < 1.0; - - // Forward rendering (Depend on MSAA and TBR) - cameraConfigs.singleForwardRadiancePass - = pipelineConfigs.isMobile || cameraConfigs.enableMSAA; + // Spot light shadow maps (Mobile or MSAA) + if (cameraConfigs.enableSingleForwardPass) { + // Currently, only support 1 spot light with shadow map on mobile platform. + // TODO(zhouzhenglong): Relex this limitation. + this.forwardLighting.addSpotlightShadowPasses( + ppl, camera, pplConfigs.mobileMaxSpotLightShadowMaps); + } - cameraConfigs.enableHDR = cameraConfigs.useFullPipeline - && pipelineConfigs.useFloatOutput; + this._tryAddReflectionProbePasses(ppl, cameraConfigs, id, mainLight, camera.scene); - cameraConfigs.radianceFormat = cameraConfigs.enableHDR - ? gfx.Format.RGBA16F : gfx.Format.RGBA8; -} + if (cameraConfigs.remainingPasses > 0 || cameraConfigs.enableShadingScale) { + context.colorName = cameraConfigs.enableShadingScale + ? `ScaledRadiance0_${id}` + : `Radiance0_${id}`; + context.depthStencilName = cameraConfigs.enableShadingScale + ? `ScaledSceneDepth_${id}` + : `SceneDepth_${id}`; + } else { + context.colorName = cameraConfigs.colorName; + context.depthStencilName = cameraConfigs.depthStencilName; + } -if (rendering) { + const pass = this._addForwardRadiancePasses( + ppl, pplConfigs, cameraConfigs, id, camera, + cameraConfigs.width, cameraConfigs.height, mainLight, + context.colorName, context.depthStencilName, + !cameraConfigs.enableMSAA, + cameraConfigs.enableStoreSceneDepth ? StoreOp.STORE : StoreOp.DISCARD); - const { QueueHint, SceneFlags, ResourceFlags, ResourceResidency } = rendering; + if (!cameraConfigs.enableStoreSceneDepth) { + context.depthStencilName = ''; + } - class ForwardLighting { - // Active lights - private readonly lights: renderer.scene.Light[] = []; - // Active spot lights with shadows (Mutually exclusive with `lights`) - private readonly shadowEnabledSpotLights: renderer.scene.SpotLight[] = []; + if (cameraConfigs.remainingPasses === 0 && cameraConfigs.enableShadingScale) { + return addCopyToScreenPass(ppl, pplConfigs, cameraConfigs, context.colorName); + } else { + return pass; + } + } + private _addCascadedShadowMapPass( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + id: number, + light: renderer.scene.DirectionalLight, + camera: renderer.scene.Camera, + ): void { + const QueueHint = rendering.QueueHint; + const SceneFlags = rendering.SceneFlags; + // ---------------------------------------------------------------- + // Dynamic states + // ---------------------------------------------------------------- + const shadowSize = ppl.pipelineSceneData.shadows.size; + const width = shadowSize.x; + const height = shadowSize.y; - // Internal cached resources - private readonly _sphere = Sphere.create(0, 0, 0, 1); - private readonly _boundingBox = new AABB(); - private readonly _rangedDirLightBoundingBox = new AABB(0.0, 0.0, 0.0, 0.5, 0.5, 0.5); + const viewport = this._viewport; + viewport.left = viewport.top = 0; + viewport.width = width; + viewport.height = height; // ---------------------------------------------------------------- - // Interface + // CSM Shadow Map // ---------------------------------------------------------------- - public cullLights(scene: renderer.RenderScene, frustum: geometry.Frustum, cameraPos?: Vec3): void { - // TODO(zhouzhenglong): Make light culling native - this.lights.length = 0; - this.shadowEnabledSpotLights.length = 0; - // spot lights - for (const light of scene.spotLights) { - if (light.baked) { - continue; - } - Sphere.set(this._sphere, light.position.x, light.position.y, light.position.z, light.range); - if (intersect.sphereFrustum(this._sphere, frustum)) { - if (light.shadowEnabled) { - this.shadowEnabledSpotLights.push(light); - } else { - this.lights.push(light); - } - } + const pass = ppl.addRenderPass(width, height, 'default'); + pass.name = 'CascadedShadowMap'; + pass.addRenderTarget(`ShadowMap${id}`, LoadOp.CLEAR, StoreOp.STORE, new Color(1, 1, 1, 1)); + pass.addDepthStencil(`ShadowDepth${id}`, LoadOp.CLEAR, StoreOp.DISCARD); + const csmLevel = ppl.pipelineSceneData.csmSupported ? light.csmLevel : 1; + + // Add shadow map viewports + for (let level = 0; level !== csmLevel; ++level) { + getCsmMainLightViewport(light, width, height, level, this._viewport, pplConfigs.screenSpaceSignY); + const queue = pass.addQueue(QueueHint.NONE, 'shadow-caster'); + if (!pplConfigs.isWebGPU) { // Temporary workaround for WebGPU + queue.setViewport(this._viewport); } - // sphere lights - for (const light of scene.sphereLights) { - if (light.baked) { - continue; - } - Sphere.set(this._sphere, light.position.x, light.position.y, light.position.z, light.range); - if (intersect.sphereFrustum(this._sphere, frustum)) { - this.lights.push(light); - } + queue + .addScene(camera, SceneFlags.OPAQUE | SceneFlags.MASK | SceneFlags.SHADOW_CASTER) + .useLightFrustum(light, level); + } + } + private _tryAddReflectionProbePasses( + ppl: rendering.BasicPipeline, + cameraConfigs: Readonly, + id: number, + mainLight: renderer.scene.DirectionalLight | null, + scene: renderer.RenderScene | null, + ): void { + const reflectionProbeManager = cclegacy.internal.reflectionProbeManager as ReflectionProbeManager | undefined; + if (!reflectionProbeManager) { + return; + } + const ResourceResidency = rendering.ResourceResidency; + const probes = reflectionProbeManager.getProbes(); + const maxProbeCount = 4; + let probeID = 0; + for (const probe of probes) { + if (!probe.needRender) { + continue; } - // point lights - for (const light of scene.pointLights) { - if (light.baked) { + const area = probe.renderArea(); + const width = Math.max(Math.floor(area.x), 1); + const height = Math.max(Math.floor(area.y), 1); + + if (probe.probeType === renderer.scene.ProbeType.PLANAR) { + if (!cameraConfigs.enablePlanarReflectionProbe) { continue; } - Sphere.set(this._sphere, light.position.x, light.position.y, light.position.z, light.range); - if (intersect.sphereFrustum(this._sphere, frustum)) { - this.lights.push(light); - } - } - // ranged dir lights - for (const light of scene.rangedDirLights) { - AABB.transform(this._boundingBox, this._rangedDirLightBoundingBox, light.node!.getWorldMatrix()); - if (intersect.aabbFrustum(this._boundingBox, frustum)) { - this.lights.push(light); - } - } + const window: renderer.RenderWindow = probe.realtimePlanarTexture!.window!; + const colorName = `PlanarProbeRT${probeID}`; + const depthStencilName = `PlanarProbeDS${probeID}`; + // ProbeResource + ppl.addRenderWindow(colorName, + cameraConfigs.radianceFormat, width, height, window); + ppl.addDepthStencil(depthStencilName, + gfx.Format.DEPTH_STENCIL, width, height, ResourceResidency.MEMORYLESS); + + // Rendering + const probePass = ppl.addRenderPass(width, height, 'default'); + probePass.name = `PlanarReflectionProbe${probeID}`; + this._buildReflectionProbePass(probePass, cameraConfigs, id, probe.camera, + colorName, depthStencilName, mainLight, scene); + } else if (EDITOR) { + for (let faceIdx = 0; faceIdx < probe.bakedCubeTextures.length; faceIdx++) { + probe.updateCameraDir(faceIdx); + const window: renderer.RenderWindow = probe.bakedCubeTextures[faceIdx].window!; + const colorName = `CubeProbeRT${probeID}${faceIdx}`; + const depthStencilName = `CubeProbeDS${probeID}${faceIdx}`; + // ProbeResource + ppl.addRenderWindow(colorName, + cameraConfigs.radianceFormat, width, height, window); + ppl.addDepthStencil(depthStencilName, + gfx.Format.DEPTH_STENCIL, width, height, ResourceResidency.MEMORYLESS); - if (cameraPos) { - this.shadowEnabledSpotLights.sort( - (lhs, rhs) => Vec3.squaredDistance(cameraPos, lhs.position) - Vec3.squaredDistance(cameraPos, rhs.position), - ); - } - } - private _addLightQueues(camera: renderer.scene.Camera, pass: rendering.BasicRenderPassBuilder): void { - for (const light of this.lights) { - const queue = pass.addQueue(QueueHint.BLEND, 'forward-add'); - switch (light.type) { - case LightType.SPHERE: - queue.name = 'sphere-light'; - break; - case LightType.SPOT: - queue.name = 'spot-light'; - break; - case LightType.POINT: - queue.name = 'point-light'; - break; - case LightType.RANGED_DIRECTIONAL: - queue.name = 'ranged-directional-light'; - break; - default: - queue.name = 'unknown-light'; + // Rendering + const probePass = ppl.addRenderPass(width, height, 'default'); + probePass.name = `CubeProbe${probeID}${faceIdx}`; + this._buildReflectionProbePass(probePass, cameraConfigs, id, probe.camera, + colorName, depthStencilName, mainLight, scene); } - queue.addScene( - camera, - SceneFlags.BLEND, - light, - ); + probe.needRender = false; } - } - public addSpotlightShadowPasses( - ppl: rendering.BasicPipeline, - camera: renderer.scene.Camera, - maxNumShadowMaps: number, - ): void { - let i = 0; - for (const light of this.shadowEnabledSpotLights) { - const shadowMapSize = ppl.pipelineSceneData.shadows.size; - const shadowPass = ppl.addRenderPass(shadowMapSize.x, shadowMapSize.y, 'default'); - shadowPass.name = `SpotLightShadowPass${i}`; - shadowPass.addRenderTarget(`SpotShadowMap${i}`, LoadOp.CLEAR, StoreOp.STORE, new Color(1, 1, 1, 1)); - shadowPass.addDepthStencil(`SpotShadowDepth${i}`, LoadOp.CLEAR, StoreOp.DISCARD); - shadowPass.addQueue(QueueHint.NONE, 'shadow-caster') - .addScene(camera, SceneFlags.OPAQUE | SceneFlags.MASK | SceneFlags.SHADOW_CASTER) - .useLightFrustum(light); - ++i; - if (i >= maxNumShadowMaps) { - break; - } + ++probeID; + if (probeID === maxProbeCount) { + break; } } - public addLightQueues(pass: rendering.BasicRenderPassBuilder, - camera: renderer.scene.Camera, maxNumShadowMaps: number): void { - this._addLightQueues(camera, pass); - let i = 0; - for (const light of this.shadowEnabledSpotLights) { - // Add spot-light pass - // Save last RenderPass to the `pass` variable - // TODO(zhouzhenglong): Fix per queue addTexture - pass.addTexture(`SpotShadowMap${i}`, 'cc_spotShadowMap'); - const queue = pass.addQueue(QueueHint.BLEND, 'forward-add'); - queue.addScene(camera, SceneFlags.BLEND, light); - ++i; - if (i >= maxNumShadowMaps) { - break; - } - } + } + private _buildReflectionProbePass( + pass: rendering.BasicRenderPassBuilder, + cameraConfigs: Readonly, + id: number, + camera: renderer.scene.Camera, + colorName: string, + depthStencilName: string, + mainLight: renderer.scene.DirectionalLight | null, + scene: renderer.RenderScene | null = null, + ): void { + const QueueHint = rendering.QueueHint; + const SceneFlags = rendering.SceneFlags; + // set viewport + const colorStoreOp = cameraConfigs.enableMSAA ? StoreOp.DISCARD : StoreOp.STORE; + + // bind output render target + if (forwardNeedClearColor(camera)) { + this._reflectionProbeClearColor.x = camera.clearColor.x; + this._reflectionProbeClearColor.y = camera.clearColor.y; + this._reflectionProbeClearColor.z = camera.clearColor.z; + const clearColor = rendering.packRGBE(this._reflectionProbeClearColor); + this._clearColor.x = clearColor.x; + this._clearColor.y = clearColor.y; + this._clearColor.z = clearColor.z; + this._clearColor.w = clearColor.w; + pass.addRenderTarget(colorName, LoadOp.CLEAR, colorStoreOp, this._clearColor); + } else { + pass.addRenderTarget(colorName, LoadOp.LOAD, colorStoreOp); } - // Notice: ForwardLighting cannot handle a lot of lights. - // If there are too many lights, the performance will be very poor. - // If many lights are needed, please implement a forward+ or deferred rendering pipeline. - public addLightPasses( - colorName: string, - depthStencilName: string, - depthStencilStoreOp: gfx.StoreOp, - id: number, // window id - width: number, - height: number, - camera: renderer.scene.Camera, - viewport: gfx.Viewport, - ppl: rendering.BasicPipeline, - pass: rendering.BasicRenderPassBuilder, - ): rendering.BasicRenderPassBuilder { - this._addLightQueues(camera, pass); - - let count = 0; - const shadowMapSize = ppl.pipelineSceneData.shadows.size; - for (const light of this.shadowEnabledSpotLights) { - const shadowPass = ppl.addRenderPass(shadowMapSize.x, shadowMapSize.y, 'default'); - shadowPass.name = 'SpotlightShadowPass'; - // Reuse csm shadow map - shadowPass.addRenderTarget(`ShadowMap${id}`, LoadOp.CLEAR, StoreOp.STORE, new Color(1, 1, 1, 1)); - shadowPass.addDepthStencil(`ShadowDepth${id}`, LoadOp.CLEAR, StoreOp.DISCARD); - shadowPass.addQueue(QueueHint.NONE, 'shadow-caster') - .addScene(camera, SceneFlags.OPAQUE | SceneFlags.MASK | SceneFlags.SHADOW_CASTER) - .useLightFrustum(light); - - // Add spot-light pass - // Save last RenderPass to the `pass` variable - ++count; - const storeOp = count === this.shadowEnabledSpotLights.length - ? depthStencilStoreOp - : StoreOp.STORE; - - pass = ppl.addRenderPass(width, height, 'default'); - pass.name = 'SpotlightWithShadowMap'; - pass.setViewport(viewport); - pass.addRenderTarget(colorName, LoadOp.LOAD); - pass.addDepthStencil(depthStencilName, LoadOp.LOAD, storeOp); - pass.addTexture(`ShadowMap${id}`, 'cc_spotShadowMap'); - const queue = pass.addQueue(QueueHint.BLEND, 'forward-add'); - queue.addScene( - camera, - SceneFlags.BLEND, - light, - ); - } - return pass; + // bind depth stencil buffer + if (camera.clearFlag & ClearFlagBit.DEPTH_STENCIL) { + pass.addDepthStencil( + depthStencilName, + LoadOp.CLEAR, + StoreOp.DISCARD, + camera.clearDepth, + camera.clearStencil, + camera.clearFlag & ClearFlagBit.DEPTH_STENCIL, + ); + } else { + pass.addDepthStencil(depthStencilName, LoadOp.LOAD, StoreOp.DISCARD); } - public isMultipleLightPassesNeeded(): boolean { - return this.shadowEnabledSpotLights.length > 0; + // Set shadow map if enabled + if (cameraConfigs.enableMainLightShadowMap) { + pass.addTexture(`ShadowMap${id}`, 'cc_shadowMap'); } + + // TODO(zhouzhenglong): Separate OPAQUE and MASK queue + + // add opaque and mask queue + pass.addQueue(QueueHint.NONE, 'reflect-map') // Currently we put OPAQUE and MASK into one queue, so QueueHint is NONE + .addScene(camera, + SceneFlags.OPAQUE | SceneFlags.MASK | SceneFlags.REFLECTION_PROBE, + mainLight || undefined, + scene ? scene : undefined); } + private _addForwardRadiancePasses( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: Readonly, + id: number, + camera: renderer.scene.Camera, + width: number, + height: number, + mainLight: renderer.scene.DirectionalLight | null, + colorName: string, + depthStencilName: string, + disableMSAA: boolean = false, + depthStencilStoreOp: gfx.StoreOp = StoreOp.DISCARD, + ): rendering.BasicRenderPassBuilder { + const QueueHint = rendering.QueueHint; + const SceneFlags = rendering.SceneFlags; + // ---------------------------------------------------------------- + // Dynamic states + // ---------------------------------------------------------------- + // Prepare camera clear color + const clearColor = camera.clearColor; // Reduce C++/TS interop + this._clearColor.x = clearColor.x; + this._clearColor.y = clearColor.y; + this._clearColor.z = clearColor.z; + this._clearColor.w = clearColor.w; + + // Prepare camera viewport + const viewport = camera.viewport; // Reduce C++/TS interop + this._viewport.left = Math.round(viewport.x * width); + this._viewport.top = Math.round(viewport.y * height); + // Here we must use camera.viewport.width instead of camera.viewport.z, which + // is undefined on native platform. The same as camera.viewport.height. + this._viewport.width = Math.max(Math.round(viewport.width * width), 1); + this._viewport.height = Math.max(Math.round(viewport.height * height), 1); + + // MSAA + const enableMSAA = !disableMSAA && cameraConfigs.enableMSAA; + assert(!enableMSAA || cameraConfigs.enableSingleForwardPass); - class BuiltinPipelineBuilder implements rendering.PipelineBuilder { - private readonly _pipelineEvent: PipelineEventProcessor = cclegacy.director.root.pipelineEvent as PipelineEventProcessor; - // Internal cached resources - private readonly _clearColor = new Color(0, 0, 0, 1); - private readonly _clearColorTransparentBlack = new Color(0, 0, 0, 0); - private readonly _reflectionProbeClearColor = new Vec3(0, 0, 0); - private readonly _viewport = new Viewport(); - private readonly _configs = new PipelineConfigs(); - private readonly _cameraConfigs = new CameraConfigs(); - // DepthOfField - private readonly _cocParams = new Vec4(0, 0, 0, 0); - private readonly _focusPos = new Vec4(0, 0, 0, 1); - private readonly _cocTexSize = new Vec4(0, 0, 0, 0); - // Bloom - private readonly _bloomParams = new Vec4(0, 0, 0, 0); - private readonly _bloomTexSize = new Vec4(0, 0, 0, 0); - private readonly _bloomWidths: Array = []; - private readonly _bloomHeights: Array = []; - private readonly _bloomTexNames: Array = []; - // Color Grading - private readonly _colorGradingTexSize = new Vec2(0, 0); - // FXAA - private readonly _fxaaParams = new Vec4(0, 0, 0, 0); - // FSR - private readonly _fsrParams = new Vec4(0, 0, 0, 0); - private readonly _fsrTexSize = new Vec4(0, 0, 0, 0); - // Materials - private readonly _copyAndTonemapMaterial = new Material(); + // ---------------------------------------------------------------- + // Forward Lighting (Main Directional Light) + // ---------------------------------------------------------------- + const pass = cameraConfigs.enableSingleForwardPass + ? this._addForwardSingleRadiancePass(ppl, pplConfigs, cameraConfigs, + id, camera, enableMSAA, width, height, mainLight, + colorName, depthStencilName, depthStencilStoreOp) + : this._addForwardMultipleRadiancePasses(ppl, cameraConfigs, + id, camera, width, height, mainLight, + colorName, depthStencilName, depthStencilStoreOp); + + // Planar Shadow + if (cameraConfigs.enableMainLightPlanarShadowMap) { + this._addPlanarShadowQueue(camera, mainLight, pass); + } - // Internal States - private _initialized = false; // TODO(zhouzhenglong): Make default effect asset loading earlier and remove this flag + // ---------------------------------------------------------------- + // Forward Lighting (Blend) + // ---------------------------------------------------------------- + // Add transparent queue - // Forward lighting - private readonly forwardLighting = new ForwardLighting(); + const sceneFlags = SceneFlags.BLEND | + (camera.geometryRenderer + ? SceneFlags.GEOMETRY + : SceneFlags.NONE); + pass + .addQueue(QueueHint.BLEND) + .addScene(camera, sceneFlags, mainLight || undefined); + + return pass; + } + private _addForwardSingleRadiancePass( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: Readonly, + id: number, + camera: renderer.scene.Camera, + enableMSAA: boolean, + width: number, + height: number, + mainLight: renderer.scene.DirectionalLight | null, + colorName: string, + depthStencilName: string, + depthStencilStoreOp: gfx.StoreOp + ): rendering.BasicRenderPassBuilder { + assert(cameraConfigs.enableSingleForwardPass); // ---------------------------------------------------------------- - // Interface + // Forward Lighting (Main Directional Light) // ---------------------------------------------------------------- - windowResize( - ppl: rendering.BasicPipeline, - window: renderer.RenderWindow, - camera: renderer.scene.Camera, - nativeWidth: number, - nativeHeight: number, - ): void { - setupPipelineConfigs(ppl, this._configs); - setupCameraConfigs(camera, this._configs, this._cameraConfigs); - const settings = this._cameraConfigs.settings; - const id = window.renderWindowId; + let pass: rendering.BasicRenderPassBuilder; + if (enableMSAA) { + const msaaRadianceName = `MsaaRadiance${id}`; + const msaaDepthStencilName = `MsaaDepthStencil${id}`; + const sampleCount = cameraConfigs.settings.msaa.sampleCount; - const width = this._cameraConfigs.enableShadingScale - ? Math.max(Math.floor(nativeWidth * this._cameraConfigs.shadingScale), 1) - : nativeWidth; - const height = this._cameraConfigs.enableShadingScale - ? Math.max(Math.floor(nativeHeight * this._cameraConfigs.shadingScale), 1) - : nativeHeight; + const msPass = ppl.addMultisampleRenderPass(width, height, sampleCount, 0, 'default'); + msPass.name = 'MsaaForwardPass'; - // Render Window (UI) - ppl.addRenderWindow(this._cameraConfigs.colorName, - Format.RGBA8, nativeWidth, nativeHeight, window, - this._cameraConfigs.depthStencilName); + // MSAA always discards depth stencil + this._buildForwardMainLightPass(msPass, cameraConfigs, id, camera, + msaaRadianceName, msaaDepthStencilName, StoreOp.DISCARD, mainLight); - if (this._cameraConfigs.enableShadingScale) { - ppl.addDepthStencil(`ScaledSceneDepth${id}`, Format.DEPTH_STENCIL, width, height); - ppl.addRenderTarget(`ScaledRadiance${id}`, this._cameraConfigs.radianceFormat, width, height); - ppl.addRenderTarget(`ScaledLdrColor${id}`, Format.RGBA8, width, height); - } else { - ppl.addDepthStencil(`SceneDepth${id}`, Format.DEPTH_STENCIL, width, height); - ppl.addRenderTarget(`Radiance${id}`, this._cameraConfigs.radianceFormat, width, height); - ppl.addRenderTarget(`LdrColor${id}`, Format.RGBA8, width, height); - } + msPass.resolveRenderTarget(msaaRadianceName, colorName); - if (this._cameraConfigs.enableFSR) { - ppl.addRenderTarget(`FsrColor${id}`, Format.RGBA8, nativeWidth, nativeHeight); - } + pass = msPass; + } else { + pass = ppl.addRenderPass(width, height, 'default'); + pass.name = 'ForwardPass'; - // MsaaRadiance - if (this._cameraConfigs.enableMSAA) { - // Notice: We never store multisample results. - // These samples are always resolved and discarded at the end of the render pass. - // So the ResourceResidency should be MEMORYLESS. - if (this._cameraConfigs.enableHDR) { - ppl.addTexture(`MsaaRadiance${id}`, TextureType.TEX2D, this._cameraConfigs.radianceFormat, width, height, 1, 1, 1, - settings.msaa.sampleCount, ResourceFlags.COLOR_ATTACHMENT, ResourceResidency.MEMORYLESS); - } else { - ppl.addTexture(`MsaaRadiance${id}`, TextureType.TEX2D, Format.RGBA8, width, height, 1, 1, 1, - settings.msaa.sampleCount, ResourceFlags.COLOR_ATTACHMENT, ResourceResidency.MEMORYLESS); - } - ppl.addTexture(`MsaaDepthStencil${id}`, TextureType.TEX2D, Format.DEPTH_STENCIL, width, height, 1, 1, 1, - settings.msaa.sampleCount, ResourceFlags.DEPTH_STENCIL_ATTACHMENT, ResourceResidency.MEMORYLESS); + this._buildForwardMainLightPass(pass, cameraConfigs, id, camera, + colorName, depthStencilName, depthStencilStoreOp, mainLight); + } + assert(pass !== undefined); + + // Forward Lighting (Additive Lights) + this.forwardLighting.addLightQueues( + pass, + camera, + pplConfigs.mobileMaxSpotLightShadowMaps, + ); + + return pass; + } + private _addForwardMultipleRadiancePasses( + ppl: rendering.BasicPipeline, + cameraConfigs: Readonly, + id: number, + camera: renderer.scene.Camera, + width: number, + height: number, + mainLight: renderer.scene.DirectionalLight | null, + colorName: string, + depthStencilName: string, + depthStencilStoreOp: gfx.StoreOp + ): rendering.BasicRenderPassBuilder { + assert(!cameraConfigs.enableSingleForwardPass); + + // Forward Lighting (Main Directional Light) + let pass = ppl.addRenderPass(width, height, 'default'); + pass.name = 'ForwardPass'; + + const firstStoreOp = this.forwardLighting.isMultipleLightPassesNeeded() + ? StoreOp.STORE + : depthStencilStoreOp; + + this._buildForwardMainLightPass(pass, cameraConfigs, + id, camera, colorName, depthStencilName, firstStoreOp, mainLight); + + // Forward Lighting (Additive Lights) + pass = this.forwardLighting + .addLightPasses(colorName, depthStencilName, depthStencilStoreOp, + id, width, height, camera, this._viewport, ppl, pass); + + return pass; + } + private _buildForwardMainLightPass( + pass: rendering.BasicRenderPassBuilder, + cameraConfigs: Readonly, + id: number, + camera: renderer.scene.Camera, + colorName: string, + depthStencilName: string, + depthStencilStoreOp: gfx.StoreOp, + mainLight: renderer.scene.DirectionalLight | null, + scene: renderer.RenderScene | null = null, + ): void { + const QueueHint = rendering.QueueHint; + const SceneFlags = rendering.SceneFlags; + // set viewport + pass.setViewport(this._viewport); + + const colorStoreOp = cameraConfigs.enableMSAA ? StoreOp.DISCARD : StoreOp.STORE; + + // bind output render target + if (forwardNeedClearColor(camera)) { + pass.addRenderTarget(colorName, LoadOp.CLEAR, colorStoreOp, this._clearColor); + } else { + pass.addRenderTarget(colorName, LoadOp.LOAD, colorStoreOp); + } + + // bind depth stencil buffer + if (DEBUG) { + if (colorName === cameraConfigs.colorName && + depthStencilName !== cameraConfigs.depthStencilName) { + warn('Default framebuffer cannot use custom depth stencil buffer'); } + } - // Mainlight ShadowMap - ppl.addRenderTarget( - `ShadowMap${id}`, - this._configs.shadowMapFormat, - this._configs.shadowMapSize.x, - this._configs.shadowMapSize.y, + if (camera.clearFlag & ClearFlagBit.DEPTH_STENCIL) { + pass.addDepthStencil( + depthStencilName, + LoadOp.CLEAR, + depthStencilStoreOp, + camera.clearDepth, + camera.clearStencil, + camera.clearFlag & ClearFlagBit.DEPTH_STENCIL, ); - ppl.addDepthStencil( - `ShadowDepth${id}`, - Format.DEPTH_STENCIL, - this._configs.shadowMapSize.x, - this._configs.shadowMapSize.y, + } else { + pass.addDepthStencil(depthStencilName, LoadOp.LOAD, depthStencilStoreOp); + } + + // Set shadow map if enabled + if (cameraConfigs.enableMainLightShadowMap) { + pass.addTexture(`ShadowMap${id}`, 'cc_shadowMap'); + } + + // TODO(zhouzhenglong): Separate OPAQUE and MASK queue + + // add opaque and mask queue + pass.addQueue(QueueHint.NONE) // Currently we put OPAQUE and MASK into one queue, so QueueHint is NONE + .addScene(camera, + SceneFlags.OPAQUE | SceneFlags.MASK, + mainLight || undefined, + scene ? scene : undefined); + } + private _addPlanarShadowQueue( + camera: renderer.scene.Camera, + mainLight: renderer.scene.DirectionalLight | null, + pass: rendering.BasicRenderPassBuilder, + ) { + const QueueHint = rendering.QueueHint; + const SceneFlags = rendering.SceneFlags; + pass.addQueue(QueueHint.BLEND, 'planar-shadow') + .addScene( + camera, + SceneFlags.SHADOW_CASTER | SceneFlags.PLANAR_SHADOW | SceneFlags.BLEND, + mainLight || undefined, ); + } + private readonly forwardLighting = new ForwardLighting(); + private readonly _viewport = new Viewport(); + private readonly _clearColor = new Color(0, 0, 0, 1); + private readonly _reflectionProbeClearColor = new Vec3(0, 0, 0); +} - // Spot-light shadow maps - if (this._cameraConfigs.singleForwardRadiancePass) { - const count = this._configs.mobileMaxSpotLightShadowMaps; - for (let i = 0; i !== count; ++i) { - ppl.addRenderTarget( - `SpotShadowMap${i}`, - this._configs.shadowMapFormat, - this._configs.shadowMapSize.x, - this._configs.shadowMapSize.y, - ); - ppl.addDepthStencil( - `SpotShadowDepth${i}`, - Format.DEPTH_STENCIL, - this._configs.shadowMapSize.x, - this._configs.shadowMapSize.y, - ); - } - } +export interface BloomPassConfigs { + enableBloom: boolean; +} - // --------------------------------------------------------- - // Post Process - // --------------------------------------------------------- - // DepthOfField - if (this._cameraConfigs.enableDOF) { - const halfWidth = Math.max(Math.floor(width / 2), 1); - const halfHeight = Math.max(Math.floor(height / 2), 1); - // `DofCoc${id}` texture will reuse ldrColorName - ppl.addRenderTarget(`DofRadiance${id}`, this._cameraConfigs.radianceFormat, width, height); - ppl.addRenderTarget(`DofPrefilter${id}`, this._cameraConfigs.radianceFormat, halfWidth, halfHeight); - ppl.addRenderTarget(`DofBokeh${id}`, this._cameraConfigs.radianceFormat, halfWidth, halfHeight); - ppl.addRenderTarget(`DofFilter${id}`, this._cameraConfigs.radianceFormat, halfWidth, halfHeight); - } - // Bloom (Kawase Dual Filter) - if (this._cameraConfigs.enableBloom) { - let bloomWidth = width; - let bloomHeight = height; - for (let i = 0; i !== settings.bloom.iterations + 1; ++i) { - bloomWidth = Math.max(Math.floor(bloomWidth / 2), 1); - bloomHeight = Math.max(Math.floor(bloomHeight / 2), 1); - ppl.addRenderTarget(`BloomTex${id}_${i}`, this._cameraConfigs.radianceFormat, bloomWidth, bloomHeight); - } - } - // Color Grading - if (this._cameraConfigs.enableColorGrading && settings.colorGrading.material && settings.colorGrading.colorGradingMap) { - settings.colorGrading.material.setProperty( - 'colorGradingMap', settings.colorGrading.colorGradingMap); - } - // FXAA - if (this._cameraConfigs.enableFXAA && this._cameraConfigs.enableShadingScale) { - ppl.addRenderTarget(`AaColor${id}`, Format.RGBA8, width, height); - } +export class BuiltinBloomPassBuilder implements rendering.PipelinePassBuilder { + getConfigOrder(): number { + return 0; + } + getRenderOrder(): number { + return 200; + } + configCamera( + camera: Readonly, + pipelineConfigs: Readonly, + cameraConfigs: CameraConfigs & BloomPassConfigs): void { + cameraConfigs.enableBloom + = cameraConfigs.settings.bloom.enabled + && !!cameraConfigs.settings.bloom.material; + if (cameraConfigs.enableBloom) { + ++cameraConfigs.remainingPasses; } - setup(cameras: renderer.scene.Camera[], ppl: rendering.BasicPipeline): void { - // TODO(zhouzhenglong): Make default effect asset loading earlier and remove _initMaterials - if (this._initMaterials(ppl)) { - return; + } + windowResize( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & BloomPassConfigs, + window: renderer.RenderWindow): void { + if (cameraConfigs.enableBloom) { + const id = window.renderWindowId; + let bloomWidth = cameraConfigs.width; + let bloomHeight = cameraConfigs.height; + for (let i = 0; i !== cameraConfigs.settings.bloom.iterations + 1; ++i) { + bloomWidth = Math.max(Math.floor(bloomWidth / 2), 1); + bloomHeight = Math.max(Math.floor(bloomHeight / 2), 1); + ppl.addRenderTarget(`BloomTex${id}_${i}`, + cameraConfigs.radianceFormat, bloomWidth, bloomHeight); } - // Render cameras - // log(`==================== One Frame ====================`); - for (const camera of cameras) { - // Skip invalid camera - if (!camera.scene || !camera.window) { - continue; - } - // Setup camera configs - setupCameraConfigs(camera, this._configs, this._cameraConfigs); - // log(`Setup camera: ${camera.node!.name}, window: ${camera.window.renderWindowId}, isFull: ${this._cameraConfigs.useFullPipeline}, ` - // + `size: ${camera.window.width}x${camera.window.height}`); + } + } - this._pipelineEvent.emit(PipelineEventType.RENDER_CAMERA_BEGIN, camera); + setup( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & BloomPassConfigs, + camera: renderer.scene.Camera, + context: PipelineContext, + prevRenderPass?: rendering.BasicRenderPassBuilder) + : rendering.BasicRenderPassBuilder | undefined { + if (!cameraConfigs.enableBloom) { + return prevRenderPass; + } - // Build pipeline - if (this._cameraConfigs.useFullPipeline) { - this._buildForwardPipeline(ppl, camera, camera.scene); - } else { - this._buildSimplePipeline(ppl, camera); - } + --cameraConfigs.remainingPasses; + assert(cameraConfigs.remainingPasses >= 0); + const id = camera.window.renderWindowId; + assert(!!cameraConfigs.settings.bloom.material); + return this._addKawaseDualFilterBloomPasses( + ppl, pplConfigs, + cameraConfigs, + cameraConfigs.settings, + cameraConfigs.settings.bloom.material, + id, + cameraConfigs.width, + cameraConfigs.height, + context.colorName); + } - this._pipelineEvent.emit(PipelineEventType.RENDER_CAMERA_END, camera); - } + private _addKawaseDualFilterBloomPasses( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & Readonly, + settings: PipelineSettings, + bloomMaterial: Material, + id: number, + width: number, + height: number, + radianceName: string, + ): rendering.BasicRenderPassBuilder { + const QueueHint = rendering.QueueHint; + // Based on Kawase Dual Filter Blur. Saves bandwidth on mobile devices. + // eslint-disable-next-line max-len + // https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph2015_2D00_mmg_2D00_marius_2D00_slides.pdf + + // Size: [prefilter(1/2), downsample(1/4), downsample(1/8), downsample(1/16), ...] + const iterations = settings.bloom.iterations; + const sizeCount = iterations + 1; + this._bloomWidths.length = sizeCount; + this._bloomHeights.length = sizeCount; + this._bloomWidths[0] = Math.max(Math.floor(width / 2), 1); + this._bloomHeights[0] = Math.max(Math.floor(height / 2), 1); + for (let i = 1; i !== sizeCount; ++i) { + this._bloomWidths[i] = Math.max(Math.floor(this._bloomWidths[i - 1] / 2), 1); + this._bloomHeights[i] = Math.max(Math.floor(this._bloomHeights[i - 1] / 2), 1); } - // ---------------------------------------------------------------- - // Pipelines - // ---------------------------------------------------------------- - private _buildSimplePipeline( - ppl: rendering.BasicPipeline, - camera: renderer.scene.Camera, - ): void { - const width = Math.max(Math.floor(camera.window.width), 1); - const height = Math.max(Math.floor(camera.window.height), 1); - const colorName = this._cameraConfigs.colorName; - const depthStencilName = this._cameraConfigs.depthStencilName; + // Bloom texture names + this._bloomTexNames.length = sizeCount; + for (let i = 0; i !== sizeCount; ++i) { + this._bloomTexNames[i] = `BloomTex${id}_${i}`; + } - const viewport = camera.viewport; // Reduce C++/TS interop - this._viewport.left = Math.round(viewport.x * width); - this._viewport.top = Math.round(viewport.y * height); - // Here we must use camera.viewport.width instead of camera.viewport.z, which - // is undefined on native platform. The same as camera.viewport.height. - this._viewport.width = Math.max(Math.round(viewport.width * width), 1); - this._viewport.height = Math.max(Math.round(viewport.height * height), 1); - - const clearColor = camera.clearColor; // Reduce C++/TS interop - this._clearColor.x = clearColor.x; - this._clearColor.y = clearColor.y; - this._clearColor.z = clearColor.z; - this._clearColor.w = clearColor.w; - - const pass = ppl.addRenderPass(width, height, 'default'); - - // bind output render target - if (forwardNeedClearColor(camera)) { - pass.addRenderTarget(colorName, LoadOp.CLEAR, StoreOp.STORE, this._clearColor); - } else { - pass.addRenderTarget(colorName, LoadOp.LOAD, StoreOp.STORE); - } + // Setup bloom parameters + this._bloomParams.x = pplConfigs.useFloatOutput ? 1 : 0; + this._bloomParams.x = 0; // unused + this._bloomParams.z = settings.bloom.threshold; + this._bloomParams.w = settings.bloom.enableAlphaMask ? 1 : 0; + + // Prefilter pass + const prefilterPass = ppl.addRenderPass(this._bloomWidths[0], this._bloomHeights[0], 'cc-bloom-prefilter'); + prefilterPass.addRenderTarget( + this._bloomTexNames[0], + LoadOp.CLEAR, + StoreOp.STORE, + this._clearColorTransparentBlack, + ); + prefilterPass.addTexture(radianceName, 'inputTexture'); + prefilterPass.setVec4('g_platform', pplConfigs.platform); + prefilterPass.setVec4('bloomParams', this._bloomParams); + prefilterPass + .addQueue(QueueHint.OPAQUE) + .addFullscreenQuad(bloomMaterial, 0); + + // Downsample passes + for (let i = 1; i !== sizeCount; ++i) { + const downPass = ppl.addRenderPass(this._bloomWidths[i], this._bloomHeights[i], 'cc-bloom-downsample'); + downPass.addRenderTarget(this._bloomTexNames[i], LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); + downPass.addTexture(this._bloomTexNames[i - 1], 'bloomTexture'); + this._bloomTexSize.x = this._bloomWidths[i - 1]; + this._bloomTexSize.y = this._bloomHeights[i - 1]; + downPass.setVec4('g_platform', pplConfigs.platform); + downPass.setVec4('bloomTexSize', this._bloomTexSize); + downPass + .addQueue(QueueHint.OPAQUE) + .addFullscreenQuad(bloomMaterial, 1); + } - // bind depth stencil buffer - if (camera.clearFlag & ClearFlagBit.DEPTH_STENCIL) { - pass.addDepthStencil( - depthStencilName, - LoadOp.CLEAR, - StoreOp.DISCARD, - camera.clearDepth, - camera.clearStencil, - camera.clearFlag & ClearFlagBit.DEPTH_STENCIL, - ); - } else { - pass.addDepthStencil(depthStencilName, LoadOp.LOAD, StoreOp.DISCARD); - } + // Upsample passes + for (let i = iterations; i-- > 0;) { + const upPass = ppl.addRenderPass(this._bloomWidths[i], this._bloomHeights[i], 'cc-bloom-upsample'); + upPass.addRenderTarget(this._bloomTexNames[i], LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); + upPass.addTexture(this._bloomTexNames[i + 1], 'bloomTexture'); + this._bloomTexSize.x = this._bloomWidths[i + 1]; + this._bloomTexSize.y = this._bloomHeights[i + 1]; + upPass.setVec4('g_platform', pplConfigs.platform); + upPass.setVec4('bloomTexSize', this._bloomTexSize); + upPass + .addQueue(QueueHint.OPAQUE) + .addFullscreenQuad(bloomMaterial, 2); + } - pass.setViewport(this._viewport); + // Combine pass + const combinePass = ppl.addRenderPass(width, height, 'cc-bloom-combine'); + combinePass.addRenderTarget(radianceName, LoadOp.LOAD, StoreOp.STORE); + combinePass.addTexture(this._bloomTexNames[0], 'bloomTexture'); + combinePass.setVec4('g_platform', pplConfigs.platform); + combinePass.setVec4('bloomParams', this._bloomParams); + combinePass + .addQueue(QueueHint.BLEND) + .addFullscreenQuad(bloomMaterial, 3); + + if (cameraConfigs.remainingPasses === 0) { + return addCopyToScreenPass(ppl, pplConfigs, cameraConfigs, radianceName); + } else { + return combinePass; + } + } + // Bloom + private readonly _clearColorTransparentBlack = new Color(0, 0, 0, 0); + private readonly _bloomParams = new Vec4(0, 0, 0, 0); + private readonly _bloomTexSize = new Vec4(0, 0, 0, 0); + private readonly _bloomWidths: Array = []; + private readonly _bloomHeights: Array = []; + private readonly _bloomTexNames: Array = []; +} - // The opaque queue is used for Reflection probe preview - pass.addQueue(QueueHint.OPAQUE) - .addScene(camera, SceneFlags.OPAQUE); +export interface ToneMappingPassConfigs { + enableToneMapping: boolean; + enableColorGrading: boolean; +} - // The blend queue is used for UI and Gizmos - let flags = SceneFlags.BLEND | SceneFlags.UI; - if (this._cameraConfigs.enableProfiler) { - flags |= SceneFlags.PROFILER; - pass.showStatistics = true; - } - pass.addQueue(QueueHint.BLEND) - .addScene(camera, flags); +export class BuiltinToneMappingPassBuilder implements rendering.PipelinePassBuilder { + getConfigOrder(): number { + return 0; + } + getRenderOrder(): number { + return 300; + } + configCamera( + camera: Readonly, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & ToneMappingPassConfigs): void { + const settings = cameraConfigs.settings; + + cameraConfigs.enableColorGrading + = settings.colorGrading.enabled + && !!settings.colorGrading.material + && !!settings.colorGrading.colorGradingMap; + + cameraConfigs.enableToneMapping + = cameraConfigs.enableHDR // From Half to RGBA8 + || cameraConfigs.enableColorGrading; // Color grading + + if (cameraConfigs.enableToneMapping) { + ++cameraConfigs.remainingPasses; + } + } + windowResize( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & ToneMappingPassConfigs): void { + if (cameraConfigs.enableColorGrading) { + assert(!!cameraConfigs.settings.colorGrading.material); + cameraConfigs.settings.colorGrading.material.setProperty( + 'colorGradingMap', + cameraConfigs.settings.colorGrading.colorGradingMap); + } + } + setup( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & ToneMappingPassConfigs, + camera: renderer.scene.Camera, + context: PipelineContext, + prevRenderPass?: rendering.BasicRenderPassBuilder) + : rendering.BasicRenderPassBuilder | undefined { + if (!cameraConfigs.enableToneMapping) { + return prevRenderPass; } - private _buildForwardPipeline( - ppl: rendering.BasicPipeline, - camera: renderer.scene.Camera, - scene: renderer.RenderScene, - ): void { - // Init - const settings = this._cameraConfigs.settings; - const nativeWidth = Math.max(Math.floor(camera.window.width), 1); - const nativeHeight = Math.max(Math.floor(camera.window.height), 1); - const width = this._cameraConfigs.enableShadingScale - ? Math.max(Math.floor(nativeWidth * this._cameraConfigs.shadingScale), 1) - : nativeWidth; - const height = this._cameraConfigs.enableShadingScale - ? Math.max(Math.floor(nativeHeight * this._cameraConfigs.shadingScale), 1) - : nativeHeight; - const id = camera.window.renderWindowId; - const colorName = this._cameraConfigs.colorName; - const sceneDepth = this._cameraConfigs.enableShadingScale - ? `ScaledSceneDepth${id}` - : `SceneDepth${id}`; - const radianceName = this._cameraConfigs.enableShadingScale - ? `ScaledRadiance${id}` - : `Radiance${id}`; - const ldrColorName = this._cameraConfigs.enableShadingScale - ? `ScaledLdrColor${id}` - : `LdrColor${id}`; - const mainLight = scene.mainLight; - - // Forward Lighting (Light Culling) - this.forwardLighting.cullLights(scene, camera.frustum); - - // Main Directional light CSM Shadow Map - if (this._cameraConfigs.enableMainLightShadowMap) { - assert(!!mainLight); - this._addCascadedShadowMapPass(ppl, id, mainLight, camera); + --cameraConfigs.remainingPasses; + assert(cameraConfigs.remainingPasses >= 0); + if (cameraConfigs.remainingPasses === 0) { + return this._addCopyAndTonemapPass(ppl, pplConfigs, cameraConfigs, + cameraConfigs.width, cameraConfigs.height, + context.colorName, cameraConfigs.colorName); + } else { + const id = cameraConfigs.renderWindowId; + const ldrColorPrefix = cameraConfigs.enableShadingScale + ? `ScaledLdrColor` + : `LdrColor`; + + const ldrColorName = getPingPongRenderTarget(context.colorName, ldrColorPrefix, id); + const radianceName = context.colorName; + context.colorName = ldrColorName; + + return this._addCopyAndTonemapPass(ppl, pplConfigs, cameraConfigs, + cameraConfigs.width, cameraConfigs.height, + radianceName, ldrColorName); + } + } + private _addCopyAndTonemapPass( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & ToneMappingPassConfigs, + width: number, + height: number, + radianceName: string, + colorName: string, + ): rendering.BasicRenderPassBuilder { + let pass: rendering.BasicRenderPassBuilder; + const settings = cameraConfigs.settings; + if (cameraConfigs.enableColorGrading) { + assert(!!settings.colorGrading.material); + assert(!!settings.colorGrading.colorGradingMap); + + const lutTex = settings.colorGrading.colorGradingMap; + this._colorGradingTexSize.x = lutTex.width; + this._colorGradingTexSize.y = lutTex.height; + + const isSquareMap = lutTex.width === lutTex.height; + if (isSquareMap) { + pass = ppl.addRenderPass(width, height, 'cc-color-grading-8x8'); + } else { + pass = ppl.addRenderPass(width, height, 'cc-color-grading-nx1'); } - - // Spot light shadow maps (Mobile or MSAA) - if (this._cameraConfigs.singleForwardRadiancePass) { - // Currently, only support 1 spot light with shadow map on mobile platform. - // TODO(zhouzhenglong): Relex this limitation. - this.forwardLighting.addSpotlightShadowPasses(ppl, camera, this._configs.mobileMaxSpotLightShadowMaps); + pass.addRenderTarget(colorName, LoadOp.CLEAR, StoreOp.STORE, sClearColorTransparentBlack); + pass.addTexture(radianceName, 'sceneColorMap'); + pass.setVec4('g_platform', pplConfigs.platform); + pass.setVec2('lutTextureSize', this._colorGradingTexSize); + pass.setFloat('contribute', settings.colorGrading.contribute); + pass.addQueue(rendering.QueueHint.OPAQUE) + .addFullscreenQuad(settings.colorGrading.material, isSquareMap ? 1 : 0); + } else { + pass = ppl.addRenderPass(width, height, 'cc-tone-mapping'); + pass.addRenderTarget(colorName, LoadOp.CLEAR, StoreOp.STORE, sClearColorTransparentBlack); + pass.addTexture(radianceName, 'inputTexture'); + pass.setVec4('g_platform', pplConfigs.platform); + if (settings.toneMapping.material) { + pass.addQueue(rendering.QueueHint.OPAQUE) + .addFullscreenQuad(settings.toneMapping.material, 0); + } else { + assert(!!cameraConfigs.copyAndTonemapMaterial); + pass.addQueue(rendering.QueueHint.OPAQUE) + .addFullscreenQuad(cameraConfigs.copyAndTonemapMaterial, 0); } + } + return pass; + } + private readonly _colorGradingTexSize = new Vec2(0, 0); +} - this._tryAddReflectionProbePasses(ppl, id, mainLight, camera.scene); - - // Forward Lighting - let lastPass: rendering.BasicRenderPassBuilder; - if (this._cameraConfigs.enablePostProcess) { // Post Process - // Radiance and DoF - if (this._cameraConfigs.enableDOF) { - assert(!!settings.depthOfField.material); - const dofRadianceName = `DofRadiance${id}`; - // Disable MSAA, depth stencil cannot be resolved cross-platformly - this._addForwardRadiancePasses(ppl, id, camera, width, height, mainLight, - dofRadianceName, sceneDepth, true, StoreOp.STORE); - this._addDepthOfFieldPasses(ppl, settings, settings.depthOfField.material, - id, camera, width, height, - dofRadianceName, sceneDepth, radianceName, ldrColorName); - } else { - this._addForwardRadiancePasses( - ppl, id, camera, width, height, mainLight, - radianceName, sceneDepth); - } - // Bloom - if (this._cameraConfigs.enableBloom) { - assert(!!settings.bloom.material); - this._addKawaseDualFilterBloomPasses( - ppl, settings, settings.bloom.material, - id, width, height, radianceName); - } - // Tone Mapping and FXAA - if (this._cameraConfigs.enableFXAA) { - assert(!!settings.fxaa.material); - const copyAndTonemapPassNeeded = this._cameraConfigs.enableHDR - || this._cameraConfigs.enableColorGrading; - const ldrColorBufferName = copyAndTonemapPassNeeded ? ldrColorName : radianceName; - // FXAA is applied after tone mapping - if (copyAndTonemapPassNeeded) { - this._addCopyAndTonemapPass(ppl, settings, width, height, radianceName, ldrColorBufferName); - } - // Apply FXAA - if (this._cameraConfigs.enableShadingScale) { - const aaColorName = `AaColor${id}`; - // Apply FXAA on scaled image - this._addFxaaPass(ppl, settings.fxaa.material, - width, height, ldrColorBufferName, aaColorName); - // Copy FXAA result to screen - if (this._cameraConfigs.enableFSR && settings.fsr.material) { - // Apply FSR - lastPass = this._addFsrPass(ppl, settings, settings.fsr.material, - id, width, height, aaColorName, - nativeWidth, nativeHeight, colorName); - } else { - // Scale FXAA result to screen - lastPass = this._addCopyPass(ppl, - nativeWidth, nativeHeight, aaColorName, colorName); - } - } else { - // Image not scaled, output FXAA result to screen directly - lastPass = this._addFxaaPass(ppl, settings.fxaa.material, - nativeWidth, nativeHeight, ldrColorBufferName, colorName); - } - } else { - // No FXAA (Size might be scaled) - lastPass = this._addTonemapResizeOrSuperResolutionPasses(ppl, settings, id, - width, height, radianceName, ldrColorName, - nativeWidth, nativeHeight, colorName); - } - } else if (this._cameraConfigs.enableHDR || this._cameraConfigs.enableShadingScale) { // HDR or Scaled LDR - this._addForwardRadiancePasses(ppl, id, camera, - width, height, mainLight, radianceName, sceneDepth); - lastPass = this._addTonemapResizeOrSuperResolutionPasses(ppl, settings, id, - width, height, radianceName, ldrColorName, - nativeWidth, nativeHeight, colorName); - } else { // LDR (Size is not scaled) - lastPass = this._addForwardRadiancePasses(ppl, id, camera, - nativeWidth, nativeHeight, mainLight, - colorName, this._cameraConfigs.depthStencilName); - } +export interface FXAAPassConfigs { + enableFXAA: boolean; +} - // UI size is not scaled, does not have AA - this._addUIQueue(camera, lastPass); +export class BuiltinFXAAPassBuilder implements rendering.PipelinePassBuilder { + getConfigOrder(): number { + return 0; + } + getRenderOrder(): number { + return 400; + } + configCamera( + camera: Readonly, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & FXAAPassConfigs): void { + cameraConfigs.enableFXAA + = cameraConfigs.settings.fxaa.enabled + && !!cameraConfigs.settings.fxaa.material; + if (cameraConfigs.enableFXAA) { + ++cameraConfigs.remainingPasses; } - - // ---------------------------------------------------------------- - // Common Passes - // ---------------------------------------------------------------- - private _addTonemapResizeOrSuperResolutionPasses( - ppl: rendering.BasicPipeline, - settings: PipelineSettings, - id: number, - width: number, - height: number, - radianceName: string, - ldrColorName: string, - nativeWidth: number, - nativeHeight: number, - colorName: string, - ): rendering.BasicRenderPassBuilder { - let lastPass: rendering.BasicRenderPassBuilder; - if (this._cameraConfigs.enableFSR && settings.fsr.material) { - // Apply FSR - this._addCopyAndTonemapPass(ppl, settings, - width, height, radianceName, ldrColorName); - lastPass = this._addFsrPass(ppl, settings, - settings.fsr.material, - id, width, height, ldrColorName, - nativeWidth, nativeHeight, colorName); + } + setup( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & FXAAPassConfigs, + camera: renderer.scene.Camera, + context: PipelineContext, + prevRenderPass?: rendering.BasicRenderPassBuilder) + : rendering.BasicRenderPassBuilder | undefined { + if (!cameraConfigs.enableFXAA) { + return prevRenderPass; + } + --cameraConfigs.remainingPasses; + assert(cameraConfigs.remainingPasses >= 0); + + const id = cameraConfigs.renderWindowId; + const ldrColorPrefix = cameraConfigs.enableShadingScale + ? `ScaledLdrColor` + : `LdrColor`; + const ldrColorName = getPingPongRenderTarget(context.colorName, ldrColorPrefix, id); + + assert(!!cameraConfigs.settings.fxaa.material); + if (cameraConfigs.remainingPasses === 0) { + if (cameraConfigs.enableShadingScale) { + this._addFxaaPass(ppl, pplConfigs, + cameraConfigs.settings.fxaa.material, + cameraConfigs.width, + cameraConfigs.height, + context.colorName, + ldrColorName); + return addCopyToScreenPass(ppl, pplConfigs, cameraConfigs, ldrColorName); } else { - // Output HDR/LDR result to screen directly (Size might be scaled) - lastPass = this._addCopyAndTonemapPass(ppl, settings, - nativeWidth, nativeHeight, radianceName, colorName); + assert(cameraConfigs.width === cameraConfigs.nativeWidth); + assert(cameraConfigs.height === cameraConfigs.nativeHeight); + return this._addFxaaPass(ppl, pplConfigs, + cameraConfigs.settings.fxaa.material, + cameraConfigs.width, + cameraConfigs.height, + context.colorName, + cameraConfigs.colorName); } + } else { + const inputColorName = context.colorName; + context.colorName = ldrColorName; + const lastPass = this._addFxaaPass(ppl, pplConfigs, + cameraConfigs.settings.fxaa.material, + cameraConfigs.width, + cameraConfigs.height, + inputColorName, + ldrColorName); return lastPass; } + } + private _addFxaaPass( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + fxaaMaterial: Material, + width: number, + height: number, + ldrColorName: string, + colorName: string, + ): rendering.BasicRenderPassBuilder { + this._fxaaParams.x = width; + this._fxaaParams.y = height; + this._fxaaParams.z = 1 / width; + this._fxaaParams.w = 1 / height; + + const pass = ppl.addRenderPass(width, height, 'cc-fxaa'); + pass.addRenderTarget(colorName, LoadOp.CLEAR, StoreOp.STORE, sClearColorTransparentBlack); + pass.addTexture(ldrColorName, 'sceneColorMap'); + pass.setVec4('g_platform', pplConfigs.platform); + pass.setVec4('texSize', this._fxaaParams); + pass.addQueue(rendering.QueueHint.OPAQUE) + .addFullscreenQuad(fxaaMaterial, 0); + return pass; + } + // FXAA + private readonly _fxaaParams = new Vec4(0, 0, 0, 0); +} - private _addCascadedShadowMapPass( - ppl: rendering.BasicPipeline, - id: number, - light: renderer.scene.DirectionalLight, - camera: renderer.scene.Camera, - ): void { - // ---------------------------------------------------------------- - // Dynamic states - // ---------------------------------------------------------------- - const width = ppl.pipelineSceneData.shadows.size.x; - const height = ppl.pipelineSceneData.shadows.size.y; - this._viewport.left = 0; - this._viewport.top = 0; - this._viewport.width = width; - this._viewport.height = height; - - // ---------------------------------------------------------------- - // CSM Shadow Map - // ---------------------------------------------------------------- - const pass = ppl.addRenderPass(width, height, 'default'); - pass.name = 'CascadedShadowMap'; - pass.addRenderTarget(`ShadowMap${id}`, LoadOp.CLEAR, StoreOp.STORE, new Color(1, 1, 1, 1)); - pass.addDepthStencil(`ShadowDepth${id}`, LoadOp.CLEAR, StoreOp.DISCARD); - const csmLevel = ppl.pipelineSceneData.csmSupported ? light.csmLevel : 1; - - // Add shadow map viewports - for (let level = 0; level !== csmLevel; ++level) { - getCsmMainLightViewport(light, width, height, level, this._viewport, this._configs.screenSpaceSignY); - const queue = pass.addQueue(QueueHint.NONE, 'shadow-caster'); - if (!this._configs.isWebGPU) { // Temporary workaround for WebGPU - queue.setViewport(this._viewport); - } - queue - .addScene(camera, SceneFlags.OPAQUE | SceneFlags.MASK | SceneFlags.SHADOW_CASTER) - .useLightFrustum(light, level); - } +export interface FSRPassConfigs { + enableFSR: boolean; +} + +export class BuiltinFsrPassBuilder implements rendering.PipelinePassBuilder { + getConfigOrder(): number { + return 0; + } + getRenderOrder(): number { + return 500; + } + configCamera( + camera: Readonly, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & FSRPassConfigs): void { + // FSR (Depend on Shading scale) + cameraConfigs.enableFSR = cameraConfigs.settings.fsr.enabled + && !!cameraConfigs.settings.fsr.material + && cameraConfigs.enableShadingScale + && cameraConfigs.shadingScale < 1.0; + + if (cameraConfigs.enableFSR) { + ++cameraConfigs.remainingPasses; } + } + setup( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & FSRPassConfigs, + camera: renderer.scene.Camera, + context: PipelineContext, + prevRenderPass?: rendering.BasicRenderPassBuilder) + : rendering.BasicRenderPassBuilder | undefined { + if (!cameraConfigs.enableFSR) { + return prevRenderPass; + } + --cameraConfigs.remainingPasses; + + const inputColorName = context.colorName; + const outputColorName + = cameraConfigs.remainingPasses === 0 + ? cameraConfigs.colorName + : getPingPongRenderTarget(context.colorName, 'UiColor', cameraConfigs.renderWindowId); + context.colorName = outputColorName; + + assert(!!cameraConfigs.settings.fsr.material); + return this._addFsrPass(ppl, pplConfigs, cameraConfigs, + cameraConfigs.settings, + cameraConfigs.settings.fsr.material, + cameraConfigs.renderWindowId, + cameraConfigs.width, + cameraConfigs.height, + inputColorName, + cameraConfigs.nativeWidth, + cameraConfigs.nativeHeight, + outputColorName); + } + private _addFsrPass( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & FSRPassConfigs, + settings: PipelineSettings, + fsrMaterial: Material, + id: number, + width: number, + height: number, + inputColorName: string, + nativeWidth: number, + nativeHeight: number, + outputColorName: string, + ): rendering.BasicRenderPassBuilder { + this._fsrTexSize.x = width; + this._fsrTexSize.y = height; + this._fsrTexSize.z = nativeWidth; + this._fsrTexSize.w = nativeHeight; + this._fsrParams.x = clamp(1.0 - settings.fsr.sharpness, 0.02, 0.98); + + const uiColorPrefix = 'UiColor'; + + const fsrColorName = getPingPongRenderTarget(outputColorName, uiColorPrefix, id); + + const easuPass = ppl.addRenderPass(nativeWidth, nativeHeight, 'cc-fsr-easu'); + easuPass.addRenderTarget(fsrColorName, LoadOp.CLEAR, StoreOp.STORE, sClearColorTransparentBlack); + easuPass.addTexture(inputColorName, 'outputResultMap'); + easuPass.setVec4('g_platform', pplConfigs.platform); + easuPass.setVec4('fsrTexSize', this._fsrTexSize); + easuPass + .addQueue(rendering.QueueHint.OPAQUE) + .addFullscreenQuad(fsrMaterial, 0); + + const rcasPass = ppl.addRenderPass(nativeWidth, nativeHeight, 'cc-fsr-rcas'); + rcasPass.addRenderTarget(outputColorName, LoadOp.CLEAR, StoreOp.STORE, sClearColorTransparentBlack); + rcasPass.addTexture(fsrColorName, 'outputResultMap'); + rcasPass.setVec4('g_platform', pplConfigs.platform); + rcasPass.setVec4('fsrTexSize', this._fsrTexSize); + rcasPass.setVec4('fsrParams', this._fsrParams); + rcasPass + .addQueue(rendering.QueueHint.OPAQUE) + .addFullscreenQuad(fsrMaterial, 1); + + return rcasPass; + } + // FSR + private readonly _fsrParams = new Vec4(0, 0, 0, 0); + private readonly _fsrTexSize = new Vec4(0, 0, 0, 0); +} - private _addCopyPass( - ppl: rendering.BasicPipeline, - width: number, - height: number, - input: string, - output: string, - ): rendering.BasicRenderPassBuilder { - const pass = ppl.addRenderPass(width, height, 'cc-tone-mapping'); - pass.addRenderTarget(output, LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); - pass.addTexture(input, 'inputTexture'); - pass.setVec4('g_platform', this._configs.platform); - pass.addQueue(QueueHint.OPAQUE) - .addFullscreenQuad(this._copyAndTonemapMaterial, 1); - return pass; +export class BuiltinUiPassBuilder implements rendering.PipelinePassBuilder { + getConfigOrder(): number { + return 0; + } + getRenderOrder(): number { + return 1000; + } + setup( + ppl: rendering.BasicPipeline, + pplConfigs: Readonly, + cameraConfigs: CameraConfigs & FSRPassConfigs, + camera: renderer.scene.Camera, + context: PipelineContext, + prevRenderPass?: rendering.BasicRenderPassBuilder) + : rendering.BasicRenderPassBuilder | undefined { + assert(!!prevRenderPass); + + let flags = rendering.SceneFlags.UI; + if (cameraConfigs.enableProfiler) { + flags |= rendering.SceneFlags.PROFILER; + prevRenderPass.showStatistics = true; } + prevRenderPass + .addQueue(rendering.QueueHint.BLEND, 'default', 'default') + .addScene(camera, flags); - private _addCopyAndTonemapPass( - ppl: rendering.BasicPipeline, - settings: PipelineSettings, - width: number, - height: number, - radianceName: string, - colorName: string, - ): rendering.BasicRenderPassBuilder { - let pass: rendering.BasicRenderPassBuilder; - if (this._cameraConfigs.enableColorGrading - && settings.colorGrading.material - && settings.colorGrading.colorGradingMap) { - const lutTex = settings.colorGrading.colorGradingMap; - this._colorGradingTexSize.x = lutTex.width; - this._colorGradingTexSize.y = lutTex.height; - - const isSquareMap = lutTex.width === lutTex.height; - if (isSquareMap) { - pass = ppl.addRenderPass(width, height, 'cc-color-grading-8x8'); + return prevRenderPass; + } +} + +if (rendering) { + + const { QueueHint, SceneFlags } = rendering; + + class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + private readonly _pipelineEvent: PipelineEventProcessor = cclegacy.director.root.pipelineEvent as PipelineEventProcessor; + private readonly _forwardPass = new BuiltinForwardPassBuilder(); + private readonly _bloomPass = new BuiltinBloomPassBuilder(); + private readonly _toneMappingPass = new BuiltinToneMappingPassBuilder(); + private readonly _fxaaPass = new BuiltinFXAAPassBuilder(); + private readonly _fsrPass = new BuiltinFsrPassBuilder(); + private readonly _uiPass = new BuiltinUiPassBuilder(); + // Internal cached resources + private readonly _clearColor = new Color(0, 0, 0, 1); + private readonly _viewport = new Viewport(); + private readonly _configs = new PipelineConfigs(); + private readonly _cameraConfigs = new CameraConfigs(); + // Materials + private readonly _copyAndTonemapMaterial = new Material(); + + // Internal States + private _initialized = false; // TODO(zhouzhenglong): Make default effect asset loading earlier and remove this flag + private _passBuilders: rendering.PipelinePassBuilder[] = []; + + private _setupPipelinePreview( + camera: renderer.scene.Camera, + cameraConfigs: CameraConfigs) { + const isEditorView: boolean + = camera.cameraUsage === CameraUsage.SCENE_VIEW + || camera.cameraUsage === CameraUsage.PREVIEW; + + if (isEditorView) { + const editorSettings = rendering.getEditorPipelineSettings() as PipelineSettings | null; + if (editorSettings) { + cameraConfigs.settings = editorSettings; } else { - pass = ppl.addRenderPass(width, height, 'cc-color-grading-nx1'); + cameraConfigs.settings = defaultSettings; } - pass.addRenderTarget(colorName, LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); - pass.addTexture(radianceName, 'sceneColorMap'); - pass.setVec4('g_platform', this._configs.platform); - pass.setVec2('lutTextureSize', this._colorGradingTexSize); - pass.setFloat('contribute', settings.colorGrading.contribute); - pass.addQueue(QueueHint.OPAQUE) - .addFullscreenQuad(settings.colorGrading.material, isSquareMap ? 1 : 0); } else { - pass = ppl.addRenderPass(width, height, 'cc-tone-mapping'); - pass.addRenderTarget(colorName, LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); - pass.addTexture(radianceName, 'inputTexture'); - pass.setVec4('g_platform', this._configs.platform); - if (settings.toneMapping.material) { - pass.addQueue(QueueHint.OPAQUE) - .addFullscreenQuad(settings.toneMapping.material, 0); + if (camera.pipelineSettings) { + cameraConfigs.settings = camera.pipelineSettings as PipelineSettings; } else { - pass.addQueue(QueueHint.OPAQUE) - .addFullscreenQuad(this._copyAndTonemapMaterial, 0); + cameraConfigs.settings = defaultSettings; } } - return pass; } - private _buildForwardMainLightPass( - pass: rendering.BasicRenderPassBuilder, - id: number, - camera: renderer.scene.Camera, - colorName: string, - depthStencilName: string, - depthStencilStoreOp: gfx.StoreOp, - mainLight: renderer.scene.DirectionalLight | null, - scene: renderer.RenderScene | null = null, - ): void { - // set viewport - pass.setViewport(this._viewport); + private _preparePipelinePasses(cameraConfigs: CameraConfigs): void { + const passBuilders = this._passBuilders; + passBuilders.length = 0; - const colorStoreOp = this._cameraConfigs.enableMSAA ? StoreOp.DISCARD : StoreOp.STORE; - - // bind output render target - if (forwardNeedClearColor(camera)) { - pass.addRenderTarget(colorName, LoadOp.CLEAR, colorStoreOp, this._clearColor); - } else { - pass.addRenderTarget(colorName, LoadOp.LOAD, colorStoreOp); - } - - // bind depth stencil buffer - if (DEBUG) { - if (colorName === this._cameraConfigs.colorName && - depthStencilName !== this._cameraConfigs.depthStencilName) { - warn('Default framebuffer cannot use custom depth stencil buffer'); + const settings = cameraConfigs.settings as PipelineSettings2; + if (settings._passes) { + for (const pass of settings._passes) { + passBuilders.push(pass); } + assert(passBuilders.length === settings._passes.length); } - if (camera.clearFlag & ClearFlagBit.DEPTH_STENCIL) { - pass.addDepthStencil( - depthStencilName, - LoadOp.CLEAR, - depthStencilStoreOp, - camera.clearDepth, - camera.clearStencil, - camera.clearFlag & ClearFlagBit.DEPTH_STENCIL, - ); - } else { - pass.addDepthStencil(depthStencilName, LoadOp.LOAD, depthStencilStoreOp); - } + passBuilders.push(this._forwardPass); - // Set shadow map if enabled - if (this._cameraConfigs.enableMainLightShadowMap) { - pass.addTexture(`ShadowMap${id}`, 'cc_shadowMap'); + if (settings.bloom.enabled) { + passBuilders.push(this._bloomPass); } - // TODO(zhouzhenglong): Separate OPAQUE and MASK queue + passBuilders.push(this._toneMappingPass); + + if (settings.fxaa.enabled) { + passBuilders.push(this._fxaaPass); + } - // add opaque and mask queue - pass.addQueue(QueueHint.NONE) // Currently we put OPAQUE and MASK into one queue, so QueueHint is NONE - .addScene(camera, - SceneFlags.OPAQUE | SceneFlags.MASK, - mainLight || undefined, - scene ? scene : undefined); + if (settings.fsr.enabled) { + passBuilders.push(this._fsrPass); + } + passBuilders.push(this._uiPass); } - private _addDepthOfFieldPasses( - ppl: rendering.BasicPipeline, - settings: PipelineSettings, - dofMaterial: Material, - id: number, + private _setupBuiltinCameraConfigs( camera: renderer.scene.Camera, - width: number, - height: number, - dofRadianceName: string, - depthStencil: string, - radianceName: string, - ldrColorName: string, - ): void { - // https://catlikecoding.com/unity/tutorials/advanced-rendering/depth-of-field/ - - this._cocParams.x = settings.depthOfField.minRange; - this._cocParams.y = settings.depthOfField.maxRange;// camera.farClip;// settings.depthOfField.focusRange; - this._cocParams.z = settings.depthOfField.blurRadius; - this._cocParams.w = settings.depthOfField.intensity; - this._focusPos.x = settings.depthOfField.focusPos.x; - this._focusPos.y = settings.depthOfField.focusPos.y; - this._focusPos.z = settings.depthOfField.focusPos.z; - this._cocTexSize.x = 1.0 / width; - this._cocTexSize.y = 1.0 / height; - this._cocTexSize.z = width; - this._cocTexSize.w = height; - const blurName = ldrColorName; - - // Blur Pass - const blurPass = ppl.addRenderPass(width, height, 'cc-dof-blur'); - blurPass.addRenderTarget(blurName, LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); - blurPass.addTexture(dofRadianceName, 'screenTex'); - blurPass.setVec4('g_platform', this._configs.platform); - blurPass.setVec4('blurParams', this._cocParams); - blurPass.setVec4('mainTexTexelSize', this._cocTexSize); - blurPass - .addQueue(QueueHint.OPAQUE) - .addCameraQuad(camera, dofMaterial, 0); // addCameraQuad will set camera related UBOs - // coc pass - const cocPass = ppl.addRenderPass(width, height, 'cc-dof-coc'); - cocPass.addRenderTarget(radianceName, LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); - cocPass.addTexture(blurName, 'colorTex'); - cocPass.addTexture(depthStencil, "DepthTex"); - cocPass.addTexture(dofRadianceName, "screenTex"); - cocPass.setVec4('g_platform', this._configs.platform); - cocPass.setMat4('proj', camera.matProj); - cocPass.setMat4('invProj', camera.matProjInv); - cocPass.setMat4('viewMatInv', camera.node.worldMatrix); - cocPass.setVec4('cocParams', this._cocParams); - cocPass.setVec4('focus', this._focusPos); - cocPass - .addQueue(QueueHint.OPAQUE) - .addCameraQuad(camera, dofMaterial, 1); + pipelineConfigs: PipelineConfigs, + cameraConfigs: CameraConfigs + ) { + const window = camera.window; + const isMainGameWindow: boolean = camera.cameraUsage === CameraUsage.GAME && !!window.swapchain; + + // Window + cameraConfigs.isMainGameWindow = isMainGameWindow; + cameraConfigs.renderWindowId = window.renderWindowId; + + // Camera + cameraConfigs.colorName = window.colorName; + cameraConfigs.depthStencilName = window.depthStencilName; + + // Pipeline + cameraConfigs.enableFullPipeline = (camera.visibility & (Layers.Enum.DEFAULT)) !== 0; + cameraConfigs.enableProfiler = DEBUG && isMainGameWindow; + cameraConfigs.remainingPasses = 0; + + // Shading scale + cameraConfigs.shadingScale = cameraConfigs.settings.shadingScale; + cameraConfigs.enableShadingScale = cameraConfigs.settings.enableShadingScale + && cameraConfigs.shadingScale !== 1.0; + + cameraConfigs.nativeWidth = Math.max(Math.floor(window.width), 1); + cameraConfigs.nativeHeight = Math.max(Math.floor(window.height), 1); + + cameraConfigs.width = cameraConfigs.enableShadingScale + ? Math.max(Math.floor(cameraConfigs.nativeWidth * cameraConfigs.shadingScale), 1) + : cameraConfigs.nativeWidth; + cameraConfigs.height = cameraConfigs.enableShadingScale + ? Math.max(Math.floor(cameraConfigs.nativeHeight * cameraConfigs.shadingScale), 1) + : cameraConfigs.nativeHeight; + + // Radiance + cameraConfigs.enableHDR = cameraConfigs.enableFullPipeline + && pipelineConfigs.useFloatOutput; + cameraConfigs.radianceFormat = cameraConfigs.enableHDR + ? gfx.Format.RGBA16F : gfx.Format.RGBA8; + + // Tone Mapping + cameraConfigs.copyAndTonemapMaterial = this._copyAndTonemapMaterial; + + // Depth + cameraConfigs.enableStoreSceneDepth = false; } - private _addKawaseDualFilterBloomPasses( - ppl: rendering.BasicPipeline, - settings: PipelineSettings, - bloomMaterial: Material, - id: number, - width: number, - height: number, - radianceName: string, + private _setupCameraConfigs( + camera: renderer.scene.Camera, + pipelineConfigs: PipelineConfigs, + cameraConfigs: CameraConfigs ): void { - // Based on Kawase Dual Filter Blur. Saves bandwidth on mobile devices. - // eslint-disable-next-line max-len - // https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph2015_2D00_mmg_2D00_marius_2D00_slides.pdf - - // Size: [prefilter(1/2), downsample(1/4), downsample(1/8), downsample(1/16), ...] - const iterations = settings.bloom.iterations; - const sizeCount = iterations + 1; - this._bloomWidths.length = sizeCount; - this._bloomHeights.length = sizeCount; - this._bloomWidths[0] = Math.max(Math.floor(width / 2), 1); - this._bloomHeights[0] = Math.max(Math.floor(height / 2), 1); - for (let i = 1; i !== sizeCount; ++i) { - this._bloomWidths[i] = Math.max(Math.floor(this._bloomWidths[i - 1] / 2), 1); - this._bloomHeights[i] = Math.max(Math.floor(this._bloomHeights[i - 1] / 2), 1); - } + this._setupPipelinePreview(camera, cameraConfigs); - // Bloom texture names - this._bloomTexNames.length = sizeCount; - for (let i = 0; i !== sizeCount; ++i) { - this._bloomTexNames[i] = `BloomTex${id}_${i}`; - } + this._preparePipelinePasses(cameraConfigs); - // Setup bloom parameters - this._bloomParams.x = this._configs.useFloatOutput ? 1 : 0; - this._bloomParams.x = 0; // unused - this._bloomParams.z = settings.bloom.threshold; - this._bloomParams.w = settings.bloom.enableAlphaMask ? 1 : 0; + sortPipelinePassBuildersByConfigOrder(this._passBuilders); - // Prefilter pass - const prefilterPass = ppl.addRenderPass(this._bloomWidths[0], this._bloomHeights[0], 'cc-bloom-prefilter'); - prefilterPass.addRenderTarget( - this._bloomTexNames[0], - LoadOp.CLEAR, - StoreOp.STORE, - this._clearColorTransparentBlack, - ); - prefilterPass.addTexture(radianceName, 'inputTexture'); - prefilterPass.setVec4('g_platform', this._configs.platform); - prefilterPass.setVec4('bloomParams', this._bloomParams); - prefilterPass - .addQueue(QueueHint.OPAQUE) - .addFullscreenQuad(bloomMaterial, 0); - - // Downsample passes - for (let i = 1; i !== sizeCount; ++i) { - const downPass = ppl.addRenderPass(this._bloomWidths[i], this._bloomHeights[i], 'cc-bloom-downsample'); - downPass.addRenderTarget(this._bloomTexNames[i], LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); - downPass.addTexture(this._bloomTexNames[i - 1], 'bloomTexture'); - this._bloomTexSize.x = this._bloomWidths[i - 1]; - this._bloomTexSize.y = this._bloomHeights[i - 1]; - downPass.setVec4('g_platform', this._configs.platform); - downPass.setVec4('bloomTexSize', this._bloomTexSize); - downPass - .addQueue(QueueHint.OPAQUE) - .addFullscreenQuad(bloomMaterial, 1); - } + this._setupBuiltinCameraConfigs(camera, pipelineConfigs, cameraConfigs); - // Upsample passes - for (let i = iterations; i-- > 0;) { - const upPass = ppl.addRenderPass(this._bloomWidths[i], this._bloomHeights[i], 'cc-bloom-upsample'); - upPass.addRenderTarget(this._bloomTexNames[i], LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); - upPass.addTexture(this._bloomTexNames[i + 1], 'bloomTexture'); - this._bloomTexSize.x = this._bloomWidths[i + 1]; - this._bloomTexSize.y = this._bloomHeights[i + 1]; - upPass.setVec4('g_platform', this._configs.platform); - upPass.setVec4('bloomTexSize', this._bloomTexSize); - upPass - .addQueue(QueueHint.OPAQUE) - .addFullscreenQuad(bloomMaterial, 2); + for (const builder of this._passBuilders) { + if (builder.configCamera) { + builder.configCamera(camera, pipelineConfigs, cameraConfigs); + } } - - // Combine pass - const combinePass = ppl.addRenderPass(width, height, 'cc-bloom-combine'); - combinePass.addRenderTarget(radianceName, LoadOp.LOAD, StoreOp.STORE); - combinePass.addTexture(this._bloomTexNames[0], 'bloomTexture'); - combinePass.setVec4('g_platform', this._configs.platform); - combinePass.setVec4('bloomParams', this._bloomParams); - combinePass - .addQueue(QueueHint.BLEND) - .addFullscreenQuad(bloomMaterial, 3); } - private _addFsrPass( + // ---------------------------------------------------------------- + // Interface + // ---------------------------------------------------------------- + windowResize( ppl: rendering.BasicPipeline, - settings: PipelineSettings, - fsrMaterial: Material, - id: number, - width: number, - height: number, - ldrColorName: string, + window: renderer.RenderWindow, + camera: renderer.scene.Camera, nativeWidth: number, nativeHeight: number, - colorName: string, - ): rendering.BasicRenderPassBuilder { - this._fsrTexSize.x = width; - this._fsrTexSize.y = height; - this._fsrTexSize.z = nativeWidth; - this._fsrTexSize.w = nativeHeight; - this._fsrParams.x = clamp(1.0 - settings.fsr.sharpness, 0.02, 0.98); - - const fsrColorName = `FsrColor${id}`; - - const easuPass = ppl.addRenderPass(nativeWidth, nativeHeight, 'cc-fsr-easu'); - easuPass.addRenderTarget(fsrColorName, LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); - easuPass.addTexture(ldrColorName, 'outputResultMap'); - easuPass.setVec4('g_platform', this._configs.platform); - easuPass.setVec4('fsrTexSize', this._fsrTexSize); - easuPass - .addQueue(QueueHint.OPAQUE) - .addFullscreenQuad(fsrMaterial, 0); - - const rcasPass = ppl.addRenderPass(nativeWidth, nativeHeight, 'cc-fsr-rcas'); - rcasPass.addRenderTarget(colorName, LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); - rcasPass.addTexture(fsrColorName, 'outputResultMap'); - rcasPass.setVec4('g_platform', this._configs.platform); - rcasPass.setVec4('fsrTexSize', this._fsrTexSize); - rcasPass.setVec4('fsrParams', this._fsrParams); - rcasPass - .addQueue(QueueHint.OPAQUE) - .addFullscreenQuad(fsrMaterial, 1); + ): void { + setupPipelineConfigs(ppl, this._configs); - return rcasPass; - } + this._setupCameraConfigs(camera, this._configs, this._cameraConfigs); - private _addFxaaPass( - ppl: rendering.BasicPipeline, - fxaaMaterial: Material, - width: number, - height: number, - ldrColorName: string, - colorName: string, - ): rendering.BasicRenderPassBuilder { - this._fxaaParams.x = width; - this._fxaaParams.y = height; - this._fxaaParams.z = 1 / width; - this._fxaaParams.w = 1 / height; - const pass = ppl.addRenderPass(width, height, 'cc-fxaa'); - pass.addRenderTarget(colorName, LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); - pass.addTexture(ldrColorName, 'sceneColorMap'); - pass.setVec4('g_platform', this._configs.platform); - pass.setVec4('texSize', this._fxaaParams); - pass.addQueue(QueueHint.OPAQUE) - .addFullscreenQuad(fxaaMaterial, 0); - return pass; - } + // Render Window (UI) + const id = window.renderWindowId; - private _addUIQueue(camera: renderer.scene.Camera, pass: rendering.BasicRenderPassBuilder): void { - let flags = SceneFlags.UI; - if (this._cameraConfigs.enableProfiler) { - flags |= SceneFlags.PROFILER; - pass.showStatistics = true; + ppl.addRenderWindow(this._cameraConfigs.colorName, + Format.RGBA8, nativeWidth, nativeHeight, window, + this._cameraConfigs.depthStencilName); + + const width = this._cameraConfigs.width; + const height = this._cameraConfigs.height; + + if (this._cameraConfigs.enableShadingScale) { + ppl.addDepthStencil(`ScaledSceneDepth_${id}`, Format.DEPTH_STENCIL, width, height); + ppl.addRenderTarget(`ScaledRadiance0_${id}`, this._cameraConfigs.radianceFormat, width, height); + ppl.addRenderTarget(`ScaledRadiance1_${id}`, this._cameraConfigs.radianceFormat, width, height); + ppl.addRenderTarget(`ScaledLdrColor0_${id}`, Format.RGBA8, width, height); + ppl.addRenderTarget(`ScaledLdrColor1_${id}`, Format.RGBA8, width, height); + } else { + ppl.addDepthStencil(`SceneDepth_${id}`, Format.DEPTH_STENCIL, width, height); + ppl.addRenderTarget(`Radiance0_${id}`, this._cameraConfigs.radianceFormat, width, height); + ppl.addRenderTarget(`Radiance1_${id}`, this._cameraConfigs.radianceFormat, width, height); + ppl.addRenderTarget(`LdrColor0_${id}`, Format.RGBA8, width, height); + ppl.addRenderTarget(`LdrColor1_${id}`, Format.RGBA8, width, height); + } + ppl.addRenderTarget(`UiColor0_${id}`, Format.RGBA8, nativeWidth, nativeHeight); + ppl.addRenderTarget(`UiColor1_${id}`, Format.RGBA8, nativeWidth, nativeHeight); + + for (const builder of this._passBuilders) { + if (builder.windowResize) { + builder.windowResize(ppl, this._configs, this._cameraConfigs, window, camera, nativeWidth, nativeHeight); + } } - pass - .addQueue(QueueHint.BLEND, 'default', 'default') - .addScene(camera, flags); } + setup(cameras: renderer.scene.Camera[], ppl: rendering.BasicPipeline): void { + // TODO(zhouzhenglong): Make default effect asset loading earlier and remove _initMaterials + if (this._initMaterials(ppl)) { + return; + } + // Render cameras + // log(`==================== One Frame ====================`); + for (const camera of cameras) { + // Skip invalid camera + if (!camera.scene || !camera.window) { + continue; + } + // Setup camera configs + this._setupCameraConfigs(camera, this._configs, this._cameraConfigs); + // log(`Setup camera: ${camera.node!.name}, window: ${camera.window.renderWindowId}, isFull: ${this._cameraConfigs.enableFullPipeline}, ` + // + `size: ${camera.window.width}x${camera.window.height}`); + + this._pipelineEvent.emit(PipelineEventType.RENDER_CAMERA_BEGIN, camera); + + // Build pipeline + if (this._cameraConfigs.enableFullPipeline) { + this._buildForwardPipeline(ppl, camera, camera.scene, this._passBuilders); + } else { + this._buildSimplePipeline(ppl, camera); + } + this._pipelineEvent.emit(PipelineEventType.RENDER_CAMERA_END, camera); + } + } // ---------------------------------------------------------------- - // Forward + // Pipelines // ---------------------------------------------------------------- - private _addForwardRadiancePasses( + private _buildSimplePipeline( ppl: rendering.BasicPipeline, - id: number, camera: renderer.scene.Camera, - width: number, - height: number, - mainLight: renderer.scene.DirectionalLight | null, - colorName: string, - depthStencilName: string, - disableMSAA: boolean = false, - depthStencilStoreOp: gfx.StoreOp = StoreOp.DISCARD, - ): rendering.BasicRenderPassBuilder { - // ---------------------------------------------------------------- - // Dynamic states - // ---------------------------------------------------------------- - // Prepare camera clear color - const clearColor = camera.clearColor; // Reduce C++/TS interop - this._clearColor.x = clearColor.x; - this._clearColor.y = clearColor.y; - this._clearColor.z = clearColor.z; - this._clearColor.w = clearColor.w; + ): void { + const width = Math.max(Math.floor(camera.window.width), 1); + const height = Math.max(Math.floor(camera.window.height), 1); + const colorName = this._cameraConfigs.colorName; + const depthStencilName = this._cameraConfigs.depthStencilName; - // Prepare camera viewport - const viewport = camera.viewport; // Reduce C++/TS interop + const viewport = camera.viewport; // Reduce C++/TS interop this._viewport.left = Math.round(viewport.x * width); this._viewport.top = Math.round(viewport.y * height); // Here we must use camera.viewport.width instead of camera.viewport.z, which @@ -1272,160 +1758,19 @@ if (rendering) { this._viewport.width = Math.max(Math.round(viewport.width * width), 1); this._viewport.height = Math.max(Math.round(viewport.height * height), 1); - // MSAA - const enableMSAA = !disableMSAA && this._cameraConfigs.enableMSAA; - assert(!enableMSAA || this._cameraConfigs.singleForwardRadiancePass); - - // ---------------------------------------------------------------- - // Forward Lighting (Main Directional Light) - // ---------------------------------------------------------------- - const pass = this._cameraConfigs.singleForwardRadiancePass - ? this._addForwardSingleRadiancePass(ppl, id, camera, enableMSAA, width, height, mainLight, - colorName, depthStencilName, depthStencilStoreOp) - : this._addForwardMultipleRadiancePasses(ppl, id, camera, width, height, mainLight, - colorName, depthStencilName, depthStencilStoreOp); - - // Planar Shadow - if (this._cameraConfigs.enableMainLightPlanarShadowMap) { - this.addPlanarShadowQueue(camera, mainLight, pass); - } - - // ---------------------------------------------------------------- - // Forward Lighting (Blend) - // ---------------------------------------------------------------- - // Add transparent queue - - const sceneFlags = SceneFlags.BLEND | - (camera.geometryRenderer - ? SceneFlags.GEOMETRY - : SceneFlags.NONE); - - pass - .addQueue(QueueHint.BLEND) - .addScene(camera, sceneFlags, mainLight || undefined); - - return pass; - } - - private _addForwardSingleRadiancePass( - ppl: rendering.BasicPipeline, - id: number, - camera: renderer.scene.Camera, - enableMSAA: boolean, - width: number, - height: number, - mainLight: renderer.scene.DirectionalLight | null, - colorName: string, - depthStencilName: string, - depthStencilStoreOp: gfx.StoreOp - ): rendering.BasicRenderPassBuilder { - assert(this._cameraConfigs.singleForwardRadiancePass); - // ---------------------------------------------------------------- - // Forward Lighting (Main Directional Light) - // ---------------------------------------------------------------- - let pass: rendering.BasicRenderPassBuilder; - if (enableMSAA) { - const msaaRadianceName = `MsaaRadiance${id}`; - const msaaDepthStencilName = `MsaaDepthStencil${id}`; - const sampleCount = this._cameraConfigs.settings.msaa.sampleCount; - - const msPass = ppl.addMultisampleRenderPass(width, height, sampleCount, 0, 'default'); - msPass.name = 'MsaaForwardPass'; - - // MSAA always discards depth stencil - this._buildForwardMainLightPass(msPass, id, camera, - msaaRadianceName, msaaDepthStencilName, StoreOp.DISCARD, mainLight); - - msPass.resolveRenderTarget(msaaRadianceName, colorName); - - pass = msPass; - } else { - pass = ppl.addRenderPass(width, height, 'default'); - pass.name = 'ForwardPass'; - - this._buildForwardMainLightPass(pass, id, camera, - colorName, depthStencilName, depthStencilStoreOp, mainLight); - } - assert(pass !== undefined); - - // Forward Lighting (Additive Lights) - this.forwardLighting.addLightQueues( - pass, - camera, - this._configs.mobileMaxSpotLightShadowMaps, - ); - - return pass; - } - public addPlanarShadowQueue( - camera: renderer.scene.Camera, - mainLight: renderer.scene.DirectionalLight | null, - pass: rendering.BasicRenderPassBuilder, - ) { - pass.addQueue(QueueHint.BLEND, 'planar-shadow') - .addScene( - camera, - SceneFlags.SHADOW_CASTER | SceneFlags.PLANAR_SHADOW | SceneFlags.BLEND, - mainLight || undefined, - ); - } - private _addForwardMultipleRadiancePasses( - ppl: rendering.BasicPipeline, - id: number, - camera: renderer.scene.Camera, - width: number, - height: number, - mainLight: renderer.scene.DirectionalLight | null, - colorName: string, - depthStencilName: string, - depthStencilStoreOp: gfx.StoreOp - ): rendering.BasicRenderPassBuilder { - assert(!this._cameraConfigs.singleForwardRadiancePass); - - // Forward Lighting (Main Directional Light) - let pass = ppl.addRenderPass(width, height, 'default'); - pass.name = 'ForwardPass'; - - const firstStoreOp = this.forwardLighting.isMultipleLightPassesNeeded() - ? StoreOp.STORE - : depthStencilStoreOp; - - this._buildForwardMainLightPass(pass, id, camera, - colorName, depthStencilName, firstStoreOp, mainLight); - - // Forward Lighting (Additive Lights) - pass = this.forwardLighting - .addLightPasses(colorName, depthStencilName, depthStencilStoreOp, - id, width, height, camera, this._viewport, ppl, pass); - - return pass; - } + const clearColor = camera.clearColor; // Reduce C++/TS interop + this._clearColor.x = clearColor.x; + this._clearColor.y = clearColor.y; + this._clearColor.z = clearColor.z; + this._clearColor.w = clearColor.w; - private _buildReflectionProbePass( - pass: rendering.BasicRenderPassBuilder, - id: number, - camera: renderer.scene.Camera, - colorName: string, - depthStencilName: string, - mainLight: renderer.scene.DirectionalLight | null, - scene: renderer.RenderScene | null = null, - ): void { - // set viewport - const colorStoreOp = this._cameraConfigs.enableMSAA ? StoreOp.DISCARD : StoreOp.STORE; + const pass = ppl.addRenderPass(width, height, 'default'); // bind output render target if (forwardNeedClearColor(camera)) { - this._reflectionProbeClearColor.x = camera.clearColor.x; - this._reflectionProbeClearColor.y = camera.clearColor.y; - this._reflectionProbeClearColor.z = camera.clearColor.z; - const clearColor = rendering.packRGBE(this._reflectionProbeClearColor); - this._clearColor.x = clearColor.x; - this._clearColor.y = clearColor.y; - this._clearColor.z = clearColor.z; - this._clearColor.w = clearColor.w; - pass.addRenderTarget(colorName, LoadOp.CLEAR, colorStoreOp, this._clearColor); + pass.addRenderTarget(colorName, LoadOp.CLEAR, StoreOp.STORE, this._clearColor); } else { - pass.addRenderTarget(colorName, LoadOp.LOAD, colorStoreOp); + pass.addRenderTarget(colorName, LoadOp.LOAD, StoreOp.STORE); } // bind depth stencil buffer @@ -1442,83 +1787,45 @@ if (rendering) { pass.addDepthStencil(depthStencilName, LoadOp.LOAD, StoreOp.DISCARD); } - // Set shadow map if enabled - if (this._cameraConfigs.enableMainLightShadowMap) { - pass.addTexture(`ShadowMap${id}`, 'cc_shadowMap'); - } + pass.setViewport(this._viewport); - // TODO(zhouzhenglong): Separate OPAQUE and MASK queue + // The opaque queue is used for Reflection probe preview + pass.addQueue(QueueHint.OPAQUE) + .addScene(camera, SceneFlags.OPAQUE); - // add opaque and mask queue - pass.addQueue(QueueHint.NONE, 'reflect-map') // Currently we put OPAQUE and MASK into one queue, so QueueHint is NONE - .addScene(camera, - SceneFlags.OPAQUE | SceneFlags.MASK | SceneFlags.REFLECTION_PROBE, - mainLight || undefined, - scene ? scene : undefined); + // The blend queue is used for UI and Gizmos + let flags = SceneFlags.BLEND | SceneFlags.UI; + if (this._cameraConfigs.enableProfiler) { + flags |= SceneFlags.PROFILER; + pass.showStatistics = true; + } + pass.addQueue(QueueHint.BLEND) + .addScene(camera, flags); } - private _tryAddReflectionProbePasses(ppl: rendering.BasicPipeline, id: number, - mainLight: renderer.scene.DirectionalLight | null, - scene: renderer.RenderScene | null, + private _buildForwardPipeline( + ppl: rendering.BasicPipeline, + camera: renderer.scene.Camera, + scene: renderer.RenderScene, + passBuilders: rendering.PipelinePassBuilder[], ): void { - const reflectionProbeManager = cclegacy.internal.reflectionProbeManager as ReflectionProbeManager | undefined; - if (!reflectionProbeManager) { - return; - } - const probes = reflectionProbeManager.getProbes(); - const maxProbeCount = 4; - let probeID = 0; - for (const probe of probes) { - if (!probe.needRender) { - continue; - } - const area = probe.renderArea(); - const width = Math.max(Math.floor(area.x), 1); - const height = Math.max(Math.floor(area.y), 1); - - if (probe.probeType === renderer.scene.ProbeType.PLANAR) { - if (!this._cameraConfigs.enablePlanarReflectionProbe) { - continue; - } - const window: renderer.RenderWindow = probe.realtimePlanarTexture!.window!; - const colorName = `PlanarProbeRT${probeID}`; - const depthStencilName = `PlanarProbeDS${probeID}`; - // ProbeResource - ppl.addRenderWindow(colorName, - this._cameraConfigs.radianceFormat, width, height, window); - ppl.addDepthStencil(depthStencilName, - gfx.Format.DEPTH_STENCIL, width, height, ResourceResidency.MEMORYLESS); + sortPipelinePassBuildersByRenderOrder(passBuilders); - // Rendering - const probePass = ppl.addRenderPass(width, height, 'default'); - probePass.name = `PlanarReflectionProbe${probeID}`; - this._buildReflectionProbePass(probePass, id, probe.camera, - colorName, depthStencilName, mainLight, scene); - } else if (EDITOR) { - for (let faceIdx = 0; faceIdx < probe.bakedCubeTextures.length; faceIdx++) { - probe.updateCameraDir(faceIdx); - const window: renderer.RenderWindow = probe.bakedCubeTextures[faceIdx].window!; - const colorName = `CubeProbeRT${probeID}${faceIdx}`; - const depthStencilName = `CubeProbeDS${probeID}${faceIdx}`; - // ProbeResource - ppl.addRenderWindow(colorName, - this._cameraConfigs.radianceFormat, width, height, window); - ppl.addDepthStencil(depthStencilName, - gfx.Format.DEPTH_STENCIL, width, height, ResourceResidency.MEMORYLESS); - - // Rendering - const probePass = ppl.addRenderPass(width, height, 'default'); - probePass.name = `CubeProbe${probeID}${faceIdx}`; - this._buildReflectionProbePass(probePass, id, probe.camera, - colorName, depthStencilName, mainLight, scene); - } - probe.needRender = false; - } - ++probeID; - if (probeID === maxProbeCount) { - break; + const context: PipelineContext = { + colorName: '', + depthStencilName: '', + }; + + let lastPass: rendering.BasicRenderPassBuilder | undefined = undefined; + + for (const builder of passBuilders) { + if (builder.setup) { + lastPass = builder.setup(ppl, this._configs, this._cameraConfigs, + camera, context, lastPass); } } + + assert(this._cameraConfigs.remainingPasses === 0); } private _initMaterials(ppl: rendering.BasicPipeline): number {