mirror of
https://github.com/cocos/cocos-engine.git
synced 2026-09-06 15:48:37 +08:00
2501 lines
100 KiB
TypeScript
2501 lines
100 KiB
TypeScript
/*
|
|
Copyright (c) 2022-2023 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 { EDITOR } from 'internal:constants';
|
|
import { assert } from '@base/debug';
|
|
import { cclegacy } from '@base/global';
|
|
import { Vec2, Vec3, Vec4, toRadian, nextPow2 } from '@base/math';
|
|
import { BufferInfo, Buffer, BufferUsageBit, ClearFlagBit, Color, DescriptorSet, LoadOp, Format, Rect, Sampler, StoreOp, Texture, Viewport, MemoryUsageBit, Filter, Address } from '../../gfx';
|
|
import { ProbeType, ReflectionProbe } from '../../render-scene/scene/reflection-probe';
|
|
import { Camera, SKYBOX_FLAG } from '../../render-scene/scene/camera';
|
|
import { CSMLevel, ShadowType } from '../../render-scene/scene/shadows';
|
|
import { Light, LightType } from '../../render-scene/scene/light';
|
|
import { DirectionalLight } from '../../render-scene/scene/directional-light';
|
|
import { RangedDirectionalLight } from '../../render-scene/scene/ranged-directional-light';
|
|
import { PointLight } from '../../render-scene/scene/point-light';
|
|
import { SphereLight } from '../../render-scene/scene/sphere-light';
|
|
import { SpotLight } from '../../render-scene/scene/spot-light';
|
|
import { supportsR32FloatTexture, supportsRGBA16HalfFloatTexture } from '../define';
|
|
import { BasicPipeline, Pipeline } from './pipeline';
|
|
import { AccessType, AttachmentType, CopyPair, LightInfo, QueueHint, ResourceResidency, SceneFlags, UpdateFrequency, UploadPair } from './types';
|
|
import { macro, geometry } from '../../core';
|
|
import { ImageAsset, Material, Texture2D } from '../../asset/assets';
|
|
import { getProfilerCamera, SRGBToLinear } from '../pipeline-funcs';
|
|
import { RenderWindow } from '../../render-scene/core/render-window';
|
|
import { RenderData, RenderGraph } from './render-graph';
|
|
import { WebComputePassBuilder, WebPipeline } from './web-pipeline';
|
|
import { DescriptorSetData, LayoutGraph, LayoutGraphData } from './layout-graph';
|
|
import { AABB } from '../../core/geometry';
|
|
import { DebugViewCompositeType, DebugViewSingleType } from '../debug-view';
|
|
import { ReflectionProbeManager } from '../../3d/reflection-probe/reflection-probe-manager';
|
|
|
|
const _rangedDirLightBoundingBox = new AABB(0.0, 0.0, 0.0, 0.5, 0.5, 0.5);
|
|
const _tmpBoundingBox = new AABB();
|
|
|
|
// Anti-aliasing type, other types will be gradually added in the future
|
|
export enum AntiAliasing {
|
|
NONE,
|
|
FXAA,
|
|
FXAAHQ,
|
|
}
|
|
|
|
export function getRTFormatBeforeToneMapping (ppl: BasicPipeline): Format {
|
|
const useFloatOutput = ppl.getMacroBool('CC_USE_FLOAT_OUTPUT');
|
|
return ppl.pipelineSceneData.isHDR && useFloatOutput && supportsRGBA16HalfFloatTexture(ppl.device) ? Format.RGBA16F : Format.RGBA8;
|
|
}
|
|
function forceEnableFloatOutput (ppl: BasicPipeline): void {
|
|
if (ppl.pipelineSceneData.isHDR && !ppl.getMacroBool('CC_USE_FLOAT_OUTPUT')) {
|
|
const supportFloatOutput = supportsRGBA16HalfFloatTexture(ppl.device);
|
|
ppl.setMacroBool('CC_USE_FLOAT_OUTPUT', supportFloatOutput);
|
|
macro.ENABLE_FLOAT_OUTPUT = supportFloatOutput;
|
|
}
|
|
}
|
|
|
|
export function validPunctualLightsCulling (pipeline: BasicPipeline, camera: Camera): void {
|
|
const sceneData = pipeline.pipelineSceneData;
|
|
const validPunctualLights = sceneData.validPunctualLights;
|
|
validPunctualLights.length = 0;
|
|
const _sphere = geometry.Sphere.create(0, 0, 0, 1);
|
|
const { spotLights } = camera.scene!;
|
|
for (let i = 0; i < spotLights.length; i++) {
|
|
const light = spotLights[i];
|
|
if (light.baked && !camera.node.scene.globals.disableLightmap) {
|
|
continue;
|
|
}
|
|
|
|
geometry.Sphere.set(_sphere, light.position.x, light.position.y, light.position.z, light.range);
|
|
if (geometry.intersect.sphereFrustum(_sphere, camera.frustum)) {
|
|
validPunctualLights.push(light);
|
|
}
|
|
}
|
|
|
|
const { sphereLights } = camera.scene!;
|
|
for (let i = 0; i < sphereLights.length; i++) {
|
|
const light = sphereLights[i];
|
|
if (light.baked && !camera.node.scene.globals.disableLightmap) {
|
|
continue;
|
|
}
|
|
geometry.Sphere.set(_sphere, light.position.x, light.position.y, light.position.z, light.range);
|
|
if (geometry.intersect.sphereFrustum(_sphere, camera.frustum)) {
|
|
validPunctualLights.push(light);
|
|
}
|
|
}
|
|
|
|
const { pointLights } = camera.scene!;
|
|
for (let i = 0; i < pointLights.length; i++) {
|
|
const light = pointLights[i];
|
|
if (light.baked) {
|
|
continue;
|
|
}
|
|
geometry.Sphere.set(_sphere, light.position.x, light.position.y, light.position.z, light.range);
|
|
if (geometry.intersect.sphereFrustum(_sphere, camera.frustum)) {
|
|
validPunctualLights.push(light);
|
|
}
|
|
}
|
|
|
|
const { rangedDirLights } = camera.scene!;
|
|
for (let i = 0; i < rangedDirLights.length; i++) {
|
|
const light = rangedDirLights[i];
|
|
AABB.transform(_tmpBoundingBox, _rangedDirLightBoundingBox, light.node!.getWorldMatrix());
|
|
if (geometry.intersect.aabbFrustum(_tmpBoundingBox, camera.frustum)) {
|
|
validPunctualLights.push(light);
|
|
}
|
|
}
|
|
// in jsb, std::vector is not synchronized, so we need to assign it manually
|
|
sceneData.validPunctualLights = validPunctualLights;
|
|
}
|
|
|
|
const _cameras: Camera[] = [];
|
|
|
|
export function getCameraUniqueID (camera: Camera): number {
|
|
if (!_cameras.includes(camera)) {
|
|
_cameras.push(camera);
|
|
}
|
|
return _cameras.indexOf(camera);
|
|
}
|
|
|
|
export function getLoadOpOfClearFlag (clearFlag: ClearFlagBit, attachment: AttachmentType): LoadOp {
|
|
let loadOp = LoadOp.CLEAR;
|
|
if (!(clearFlag & ClearFlagBit.COLOR)
|
|
&& attachment === AttachmentType.RENDER_TARGET) {
|
|
if (clearFlag & SKYBOX_FLAG) {
|
|
loadOp = LoadOp.CLEAR;
|
|
} else {
|
|
loadOp = LoadOp.LOAD;
|
|
}
|
|
}
|
|
if ((clearFlag & ClearFlagBit.DEPTH_STENCIL) !== ClearFlagBit.DEPTH_STENCIL
|
|
&& attachment === AttachmentType.DEPTH_STENCIL) {
|
|
if (!(clearFlag & ClearFlagBit.DEPTH)) loadOp = LoadOp.LOAD;
|
|
if (!(clearFlag & ClearFlagBit.STENCIL)) loadOp = LoadOp.LOAD;
|
|
}
|
|
return loadOp;
|
|
}
|
|
|
|
export function getRenderArea (
|
|
camera: Camera,
|
|
width: number,
|
|
height: number,
|
|
light: Light | null = null,
|
|
level = 0,
|
|
out: Rect | undefined = undefined,
|
|
): Rect {
|
|
out = out || new Rect();
|
|
const vp = camera ? camera.viewport : new Rect(0, 0, 1, 1);
|
|
const w = width;
|
|
const h = height;
|
|
out.x = vp.x * w;
|
|
out.y = vp.y * h;
|
|
out.width = vp.width * w;
|
|
out.height = vp.height * h;
|
|
if (light) {
|
|
switch (light.type) {
|
|
case LightType.DIRECTIONAL: {
|
|
const mainLight = light as DirectionalLight;
|
|
if (mainLight.shadowFixedArea || mainLight.csmLevel === CSMLevel.LEVEL_1) {
|
|
out.x = 0;
|
|
out.y = 0;
|
|
out.width = w;
|
|
out.height = h;
|
|
} else {
|
|
const screenSpaceSignY = cclegacy.director.root.device.capabilities.screenSpaceSignY;
|
|
out.x = level % 2 * 0.5 * w;
|
|
if (screenSpaceSignY > 0) {
|
|
out.y = (1 - Math.floor(level / 2)) * 0.5 * h;
|
|
} else {
|
|
out.y = Math.floor(level / 2) * 0.5 * h;
|
|
}
|
|
out.width = 0.5 * w;
|
|
out.height = 0.5 * h;
|
|
}
|
|
break;
|
|
}
|
|
case LightType.SPOT: {
|
|
out.x = 0;
|
|
out.y = 0;
|
|
out.width = w;
|
|
out.height = h;
|
|
break;
|
|
}
|
|
default:
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
class FxaaData {
|
|
declare fxaaMaterial: Material;
|
|
private _updateFxaaPass (): void {
|
|
if (!this.fxaaMaterial) return;
|
|
|
|
const combinePass = this.fxaaMaterial.passes[0];
|
|
combinePass.beginChangeStatesSilently();
|
|
combinePass.tryCompile();
|
|
combinePass.endChangeStatesSilently();
|
|
}
|
|
private _init (): void {
|
|
if (this.fxaaMaterial) return;
|
|
this.fxaaMaterial = new Material();
|
|
this.fxaaMaterial._uuid = 'builtin-fxaa-material';
|
|
this.fxaaMaterial.initialize({ effectName: 'pipeline/post-process/fxaa-hq' });
|
|
for (let i = 0; i < this.fxaaMaterial.passes.length; ++i) {
|
|
this.fxaaMaterial.passes[i].tryCompile();
|
|
}
|
|
this._updateFxaaPass();
|
|
}
|
|
constructor () {
|
|
this._init();
|
|
}
|
|
}
|
|
|
|
export function buildCopyPass (ppl: BasicPipeline, inOuts: CopyPair[]): void {
|
|
ppl.addCopyPass(inOuts);
|
|
}
|
|
|
|
let fxaaData: FxaaData | null = null;
|
|
export function buildFxaaPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
inputRT: string,
|
|
inputDS: string,
|
|
): { rtName: string; dsName: string; } {
|
|
if (!fxaaData) {
|
|
fxaaData = new FxaaData();
|
|
}
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const cameraName = `Camera${cameraID}`;
|
|
let width = camera.window.width;
|
|
let height = camera.window.height;
|
|
const area = getRenderArea(camera, width, height);
|
|
width = area.width;
|
|
height = area.height;
|
|
// Start
|
|
const clearColor = new Color(0, 0, 0, 1);
|
|
if (camera.clearFlag & ClearFlagBit.COLOR) {
|
|
clearColor.x = camera.clearColor.x;
|
|
clearColor.y = camera.clearColor.y;
|
|
clearColor.z = camera.clearColor.z;
|
|
}
|
|
clearColor.w = camera.clearColor.w;
|
|
|
|
const fxaaPassRTName = `dsFxaaPassColor${cameraName}`;
|
|
|
|
// ppl.updateRenderWindow(inputRT, camera.window);
|
|
if (!ppl.containsResource(fxaaPassRTName)) {
|
|
ppl.addRenderTarget(fxaaPassRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(fxaaPassRTName, width, height);
|
|
const fxaaPassIdx = 0;
|
|
const fxaaPass = ppl.addRenderPass(width, height, 'fxaa');
|
|
fxaaPass.name = `CameraFxaaPass${cameraID}`;
|
|
fxaaPass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
if (ppl.containsResource(inputRT)) {
|
|
fxaaPass.addTexture(inputRT, 'sceneColorMap');
|
|
}
|
|
fxaaPass.addRenderTarget(fxaaPassRTName, LoadOp.CLEAR, StoreOp.STORE, clearColor);
|
|
fxaaData.fxaaMaterial.setProperty('texSize', new Vec4(width, height, 1.0 / width, 1.0 / height), fxaaPassIdx);
|
|
fxaaPass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad(
|
|
camera,
|
|
fxaaData.fxaaMaterial,
|
|
fxaaPassIdx,
|
|
SceneFlags.NONE,
|
|
);
|
|
return { rtName: fxaaPassRTName, dsName: inputDS };
|
|
}
|
|
|
|
export const MAX_BLOOM_FILTER_PASS_NUM = 6;
|
|
export const BLOOM_PREFILTERPASS_INDEX = 0;
|
|
export const BLOOM_DOWNSAMPLEPASS_INDEX = 1;
|
|
export const BLOOM_UPSAMPLEPASS_INDEX = BLOOM_DOWNSAMPLEPASS_INDEX + MAX_BLOOM_FILTER_PASS_NUM;
|
|
export const BLOOM_COMBINEPASS_INDEX = BLOOM_UPSAMPLEPASS_INDEX + MAX_BLOOM_FILTER_PASS_NUM;
|
|
class BloomData {
|
|
declare bloomMaterial: Material;
|
|
threshold = 0.1;
|
|
iterations = 2;
|
|
intensity = 0.8;
|
|
private _updateBloomPass (): void {
|
|
if (!this.bloomMaterial) return;
|
|
|
|
const prefilterPass = this.bloomMaterial.passes[BLOOM_PREFILTERPASS_INDEX];
|
|
prefilterPass.beginChangeStatesSilently();
|
|
prefilterPass.tryCompile();
|
|
prefilterPass.endChangeStatesSilently();
|
|
|
|
for (let i = 0; i < MAX_BLOOM_FILTER_PASS_NUM; ++i) {
|
|
const downsamplePass = this.bloomMaterial.passes[BLOOM_DOWNSAMPLEPASS_INDEX + i];
|
|
downsamplePass.beginChangeStatesSilently();
|
|
downsamplePass.tryCompile();
|
|
downsamplePass.endChangeStatesSilently();
|
|
|
|
const upsamplePass = this.bloomMaterial.passes[BLOOM_UPSAMPLEPASS_INDEX + i];
|
|
upsamplePass.beginChangeStatesSilently();
|
|
upsamplePass.tryCompile();
|
|
upsamplePass.endChangeStatesSilently();
|
|
}
|
|
|
|
const combinePass = this.bloomMaterial.passes[BLOOM_COMBINEPASS_INDEX];
|
|
combinePass.beginChangeStatesSilently();
|
|
combinePass.tryCompile();
|
|
combinePass.endChangeStatesSilently();
|
|
}
|
|
private _init (): void {
|
|
if (this.bloomMaterial) return;
|
|
this.bloomMaterial = new Material();
|
|
this.bloomMaterial._uuid = 'builtin-bloom-material';
|
|
this.bloomMaterial.initialize({ effectName: 'pipeline/post-process/bloom' });
|
|
for (let i = 0; i < this.bloomMaterial.passes.length; ++i) {
|
|
this.bloomMaterial.passes[i].tryCompile();
|
|
}
|
|
this._updateBloomPass();
|
|
}
|
|
constructor () {
|
|
this._init();
|
|
}
|
|
}
|
|
let bloomData: BloomData | null = null;
|
|
export function buildBloomPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
inputRT: string,
|
|
threshold = 0.6,
|
|
iterations = 2,
|
|
intensity = 2.0,
|
|
): { rtName: string; dsName: string; } {
|
|
if (!bloomData) {
|
|
bloomData = new BloomData();
|
|
}
|
|
bloomData.threshold = threshold;
|
|
bloomData.iterations = iterations;
|
|
bloomData.intensity = intensity;
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const cameraName = `Camera${cameraID}`;
|
|
let width = camera.window.width;
|
|
let height = camera.window.height;
|
|
const area = getRenderArea(camera, width, height);
|
|
width = area.width;
|
|
height = area.height;
|
|
// Start bloom
|
|
const bloomClearColor = new Color(0, 0, 0, 1);
|
|
if (camera.clearFlag & ClearFlagBit.COLOR) {
|
|
bloomClearColor.x = camera.clearColor.x;
|
|
bloomClearColor.y = camera.clearColor.y;
|
|
bloomClearColor.z = camera.clearColor.z;
|
|
}
|
|
bloomClearColor.w = camera.clearColor.w;
|
|
// ==== Bloom prefilter ===
|
|
const bloomPassPrefilterRTName = `dsBloomPassPrefilterColor${cameraName}`;
|
|
const bloomPassPrefilterDSName = `dsBloomPassPrefilterDS${cameraName}`;
|
|
|
|
width >>= 1;
|
|
height >>= 1;
|
|
if (!ppl.containsResource(bloomPassPrefilterRTName)) {
|
|
ppl.addRenderTarget(bloomPassPrefilterRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED);
|
|
ppl.addDepthStencil(bloomPassPrefilterDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(bloomPassPrefilterRTName, width, height);
|
|
ppl.updateDepthStencil(bloomPassPrefilterDSName, width, height);
|
|
const bloomPrefilterPass = ppl.addRenderPass(width, height, 'bloom-prefilter');
|
|
bloomPrefilterPass.name = `CameraBloomPrefilterPass${cameraID}`;
|
|
bloomPrefilterPass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
if (ppl.containsResource(inputRT)) {
|
|
bloomPrefilterPass.addTexture(inputRT, 'outputResultMap');
|
|
}
|
|
bloomPrefilterPass.addRenderTarget(bloomPassPrefilterRTName, LoadOp.CLEAR, StoreOp.STORE, bloomClearColor);
|
|
bloomData.bloomMaterial.setProperty('texSize', new Vec4(0, 0, bloomData.threshold, 0), 0);
|
|
bloomPrefilterPass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad(
|
|
camera,
|
|
bloomData.bloomMaterial,
|
|
0,
|
|
SceneFlags.NONE,
|
|
);
|
|
// === Bloom downSampler ===
|
|
for (let i = 0; i < bloomData.iterations; ++i) {
|
|
const texSize = new Vec4(width, height, 0, 0);
|
|
const bloomPassDownSampleRTName = `dsBloomPassDownSampleColor${cameraName}${i}`;
|
|
const bloomPassDownSampleDSName = `dsBloomPassDownSampleDS${cameraName}${i}`;
|
|
width >>= 1;
|
|
height >>= 1;
|
|
if (!ppl.containsResource(bloomPassDownSampleRTName)) {
|
|
ppl.addRenderTarget(bloomPassDownSampleRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED);
|
|
ppl.addDepthStencil(bloomPassDownSampleDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(bloomPassDownSampleRTName, width, height);
|
|
ppl.updateDepthStencil(bloomPassDownSampleDSName, width, height);
|
|
const bloomDownSamplePass = ppl.addRenderPass(width, height, `bloom-downsample${i}`);
|
|
bloomDownSamplePass.name = `CameraBloomDownSamplePass${cameraID}${i}`;
|
|
bloomDownSamplePass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
if (i === 0) {
|
|
bloomDownSamplePass.addTexture(bloomPassPrefilterRTName, 'bloomTexture');
|
|
} else {
|
|
bloomDownSamplePass.addTexture(`dsBloomPassDownSampleColor${cameraName}${i - 1}`, 'bloomTexture');
|
|
}
|
|
bloomDownSamplePass.addRenderTarget(bloomPassDownSampleRTName, LoadOp.CLEAR, StoreOp.STORE, bloomClearColor);
|
|
bloomData.bloomMaterial.setProperty('texSize', texSize, BLOOM_DOWNSAMPLEPASS_INDEX + i);
|
|
bloomDownSamplePass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad(
|
|
camera,
|
|
bloomData.bloomMaterial,
|
|
BLOOM_DOWNSAMPLEPASS_INDEX + i,
|
|
SceneFlags.NONE,
|
|
);
|
|
}
|
|
// === Bloom upSampler ===
|
|
for (let i = 0; i < bloomData.iterations; ++i) {
|
|
const texSize = new Vec4(width, height, 0, 0);
|
|
const bloomPassUpSampleRTName = `dsBloomPassUpSampleColor${cameraName}${bloomData.iterations - 1 - i}`;
|
|
const bloomPassUpSampleDSName = `dsBloomPassUpSampleDS${cameraName}${bloomData.iterations - 1 - i}`;
|
|
width <<= 1;
|
|
height <<= 1;
|
|
if (!ppl.containsResource(bloomPassUpSampleRTName)) {
|
|
ppl.addRenderTarget(bloomPassUpSampleRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED);
|
|
ppl.addDepthStencil(bloomPassUpSampleDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(bloomPassUpSampleRTName, width, height);
|
|
ppl.updateDepthStencil(bloomPassUpSampleDSName, width, height);
|
|
const bloomUpSamplePass = ppl.addRenderPass(width, height, `bloom-upsample${i}`);
|
|
bloomUpSamplePass.name = `CameraBloomUpSamplePass${cameraID}${bloomData.iterations - 1 - i}`;
|
|
bloomUpSamplePass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
if (i === 0) {
|
|
bloomUpSamplePass.addTexture(`dsBloomPassDownSampleColor${cameraName}${bloomData.iterations - 1}`, 'bloomTexture');
|
|
} else {
|
|
bloomUpSamplePass.addTexture(`dsBloomPassUpSampleColor${cameraName}${bloomData.iterations - i}`, 'bloomTexture');
|
|
}
|
|
bloomUpSamplePass.addRenderTarget(bloomPassUpSampleRTName, LoadOp.CLEAR, StoreOp.STORE, bloomClearColor);
|
|
bloomData.bloomMaterial.setProperty('texSize', texSize, BLOOM_UPSAMPLEPASS_INDEX + i);
|
|
bloomUpSamplePass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad(
|
|
camera,
|
|
bloomData.bloomMaterial,
|
|
BLOOM_UPSAMPLEPASS_INDEX + i,
|
|
SceneFlags.NONE,
|
|
);
|
|
}
|
|
// === Bloom Combine Pass ===
|
|
const bloomPassCombineRTName = `dsBloomPassCombineColor${cameraName}`;
|
|
const bloomPassCombineDSName = `dsBloomPassCombineDS${cameraName}`;
|
|
|
|
width = area.width;
|
|
height = area.height;
|
|
if (!ppl.containsResource(bloomPassCombineRTName)) {
|
|
ppl.addRenderTarget(bloomPassCombineRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED);
|
|
ppl.addDepthStencil(bloomPassCombineDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(bloomPassCombineRTName, width, height);
|
|
ppl.updateDepthStencil(bloomPassCombineDSName, width, height);
|
|
const bloomCombinePass = ppl.addRenderPass(width, height, 'bloom-combine');
|
|
bloomCombinePass.name = `CameraBloomCombinePass${cameraID}`;
|
|
bloomCombinePass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
bloomCombinePass.addTexture(inputRT, 'outputResultMap');
|
|
bloomCombinePass.addTexture(`dsBloomPassUpSampleColor${cameraName}${0}`, 'bloomTexture');
|
|
bloomCombinePass.addRenderTarget(bloomPassCombineRTName, LoadOp.CLEAR, StoreOp.STORE, bloomClearColor);
|
|
bloomData.bloomMaterial.setProperty('texSize', new Vec4(0, 0, 0, bloomData.intensity), BLOOM_COMBINEPASS_INDEX);
|
|
bloomCombinePass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad(
|
|
camera,
|
|
bloomData.bloomMaterial,
|
|
BLOOM_COMBINEPASS_INDEX,
|
|
SceneFlags.NONE,
|
|
);
|
|
return { rtName: bloomPassCombineRTName, dsName: bloomPassCombineDSName };
|
|
}
|
|
|
|
export class PostInfo {
|
|
declare postMaterial: Material;
|
|
antiAliasing: AntiAliasing = AntiAliasing.NONE;
|
|
private _init (): void {
|
|
this.postMaterial = new Material();
|
|
this.postMaterial.name = 'builtin-post-process-material';
|
|
this.postMaterial.initialize({
|
|
effectName: 'pipeline/post-process',
|
|
defines: {
|
|
// Anti-aliasing type, currently only fxaa, so 1 means fxaa
|
|
ANTIALIAS_TYPE: this.antiAliasing,
|
|
},
|
|
});
|
|
for (let i = 0; i < this.postMaterial.passes.length; ++i) {
|
|
this.postMaterial.passes[i].tryCompile();
|
|
}
|
|
}
|
|
constructor (antiAliasing: AntiAliasing = AntiAliasing.NONE) {
|
|
this.antiAliasing = antiAliasing;
|
|
this._init();
|
|
}
|
|
}
|
|
|
|
let postInfo: PostInfo;
|
|
|
|
export function buildPostprocessPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
inputTex: string,
|
|
): { rtName: string; dsName: string; } {
|
|
if (!postInfo) {
|
|
postInfo = new PostInfo();
|
|
}
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
const postprocessPassRTName = `postprocessPassRTName${cameraID}`;
|
|
const postprocessPassDS = `postprocessPassDS${cameraID}`;
|
|
if (!ppl.containsResource(postprocessPassRTName)) {
|
|
ppl.addRenderWindow(postprocessPassRTName, Format.BGRA8, width, height, camera.window);
|
|
ppl.addDepthStencil(postprocessPassDS, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderWindow(postprocessPassRTName, camera.window);
|
|
ppl.updateDepthStencil(postprocessPassDS, width, height);
|
|
const postprocessPass = ppl.addRenderPass(width, height, 'post-process');
|
|
postprocessPass.name = `CameraPostprocessPass${cameraID}`;
|
|
postprocessPass.setViewport(new Viewport(area.x, area.y, area.width, area.height));
|
|
if (ppl.containsResource(inputTex)) {
|
|
postprocessPass.addTexture(inputTex, 'outputResultMap');
|
|
}
|
|
const postClearColor = new Color(0, 0, 0, camera.clearColor.w);
|
|
if (camera.clearFlag & ClearFlagBit.COLOR) {
|
|
postClearColor.x = camera.clearColor.x;
|
|
postClearColor.y = camera.clearColor.y;
|
|
postClearColor.z = camera.clearColor.z;
|
|
}
|
|
postprocessPass.addRenderTarget(
|
|
postprocessPassRTName,
|
|
getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET),
|
|
|
|
StoreOp.STORE,
|
|
|
|
postClearColor,
|
|
);
|
|
postprocessPass.addDepthStencil(
|
|
postprocessPassDS,
|
|
getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL),
|
|
StoreOp.STORE,
|
|
|
|
camera.clearDepth,
|
|
|
|
camera.clearStencil,
|
|
|
|
camera.clearFlag,
|
|
);
|
|
postprocessPass.addQueue(QueueHint.NONE).addFullscreenQuad(postInfo.postMaterial, 0, SceneFlags.NONE);
|
|
if (getProfilerCamera() === camera) {
|
|
postprocessPass.showStatistics = true;
|
|
}
|
|
return { rtName: postprocessPassRTName, dsName: postprocessPassDS };
|
|
}
|
|
|
|
export function buildForwardPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
isOffScreen: boolean,
|
|
enabledAlpha = true,
|
|
): { rtName: string; dsName: string; } {
|
|
if (EDITOR) {
|
|
ppl.setMacroInt('CC_PIPELINE_TYPE', 0);
|
|
}
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const cameraName = `Camera${cameraID}`;
|
|
const shadowInfo = buildShadowPasses(cameraName, camera, ppl);
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
|
|
const forwardPassRTName = `dsForwardPassColor${cameraName}`;
|
|
const forwardPassDSName = `dsForwardPassDS${cameraName}`;
|
|
if (!ppl.containsResource(forwardPassRTName)) {
|
|
if (!isOffScreen) {
|
|
ppl.addRenderWindow(forwardPassRTName, Format.BGRA8, width, height, camera.window);
|
|
} else {
|
|
ppl.addRenderTarget(forwardPassRTName, getRTFormatBeforeToneMapping(ppl), width, height, ResourceResidency.PERSISTENT);
|
|
}
|
|
ppl.addDepthStencil(forwardPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
if (!isOffScreen) {
|
|
ppl.updateRenderWindow(forwardPassRTName, camera.window);
|
|
ppl.updateDepthStencil(forwardPassDSName, width, height);
|
|
} else {
|
|
ppl.updateRenderTarget(forwardPassRTName, width, height);
|
|
ppl.updateDepthStencil(forwardPassDSName, width, height);
|
|
}
|
|
const forwardPass = ppl.addRenderPass(width, height, 'default');
|
|
forwardPass.name = `CameraForwardPass${cameraID}`;
|
|
forwardPass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
for (const dirShadowName of shadowInfo.mainLightShadowNames) {
|
|
if (ppl.containsResource(dirShadowName)) {
|
|
forwardPass.addTexture(dirShadowName, 'cc_shadowMap');
|
|
}
|
|
}
|
|
for (const spotShadowName of shadowInfo.spotLightShadowNames) {
|
|
if (ppl.containsResource(spotShadowName)) {
|
|
forwardPass.addTexture(spotShadowName, 'cc_spotShadowMap');
|
|
}
|
|
}
|
|
forwardPass.addRenderTarget(
|
|
forwardPassRTName,
|
|
isOffScreen ? LoadOp.CLEAR : getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET),
|
|
StoreOp.STORE,
|
|
new Color(camera.clearColor.x, camera.clearColor.y, camera.clearColor.z, camera.clearColor.w),
|
|
);
|
|
forwardPass.addDepthStencil(
|
|
forwardPassDSName,
|
|
isOffScreen ? LoadOp.CLEAR : getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL),
|
|
// If the depth texture is used by subsequent passes, it must be set to store.
|
|
isOffScreen ? StoreOp.DISCARD : StoreOp.STORE,
|
|
camera.clearDepth,
|
|
camera.clearStencil,
|
|
camera.clearFlag,
|
|
);
|
|
forwardPass
|
|
.addQueue(QueueHint.RENDER_OPAQUE)
|
|
.addSceneOfCamera(
|
|
camera,
|
|
new LightInfo(),
|
|
SceneFlags.OPAQUE_OBJECT | SceneFlags.PLANAR_SHADOW | SceneFlags.CUTOUT_OBJECT
|
|
| SceneFlags.DEFAULT_LIGHTING | SceneFlags.DRAW_INSTANCING,
|
|
);
|
|
let sceneFlags = SceneFlags.TRANSPARENT_OBJECT | SceneFlags.GEOMETRY;
|
|
if (!isOffScreen) {
|
|
sceneFlags |= SceneFlags.UI;
|
|
forwardPass.showStatistics = true;
|
|
}
|
|
if (enabledAlpha) {
|
|
forwardPass
|
|
.addQueue(QueueHint.RENDER_TRANSPARENT)
|
|
.addSceneOfCamera(camera, new LightInfo(), sceneFlags);
|
|
}
|
|
return { rtName: forwardPassRTName, dsName: forwardPassDSName };
|
|
}
|
|
|
|
let shadowPass;
|
|
export function buildShadowPass (
|
|
passName: Readonly<string>,
|
|
ppl: BasicPipeline,
|
|
camera: Camera,
|
|
light: Light,
|
|
level: number,
|
|
width: Readonly<number>,
|
|
height: Readonly<number>,
|
|
): void {
|
|
const fboW = width;
|
|
const fboH = height;
|
|
const area = getRenderArea(camera, width, height, light, level);
|
|
width = area.width;
|
|
height = area.height;
|
|
const device = ppl.device;
|
|
const shadowMapName = passName;
|
|
if (!ppl.containsResource(shadowMapName)) {
|
|
const format = supportsR32FloatTexture(device) ? Format.R32F : Format.RGBA8;
|
|
ppl.addRenderTarget(shadowMapName, format, fboW, fboH, ResourceResidency.MANAGED);
|
|
ppl.addDepthStencil(`${shadowMapName}Depth`, Format.DEPTH_STENCIL, fboW, fboH, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(shadowMapName, fboW, fboH);
|
|
ppl.updateDepthStencil(`${shadowMapName}Depth`, fboW, fboH);
|
|
if (!level) {
|
|
shadowPass = ppl.addRenderPass(width, height, 'default');
|
|
shadowPass.name = passName;
|
|
shadowPass.setViewport(new Viewport(0, 0, fboW, fboH));
|
|
shadowPass.addRenderTarget(shadowMapName, LoadOp.CLEAR, StoreOp.STORE, new Color(1, 1, 1, camera.clearColor.w));
|
|
shadowPass.addDepthStencil(
|
|
`${shadowMapName}Depth`,
|
|
LoadOp.CLEAR,
|
|
StoreOp.DISCARD,
|
|
camera.clearDepth,
|
|
camera.clearStencil,
|
|
ClearFlagBit.DEPTH_STENCIL,
|
|
);
|
|
}
|
|
const queue = shadowPass.addQueue(QueueHint.RENDER_OPAQUE, 'shadow-caster');
|
|
queue.addSceneOfCamera(
|
|
camera,
|
|
new LightInfo(light, level),
|
|
SceneFlags.SHADOW_CASTER | SceneFlags.OPAQUE_OBJECT | SceneFlags.TRANSPARENT_OBJECT,
|
|
);
|
|
queue.setViewport(new Viewport(area.x, area.y, area.width, area.height));
|
|
}
|
|
|
|
export function buildReflectionProbePasss (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
): void {
|
|
const reflectionProbeManager = cclegacy.internal.reflectionProbeManager as ReflectionProbeManager;
|
|
if (!reflectionProbeManager) return;
|
|
const probes = reflectionProbeManager.getProbes();
|
|
if (probes.length === 0) return;
|
|
for (let i = 0; i < probes.length; i++) {
|
|
const probe = probes[i];
|
|
if (probe.needRender) {
|
|
if (probes[i].probeType === ProbeType.PLANAR) {
|
|
buildReflectionProbePass(camera, ppl, probe, probe.realtimePlanarTexture!.window!, 0);
|
|
} else if (EDITOR) {
|
|
for (let faceIdx = 0; faceIdx < probe.bakedCubeTextures.length; faceIdx++) {
|
|
probe.updateCameraDir(faceIdx);
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
buildReflectionProbePass(camera, ppl, probe, probe.bakedCubeTextures[faceIdx].window!, faceIdx);
|
|
}
|
|
probe.needRender = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function buildReflectionProbePass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
probe: ReflectionProbe,
|
|
renderWindow: RenderWindow,
|
|
faceIdx: number,
|
|
): void {
|
|
const cameraName = `Camera${faceIdx}`;
|
|
const area = probe.renderArea();
|
|
const width = area.x;
|
|
const height = area.y;
|
|
const probeCamera = probe.camera;
|
|
|
|
const probePassRTName = `reflectionProbePassColor${cameraName}`;
|
|
const probePassDSName = `reflectionProbePassDS${cameraName}`;
|
|
|
|
if (!ppl.containsResource(probePassRTName)) {
|
|
ppl.addRenderWindow(probePassRTName, Format.RGBA8, width, height, renderWindow);
|
|
ppl.addDepthStencil(probePassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.EXTERNAL);
|
|
}
|
|
ppl.updateRenderWindow(probePassRTName, renderWindow);
|
|
ppl.updateDepthStencil(probePassDSName, width, height);
|
|
|
|
const probePass = ppl.addRenderPass(width, height, 'default');
|
|
probePass.name = `ReflectionProbePass${faceIdx}`;
|
|
probePass.setViewport(new Viewport(0, 0, width, height));
|
|
probePass.addRenderTarget(
|
|
probePassRTName,
|
|
getLoadOpOfClearFlag(probeCamera.clearFlag, AttachmentType.RENDER_TARGET),
|
|
StoreOp.STORE,
|
|
new Color(probeCamera.clearColor.x, probeCamera.clearColor.y, probeCamera.clearColor.z, probeCamera.clearColor.w),
|
|
);
|
|
probePass.addDepthStencil(
|
|
probePassDSName,
|
|
getLoadOpOfClearFlag(probeCamera.clearFlag, AttachmentType.DEPTH_STENCIL),
|
|
StoreOp.STORE,
|
|
probeCamera.clearDepth,
|
|
probeCamera.clearStencil,
|
|
probeCamera.clearFlag,
|
|
);
|
|
const passBuilder = probePass.addQueue(QueueHint.RENDER_OPAQUE);
|
|
passBuilder.addSceneOfCamera(camera, new LightInfo(), SceneFlags.REFLECTION_PROBE);
|
|
updateCameraUBO(passBuilder as unknown as any, probeCamera, ppl);
|
|
}
|
|
|
|
export class ShadowInfo {
|
|
shadowEnabled = false;
|
|
mainLightShadowNames = new Array<string>();
|
|
spotLightShadowNames = new Array<string>();
|
|
validLights: Light[] = [];
|
|
reset (): void {
|
|
this.shadowEnabled = false;
|
|
this.mainLightShadowNames.length = 0;
|
|
this.spotLightShadowNames.length = 0;
|
|
this.validLights.length = 0;
|
|
}
|
|
}
|
|
|
|
export function buildShadowPasses (cameraName: string, camera: Camera, ppl: BasicPipeline): ShadowInfo {
|
|
validPunctualLightsCulling(ppl, camera);
|
|
const pipeline = ppl;
|
|
const shadow = pipeline.pipelineSceneData.shadows;
|
|
const validPunctualLights = ppl.pipelineSceneData.validPunctualLights;
|
|
shadowInfo.reset();
|
|
const shadows = ppl.pipelineSceneData.shadows;
|
|
if (!shadow.enabled || shadow.type !== ShadowType.ShadowMap) { return shadowInfo; }
|
|
shadowInfo.shadowEnabled = true;
|
|
let n = 0;
|
|
let m = 0;
|
|
for (;n < shadow.maxReceived && m < validPunctualLights.length;) {
|
|
const light = validPunctualLights[m];
|
|
if (light.type === LightType.SPOT) {
|
|
const spotLight = light as SpotLight;
|
|
if (spotLight.shadowEnabled) {
|
|
shadowInfo.validLights.push(light);
|
|
n++;
|
|
}
|
|
}
|
|
m++;
|
|
}
|
|
|
|
const { mainLight } = camera.scene!;
|
|
// build shadow map
|
|
const mapWidth = shadows.size.x;
|
|
const mapHeight = shadows.size.y;
|
|
if (mainLight && mainLight.shadowEnabled) {
|
|
shadowInfo.mainLightShadowNames[0] = `MainLightShadow${cameraName}`;
|
|
if (mainLight.shadowFixedArea) {
|
|
buildShadowPass(
|
|
shadowInfo.mainLightShadowNames[0],
|
|
ppl,
|
|
camera,
|
|
mainLight,
|
|
0,
|
|
mapWidth,
|
|
mapHeight,
|
|
);
|
|
} else {
|
|
const csmLevel = pipeline.pipelineSceneData.csmSupported ? mainLight.csmLevel : 1;
|
|
shadowInfo.mainLightShadowNames[0] = `MainLightShadow${cameraName}`;
|
|
for (let i = 0; i < csmLevel; i++) {
|
|
buildShadowPass(
|
|
shadowInfo.mainLightShadowNames[0],
|
|
ppl,
|
|
camera,
|
|
mainLight,
|
|
i,
|
|
mapWidth,
|
|
mapHeight,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let l = 0; l < shadowInfo.validLights.length; l++) {
|
|
const light = shadowInfo.validLights[l];
|
|
const passName = `SpotLightShadow${l.toString()}${cameraName}`;
|
|
shadowInfo.spotLightShadowNames[l] = passName;
|
|
buildShadowPass(
|
|
passName,
|
|
ppl,
|
|
camera,
|
|
light,
|
|
0,
|
|
mapWidth,
|
|
mapHeight,
|
|
);
|
|
}
|
|
return shadowInfo;
|
|
}
|
|
const shadowInfo = new ShadowInfo();
|
|
|
|
export class GBufferInfo {
|
|
color!: string;
|
|
normal!: string;
|
|
emissive!: string;
|
|
ds!: string;
|
|
}
|
|
// deferred passes
|
|
export function buildGBufferPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
): GBufferInfo {
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
const gBufferPassRTName = `gBufferPassColorCamera`;
|
|
const gBufferPassNormal = `gBufferPassNormal`;
|
|
const gBufferPassEmissive = `gBufferPassEmissive`;
|
|
const gBufferPassDSName = `gBufferPassDSCamera`;
|
|
if (!ppl.containsResource(gBufferPassRTName)) {
|
|
const colFormat = Format.RGBA16F;
|
|
ppl.addRenderTarget(gBufferPassRTName, colFormat, width, height, ResourceResidency.MANAGED);
|
|
ppl.addRenderTarget(gBufferPassNormal, colFormat, width, height, ResourceResidency.MANAGED);
|
|
ppl.addRenderTarget(gBufferPassEmissive, colFormat, width, height, ResourceResidency.MANAGED);
|
|
ppl.addDepthStencil(gBufferPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(gBufferPassRTName, width, height);
|
|
ppl.updateRenderTarget(gBufferPassNormal, width, height);
|
|
ppl.updateRenderTarget(gBufferPassEmissive, width, height);
|
|
ppl.updateDepthStencil(gBufferPassDSName, width, height);
|
|
// gbuffer pass
|
|
const gBufferPass = ppl.addRenderPass(width, height, 'default');
|
|
gBufferPass.name = `CameraGBufferPass${cameraID}`;
|
|
gBufferPass.setViewport(new Viewport(area.x, area.y, area.width, area.height));
|
|
const rtColor = new Color(0, 0, 0, 0);
|
|
if (camera.clearFlag & ClearFlagBit.COLOR) {
|
|
if (ppl.pipelineSceneData.isHDR) {
|
|
SRGBToLinear(rtColor, camera.clearColor);
|
|
} else {
|
|
rtColor.x = camera.clearColor.x;
|
|
rtColor.y = camera.clearColor.y;
|
|
rtColor.z = camera.clearColor.z;
|
|
}
|
|
}
|
|
gBufferPass.addRenderTarget(gBufferPassRTName, LoadOp.CLEAR, StoreOp.STORE, rtColor);
|
|
gBufferPass.addRenderTarget(gBufferPassNormal, LoadOp.CLEAR, StoreOp.STORE, new Color(0, 0, 0, 0));
|
|
gBufferPass.addRenderTarget(gBufferPassEmissive, LoadOp.CLEAR, StoreOp.STORE, new Color(0, 0, 0, 0));
|
|
gBufferPass.addDepthStencil(gBufferPassDSName, LoadOp.CLEAR, StoreOp.STORE, camera.clearDepth, camera.clearStencil, camera.clearFlag);
|
|
gBufferPass
|
|
.addQueue(QueueHint.RENDER_OPAQUE)
|
|
.addSceneOfCamera(camera, new LightInfo(), SceneFlags.OPAQUE_OBJECT | SceneFlags.CUTOUT_OBJECT);
|
|
const gBufferInfo = new GBufferInfo();
|
|
gBufferInfo.color = gBufferPassRTName;
|
|
gBufferInfo.normal = gBufferPassNormal;
|
|
gBufferInfo.emissive = gBufferPassEmissive;
|
|
gBufferInfo.ds = gBufferPassDSName;
|
|
return gBufferInfo;
|
|
}
|
|
|
|
export class LightingInfo {
|
|
declare deferredLightingMaterial: Material;
|
|
public enableCluster: number;
|
|
|
|
private _init (): void {
|
|
this.deferredLightingMaterial = new Material();
|
|
this.deferredLightingMaterial.name = 'builtin-deferred-material';
|
|
this.deferredLightingMaterial.initialize({
|
|
effectName: 'pipeline/deferred-lighting',
|
|
defines: { CC_ENABLE_CLUSTERED_LIGHT_CULLING: this.enableCluster, CC_RECEIVE_SHADOW: 1 },
|
|
});
|
|
for (let i = 0; i < this.deferredLightingMaterial.passes.length; ++i) {
|
|
this.deferredLightingMaterial.passes[i].tryCompile();
|
|
}
|
|
}
|
|
constructor (clusterEn: boolean) {
|
|
this.enableCluster = clusterEn ? 1 : 0;
|
|
this._init();
|
|
}
|
|
}
|
|
|
|
let lightingInfo: LightingInfo;
|
|
|
|
// deferred lighting pass
|
|
export function buildLightingPass (camera: Camera, ppl: BasicPipeline, gBuffer: GBufferInfo): { rtName: string } {
|
|
if (!lightingInfo) {
|
|
lightingInfo = new LightingInfo(false);
|
|
}
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const cameraName = `Camera${cameraID}`;
|
|
const cameraInfo = buildShadowPasses(cameraName, camera, ppl);
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
|
|
const deferredLightingPassRTName = `deferredLightingPassRTName`;
|
|
if (!ppl.containsResource(deferredLightingPassRTName)) {
|
|
ppl.addRenderTarget(deferredLightingPassRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(deferredLightingPassRTName, width, height);
|
|
// lighting pass
|
|
const lightingPass = ppl.addRenderPass(width, height, 'deferred-lighting');
|
|
lightingPass.name = `CameraLightingPass${cameraID}`;
|
|
lightingPass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
for (const dirShadowName of cameraInfo.mainLightShadowNames) {
|
|
if (ppl.containsResource(dirShadowName)) {
|
|
lightingPass.addTexture(dirShadowName, 'cc_shadowMap');
|
|
}
|
|
}
|
|
for (const spotShadowName of cameraInfo.spotLightShadowNames) {
|
|
if (ppl.containsResource(spotShadowName)) {
|
|
lightingPass.addTexture(spotShadowName, 'cc_spotShadowMap');
|
|
}
|
|
}
|
|
if (ppl.containsResource(gBuffer.color)) {
|
|
lightingPass.addTexture(gBuffer.color, 'albedoMap');
|
|
lightingPass.addTexture(gBuffer.normal, 'normalMap');
|
|
lightingPass.addTexture(gBuffer.emissive, 'emissiveMap');
|
|
lightingPass.addTexture(gBuffer.ds, 'depthStencil');
|
|
}
|
|
const lightingClearColor = new Color(0, 0, 0, 0);
|
|
if (camera.clearFlag & ClearFlagBit.COLOR) {
|
|
lightingClearColor.x = camera.clearColor.x;
|
|
lightingClearColor.y = camera.clearColor.y;
|
|
lightingClearColor.z = camera.clearColor.z;
|
|
}
|
|
lightingClearColor.w = 0;
|
|
lightingPass.addRenderTarget(deferredLightingPassRTName, LoadOp.CLEAR, StoreOp.STORE, lightingClearColor);
|
|
lightingPass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad(
|
|
camera,
|
|
lightingInfo.deferredLightingMaterial,
|
|
0,
|
|
SceneFlags.VOLUMETRIC_LIGHTING,
|
|
);
|
|
// lightingPass.addQueue(QueueHint.RENDER_TRANSPARENT).addSceneOfCamera(camera, new LightInfo(),
|
|
// SceneFlags.TRANSPARENT_OBJECT | SceneFlags.PLANAR_SHADOW | SceneFlags.GEOMETRY);
|
|
return { rtName: deferredLightingPassRTName };
|
|
}
|
|
|
|
function getClearFlags (attachment: AttachmentType, clearFlag: ClearFlagBit, loadOp: LoadOp): ClearFlagBit {
|
|
switch (attachment) {
|
|
case AttachmentType.DEPTH_STENCIL:
|
|
if (loadOp === LoadOp.CLEAR) {
|
|
if (clearFlag & ClearFlagBit.DEPTH_STENCIL) {
|
|
return clearFlag;
|
|
} else {
|
|
return ClearFlagBit.DEPTH_STENCIL;
|
|
}
|
|
} else {
|
|
return ClearFlagBit.NONE;
|
|
}
|
|
case AttachmentType.RENDER_TARGET:
|
|
default:
|
|
if (loadOp === LoadOp.CLEAR) {
|
|
return ClearFlagBit.COLOR;
|
|
} else {
|
|
return ClearFlagBit.NONE;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function buildUIPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
): void {
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const cameraName = `Camera${cameraID}`;
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
|
|
const dsUIAndProfilerPassRTName = `dsUIAndProfilerPassColor${cameraName}`;
|
|
const dsUIAndProfilerPassDSName = `dsUIAndProfilerPassDS${cameraName}`;
|
|
if (!ppl.containsResource(dsUIAndProfilerPassRTName)) {
|
|
ppl.addRenderWindow(dsUIAndProfilerPassRTName, Format.BGRA8, width, height, camera.window);
|
|
ppl.addDepthStencil(dsUIAndProfilerPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderWindow(dsUIAndProfilerPassRTName, camera.window);
|
|
ppl.updateDepthStencil(dsUIAndProfilerPassDSName, width, height);
|
|
const uiAndProfilerPass = ppl.addRenderPass(width, height, 'default');
|
|
uiAndProfilerPass.name = `CameraUIAndProfilerPass${cameraID}`;
|
|
uiAndProfilerPass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
uiAndProfilerPass.addRenderTarget(
|
|
dsUIAndProfilerPassRTName,
|
|
getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET),
|
|
StoreOp.STORE,
|
|
new Color(camera.clearColor.x, camera.clearColor.y, camera.clearColor.z, camera.clearColor.w),
|
|
);
|
|
uiAndProfilerPass.addDepthStencil(
|
|
dsUIAndProfilerPassDSName,
|
|
getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL),
|
|
StoreOp.STORE,
|
|
camera.clearDepth,
|
|
|
|
camera.clearStencil,
|
|
|
|
camera.clearFlag,
|
|
);
|
|
const sceneFlags = SceneFlags.UI;
|
|
uiAndProfilerPass
|
|
.addQueue(QueueHint.RENDER_TRANSPARENT)
|
|
.addSceneOfCamera(camera, new LightInfo(), sceneFlags);
|
|
if (getProfilerCamera() === camera) {
|
|
uiAndProfilerPass.showStatistics = true;
|
|
}
|
|
}
|
|
|
|
export function updateCameraUBO (setter: any, camera: Readonly<Camera>, ppl: Readonly<BasicPipeline>): void {
|
|
const pipeline = cclegacy.director.root!.pipeline as WebPipeline;
|
|
const sceneData = ppl.pipelineSceneData;
|
|
const skybox = sceneData.skybox;
|
|
setter.addConstant('CCCamera');
|
|
setter.setMat4('cc_matView', camera.matView);
|
|
setter.setMat4('cc_matViewInv', camera.node.worldMatrix);
|
|
setter.setMat4('cc_matProj', camera.matProj);
|
|
setter.setMat4('cc_matProjInv', camera.matProjInv);
|
|
setter.setMat4('cc_matViewProj', camera.matViewProj);
|
|
setter.setMat4('cc_matViewProjInv', camera.matViewProjInv);
|
|
setter.setVec4('cc_cameraPos', new Vec4(camera.position.x, camera.position.y, camera.position.z, pipeline.getCombineSignY()));
|
|
// eslint-disable-next-line max-len
|
|
setter.setVec4('cc_surfaceTransform', new Vec4(camera.surfaceTransform, 0.0, Math.cos(toRadian(skybox.getRotationAngle())), Math.sin(toRadian(skybox.getRotationAngle()))));
|
|
// eslint-disable-next-line max-len
|
|
setter.setVec4('cc_screenScale', new Vec4(sceneData.shadingScale, sceneData.shadingScale, 1.0 / sceneData.shadingScale, 1.0 / sceneData.shadingScale));
|
|
setter.setVec4('cc_exposure', new Vec4(camera.exposure, 1.0 / camera.exposure, sceneData.isHDR ? 1.0 : 0.0, 1.0 / Camera.standardExposureValue));
|
|
}
|
|
|
|
function bindDescValue (desc: DescriptorSet, binding: number, value): void {
|
|
if (value instanceof Buffer) {
|
|
desc.bindBuffer(binding, value);
|
|
} else if (value instanceof Texture) {
|
|
desc.bindTexture(binding, value);
|
|
} else if (value instanceof Sampler) {
|
|
desc.bindSampler(binding, value);
|
|
}
|
|
}
|
|
|
|
function bindGlobalDesc (desc: DescriptorSet, binding: number, value): void {
|
|
bindDescValue(desc, binding, value);
|
|
}
|
|
|
|
export function getDescBinding (descId, descData: DescriptorSetData): number {
|
|
const layoutData = descData;
|
|
// find descriptor binding
|
|
for (const block of layoutData.descriptorSetLayoutData.descriptorBlocks) {
|
|
for (let i = 0; i !== block.descriptors.length; ++i) {
|
|
if (descId === block.descriptors[i].descriptorID) {
|
|
return block.offset + i;
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
export function getDescBindingFromName (bindingName: string): number {
|
|
const pipeline = cclegacy.director.root.pipeline as WebPipeline;
|
|
const layoutGraph = pipeline.layoutGraph;
|
|
const vertIds = layoutGraph.vertices();
|
|
const descId = layoutGraph.attributeIndex.get(bindingName);
|
|
let currDesData: DescriptorSetData;
|
|
for (const i of vertIds) {
|
|
const layout = layoutGraph.getLayout(i);
|
|
for (const [k, descData] of layout.descriptorSets) {
|
|
const layoutData = descData.descriptorSetLayoutData;
|
|
const blocks = layoutData.descriptorBlocks;
|
|
for (const b of blocks) {
|
|
for (const ds of b.descriptors) {
|
|
if (ds.descriptorID === descId) {
|
|
currDesData = descData;
|
|
return getDescBinding(descId, currDesData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
const uniformMap: Map<string, Float32Array> = new Map();
|
|
function applyGlobalDescBinding (data: RenderData, layout: string, isUpdate = false): void {
|
|
const constants = data.constants;
|
|
const samplers = data.samplers;
|
|
const textures = data.textures;
|
|
const root = cclegacy.director.root;
|
|
const device = root.device;
|
|
const pipeline = root.pipeline as WebPipeline;
|
|
const descriptorSetData = getDescriptorSetDataFromLayout(layout)!;
|
|
const descriptorSet = descriptorSetData.descriptorSet!;
|
|
for (const [key, value] of constants) {
|
|
const bindId = getDescBinding(key, descriptorSetData);
|
|
if (bindId === -1) { continue; }
|
|
const uniformKey = `${layout}${bindId}`;
|
|
let buffer = descriptorSet.getBuffer(bindId);
|
|
let haveBuff = true;
|
|
if (!buffer && !isUpdate) {
|
|
buffer = device.createBuffer(new BufferInfo(
|
|
BufferUsageBit.UNIFORM | BufferUsageBit.TRANSFER_DST,
|
|
MemoryUsageBit.HOST | MemoryUsageBit.DEVICE,
|
|
value.length * 4,
|
|
value.length * 4,
|
|
));
|
|
haveBuff = false;
|
|
}
|
|
if (isUpdate) {
|
|
let currUniform = uniformMap.get(uniformKey);
|
|
if (!currUniform) {
|
|
uniformMap.set(uniformKey, new Float32Array(value));
|
|
currUniform = uniformMap.get(uniformKey)!;
|
|
}
|
|
currUniform.set(value);
|
|
buffer.update(currUniform);
|
|
}
|
|
if (!haveBuff) bindGlobalDesc(descriptorSet, bindId, buffer);
|
|
}
|
|
for (const [key, value] of textures) {
|
|
const bindId = getDescBinding(key, descriptorSetData);
|
|
if (bindId === -1) { continue; }
|
|
const tex = descriptorSet.getTexture(bindId);
|
|
if (!tex || (isUpdate && value !== pipeline.defaultTexture)) {
|
|
bindGlobalDesc(descriptorSet, bindId, value);
|
|
}
|
|
}
|
|
for (const [key, value] of samplers) {
|
|
const bindId = getDescBinding(key, descriptorSetData);
|
|
if (bindId === -1) { continue; }
|
|
const sampler = descriptorSet.getSampler(bindId);
|
|
if (!sampler || (isUpdate && value !== pipeline.defaultSampler)) {
|
|
bindGlobalDesc(descriptorSet, bindId, value);
|
|
}
|
|
}
|
|
}
|
|
const layouts: Map<string, DescriptorSetData> = new Map();
|
|
export function getDescriptorSetDataFromLayout (layoutName: string): DescriptorSetData | undefined {
|
|
const descLayout = layouts.get(layoutName);
|
|
if (descLayout) {
|
|
return descLayout;
|
|
}
|
|
const webPip = cclegacy.director.root.pipeline as WebPipeline;
|
|
const stageId = webPip.layoutGraph.locateChild(webPip.layoutGraph.nullVertex(), layoutName);
|
|
assert(stageId !== 0xFFFFFFFF);
|
|
const layout = webPip.layoutGraph.getLayout(stageId);
|
|
const layoutData = layout.descriptorSets.get(UpdateFrequency.PER_PASS);
|
|
layouts.set(layoutName, layoutData!);
|
|
return layoutData;
|
|
}
|
|
|
|
export function getDescriptorSetDataFromLayoutId (id: number): DescriptorSetData | undefined {
|
|
const webPip = cclegacy.director.root.pipeline as WebPipeline;
|
|
const layout = webPip.layoutGraph.getLayout(id);
|
|
const layoutData = layout.descriptorSets.get(UpdateFrequency.PER_PASS);
|
|
return layoutData;
|
|
}
|
|
|
|
export function initGlobalDescBinding (data: RenderData, layoutName = 'default'): void {
|
|
applyGlobalDescBinding(data, layoutName);
|
|
}
|
|
|
|
export function updateGlobalDescBinding (data: RenderData, layoutName = 'default'): void {
|
|
applyGlobalDescBinding(data, layoutName, true);
|
|
}
|
|
|
|
export function mergeSrcToTargetDesc (fromDesc, toDesc, isForce = false): number[] {
|
|
fromDesc.update();
|
|
const fromGpuDesc = fromDesc.gpuDescriptorSet;
|
|
const toGpuDesc = toDesc.gpuDescriptorSet;
|
|
const extResId: number[] = [];
|
|
if (isForce) {
|
|
toGpuDesc.gpuDescriptors = fromGpuDesc.gpuDescriptors;
|
|
toGpuDesc.descriptorIndices = fromGpuDesc.descriptorIndices;
|
|
return extResId;
|
|
}
|
|
for (let i = 0; i < toGpuDesc.gpuDescriptors.length; i++) {
|
|
const fromRes = fromGpuDesc.gpuDescriptors[i];
|
|
if (!fromRes) continue;
|
|
const currRes = toGpuDesc.gpuDescriptors[i];
|
|
if (!currRes.gpuBuffer && fromRes.gpuBuffer) {
|
|
currRes.gpuBuffer = fromRes.gpuBuffer;
|
|
extResId.push(i);
|
|
} else if ('gpuTextureView' in currRes && !currRes.gpuTextureView) {
|
|
currRes.gpuTextureView = fromRes.gpuTextureView;
|
|
currRes.gpuSampler = fromRes.gpuSampler;
|
|
extResId.push(i);
|
|
} else if ('gpuTexture' in currRes && !currRes.gpuTexture) {
|
|
currRes.gpuTexture = fromRes.gpuTexture;
|
|
currRes.gpuSampler = fromRes.gpuSampler;
|
|
extResId.push(i);
|
|
}
|
|
}
|
|
return extResId;
|
|
}
|
|
|
|
const _varianceArray: number[] = [0.0484, 0.187, 0.567, 1.99, 7.41];
|
|
const _strengthParameterArray: number[] = [0.100, 0.118, 0.113, 0.358, 0.078];
|
|
const _vec3Temp: Vec3 = new Vec3();
|
|
const _vec3Temp2: Vec3 = new Vec3();
|
|
const _vec4Temp: Vec4 = new Vec4();
|
|
const _vec4Temp2: Vec4 = new Vec4();
|
|
|
|
export const COPY_INPUT_DS_PASS_INDEX = 0;
|
|
export const SSSS_BLUR_X_PASS_INDEX = 1;
|
|
export const SSSS_BLUR_Y_PASS_INDEX = 2;
|
|
export const EXPONENT = 2.0;
|
|
export const I_SAMPLES_COUNT = 25;
|
|
class SSSSBlurData {
|
|
declare ssssBlurMaterial: Material;
|
|
ssssFov = 45.0 / 57.3;
|
|
ssssWidth = 0.01;
|
|
boundingBox = 0.4;
|
|
ssssScale = 3.0;
|
|
|
|
get ssssStrength (): Vec3 {
|
|
return this._v3SSSSStrength;
|
|
}
|
|
set ssssStrength (val: Vec3) {
|
|
this._v3SSSSStrength = val;
|
|
this._updateSampleCount();
|
|
}
|
|
|
|
get ssssFallOff (): Vec3 {
|
|
return this._v3SSSSFallOff;
|
|
}
|
|
set ssssFallOff (val: Vec3) {
|
|
this._v3SSSSFallOff = val;
|
|
this._updateSampleCount();
|
|
}
|
|
|
|
get kernel (): Vec4[] {
|
|
return this._kernel;
|
|
}
|
|
|
|
private _v3SSSSStrength = new Vec3(0.48, 0.41, 0.28);
|
|
private _v3SSSSFallOff = new Vec3(1.0, 0.37, 0.3);
|
|
private _kernel: Vec4[] = [];
|
|
|
|
/**
|
|
* We use a falloff to modulate the shape of the profile. Big falloffs
|
|
* spreads the shape making it wider, while small falloffs make it
|
|
* narrower.
|
|
*/
|
|
private _gaussian (out: Vec3, variance: number, r: number): void {
|
|
const xx = r / (0.001 + this._v3SSSSFallOff.x);
|
|
out.x = Math.exp((-(xx * xx)) / (2.0 * variance)) / (2.0 * 3.14 * variance);
|
|
const yy = r / (0.001 + this._v3SSSSFallOff.y);
|
|
out.y = Math.exp((-(yy * yy)) / (2.0 * variance)) / (2.0 * 3.14 * variance);
|
|
const zz = r / (0.001 + this._v3SSSSFallOff.z);
|
|
out.z = Math.exp((-(zz * zz)) / (2.0 * variance)) / (2.0 * 3.14 * variance);
|
|
}
|
|
|
|
/**
|
|
* We used the red channel of the original skin profile defined in
|
|
* [d'Eon07] for all three channels. We noticed it can be used for green
|
|
* and blue channels (scaled using the falloff parameter) without
|
|
* introducing noticeable differences and allowing for total control over
|
|
* the profile. For example, it allows to create blue SSS gradients, which
|
|
* could be useful in case of rendering blue creatures.
|
|
*/
|
|
private _profile (out: Vec3, val: number): void {
|
|
for (let i = 0; i < 5; i++) {
|
|
this._gaussian(_vec3Temp2, _varianceArray[i], val);
|
|
_vec3Temp2.multiplyScalar(_strengthParameterArray[i]);
|
|
out.add(_vec3Temp2);
|
|
}
|
|
}
|
|
|
|
private _updateSampleCount (): void {
|
|
const strength = this._v3SSSSStrength;
|
|
const nSamples = I_SAMPLES_COUNT;
|
|
const range = nSamples > 20 ? 3.0 : 2.0;
|
|
|
|
// Calculate the offsets:
|
|
const step = 2.0 * range / (nSamples - 1);
|
|
for (let i = 0; i < nSamples; i++) {
|
|
const o = -range + i * step;
|
|
const sign = o < 0.0 ? -1.0 : 1.0;
|
|
// eslint-disable-next-line no-restricted-properties
|
|
this._kernel[i].w = range * sign * Math.abs(o ** EXPONENT) / range ** EXPONENT;
|
|
}
|
|
|
|
// Calculate the weights:
|
|
for (let i = 0; i < nSamples; i++) {
|
|
const w0 = i > 0 ? Math.abs(this._kernel[i].w - this._kernel[i - 1].w) : 0.0;
|
|
const w1 = i < nSamples - 1 ? Math.abs(this._kernel[i].w - this._kernel[i + 1].w) : 0.0;
|
|
const area = (w0 + w1) / 2.0;
|
|
_vec3Temp.set(0);
|
|
this._profile(_vec3Temp, this._kernel[i].w);
|
|
_vec3Temp.multiplyScalar(area);
|
|
this._kernel[i].x = _vec3Temp.x;
|
|
this._kernel[i].y = _vec3Temp.y;
|
|
this._kernel[i].z = _vec3Temp.z;
|
|
}
|
|
|
|
// We want the offset 0.0 to come first:
|
|
const remainder = nSamples % 2;
|
|
_vec4Temp.set(this._kernel[(nSamples - remainder) / 2]);
|
|
for (let i = (nSamples - remainder) / 2; i > 0; i--) {
|
|
_vec4Temp2.set(this._kernel[i - 1]);
|
|
this._kernel[i].set(_vec4Temp2);
|
|
}
|
|
this._kernel[0].set(_vec4Temp);
|
|
|
|
// Calculate the sum of the weights, we will need to normalize them below:
|
|
_vec3Temp.set(0.0);
|
|
for (let i = 0; i < nSamples; i++) {
|
|
_vec3Temp.add3f(this._kernel[i].x, this._kernel[i].y, this._kernel[i].z);
|
|
}
|
|
// Normalize the weights:
|
|
for (let i = 0; i < nSamples; i++) {
|
|
this._kernel[i].x /= _vec3Temp.x;
|
|
this._kernel[i].y /= _vec3Temp.y;
|
|
this._kernel[i].z /= _vec3Temp.z;
|
|
}
|
|
|
|
// Tweak them using the desired strength. The first one is:
|
|
// lerp(1.0, kernel[0].rgb, strength)
|
|
this._kernel[0].x = (1.0 - strength.x) * 1.0 + strength.x * this._kernel[0].x;
|
|
this._kernel[0].y = (1.0 - strength.y) * 1.0 + strength.y * this._kernel[0].y;
|
|
this._kernel[0].z = (1.0 - strength.z) * 1.0 + strength.z * this._kernel[0].z;
|
|
|
|
// The others:
|
|
// lerp(0.0, kernel[0].rgb, strength)
|
|
for (let i = 1; i < nSamples; i++) {
|
|
this._kernel[i].x *= strength.x;
|
|
this._kernel[i].y *= strength.y;
|
|
this._kernel[i].z *= strength.z;
|
|
}
|
|
}
|
|
|
|
private _updateBlurPass (): void {
|
|
if (!this.ssssBlurMaterial) return;
|
|
|
|
const copyInputDSPass = this.ssssBlurMaterial.passes[COPY_INPUT_DS_PASS_INDEX];
|
|
copyInputDSPass.beginChangeStatesSilently();
|
|
copyInputDSPass.tryCompile();
|
|
copyInputDSPass.endChangeStatesSilently();
|
|
const ssssBlurXPass = this.ssssBlurMaterial.passes[SSSS_BLUR_X_PASS_INDEX];
|
|
ssssBlurXPass.beginChangeStatesSilently();
|
|
ssssBlurXPass.tryCompile();
|
|
ssssBlurXPass.endChangeStatesSilently();
|
|
const ssssBlurYPass = this.ssssBlurMaterial.passes[SSSS_BLUR_Y_PASS_INDEX];
|
|
ssssBlurYPass.beginChangeStatesSilently();
|
|
ssssBlurYPass.tryCompile();
|
|
ssssBlurYPass.endChangeStatesSilently();
|
|
}
|
|
|
|
private _init (): void {
|
|
if (this.ssssBlurMaterial) return;
|
|
|
|
this.ssssBlurMaterial = new Material();
|
|
this.ssssBlurMaterial._uuid = 'builtin-ssssBlur-material';
|
|
this.ssssBlurMaterial.initialize({ effectName: 'pipeline/ssss-blur' });
|
|
for (let i = 0; i < this.ssssBlurMaterial.passes.length; ++i) {
|
|
this.ssssBlurMaterial.passes[i].tryCompile();
|
|
}
|
|
this._updateBlurPass();
|
|
|
|
for (let i = 0; i < I_SAMPLES_COUNT; i++) {
|
|
this._kernel[i] = new Vec4();
|
|
}
|
|
this._updateSampleCount();
|
|
}
|
|
|
|
constructor () {
|
|
this._init();
|
|
}
|
|
}
|
|
|
|
let ssssBlurData: SSSSBlurData | null = null;
|
|
export function hasSkinObject (ppl: BasicPipeline): boolean {
|
|
const sceneData = ppl.pipelineSceneData;
|
|
return sceneData.skin.enabled && sceneData.standardSkinModel !== null;
|
|
}
|
|
|
|
function _buildSSSSBlurPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
inputRT: string,
|
|
inputDS: string,
|
|
): { rtName: string; dsName: string; } {
|
|
const sceneData = ppl.pipelineSceneData;
|
|
const skin = sceneData.skin;
|
|
const standardSkinModel = sceneData.standardSkinModel;
|
|
if (!skin.enabled && standardSkinModel) return { rtName: inputRT, dsName: inputDS };
|
|
|
|
if (!ssssBlurData) ssssBlurData = new SSSSBlurData();
|
|
ssssBlurData.ssssFov = camera.fov;
|
|
ssssBlurData.ssssWidth = skin.blurRadius;
|
|
if (standardSkinModel && standardSkinModel.worldBounds) {
|
|
const halfExtents = standardSkinModel.worldBounds.halfExtents;
|
|
ssssBlurData.boundingBox = Math.min(halfExtents.x, halfExtents.y, halfExtents.z) * 2.0;
|
|
}
|
|
ssssBlurData.ssssScale = skin.sssIntensity;
|
|
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const cameraName = `Camera${cameraID}`;
|
|
const webPipeline = (ppl as WebPipeline);
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
|
|
// Start blur
|
|
const ssssBlurClearColor = new Color(0, 0, 0, 1);
|
|
if (camera.clearFlag & ClearFlagBit.COLOR) {
|
|
ssssBlurClearColor.x = camera.clearColor.x;
|
|
ssssBlurClearColor.y = camera.clearColor.y;
|
|
ssssBlurClearColor.z = camera.clearColor.z;
|
|
}
|
|
ssssBlurClearColor.w = camera.clearColor.w;
|
|
|
|
const ssssBlurRTName = `dsSSSSBlurColor${cameraName}`;
|
|
const ssssBlurDSName = `dsSSSSBlurDSColor${cameraName}`;
|
|
if (!ppl.containsResource(ssssBlurRTName)) {
|
|
ppl.addRenderTarget(ssssBlurRTName, getRTFormatBeforeToneMapping(ppl), width, height, ResourceResidency.MANAGED);
|
|
ppl.addRenderTarget(ssssBlurDSName, Format.RGBA8, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(ssssBlurRTName, width, height);
|
|
ppl.updateRenderTarget(ssssBlurDSName, width, height);
|
|
|
|
// ==== Copy input DS ===
|
|
const copyInputDSPass = ppl.addRenderPass(width, height, 'copy-pass');
|
|
copyInputDSPass.name = `CameraCopyDSPass${cameraID}`;
|
|
copyInputDSPass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
if (ppl.containsResource(inputDS)) {
|
|
const verId = webPipeline.resourceGraph.vertex(inputDS);
|
|
const sampler = webPipeline.resourceGraph.getSampler(verId);
|
|
sampler.minFilter = Filter.POINT;
|
|
sampler.magFilter = Filter.POINT;
|
|
sampler.mipFilter = Filter.NONE;
|
|
copyInputDSPass.addTexture(inputDS, 'depthRaw');
|
|
}
|
|
copyInputDSPass.addRenderTarget(
|
|
ssssBlurDSName,
|
|
LoadOp.CLEAR,
|
|
|
|
StoreOp.STORE,
|
|
|
|
new Color(1.0, 0.0, 0.0, 0.0),
|
|
);
|
|
copyInputDSPass.addQueue(QueueHint.RENDER_OPAQUE | QueueHint.RENDER_TRANSPARENT).addCameraQuad(
|
|
camera,
|
|
ssssBlurData.ssssBlurMaterial,
|
|
COPY_INPUT_DS_PASS_INDEX,
|
|
SceneFlags.NONE,
|
|
);
|
|
|
|
// ==== SSSS Blur X Pass ===
|
|
const ssssblurXPass = ppl.addRenderPass(width, height, 'ssss-blurX');
|
|
ssssblurXPass.name = `CameraSSSSBlurXPass${cameraID}`;
|
|
ssssblurXPass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
if (ppl.containsResource(inputRT)) {
|
|
const verId = webPipeline.resourceGraph.vertex(inputRT);
|
|
const sampler = webPipeline.resourceGraph.getSampler(verId);
|
|
sampler.minFilter = Filter.POINT;
|
|
sampler.magFilter = Filter.POINT;
|
|
sampler.mipFilter = Filter.NONE;
|
|
ssssblurXPass.addTexture(inputRT, 'colorTex');
|
|
}
|
|
if (ppl.containsResource(ssssBlurDSName)) {
|
|
const verId = webPipeline.resourceGraph.vertex(ssssBlurDSName);
|
|
const sampler = webPipeline.resourceGraph.getSampler(verId);
|
|
sampler.minFilter = Filter.POINT;
|
|
sampler.magFilter = Filter.POINT;
|
|
sampler.mipFilter = Filter.NONE;
|
|
ssssblurXPass.addTexture(ssssBlurDSName, 'depthTex');
|
|
}
|
|
ssssblurXPass.addRenderTarget(ssssBlurRTName, LoadOp.CLEAR, StoreOp.STORE, ssssBlurClearColor);
|
|
ssssblurXPass.addDepthStencil(
|
|
inputDS,
|
|
LoadOp.LOAD,
|
|
StoreOp.STORE,
|
|
camera.clearDepth,
|
|
camera.clearStencil,
|
|
camera.clearFlag,
|
|
);
|
|
ssssBlurData.ssssBlurMaterial.setProperty('blurInfo', new Vec4(
|
|
ssssBlurData.ssssFov,
|
|
ssssBlurData.ssssWidth,
|
|
ssssBlurData.boundingBox,
|
|
ssssBlurData.ssssScale,
|
|
), SSSS_BLUR_X_PASS_INDEX);
|
|
ssssBlurData.ssssBlurMaterial.setProperty('kernel', ssssBlurData.kernel, SSSS_BLUR_X_PASS_INDEX);
|
|
ssssblurXPass.addQueue(QueueHint.RENDER_OPAQUE | QueueHint.RENDER_TRANSPARENT).addCameraQuad(
|
|
camera,
|
|
ssssBlurData.ssssBlurMaterial,
|
|
SSSS_BLUR_X_PASS_INDEX,
|
|
SceneFlags.NONE,
|
|
);
|
|
|
|
// === SSSS Blur Y Pass ===
|
|
const ssssblurYPass = ppl.addRenderPass(width, height, 'ssss-blurY');
|
|
ssssblurYPass.name = `CameraSSSSBlurYPass${cameraID}`;
|
|
ssssblurYPass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
if (ppl.containsResource(ssssBlurRTName)) {
|
|
const verId = webPipeline.resourceGraph.vertex(ssssBlurRTName);
|
|
const sampler = webPipeline.resourceGraph.getSampler(verId);
|
|
sampler.minFilter = Filter.POINT;
|
|
sampler.magFilter = Filter.POINT;
|
|
sampler.mipFilter = Filter.NONE;
|
|
ssssblurYPass.addTexture(ssssBlurRTName, 'colorTex');
|
|
}
|
|
if (ppl.containsResource(ssssBlurDSName)) {
|
|
const verId = webPipeline.resourceGraph.vertex(ssssBlurDSName);
|
|
const sampler = webPipeline.resourceGraph.getSampler(verId);
|
|
sampler.minFilter = Filter.POINT;
|
|
sampler.magFilter = Filter.POINT;
|
|
sampler.mipFilter = Filter.NONE;
|
|
ssssblurYPass.addTexture(ssssBlurDSName, 'depthTex');
|
|
}
|
|
ssssblurYPass.addRenderTarget(inputRT, LoadOp.LOAD, StoreOp.STORE, ssssBlurClearColor);
|
|
ssssblurYPass.addDepthStencil(
|
|
inputDS,
|
|
LoadOp.LOAD,
|
|
StoreOp.STORE,
|
|
camera.clearDepth,
|
|
camera.clearStencil,
|
|
camera.clearFlag,
|
|
);
|
|
ssssBlurData.ssssBlurMaterial.setProperty('blurInfo', new Vec4(
|
|
ssssBlurData.ssssFov,
|
|
ssssBlurData.ssssWidth,
|
|
ssssBlurData.boundingBox,
|
|
ssssBlurData.ssssScale,
|
|
), SSSS_BLUR_Y_PASS_INDEX);
|
|
ssssBlurData.ssssBlurMaterial.setProperty('kernel', ssssBlurData.kernel, SSSS_BLUR_Y_PASS_INDEX);
|
|
ssssblurYPass.addQueue(QueueHint.RENDER_OPAQUE | QueueHint.RENDER_TRANSPARENT).addCameraQuad(
|
|
camera,
|
|
ssssBlurData.ssssBlurMaterial,
|
|
SSSS_BLUR_Y_PASS_INDEX,
|
|
SceneFlags.NONE,
|
|
);
|
|
return { rtName: inputRT, dsName: inputDS };
|
|
}
|
|
|
|
class ToneMappingInfo {
|
|
declare toneMappingMaterial: Material;
|
|
private _init (): void {
|
|
this.toneMappingMaterial = new Material();
|
|
this.toneMappingMaterial.name = 'builtin-tone-mapping-material';
|
|
this.toneMappingMaterial.initialize({
|
|
effectName: 'pipeline/tone-mapping',
|
|
});
|
|
for (let i = 0; i < this.toneMappingMaterial.passes.length; ++i) {
|
|
this.toneMappingMaterial.passes[i].tryCompile();
|
|
}
|
|
}
|
|
constructor () {
|
|
this._init();
|
|
}
|
|
}
|
|
|
|
let toneMappingInfo: ToneMappingInfo | null = null;
|
|
export function buildToneMappingPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
inputRT: string,
|
|
inputDS: string,
|
|
): { rtName: string; dsName: string; } {
|
|
if (!ppl.pipelineSceneData.isHDR || !ppl.getMacroBool('CC_USE_FLOAT_OUTPUT')) return { rtName: inputRT, dsName: inputDS };
|
|
if (!toneMappingInfo) {
|
|
toneMappingInfo = new ToneMappingInfo();
|
|
}
|
|
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
|
|
const toneMappingClearColor = new Color(0, 0, 0, camera.clearColor.w);
|
|
if (camera.clearFlag & ClearFlagBit.COLOR) {
|
|
toneMappingClearColor.x = camera.clearColor.x;
|
|
toneMappingClearColor.y = camera.clearColor.y;
|
|
toneMappingClearColor.z = camera.clearColor.z;
|
|
}
|
|
|
|
const toneMappingPassRTName = `toneMappingPassRTName${cameraID}`;
|
|
const toneMappingPassDS = `toneMappingPassDS${cameraID}`;
|
|
if (!ppl.containsResource(toneMappingPassRTName)) {
|
|
ppl.addRenderTarget(toneMappingPassRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED);
|
|
ppl.addDepthStencil(toneMappingPassDS, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(toneMappingPassRTName, width, height);
|
|
ppl.updateDepthStencil(toneMappingPassDS, width, height);
|
|
const toneMappingPass = ppl.addRenderPass(width, height, 'tone-mapping');
|
|
toneMappingPass.name = `CameraToneMappingPass${cameraID}`;
|
|
toneMappingPass.setViewport(new Viewport(area.x, area.y, area.width, area.height));
|
|
if (ppl.containsResource(inputRT)) {
|
|
toneMappingPass.addTexture(inputRT, 'u_texSampler');
|
|
}
|
|
toneMappingPass.addRenderTarget(
|
|
toneMappingPassRTName,
|
|
getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET),
|
|
StoreOp.STORE,
|
|
|
|
toneMappingClearColor,
|
|
);
|
|
toneMappingPass.addDepthStencil(
|
|
toneMappingPassDS,
|
|
getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL),
|
|
StoreOp.STORE,
|
|
camera.clearDepth,
|
|
|
|
camera.clearStencil,
|
|
|
|
camera.clearFlag,
|
|
);
|
|
toneMappingPass.addQueue(QueueHint.NONE).addFullscreenQuad(toneMappingInfo.toneMappingMaterial, 0, SceneFlags.NONE);
|
|
toneMappingPass.addQueue(QueueHint.RENDER_TRANSPARENT).addSceneOfCamera(
|
|
camera,
|
|
new LightInfo(),
|
|
SceneFlags.UI,
|
|
);
|
|
if (getProfilerCamera() === camera) {
|
|
toneMappingPass.showStatistics = true;
|
|
}
|
|
return { rtName: toneMappingPassRTName, dsName: toneMappingPassDS };
|
|
}
|
|
|
|
export function buildTransparencyPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
inputRT: string,
|
|
inputDS: string,
|
|
hasDeferredTransparencyObject: boolean,
|
|
): { rtName: string; dsName: string; } {
|
|
if (hasDeferredTransparencyObject) return { rtName: inputRT, dsName: inputDS };
|
|
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const cameraName = `Camera${cameraID}`;
|
|
const cameraInfo = buildShadowPasses(cameraName, camera, ppl);
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
|
|
const alphaPass = ppl.addRenderPass(width, height, 'default');
|
|
alphaPass.name = `CameraAlphaPass${cameraID}`;
|
|
alphaPass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
for (const dirShadowName of cameraInfo.mainLightShadowNames) {
|
|
if (ppl.containsResource(dirShadowName)) {
|
|
alphaPass.addTexture(dirShadowName, 'cc_shadowMap');
|
|
}
|
|
}
|
|
for (const spotShadowName of cameraInfo.spotLightShadowNames) {
|
|
if (ppl.containsResource(spotShadowName)) {
|
|
alphaPass.addTexture(spotShadowName, 'cc_spotShadowMap');
|
|
}
|
|
}
|
|
alphaPass.addRenderTarget(
|
|
inputRT,
|
|
LoadOp.LOAD,
|
|
|
|
StoreOp.STORE,
|
|
new Color(camera.clearDepth, camera.clearStencil, 0, 0),
|
|
);
|
|
alphaPass.addDepthStencil(
|
|
inputDS,
|
|
LoadOp.LOAD,
|
|
|
|
StoreOp.STORE,
|
|
camera.clearDepth,
|
|
|
|
camera.clearStencil,
|
|
|
|
camera.clearFlag,
|
|
);
|
|
alphaPass
|
|
.addQueue(QueueHint.RENDER_TRANSPARENT)
|
|
.addSceneOfCamera(camera, new LightInfo(), SceneFlags.TRANSPARENT_OBJECT | SceneFlags.GEOMETRY);
|
|
return { rtName: inputRT, dsName: inputDS };
|
|
}
|
|
|
|
function _buildSpecularPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
inputRT: string,
|
|
inputDS: string,
|
|
): { rtName: string; dsName: string; } {
|
|
if (EDITOR) {
|
|
ppl.setMacroInt('CC_PIPELINE_TYPE', 0);
|
|
}
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const cameraName = `Camera${cameraID}`;
|
|
const cameraInfo = buildShadowPasses(cameraName, camera, ppl);
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
|
|
const specalurPass = ppl.addRenderPass(width, height, 'specular-pass');
|
|
specalurPass.name = `CameraSpecalurPass${cameraID}`;
|
|
specalurPass.setViewport(new Viewport(area.x, area.y, width, height));
|
|
for (const dirShadowName of cameraInfo.mainLightShadowNames) {
|
|
if (ppl.containsResource(dirShadowName)) {
|
|
specalurPass.addTexture(dirShadowName, 'cc_shadowMap');
|
|
}
|
|
}
|
|
for (const spotShadowName of cameraInfo.spotLightShadowNames) {
|
|
if (ppl.containsResource(spotShadowName)) {
|
|
specalurPass.addTexture(spotShadowName, 'cc_spotShadowMap');
|
|
}
|
|
}
|
|
specalurPass.addRenderTarget(
|
|
inputRT,
|
|
LoadOp.LOAD,
|
|
|
|
StoreOp.STORE,
|
|
new Color(camera.clearColor.x, camera.clearColor.y, camera.clearColor.z, camera.clearColor.w),
|
|
);
|
|
specalurPass.addDepthStencil(
|
|
inputDS,
|
|
LoadOp.LOAD,
|
|
|
|
StoreOp.STORE,
|
|
camera.clearDepth,
|
|
|
|
camera.clearStencil,
|
|
|
|
camera.clearFlag,
|
|
);
|
|
specalurPass
|
|
.addQueue(QueueHint.RENDER_OPAQUE, 'default')
|
|
.addSceneOfCamera(
|
|
camera,
|
|
new LightInfo(),
|
|
SceneFlags.TRANSPARENT_OBJECT | SceneFlags.DEFAULT_LIGHTING | SceneFlags.PLANAR_SHADOW
|
|
| SceneFlags.CUTOUT_OBJECT | SceneFlags.DRAW_INSTANCING | SceneFlags.GEOMETRY,
|
|
);
|
|
specalurPass
|
|
.addQueue(QueueHint.RENDER_TRANSPARENT, 'forward-add')
|
|
.addSceneOfCamera(
|
|
camera,
|
|
new LightInfo(),
|
|
SceneFlags.TRANSPARENT_OBJECT | SceneFlags.DEFAULT_LIGHTING | SceneFlags.PLANAR_SHADOW
|
|
| SceneFlags.CUTOUT_OBJECT | SceneFlags.DRAW_INSTANCING | SceneFlags.GEOMETRY,
|
|
);
|
|
return { rtName: inputRT, dsName: inputDS };
|
|
}
|
|
|
|
export function buildSSSSPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
inputRT: string,
|
|
inputDS: string,
|
|
): { rtName: string; dsName: string; } {
|
|
if (hasSkinObject(ppl)) {
|
|
forceEnableFloatOutput(ppl);
|
|
const blurInfo = _buildSSSSBlurPass(camera, ppl, inputRT, inputDS);
|
|
const specularInfo = _buildSpecularPass(camera, ppl, blurInfo.rtName, blurInfo.dsName);
|
|
return { rtName: specularInfo.rtName, dsName: specularInfo.dsName };
|
|
} else {
|
|
const specularInfo = _buildSpecularPass(camera, ppl, inputRT, inputDS);
|
|
return { rtName: specularInfo.rtName, dsName: specularInfo.dsName };
|
|
}
|
|
}
|
|
|
|
class HBAOParams {
|
|
declare hbaoMaterial: Material;
|
|
declare randomTexture: Texture2D;
|
|
|
|
get uvDepthToEyePosParams (): Vec4 {
|
|
return this._uvDepthToEyePosParams;
|
|
}
|
|
|
|
get radiusParam (): Vec4 {
|
|
return this._radiusParam;
|
|
}
|
|
|
|
get miscParam (): Vec4 {
|
|
return this._miscParam;
|
|
}
|
|
|
|
get blurParam (): Vec4 {
|
|
return this._blurParam;
|
|
}
|
|
|
|
set depthTexFullResolution (val: Vec2) {
|
|
this._depthTexFullResolution.set(val);
|
|
}
|
|
|
|
set depthTexResolution (val: Vec2) {
|
|
this._depthTexResolution.set(val);
|
|
}
|
|
|
|
set sceneScale (val: number) {
|
|
this._sceneScale = val;
|
|
}
|
|
|
|
set cameraFov (val: number) {
|
|
this._cameraFov = val;
|
|
}
|
|
|
|
set radiusScale (val: number) {
|
|
this._radiusScale = val;
|
|
}
|
|
|
|
set angleBiasDegree (val: number) {
|
|
this._angleBiasDegree = val;
|
|
}
|
|
|
|
set aoStrength (val: number) {
|
|
this._aoStrength = val;
|
|
}
|
|
|
|
set blurSharpness (val: number) {
|
|
this._blurSharpness = val;
|
|
}
|
|
|
|
set aoSaturation (val: number) {
|
|
this._aoSaturation = val;
|
|
}
|
|
|
|
private _uvDepthToEyePosParams = new Vec4();
|
|
private _radiusParam = new Vec4();
|
|
private _miscParam = new Vec4();
|
|
private _blurParam = new Vec4();
|
|
|
|
private _depthTexFullResolution = new Vec2(1024);
|
|
private _depthTexResolution = new Vec2(1024);
|
|
private _sceneScale = 1.0;
|
|
private _cameraFov = toRadian(45.0);
|
|
private _radiusScale = 1.0;
|
|
private _angleBiasDegree = 10.0;
|
|
private _aoStrength = 1.0;
|
|
private _blurSharpness = 8;
|
|
private _aoSaturation = 1.0;
|
|
|
|
private _randomDirAndJitter: number[] = [
|
|
238, 91, 87, 255, 251, 44, 119, 255, 247, 64, 250, 255, 232, 5, 225, 255,
|
|
253, 177, 140, 255, 250, 51, 84, 255, 243, 76, 97, 255, 252, 36, 232, 255,
|
|
235, 100, 24, 255, 252, 36, 158, 255, 254, 20, 142, 255, 245, 135, 124, 255,
|
|
251, 43, 121, 255, 253, 31, 145, 255, 235, 98, 160, 255, 240, 146, 198, 255,
|
|
];
|
|
private _init (): void {
|
|
if (this.hbaoMaterial) return;
|
|
this.hbaoMaterial = new Material();
|
|
this.hbaoMaterial.name = 'builtin-hbao-material';
|
|
this.hbaoMaterial.initialize({ effectName: 'pipeline/post-process/hbao' });
|
|
for (let i = 0; i < this.hbaoMaterial.passes.length; ++i) {
|
|
this.hbaoMaterial.passes[i].tryCompile();
|
|
}
|
|
|
|
const width = 4;
|
|
const height = 4;
|
|
const pixelFormat = Texture2D.PixelFormat.RGBA8888;
|
|
const arrayBuffer = new Uint8Array(width * height * 4);
|
|
for (let i = 0; i < this._randomDirAndJitter.length; i++) {
|
|
arrayBuffer[i] = this._randomDirAndJitter[i];
|
|
}
|
|
const image = new ImageAsset({
|
|
width,
|
|
height,
|
|
_data: arrayBuffer,
|
|
_compressed: false,
|
|
format: pixelFormat,
|
|
});
|
|
this.randomTexture = new Texture2D();
|
|
this.randomTexture.setFilters(Texture2D.Filter.NEAREST, Texture2D.Filter.NEAREST);
|
|
this.randomTexture.setMipFilter(Texture2D.Filter.NONE);
|
|
this.randomTexture.setWrapMode(Texture2D.WrapMode.REPEAT, Texture2D.WrapMode.REPEAT, Texture2D.WrapMode.REPEAT);
|
|
this.randomTexture.image = image;
|
|
this.hbaoMaterial.setProperty('RandomTex', this.randomTexture, 0);
|
|
}
|
|
|
|
public update (): void {
|
|
// should be same value as shader
|
|
const HALF_KERNEL_RADIUS = 4;
|
|
const INV_LN2 = 1.44269504;
|
|
const SQRT_LN2 = 0.8325546;
|
|
|
|
const gR = this._radiusScale * this._sceneScale;
|
|
const gR2 = gR * gR;
|
|
const gNegInvR2 = -1.0 / gR2;
|
|
const gMaxRadiusPixels = 0.1 * Math.min(this._depthTexFullResolution.x, this._depthTexFullResolution.y);
|
|
this._radiusParam.set(gR, gR2, gNegInvR2, gMaxRadiusPixels);
|
|
|
|
const vec2 = new Vec2(this._depthTexResolution.y / this._depthTexResolution.x, 1.0);
|
|
const gFocalLen = new Vec2(vec2.x / Math.tan(this._cameraFov * 0.5), vec2.y / Math.tan(this._cameraFov * 0.5));
|
|
const gTanAngleBias = Math.tan(toRadian(this._angleBiasDegree));
|
|
const gStrength = this._aoStrength;
|
|
this._miscParam.set(gFocalLen.x, gFocalLen.y, gTanAngleBias, gStrength);
|
|
|
|
const gUVToViewA = new Vec2(2.0 / gFocalLen.x, -2.0 / gFocalLen.y);
|
|
const gUVToViewB = new Vec2(-1.0 / gFocalLen.x, 1.0 / gFocalLen.y);
|
|
this._uvDepthToEyePosParams.set(gUVToViewA.x, gUVToViewA.y, gUVToViewB.x, gUVToViewB.y);
|
|
|
|
const BlurSigma = (HALF_KERNEL_RADIUS + 1.0) * 0.5;
|
|
const gBlurFallOff = INV_LN2 / (2.0 * BlurSigma * BlurSigma);
|
|
const gBlurDepthThreshold = 2.0 * SQRT_LN2 * (this._sceneScale / this._blurSharpness);
|
|
this._blurParam.set(gBlurFallOff, gBlurDepthThreshold, this._blurSharpness / 8.0, this._aoSaturation);
|
|
}
|
|
|
|
constructor () {
|
|
this._init();
|
|
this.update();
|
|
}
|
|
}
|
|
|
|
let _hbaoParams: HBAOParams | null = null;
|
|
const vec2 = new Vec2();
|
|
function _buildHBAOPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
inputRT: string,
|
|
inputDS: string,
|
|
): { rtName: string; dsName: string; } {
|
|
if (!_hbaoParams) return { rtName: inputRT, dsName: inputDS };
|
|
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
|
|
const hbaoClearColor = new Color(0, 0, 0, camera.clearColor.w);
|
|
|
|
const hbaoRTName = `hbaoRTName${cameraID}`;
|
|
if (!ppl.containsResource(hbaoRTName)) {
|
|
ppl.addRenderTarget(hbaoRTName, Format.BGRA8, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(hbaoRTName, width, height);
|
|
const hbaoPass = ppl.addRenderPass(width, height, 'hbao-pass');
|
|
hbaoPass.name = `CameraHBAOPass${cameraID}`;
|
|
hbaoPass.setViewport(new Viewport(area.x, area.y, area.width, area.height));
|
|
if (ppl.containsResource(inputDS)) {
|
|
const webPipeline = (ppl as WebPipeline);
|
|
const verId = webPipeline.resourceGraph.vertex(inputDS);
|
|
const sampler = webPipeline.resourceGraph.getSampler(verId);
|
|
sampler.minFilter = sampler.magFilter = Filter.POINT;
|
|
sampler.mipFilter = Filter.NONE;
|
|
sampler.addressU = sampler.addressV = Address.CLAMP;
|
|
hbaoPass.addTexture(inputDS, 'DepthTex');
|
|
}
|
|
hbaoPass.addRenderTarget(
|
|
hbaoRTName,
|
|
LoadOp.LOAD,
|
|
StoreOp.STORE,
|
|
hbaoClearColor,
|
|
);
|
|
const passIdx = 0;
|
|
_hbaoParams.hbaoMaterial.setProperty('uvDepthToEyePosParams', _hbaoParams.uvDepthToEyePosParams, passIdx);
|
|
_hbaoParams.hbaoMaterial.setProperty('radiusParam', _hbaoParams.radiusParam, passIdx);
|
|
_hbaoParams.hbaoMaterial.setProperty('miscParam', _hbaoParams.miscParam, passIdx);
|
|
// eslint-disable-next-line max-len
|
|
_hbaoParams.hbaoMaterial.setProperty('randomTexSize', new Vec4(_hbaoParams.randomTexture.width, _hbaoParams.randomTexture.height, 1.0 / _hbaoParams.randomTexture.width, 1.0 / _hbaoParams.randomTexture.height), passIdx);
|
|
_hbaoParams.hbaoMaterial.setProperty('blurParam', _hbaoParams.blurParam, passIdx);
|
|
hbaoPass.addQueue(QueueHint.RENDER_TRANSPARENT | QueueHint.RENDER_OPAQUE).addCameraQuad(
|
|
camera,
|
|
_hbaoParams.hbaoMaterial,
|
|
passIdx,
|
|
SceneFlags.NONE,
|
|
);
|
|
return { rtName: hbaoRTName, dsName: inputDS };
|
|
}
|
|
|
|
function _buildHBAOBlurPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
inputRT: string,
|
|
inputDS: string,
|
|
isYPass: boolean,
|
|
): { rtName: string; dsName: string; } {
|
|
if (!_hbaoParams) return { rtName: inputRT, dsName: inputDS };
|
|
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
|
|
const hbaoClearColor = new Color(0, 0, 0, camera.clearColor.w);
|
|
|
|
let inputRTName = `hbaoRTName${cameraID}`;
|
|
let outputRTName = `hbaoBluredRTName${cameraID}`;
|
|
let shaderPassName = 'blurx-pass';
|
|
let blurPassName = `CameraHBAOBluredXPass${cameraID}`;
|
|
if (isYPass) {
|
|
outputRTName = `hbaoRTName${cameraID}`;
|
|
inputRTName = `hbaoBluredRTName${cameraID}`;
|
|
shaderPassName = 'blury-pass';
|
|
blurPassName = `CameraHBAOBluredYPass${cameraID}`;
|
|
}
|
|
|
|
if (!ppl.containsResource(outputRTName)) {
|
|
ppl.addRenderTarget(outputRTName, Format.BGRA8, width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(outputRTName, width, height);
|
|
const blurPass = ppl.addRenderPass(width, height, shaderPassName);
|
|
blurPass.name = blurPassName;
|
|
blurPass.setViewport(new Viewport(area.x, area.y, area.width, area.height));
|
|
if (ppl.containsResource(inputDS)) {
|
|
const webPipeline = (ppl as WebPipeline);
|
|
const verId = webPipeline.resourceGraph.vertex(inputDS);
|
|
const sampler = webPipeline.resourceGraph.getSampler(verId);
|
|
sampler.minFilter = sampler.magFilter = Filter.POINT;
|
|
sampler.mipFilter = Filter.NONE;
|
|
sampler.addressU = sampler.addressV = Address.CLAMP;
|
|
blurPass.addTexture(inputDS, 'DepthTex');
|
|
}
|
|
if (ppl.containsResource(inputRTName)) {
|
|
const webPipeline = (ppl as WebPipeline);
|
|
const verId = webPipeline.resourceGraph.vertex(inputRTName);
|
|
const sampler = webPipeline.resourceGraph.getSampler(verId);
|
|
sampler.minFilter = sampler.magFilter = Filter.LINEAR;
|
|
sampler.mipFilter = Filter.NONE;
|
|
sampler.addressU = sampler.addressV = Address.CLAMP;
|
|
blurPass.addTexture(inputRTName, 'AOTexNearest');
|
|
}
|
|
blurPass.addRenderTarget(
|
|
outputRTName,
|
|
LoadOp.LOAD,
|
|
StoreOp.STORE,
|
|
hbaoClearColor,
|
|
);
|
|
const passIdx = isYPass ? 2 : 1;
|
|
_hbaoParams.hbaoMaterial.setProperty('uvDepthToEyePosParams', _hbaoParams.uvDepthToEyePosParams, passIdx);
|
|
_hbaoParams.hbaoMaterial.setProperty('radiusParam', _hbaoParams.radiusParam, passIdx);
|
|
_hbaoParams.hbaoMaterial.setProperty('miscParam', _hbaoParams.miscParam, passIdx);
|
|
_hbaoParams.hbaoMaterial.setProperty('blurParam', _hbaoParams.blurParam, passIdx);
|
|
blurPass.addQueue(QueueHint.RENDER_TRANSPARENT | QueueHint.RENDER_OPAQUE).addCameraQuad(
|
|
camera,
|
|
_hbaoParams.hbaoMaterial,
|
|
passIdx,
|
|
SceneFlags.NONE,
|
|
);
|
|
return { rtName: outputRTName, dsName: inputDS };
|
|
}
|
|
|
|
function _buildHBAOCombinedPass (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
inputRT: string,
|
|
inputDS: string,
|
|
outputRT: string,
|
|
): { rtName: string; dsName: string; } {
|
|
if (!_hbaoParams) return { rtName: inputRT, dsName: inputDS };
|
|
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
|
|
const hbaoClearColor = new Color(0, 0, 0, camera.clearColor.w);
|
|
|
|
const outputRTName = outputRT;
|
|
if (!ppl.containsResource(outputRTName)) {
|
|
ppl.addRenderTarget(outputRTName, getRTFormatBeforeToneMapping(ppl), width, height, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateRenderTarget(outputRTName, width, height);
|
|
const hbaoPass = ppl.addRenderPass(width, height, 'combine-pass');
|
|
hbaoPass.name = `CameraHBAOCombinedPass${cameraID}`;
|
|
hbaoPass.setViewport(new Viewport(area.x, area.y, area.width, area.height));
|
|
|
|
const inputRTName = inputRT;
|
|
if (ppl.containsResource(inputRTName)) {
|
|
const webPipeline = (ppl as WebPipeline);
|
|
const verId = webPipeline.resourceGraph.vertex(inputRTName);
|
|
const sampler = webPipeline.resourceGraph.getSampler(verId);
|
|
sampler.minFilter = sampler.magFilter = Filter.LINEAR;
|
|
sampler.mipFilter = Filter.NONE;
|
|
sampler.addressU = sampler.addressV = Address.CLAMP;
|
|
hbaoPass.addTexture(inputRTName, 'AOTexNearest');
|
|
}
|
|
|
|
hbaoPass.addRenderTarget(
|
|
outputRTName,
|
|
LoadOp.LOAD,
|
|
StoreOp.STORE,
|
|
hbaoClearColor,
|
|
);
|
|
const passIdx = 3;
|
|
_hbaoParams.hbaoMaterial.setProperty('uvDepthToEyePosParams', _hbaoParams.uvDepthToEyePosParams, passIdx);
|
|
_hbaoParams.hbaoMaterial.setProperty('radiusParam', _hbaoParams.radiusParam, passIdx);
|
|
_hbaoParams.hbaoMaterial.setProperty('miscParam', _hbaoParams.miscParam, passIdx);
|
|
_hbaoParams.hbaoMaterial.setProperty('blurParam', _hbaoParams.blurParam, passIdx);
|
|
hbaoPass.addQueue(QueueHint.RENDER_TRANSPARENT | QueueHint.RENDER_OPAQUE).addCameraQuad(
|
|
camera,
|
|
_hbaoParams.hbaoMaterial,
|
|
passIdx,
|
|
SceneFlags.NONE,
|
|
);
|
|
return { rtName: outputRTName, dsName: inputDS };
|
|
}
|
|
|
|
export function buildHBAOPasses (
|
|
camera: Camera,
|
|
ppl: BasicPipeline,
|
|
inputRT: string,
|
|
inputDS: string,
|
|
radiusScale = 1.0,
|
|
angleBiasDegree = 10.0,
|
|
blurSharpness = 3,
|
|
aoSaturation = 1.0,
|
|
aoStrength = 1.0,
|
|
needBlur = true,
|
|
): { rtName: string; dsName: string; } {
|
|
const area = getRenderArea(camera, camera.window.width, camera.window.height);
|
|
const width = area.width;
|
|
const height = area.height;
|
|
|
|
// params
|
|
if (!_hbaoParams) _hbaoParams = new HBAOParams();
|
|
// todo: nearest object distance from camera
|
|
const sceneScale = camera.nearClip;
|
|
// todo: Half Res Depth Tex
|
|
_hbaoParams.depthTexFullResolution = vec2.set(width, height);
|
|
_hbaoParams.depthTexResolution = vec2.set(width, height);
|
|
_hbaoParams.sceneScale = sceneScale;
|
|
_hbaoParams.cameraFov = camera.fov;
|
|
_hbaoParams.radiusScale = radiusScale;
|
|
_hbaoParams.angleBiasDegree = angleBiasDegree;
|
|
_hbaoParams.aoStrength = aoStrength;
|
|
_hbaoParams.blurSharpness = blurSharpness;
|
|
_hbaoParams.aoSaturation = aoSaturation;
|
|
_hbaoParams.update();
|
|
|
|
// debug view
|
|
const director = cclegacy.director;
|
|
const root = director.root;
|
|
if (root.debugView) {
|
|
if (root.debugView.isEnabled()
|
|
&& (root.debugView.singleMode !== DebugViewSingleType.NONE && root.debugView.singleMode !== DebugViewSingleType.AO
|
|
|| !root.debugView.isCompositeModeEnabled(DebugViewCompositeType.AO))) {
|
|
return { rtName: inputRT, dsName: inputDS };
|
|
}
|
|
}
|
|
|
|
// passes
|
|
const hbaoInfo = _buildHBAOPass(camera, ppl, inputRT, inputDS);
|
|
let hbaoCombinedInputRTName = hbaoInfo.rtName;
|
|
if (needBlur) {
|
|
const haboBlurInfoX = _buildHBAOBlurPass(camera, ppl, hbaoInfo.rtName, inputDS, false);
|
|
const haboBlurInfoY = _buildHBAOBlurPass(camera, ppl, haboBlurInfoX.rtName, inputDS, true);
|
|
hbaoCombinedInputRTName = haboBlurInfoY.rtName;
|
|
}
|
|
const haboCombined = _buildHBAOCombinedPass(camera, ppl, hbaoCombinedInputRTName, inputDS, inputRT);
|
|
return { rtName: haboCombined.rtName, dsName: inputDS };
|
|
}
|
|
|
|
export const MAX_LIGHTS_PER_CLUSTER = 200;
|
|
export const CLUSTERS_X = 16;
|
|
export const CLUSTERS_Y = 8;
|
|
export const CLUSTERS_Z = 24;
|
|
export const CLUSTER_COUNT = CLUSTERS_X * CLUSTERS_Y * CLUSTERS_Z;
|
|
|
|
class ClusterLightData {
|
|
declare clusterBuildCS: Material;
|
|
declare clusterLightCullingCS: Material;
|
|
clusters_x_threads = 16;
|
|
clusters_y_threads = 8;
|
|
clusters_z_threads = 1;
|
|
dispatchX = 1;
|
|
dispatchY = 1;
|
|
dispatchZ = 1;
|
|
|
|
private _initMaterial (id: string, effect: string): Material {
|
|
const mat = new Material();
|
|
mat.name = id;
|
|
mat.initialize({ effectName: effect });
|
|
for (let i = 0; i < mat.passes.length; ++i) {
|
|
mat.passes[i].tryCompile();
|
|
}
|
|
return mat;
|
|
}
|
|
|
|
private _init (): void {
|
|
this.clusterBuildCS = this._initMaterial('builtin-cluster-build-cs-material', 'pipeline/cluster-build');
|
|
this.clusterLightCullingCS = this._initMaterial('builtin-cluster-culling-cs-material', 'pipeline/cluster-culling');
|
|
|
|
this.dispatchX = CLUSTERS_X / this.clusters_x_threads;
|
|
this.dispatchY = CLUSTERS_Y / this.clusters_y_threads;
|
|
this.dispatchZ = CLUSTERS_Z / this.clusters_z_threads;
|
|
}
|
|
|
|
constructor () {
|
|
this._init();
|
|
}
|
|
}
|
|
|
|
let _clusterLightData: ClusterLightData | null = null;
|
|
export function buildLightClusterBuildPass (
|
|
camera: Camera,
|
|
clusterData: ClusterLightData,
|
|
ppl: Pipeline,
|
|
): void {
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const clusterBufferName = `clusterBuffer${cameraID}`;
|
|
|
|
const clusterBufferSize = CLUSTER_COUNT * 2 * 4 * 4;
|
|
if (!ppl.containsResource(clusterBufferName)) {
|
|
ppl.addStorageBuffer(clusterBufferName, Format.UNKNOWN, clusterBufferSize, ResourceResidency.MANAGED);
|
|
}
|
|
ppl.updateStorageBuffer(clusterBufferName, clusterBufferSize);
|
|
|
|
const clusterPass = ppl.addComputePass('cluster-build-cs');
|
|
clusterPass.addStorageBuffer(clusterBufferName, AccessType.WRITE, 'b_clustersBuffer');
|
|
clusterPass.addQueue()
|
|
.addDispatch(clusterData.dispatchX, clusterData.dispatchY, clusterData.dispatchZ, clusterData.clusterBuildCS, 0);
|
|
|
|
const width = camera.width * ppl.pipelineSceneData.shadingScale;
|
|
const height = camera.height * ppl.pipelineSceneData.shadingScale;
|
|
if ('setCurrConstant' in clusterPass) { // web-pipeline
|
|
(clusterPass as WebComputePassBuilder).addConstant('CCConst', 'cluster-build-cs');
|
|
}
|
|
clusterPass.setVec4('cc_nearFar', new Vec4(camera.nearClip, camera.farClip, camera.getClipSpaceMinz(), 0));
|
|
clusterPass.setVec4('cc_viewPort', new Vec4(0, 0, width, height));
|
|
clusterPass.setVec4('cc_workGroup', new Vec4(CLUSTERS_X, CLUSTERS_Y, CLUSTERS_Z, 0));
|
|
clusterPass.setMat4('cc_matView', camera.matView);
|
|
clusterPass.setMat4('cc_matProjInv', camera.matProjInv);
|
|
}
|
|
|
|
export function buildLightClusterCullingPass (
|
|
camera: Camera,
|
|
clusterData: ClusterLightData,
|
|
ppl: Pipeline,
|
|
): void {
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const clusterBufferName = `clusterBuffer${cameraID}`;
|
|
const clusterLightBufferName = `clusterLightBuffer${cameraID}`;
|
|
const clusterGlobalIndexBufferName = `globalIndexBuffer${cameraID}`;
|
|
const clusterLightIndicesBufferName = `clusterLightIndicesBuffer${cameraID}`;
|
|
const clusterLightGridBufferName = `clusterLightGridBuffer${cameraID}`;
|
|
|
|
// index buffer
|
|
const lightIndexBufferSize = MAX_LIGHTS_PER_CLUSTER * CLUSTER_COUNT * 4;
|
|
const lightGridBufferSize = CLUSTER_COUNT * 4 * 4;
|
|
if (!ppl.containsResource(clusterLightIndicesBufferName)) {
|
|
ppl.addStorageBuffer(clusterLightIndicesBufferName, Format.UNKNOWN, lightIndexBufferSize, ResourceResidency.MANAGED);
|
|
}
|
|
if (!ppl.containsResource(clusterLightGridBufferName)) {
|
|
ppl.addStorageBuffer(clusterLightGridBufferName, Format.UNKNOWN, lightGridBufferSize, ResourceResidency.MANAGED);
|
|
}
|
|
|
|
const clusterPass = ppl.addComputePass('cluster-culling-cs');
|
|
clusterPass.addStorageBuffer(clusterLightBufferName, AccessType.READ, 'b_ccLightsBuffer');
|
|
clusterPass.addStorageBuffer(clusterBufferName, AccessType.READ, 'b_clustersBuffer');
|
|
clusterPass.addStorageBuffer(clusterLightIndicesBufferName, AccessType.WRITE, 'b_clusterLightIndicesBuffer');
|
|
clusterPass.addStorageBuffer(clusterLightGridBufferName, AccessType.WRITE, 'b_clusterLightGridBuffer');
|
|
clusterPass.addStorageBuffer(clusterGlobalIndexBufferName, AccessType.WRITE, 'b_globalIndexBuffer');
|
|
clusterPass.addQueue()
|
|
.addDispatch(clusterData.dispatchX, clusterData.dispatchY, clusterData.dispatchZ, clusterData.clusterLightCullingCS, 0);
|
|
|
|
const width = camera.width * ppl.pipelineSceneData.shadingScale;
|
|
const height = camera.height * ppl.pipelineSceneData.shadingScale;
|
|
if ('setCurrConstant' in clusterPass) { // web-pipeline
|
|
(clusterPass as WebComputePassBuilder).addConstant('CCConst', 'cluster-build-cs');
|
|
}
|
|
clusterPass.setVec4('cc_nearFar', new Vec4(camera.nearClip, camera.farClip, camera.getClipSpaceMinz(), 0));
|
|
clusterPass.setVec4('cc_viewPort', new Vec4(width, height, width, height));
|
|
clusterPass.setVec4('cc_workGroup', new Vec4(CLUSTERS_X, CLUSTERS_Y, CLUSTERS_Z, 0));
|
|
clusterPass.setMat4('cc_matView', camera.matView);
|
|
clusterPass.setMat4('cc_matProjInv', camera.matProjInv);
|
|
}
|
|
|
|
export function buildLightBuffer (size: number, floatPerLight: number, camera: Camera, pipeline: BasicPipeline): ArrayBuffer {
|
|
const buffer = new ArrayBuffer(size);
|
|
const view = new Float32Array(buffer);
|
|
|
|
const data = pipeline.pipelineSceneData;
|
|
const lightMeterScale = 10000.0;
|
|
const exposure = camera.exposure;
|
|
|
|
// gather light data
|
|
let index = 0;
|
|
for (const light of data.validPunctualLights) {
|
|
const offset = index * floatPerLight;
|
|
const positionOffset = offset + 0;
|
|
const colorOffset = offset + 4;
|
|
const sizeRangeAngleOffset = offset + 8;
|
|
const directionOffset = offset + 12;
|
|
const boundSizeOffset = offset + 16;
|
|
|
|
let luminanceHDR = 0;
|
|
let luminanceLDR = 0;
|
|
let position: Vec3 | null;
|
|
if (light.type === LightType.POINT) {
|
|
const point = light as PointLight;
|
|
position = point.position;
|
|
luminanceLDR = point.luminanceLDR;
|
|
luminanceHDR = point.luminanceHDR;
|
|
|
|
view[sizeRangeAngleOffset] = 0;
|
|
view[sizeRangeAngleOffset + 1] = point.range;
|
|
view[sizeRangeAngleOffset + 2] = 0;
|
|
view[sizeRangeAngleOffset + 3] = 0;
|
|
} else if (light.type === LightType.SPHERE) {
|
|
const sphere = light as SphereLight;
|
|
position = sphere.position;
|
|
luminanceLDR = sphere.luminanceLDR;
|
|
luminanceHDR = sphere.luminanceHDR;
|
|
|
|
view[sizeRangeAngleOffset] = sphere.size;
|
|
view[sizeRangeAngleOffset + 1] = sphere.range;
|
|
view[sizeRangeAngleOffset + 2] = 0;
|
|
view[sizeRangeAngleOffset + 3] = 0;
|
|
} else if (light.type === LightType.SPOT) {
|
|
const spot = light as SpotLight;
|
|
position = spot.position;
|
|
luminanceLDR = spot.luminanceLDR;
|
|
luminanceHDR = spot.luminanceHDR;
|
|
|
|
view[sizeRangeAngleOffset] = spot.size;
|
|
view[sizeRangeAngleOffset + 1] = spot.range;
|
|
view[sizeRangeAngleOffset + 2] = spot.spotAngle;
|
|
view[sizeRangeAngleOffset + 3] = 0;
|
|
|
|
const dir = spot.direction;
|
|
view[directionOffset] = dir.x;
|
|
view[directionOffset + 1] = dir.y;
|
|
view[directionOffset + 2] = dir.z;
|
|
view[directionOffset + 3] = 0;
|
|
} else if (light.type === LightType.RANGED_DIRECTIONAL) {
|
|
const directional = light as RangedDirectionalLight;
|
|
position = directional.position;
|
|
luminanceLDR = directional.illuminanceLDR;
|
|
luminanceHDR = directional.illuminanceHDR;
|
|
|
|
const right = directional.right;
|
|
view[sizeRangeAngleOffset] = right.x;
|
|
view[sizeRangeAngleOffset + 1] = right.y;
|
|
view[sizeRangeAngleOffset + 2] = right.z;
|
|
view[sizeRangeAngleOffset + 3] = 0;
|
|
|
|
const dir = directional.direction;
|
|
view[directionOffset] = dir.x;
|
|
view[directionOffset + 1] = dir.y;
|
|
view[directionOffset + 2] = dir.z;
|
|
view[directionOffset + 3] = 0;
|
|
|
|
const scale = directional.scale;
|
|
view[boundSizeOffset] = scale.x * 0.5;
|
|
view[boundSizeOffset + 1] = scale.y * 0.5;
|
|
view[boundSizeOffset + 2] = scale.z * 0.5;
|
|
view[boundSizeOffset + 3] = 0;
|
|
}
|
|
// position
|
|
view[positionOffset] = position!.x;
|
|
view[positionOffset + 1] = position!.y;
|
|
view[positionOffset + 2] = position!.z;
|
|
view[positionOffset + 3] = light.type;
|
|
|
|
// color
|
|
const color = light.color;
|
|
if (light.useColorTemperature) {
|
|
const tempRGB = light.colorTemperatureRGB;
|
|
view[colorOffset] = color.x * tempRGB.x;
|
|
view[colorOffset + 1] = color.y * tempRGB.y;
|
|
view[colorOffset + 2] = color.z * tempRGB.z;
|
|
} else {
|
|
view[colorOffset] = color.x;
|
|
view[colorOffset + 1] = color.y;
|
|
view[colorOffset + 2] = color.z;
|
|
}
|
|
view[colorOffset + 3] = data.isHDR ? luminanceHDR * exposure * lightMeterScale : luminanceLDR;
|
|
index++;
|
|
}
|
|
// last float of first light data
|
|
view[3 * 4 + 3] = data.validPunctualLights.length;
|
|
return buffer;
|
|
}
|
|
|
|
export function buildStandardLightData (camera: Camera, pipeline: BasicPipeline): void {
|
|
validPunctualLightsCulling(pipeline, camera);
|
|
}
|
|
|
|
export function buildClusterLightData (camera: Camera, pipeline: BasicPipeline): void {
|
|
validPunctualLightsCulling(pipeline, camera);
|
|
|
|
// build cluster light data
|
|
const data = pipeline.pipelineSceneData;
|
|
const validLightCountForBuffer = nextPow2(Math.max(data.validPunctualLights.length, 1));
|
|
|
|
const lightBufferFloatNum = 20; // 5 * vec4
|
|
const clusterLightBufferSize = validLightCountForBuffer * 4 * lightBufferFloatNum;
|
|
|
|
const cameraID = getCameraUniqueID(camera);
|
|
const clusterLightBufferName = `clusterLightBuffer${cameraID}`;
|
|
const clusterGlobalIndexBufferName = `globalIndexBuffer${cameraID}`;
|
|
|
|
const ppl = (pipeline as Pipeline);
|
|
if (!ppl.containsResource(clusterGlobalIndexBufferName)) {
|
|
ppl.addStorageBuffer(clusterGlobalIndexBufferName, Format.UNKNOWN, 4, ResourceResidency.PERSISTENT);
|
|
}
|
|
|
|
if (!ppl.containsResource(clusterLightBufferName)) {
|
|
ppl.addStorageBuffer(clusterLightBufferName, Format.UNKNOWN, clusterLightBufferSize, ResourceResidency.PERSISTENT);
|
|
}
|
|
ppl.updateStorageBuffer(clusterLightBufferName, clusterLightBufferSize);
|
|
|
|
const buffer = buildLightBuffer(clusterLightBufferSize, lightBufferFloatNum, camera, pipeline);
|
|
|
|
// global index buffer
|
|
const globalIndexBuffer = new ArrayBuffer(4);
|
|
const globalIndexBufferView = new Uint32Array(globalIndexBuffer);
|
|
globalIndexBufferView[0] = 0;
|
|
|
|
const uploadPair1 = new UploadPair(new Uint8Array(buffer), clusterLightBufferName);
|
|
const uploadPair2 = new UploadPair(new Uint8Array(globalIndexBuffer), clusterGlobalIndexBufferName);
|
|
ppl.addUploadPass([uploadPair1, uploadPair2]);
|
|
}
|
|
|
|
export function buildClusterPasses (camera: Camera, pipeline: BasicPipeline): void {
|
|
buildClusterLightData(camera, pipeline);
|
|
|
|
const ppl = (pipeline as Pipeline);
|
|
if (!_clusterLightData) _clusterLightData = new ClusterLightData();
|
|
|
|
buildLightClusterBuildPass(camera, _clusterLightData, ppl);
|
|
buildLightClusterCullingPass(camera, _clusterLightData, ppl);
|
|
}
|
|
|
|
function hashCombine (hash, currHash: number): number {
|
|
return currHash ^= (hash >>> 0) + 0x9e3779b9 + (currHash << 6) + (currHash >> 2);
|
|
}
|
|
|
|
export function hashCombineNum (val: number, currHash: number): number {
|
|
const hash = 5381;
|
|
return hashCombine((hash * 33) ^ val, currHash);
|
|
}
|
|
|
|
export function hashCombineStr (str: string, currHash: number): number {
|
|
// DJB2 HASH
|
|
let hash = 5381;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = (hash * 33) ^ str.charCodeAt(i);
|
|
}
|
|
return hashCombine(hash, currHash);
|
|
}
|
|
|
|
export function bool (val): boolean {
|
|
return !!val;
|
|
}
|
|
|
|
export function getSubpassOrPassID (sceneId: number, rg: RenderGraph, lg: LayoutGraphData): number {
|
|
const queueId = rg.getParent(sceneId);
|
|
assert(queueId !== 0xFFFFFFFF);
|
|
const subpassOrPassID = rg.getParent(queueId);
|
|
assert(subpassOrPassID !== 0xFFFFFFFF);
|
|
const passId = rg.getParent(subpassOrPassID);
|
|
let layoutId = lg.nullVertex();
|
|
// single render pass
|
|
if (passId === rg.nullVertex()) {
|
|
const layoutName: string = rg.getLayout(subpassOrPassID);
|
|
assert(!!layoutName);
|
|
layoutId = lg.locateChild(lg.nullVertex(), layoutName);
|
|
} else {
|
|
const passLayoutName: string = rg.getLayout(passId);
|
|
assert(!!passLayoutName);
|
|
const passLayoutId = lg.locateChild(lg.nullVertex(), passLayoutName);
|
|
assert(passLayoutId !== lg.nullVertex());
|
|
|
|
const subpassLayoutName: string = rg.getLayout(subpassOrPassID);
|
|
assert(!!subpassLayoutName);
|
|
const subpassLayoutId = lg.locateChild(passLayoutId, subpassLayoutName);
|
|
assert(subpassLayoutId !== lg.nullVertex());
|
|
layoutId = subpassLayoutId;
|
|
}
|
|
assert(layoutId !== lg.nullVertex());
|
|
return layoutId;
|
|
}
|