playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
312 lines (311 loc) • 11.7 kB
JavaScript
import { Color } from "../../core/math/color.js";
import { Vec3 } from "../../core/math/vec3.js";
import {
ADDRESS_CLAMP_TO_EDGE,
BLENDEQUATION_ADD,
BLENDMODE_ONE,
BLENDMODE_SRC_ALPHA,
BLENDMODE_ZERO,
FILTER_LINEAR,
PIXELFORMAT_RGBA16F,
PIXELFORMAT_RGBA32F,
PIXELFORMAT_RGBA8,
SEMANTIC_POSITION,
SHADERLANGUAGE_GLSL,
SHADERLANGUAGE_WGSL
} from "../../platform/graphics/constants.js";
import { BlendState } from "../../platform/graphics/blend-state.js";
import { FramePass } from "../../platform/graphics/frame-pass.js";
import { RenderTarget } from "../../platform/graphics/render-target.js";
import { Texture } from "../../platform/graphics/texture.js";
import { RenderPassShaderQuad } from "../../scene/graphics/render-pass-shader-quad.js";
import { shadowTypeInfo } from "../../scene/constants.js";
import { ShaderChunks } from "../../scene/shader-lib/shader-chunks.js";
import { ShaderUtils } from "../../scene/shader-lib/shader-utils.js";
import glslVolumetricFogPS from "../../scene/shader-lib/glsl/chunks/render-pass/frag/volumetricFog.js";
import wgslVolumetricFogPS from "../../scene/shader-lib/wgsl/chunks/render-pass/frag/volumetricFog.js";
import glslVolumetricFogCombinePS from "../../scene/shader-lib/glsl/chunks/render-pass/frag/volumetricFogCombine.js";
import wgslVolumetricFogCombinePS from "../../scene/shader-lib/wgsl/chunks/render-pass/frag/volumetricFogCombine.js";
const _tempDir = new Vec3();
class RenderPassVolumetricFog extends RenderPassShaderQuad {
light = null;
shadowsEnabled = false;
lightRenderData = null;
tint = new Color(1, 1, 1);
density = 0.01;
heightBase = 0;
heightFalloff = 0.05;
anisotropy = 0.6;
intensity = 1;
ambientColor = new Color(1, 1, 1);
ambientIntensity = 0.02;
maxDistance = 300;
steps = 24;
noiseOffset = 0;
exposure = 1;
_variantKey = null;
constructor(device, cameraComponent) {
super(device);
this.cameraComponent = cameraComponent;
const scope = device.scope;
this.cameraPosId = scope.resolve("uFogCameraPos");
this.cameraFwdId = scope.resolve("uFogCameraFwd");
this.invViewId = scope.resolve("uFogInvView");
this.projScaleId = scope.resolve("uFogProjScale");
this.tintId = scope.resolve("uFogTint");
this.lightColorId = scope.resolve("uFogLightColor");
this.lightDirId = scope.resolve("uFogLightDir");
this.ambientId = scope.resolve("uFogAmbient");
this.fogParamsId = scope.resolve("uFogParams");
this.scatterParamsId = scope.resolve("uFogScatterParams");
this.shadowMapId = scope.resolve("uFogShadowMap");
this.shadowMatrixPaletteId = scope.resolve("uFogShadowMatrixPalette[0]");
this.shadowCascadeDistancesId = scope.resolve("uFogShadowCascadeDistances");
this.shadowParamsId = scope.resolve("uFogShadowParams");
this._cameraPos = new Float32Array(3);
this._cameraFwd = new Float32Array(3);
this._projScale = new Float32Array(2);
this._tint = new Float32Array(3);
this._lightColor = new Float32Array(3);
this._lightDir = new Float32Array(3);
this._ambient = new Float32Array(3);
this._fogParams = new Float32Array(4);
this._scatterParams = new Float32Array(4);
this._shadowParams = new Float32Array(4);
}
updateShaderVariant(shadows, pcf) {
const depthMapLinear = this.cameraComponent.shaderParams.sceneDepthMapLinear;
const key = `${shadows}-${pcf}-${depthMapLinear}`;
if (this._variantKey !== key) {
this._variantKey = key;
const defines = /* @__PURE__ */ new Map();
ShaderUtils.addScreenDepthChunkDefines(this.cameraComponent.shaderParams, defines);
if (shadows) defines.set("FOG_SHADOWS", "");
if (pcf) defines.set("FOG_SHADOW_PCF", "");
this.shader = ShaderUtils.createShader(this.device, {
uniqueName: `VolumetricFogShader-${key}`,
attributes: { aPosition: SEMANTIC_POSITION },
vertexChunk: "quadVS",
fragmentChunk: "volumetricFogPS",
fragmentDefines: defines
});
}
}
execute() {
const { light } = this;
const camera = this.cameraComponent.camera;
const node = camera._node;
this.invViewId.setValue(node.getWorldTransform().data);
const pos = node.getPosition();
this._cameraPos[0] = pos.x;
this._cameraPos[1] = pos.y;
this._cameraPos[2] = pos.z;
this.cameraPosId.setValue(this._cameraPos);
const fwd = node.forward;
this._cameraFwd[0] = fwd.x;
this._cameraFwd[1] = fwd.y;
this._cameraFwd[2] = fwd.z;
this.cameraFwdId.setValue(this._cameraFwd);
const projData = camera.projectionMatrix.data;
this._projScale[0] = 1 / projData[0];
this._projScale[1] = 1 / projData[5];
this.projScaleId.setValue(this._projScale);
const lightScale = this.intensity * this.exposure;
if (light) {
light._node.getWorldTransform().getY(_tempDir).normalize();
this._lightDir[0] = _tempDir.x;
this._lightDir[1] = _tempDir.y;
this._lightDir[2] = _tempDir.z;
const color = light._colorLinear;
this._lightColor[0] = color[0] * lightScale;
this._lightColor[1] = color[1] * lightScale;
this._lightColor[2] = color[2] * lightScale;
} else {
this._lightDir[0] = 0;
this._lightDir[1] = 1;
this._lightDir[2] = 0;
this._lightColor[0] = 0;
this._lightColor[1] = 0;
this._lightColor[2] = 0;
}
this.lightDirId.setValue(this._lightDir);
this.lightColorId.setValue(this._lightColor);
if (this.shadowsEnabled && light && this.lightRenderData) {
const lightRenderData = this.lightRenderData;
const biases = light._getUniformBiasValues(lightRenderData);
this.shadowMapId.setValue(lightRenderData.shadowBuffer);
this.shadowMatrixPaletteId.setValue(light._shadowMatrixPalette);
this.shadowCascadeDistancesId.setValue(light._shadowCascadeDistances);
this._shadowParams[0] = light.numCascades;
this._shadowParams[1] = biases.bias;
this._shadowParams[2] = 0;
this._shadowParams[3] = light.shadowDistance;
this.shadowParamsId.setValue(this._shadowParams);
}
const { tint, ambientColor, ambientIntensity } = this;
this._tint[0] = tint.r;
this._tint[1] = tint.g;
this._tint[2] = tint.b;
this.tintId.setValue(this._tint);
const ambientScale = ambientIntensity * this.exposure;
this._ambient[0] = ambientColor.r * ambientScale;
this._ambient[1] = ambientColor.g * ambientScale;
this._ambient[2] = ambientColor.b * ambientScale;
this.ambientId.setValue(this._ambient);
this._fogParams[0] = this.density;
this._fogParams[1] = this.heightBase;
this._fogParams[2] = this.heightFalloff;
this._fogParams[3] = this.maxDistance;
this.fogParamsId.setValue(this._fogParams);
this._scatterParams[0] = this.anisotropy;
this._scatterParams[1] = this.steps;
this._scatterParams[2] = this.noiseOffset;
this._scatterParams[3] = light ? light.shadowIntensity : 1;
this.scatterParamsId.setValue(this._scatterParams);
super.execute();
}
}
class RenderPassVolumetricFogCombine extends RenderPassShaderQuad {
constructor(device, cameraComponent, fogTexture) {
super(device);
this.fogTexture = fogTexture;
const defines = /* @__PURE__ */ new Map();
ShaderUtils.addScreenDepthChunkDefines(cameraComponent.shaderParams, defines);
this.shader = ShaderUtils.createShader(device, {
uniqueName: `VolumetricFogCombineShader-${cameraComponent.shaderParams.sceneDepthMapLinear}`,
attributes: { aPosition: SEMANTIC_POSITION },
vertexChunk: "quadVS",
fragmentChunk: "volumetricFogCombinePS",
fragmentDefines: defines
});
this.blendState = new BlendState(
true,
BLENDEQUATION_ADD,
BLENDMODE_ONE,
BLENDMODE_SRC_ALPHA,
BLENDEQUATION_ADD,
BLENDMODE_ZERO,
BLENDMODE_ONE
);
this.fogTextureId = device.scope.resolve("uFogTexture");
this.fogTextureSizeId = device.scope.resolve("uFogTextureSize");
this._fogTextureSize = new Float32Array(4);
}
execute() {
const { fogTexture } = this;
this.fogTextureId.setValue(fogTexture);
this._fogTextureSize[0] = fogTexture.width;
this._fogTextureSize[1] = fogTexture.height;
this._fogTextureSize[2] = 1 / fogTexture.width;
this._fogTextureSize[3] = 1 / fogTexture.height;
this.fogTextureSizeId.setValue(this._fogTextureSize);
super.execute();
}
}
class FramePassVolumetricFog extends FramePass {
light = null;
tint = new Color(1, 1, 1);
density = 0.01;
heightBase = 0;
heightFalloff = 0.05;
anisotropy = 0.6;
intensity = 1;
ambientColor = new Color(1, 1, 1);
ambientIntensity = 0.02;
maxDistance = 300;
steps = 24;
temporalDither = false;
_scale = 0.5;
_frameIndex = 0;
constructor(device, cameraComponent, sceneTexture, sceneRenderTarget) {
super(device);
this.cameraComponent = cameraComponent;
ShaderChunks.get(device, SHADERLANGUAGE_GLSL).set("volumetricFogPS", glslVolumetricFogPS);
ShaderChunks.get(device, SHADERLANGUAGE_WGSL).set("volumetricFogPS", wgslVolumetricFogPS);
ShaderChunks.get(device, SHADERLANGUAGE_GLSL).set("volumetricFogCombinePS", glslVolumetricFogCombinePS);
ShaderChunks.get(device, SHADERLANGUAGE_WGSL).set("volumetricFogCombinePS", wgslVolumetricFogCombinePS);
const format = device.getRenderableHdrFormat([PIXELFORMAT_RGBA16F, PIXELFORMAT_RGBA32F], true, 1) ?? PIXELFORMAT_RGBA8;
this.fogTexture = new Texture(device, {
name: "VolumetricFogTexture",
width: 4,
height: 4,
format,
mipmaps: false,
minFilter: FILTER_LINEAR,
magFilter: FILTER_LINEAR,
addressU: ADDRESS_CLAMP_TO_EDGE,
addressV: ADDRESS_CLAMP_TO_EDGE
});
this.fogRenderTarget = new RenderTarget({
name: "VolumetricFogRT",
colorBuffer: this.fogTexture,
depth: false
});
this.fogPass = new RenderPassVolumetricFog(device, cameraComponent);
this.fogPass.init(this.fogRenderTarget, {
resizeSource: sceneTexture,
scaleX: this._scale,
scaleY: this._scale
});
this.fogPass.setClearColor(new Color(0, 0, 0, 1));
this.beforePasses.push(this.fogPass);
this.combinePass = new RenderPassVolumetricFogCombine(device, cameraComponent, this.fogTexture);
this.combinePass.init(sceneRenderTarget);
this.beforePasses.push(this.combinePass);
}
destroy() {
this.beforePasses.forEach((pass) => pass.destroy());
this.beforePasses.length = 0;
this.fogPass = null;
this.combinePass = null;
if (this.fogRenderTarget) {
this.fogRenderTarget.destroyTextureBuffers();
this.fogRenderTarget.destroy();
this.fogRenderTarget = null;
this.fogTexture = null;
}
}
set scale(value) {
this._scale = value;
this.fogPass.scaleX = value;
this.fogPass.scaleY = value;
}
get scale() {
return this._scale;
}
frameUpdate() {
super.frameUpdate();
const { light, fogPass } = this;
const camera = this.cameraComponent.camera;
let shadows = false;
let pcf = false;
let lightRenderData = null;
if (light && light.castShadows && light.shadowIntensity > 0) {
lightRenderData = light.getRenderData(camera, 0);
if (lightRenderData.shadowBuffer) {
shadows = true;
pcf = !!shadowTypeInfo.get(light._shadowType)?.pcf;
}
}
fogPass.updateShaderVariant(shadows, pcf);
fogPass.shadowsEnabled = shadows;
fogPass.lightRenderData = shadows ? lightRenderData : null;
fogPass.light = light;
fogPass.tint.copy(this.tint);
fogPass.density = this.density;
fogPass.heightBase = this.heightBase;
fogPass.heightFalloff = this.heightFalloff;
fogPass.anisotropy = this.anisotropy;
fogPass.intensity = this.intensity;
fogPass.ambientColor.copy(this.ambientColor);
fogPass.ambientIntensity = this.ambientIntensity;
fogPass.maxDistance = this.maxDistance;
fogPass.steps = Math.max(1, Math.floor(this.steps));
fogPass.exposure = this.cameraComponent.system.app.scene.exposure;
this._frameIndex = (this._frameIndex + 1) % 16;
fogPass.noiseOffset = this.temporalDither ? this._frameIndex * 0.618034 : 0;
}
}
export {
FramePassVolumetricFog
};