Add world background renderer and update advanced rendering composer

This commit is contained in:
2026-04-13 14:10:06 +02:00
parent 08cbc97446
commit e54ae8e121
2 changed files with 269 additions and 2 deletions

View File

@@ -110,7 +110,8 @@ export function createAdvancedRenderingComposer(
renderer: WebGLRenderer,
scene: Scene,
camera: PerspectiveCamera,
settings: AdvancedRenderingSettings
settings: AdvancedRenderingSettings,
backgroundScene: Scene | null = null
): EffectComposer {
// The scene is always rendered into the composer's offscreen targets first,
// so those targets need depth for correct visibility even when no effect samples it.
@@ -121,7 +122,14 @@ export function createAdvancedRenderingComposer(
frameBufferType: renderer.capabilities.isWebGL2 ? HalfFloatType : UnsignedByteType
});
composer.addPass(new RenderPass(scene, camera));
if (backgroundScene !== null) {
composer.addPass(new RenderPass(backgroundScene, camera));
const mainRenderPass = new RenderPass(scene, camera);
mainRenderPass.clear = false;
composer.addPass(mainRenderPass);
} else {
composer.addPass(new RenderPass(scene, camera));
}
const effects: Array<BloomEffect | DepthOfFieldEffect | ToneMappingEffect | SMAAEffect> = [];

View File

@@ -0,0 +1,259 @@
import {
BackSide,
Color,
Group,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
Scene,
ShaderMaterial,
SphereGeometry,
Texture
} from "three";
import type { WorldBackgroundSettings } from "../document/world-settings";
const BACKGROUND_SPHERE_RADIUS = 320;
const BACKGROUND_SPHERE_WIDTH_SEGMENTS = 48;
const BACKGROUND_SPHERE_HEIGHT_SEGMENTS = 24;
const DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR = "#0d1116";
const NIGHT_BACKGROUND_EPSILON = 1e-4;
function clamp(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max);
}
function lerp(left: number, right: number, amount: number) {
return left + (right - left) * amount;
}
function resolveGradientColors(background: WorldBackgroundSettings) {
if (background.mode === "solid") {
return {
topColorHex: background.colorHex,
bottomColorHex: background.colorHex
};
}
if (background.mode === "verticalGradient") {
return {
topColorHex: background.topColorHex,
bottomColorHex: background.bottomColorHex
};
}
return {
topColorHex: DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR,
bottomColorHex: DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR
};
}
export interface WorldBackgroundOverlayState {
texture: Texture | null;
opacity: number;
environmentIntensity: number;
}
export interface WorldEnvironmentState {
texture: Texture | null;
intensity: number;
}
export function resolveWorldEnvironmentState(
background: WorldBackgroundSettings,
backgroundTexture: Texture | null,
overlay: WorldBackgroundOverlayState | null
): WorldEnvironmentState {
const baseTexture = background.mode === "image" ? backgroundTexture : null;
const baseIntensity =
background.mode === "image" ? background.environmentIntensity : 0;
const overlayTexture = overlay?.texture ?? null;
const overlayOpacity = clamp(overlay?.opacity ?? 0, 0, 1);
const overlayIntensity = overlay?.environmentIntensity ?? 0;
if (
baseTexture !== null &&
overlayTexture !== null &&
overlayOpacity > NIGHT_BACKGROUND_EPSILON &&
overlayOpacity < 1 - NIGHT_BACKGROUND_EPSILON
) {
if (overlayOpacity < 0.5) {
return {
texture: baseTexture,
intensity: lerp(baseIntensity, 0, overlayOpacity * 2)
};
}
return {
texture: overlayTexture,
intensity: lerp(0, overlayIntensity, (overlayOpacity - 0.5) * 2)
};
}
if (overlayTexture !== null && overlayOpacity > NIGHT_BACKGROUND_EPSILON) {
if (baseTexture === null) {
return {
texture: overlayTexture,
intensity: overlayIntensity * overlayOpacity
};
}
if (overlayOpacity >= 1 - NIGHT_BACKGROUND_EPSILON) {
return {
texture: overlayTexture,
intensity: overlayIntensity
};
}
}
if (baseTexture !== null) {
return {
texture: baseTexture,
intensity: baseIntensity
};
}
if (overlayTexture !== null && overlayOpacity > NIGHT_BACKGROUND_EPSILON) {
return {
texture: overlayTexture,
intensity: overlayIntensity * overlayOpacity
};
}
return {
texture: null,
intensity: 1
};
}
const GRADIENT_VERTEX_SHADER = `
varying vec3 vWorldPosition;
void main() {
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const GRADIENT_FRAGMENT_SHADER = `
uniform vec3 uTopColor;
uniform vec3 uBottomColor;
varying vec3 vWorldPosition;
void main() {
vec3 direction = normalize(vWorldPosition - cameraPosition);
float gradientAmount = clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
vec3 color = mix(uBottomColor, uTopColor, gradientAmount);
gl_FragColor = vec4(color, 1.0);
}
`;
export class WorldBackgroundRenderer {
readonly scene = new Scene();
private readonly anchor = new Group();
private readonly geometry = new SphereGeometry(
BACKGROUND_SPHERE_RADIUS,
BACKGROUND_SPHERE_WIDTH_SEGMENTS,
BACKGROUND_SPHERE_HEIGHT_SEGMENTS
);
private readonly gradientMaterial = new ShaderMaterial({
uniforms: {
uTopColor: {
value: new Color(DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR)
},
uBottomColor: {
value: new Color(DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR)
}
},
vertexShader: GRADIENT_VERTEX_SHADER,
fragmentShader: GRADIENT_FRAGMENT_SHADER,
side: BackSide,
depthTest: false,
depthWrite: false,
fog: false
});
private readonly imageMaterial = new MeshBasicMaterial({
color: 0xffffff,
side: BackSide,
depthTest: false,
depthWrite: false,
fog: false
});
private readonly overlayMaterial = new MeshBasicMaterial({
color: 0xffffff,
side: BackSide,
depthTest: false,
depthWrite: false,
fog: false,
transparent: true,
opacity: 0
});
private readonly gradientMesh = new Mesh(this.geometry, this.gradientMaterial);
private readonly imageMesh = new Mesh(this.geometry, this.imageMaterial);
private readonly overlayMesh = new Mesh(this.geometry, this.overlayMaterial);
constructor() {
this.gradientMesh.renderOrder = -1002;
this.imageMesh.renderOrder = -1001;
this.overlayMesh.renderOrder = -1000;
for (const mesh of [this.gradientMesh, this.imageMesh, this.overlayMesh]) {
mesh.frustumCulled = false;
}
this.anchor.add(this.gradientMesh);
this.anchor.add(this.imageMesh);
this.anchor.add(this.overlayMesh);
this.scene.add(this.anchor);
}
update(
background: WorldBackgroundSettings,
backgroundTexture: Texture | null,
overlay: WorldBackgroundOverlayState | null
) {
const gradientColors = resolveGradientColors(background);
this.gradientMaterial.uniforms.uTopColor.value.set(
gradientColors.topColorHex
);
this.gradientMaterial.uniforms.uBottomColor.value.set(
gradientColors.bottomColorHex
);
const showImageBackground =
background.mode === "image" && backgroundTexture !== null;
if (this.imageMaterial.map !== backgroundTexture) {
this.imageMaterial.map = backgroundTexture;
this.imageMaterial.needsUpdate = true;
}
this.gradientMesh.visible = !showImageBackground;
this.imageMesh.visible = showImageBackground;
const overlayTexture = overlay?.texture ?? null;
const overlayOpacity =
overlayTexture === null ? 0 : clamp(overlay?.opacity ?? 0, 0, 1);
if (this.overlayMaterial.map !== overlayTexture) {
this.overlayMaterial.map = overlayTexture;
this.overlayMaterial.needsUpdate = true;
}
this.overlayMaterial.opacity = overlayOpacity;
this.overlayMesh.visible = overlayOpacity > NIGHT_BACKGROUND_EPSILON;
}
syncToCamera(camera: PerspectiveCamera) {
this.anchor.position.copy(camera.position);
}
dispose() {
this.geometry.dispose();
this.gradientMaterial.dispose();
this.imageMaterial.dispose();
this.overlayMaterial.dispose();
}
}