auto-git:

[change] src/rendering/precomputed-shader-sky-environment-cache.ts
 [change] src/rendering/quantized-environment-blend-cache.ts
 [change] src/rendering/world-background-renderer.ts
 [change] src/rendering/world-shader-sky.ts
 [change] src/runtime-three/runtime-host.ts
This commit is contained in:
2026-04-22 17:57:03 +02:00
parent 7c73d9bc86
commit 23d0be0d2c
7 changed files with 69 additions and 63 deletions

View File

@@ -106,14 +106,14 @@ class RendererShaderSkyEnvironmentTextureBuilder {
} }
} }
export class PrecomputedShaderSkyEnvironmentCache export class PrecomputedShaderSkyEnvironmentCache implements WorldShaderSkyEnvironmentTextureResolver {
implements WorldShaderSkyEnvironmentTextureResolver
{
private currentKey: string | null = null; private currentKey: string | null = null;
private readonly phaseEntries = createEmptyPhaseEntries(); private readonly phaseEntries = createEmptyPhaseEntries();
private disposed = false; private disposed = false;
constructor(private readonly options: PrecomputedShaderSkyEnvironmentCacheOptions) {} constructor(
private readonly options: PrecomputedShaderSkyEnvironmentCacheOptions
) {}
syncPhaseTextures(states: WorldShaderSkyEnvironmentPhaseStates) { syncPhaseTextures(states: WorldShaderSkyEnvironmentPhaseStates) {
if (this.disposed) { if (this.disposed) {
@@ -132,9 +132,7 @@ export class PrecomputedShaderSkyEnvironmentCache
for (const phase of SHADER_SKY_PHASES) { for (const phase of SHADER_SKY_PHASES) {
const state = states[phase]; const state = states[phase];
nextEntries[phase] = nextEntries[phase] =
state === null state === null ? null : this.options.buildEnvironmentTexture(state);
? null
: this.options.buildEnvironmentTexture(state);
} }
} catch (error) { } catch (error) {
disposePhaseEntries(nextEntries); disposePhaseEntries(nextEntries);
@@ -166,7 +164,8 @@ export class PrecomputedShaderSkyEnvironmentCache
return null; return null;
} }
const baseTexture = this.phaseEntries[phaseBlend.basePhase]?.texture ?? null; const baseTexture =
this.phaseEntries[phaseBlend.basePhase]?.texture ?? null;
if (baseTexture === null) { if (baseTexture === null) {
return null; return null;
@@ -188,8 +187,7 @@ export class PrecomputedShaderSkyEnvironmentCache
baseTexture, baseTexture,
overlayTexture, overlayTexture,
phaseBlend.blendAmount phaseBlend.blendAmount
) ?? ) ?? (phaseBlend.blendAmount >= 0.5 ? overlayTexture : baseTexture)
(phaseBlend.blendAmount >= 0.5 ? overlayTexture : baseTexture)
); );
} }

View File

@@ -109,20 +109,16 @@ function resolveTextureDimensions(texture: Texture): {
height?: number; height?: number;
} }
| undefined; | undefined;
const sourceData = texture.source.data as const sourceData = texture.source.data as {
| { width?: number;
width?: number; height?: number;
height?: number; } | null;
}
| null;
return { return {
width: Math.max( width: Math.max(
1, 1,
Math.floor( Math.floor(
image?.width ?? image?.width ?? sourceData?.width ?? DEFAULT_BLEND_RENDER_TARGET_WIDTH
sourceData?.width ??
DEFAULT_BLEND_RENDER_TARGET_WIDTH
) )
), ),
height: Math.max( height: Math.max(
@@ -186,9 +182,7 @@ export function createQuantizedEnvironmentBlendCacheKey(
return `${getOrderedPairCacheKey(baseTexture, overlayTexture)}@${bucketIndex}`; return `${getOrderedPairCacheKey(baseTexture, overlayTexture)}@${bucketIndex}`;
} }
export class QuantizedEnvironmentBlendCache export class QuantizedEnvironmentBlendCache implements WorldEnvironmentBlendTextureResolver {
implements WorldEnvironmentBlendTextureResolver
{
private readonly bucketCount: number; private readonly bucketCount: number;
private readonly buildBlendTexture: QuantizedEnvironmentBlendCacheOptions["buildBlendTexture"]; private readonly buildBlendTexture: QuantizedEnvironmentBlendCacheOptions["buildBlendTexture"];
private readonly disposeBuildResources: (() => void) | undefined; private readonly disposeBuildResources: (() => void) | undefined;
@@ -208,9 +202,7 @@ export class QuantizedEnvironmentBlendCache
constructor(options: QuantizedEnvironmentBlendCacheOptions) { constructor(options: QuantizedEnvironmentBlendCacheOptions) {
this.bucketCount = Math.max( this.bucketCount = Math.max(
1, 1,
Math.floor( Math.floor(options.bucketCount ?? DEFAULT_QUANTIZED_BLEND_BUCKET_COUNT)
options.bucketCount ?? DEFAULT_QUANTIZED_BLEND_BUCKET_COUNT
)
); );
this.buildBlendTexture = options.buildBlendTexture; this.buildBlendTexture = options.buildBlendTexture;
this.disposeBuildResources = options.disposeBuildResources; this.disposeBuildResources = options.disposeBuildResources;
@@ -359,7 +351,11 @@ export class QuantizedEnvironmentBlendCache
} }
private schedulePendingBuilds() { private schedulePendingBuilds() {
if (this.disposed || this.buildScheduled || this.pendingBuilds.length === 0) { if (
this.disposed ||
this.buildScheduled ||
this.pendingBuilds.length === 0
) {
return; return;
} }

View File

@@ -756,8 +756,7 @@ function applyShaderSkyStateToMaterial(
material.uniforms.uCloudSoftness.value = state.clouds.softness; material.uniforms.uCloudSoftness.value = state.clouds.softness;
material.uniforms.uCloudScale.value = state.clouds.scale; material.uniforms.uCloudScale.value = state.clouds.scale;
material.uniforms.uCloudHeight.value = state.clouds.height; material.uniforms.uCloudHeight.value = state.clouds.height;
material.uniforms.uCloudHeightVariation.value = material.uniforms.uCloudHeightVariation.value = state.clouds.heightVariation;
state.clouds.heightVariation;
material.uniforms.uCloudTint.value.set(state.clouds.tintHex); material.uniforms.uCloudTint.value.set(state.clouds.tintHex);
material.uniforms.uCloudOpacity.value = state.clouds.opacity; material.uniforms.uCloudOpacity.value = state.clouds.opacity;
material.uniforms.uCloudOpacityRandomness.value = material.uniforms.uCloudOpacityRandomness.value =
@@ -904,7 +903,9 @@ export class WorldBackgroundRenderer {
} }
applyShaderSkyStateToMaterial(this.shaderSkyMaterial, shaderSkyState); applyShaderSkyStateToMaterial(this.shaderSkyMaterial, shaderSkyState);
this.syncEnvironmentCaptureState(showShaderBackground ? shaderSkyState : null); this.syncEnvironmentCaptureState(
showShaderBackground ? shaderSkyState : null
);
this.shaderMesh.visible = showShaderBackground; this.shaderMesh.visible = showShaderBackground;
this.gradientMesh.visible = !showShaderBackground && !showImageBackground; this.gradientMesh.visible = !showShaderBackground && !showImageBackground;
this.imageMesh.visible = showImageBackground; this.imageMesh.visible = showImageBackground;

View File

@@ -98,7 +98,10 @@ function resolveTimeWindowMidpoint(
startTimeOfDayHours: number, startTimeOfDayHours: number,
endTimeOfDayHours: number endTimeOfDayHours: number
): number { ): number {
const wrappedEndTime = wrapTimeForward(endTimeOfDayHours, startTimeOfDayHours); const wrappedEndTime = wrapTimeForward(
endTimeOfDayHours,
startTimeOfDayHours
);
return normalizeTimeOfDayHours( return normalizeTimeOfDayHours(
startTimeOfDayHours + (wrappedEndTime - startTimeOfDayHours) / 2 startTimeOfDayHours + (wrappedEndTime - startTimeOfDayHours) / 2
@@ -423,10 +426,8 @@ export function resolveWorldShaderSkyEnvironmentPhaseStates(
}; };
} }
const dawnHalfDuration = const dawnHalfDuration = Math.max(timeSettings.dawnDurationHours, 0.001) / 2;
Math.max(timeSettings.dawnDurationHours, 0.001) / 2; const duskHalfDuration = Math.max(timeSettings.duskDurationHours, 0.001) / 2;
const duskHalfDuration =
Math.max(timeSettings.duskDurationHours, 0.001) / 2;
const dawnStart = timeSettings.sunriseTimeOfDayHours - dawnHalfDuration; const dawnStart = timeSettings.sunriseTimeOfDayHours - dawnHalfDuration;
const dawnEnd = timeSettings.sunriseTimeOfDayHours + dawnHalfDuration; const dawnEnd = timeSettings.sunriseTimeOfDayHours + dawnHalfDuration;
const duskStart = timeSettings.sunsetTimeOfDayHours - duskHalfDuration; const duskStart = timeSettings.sunsetTimeOfDayHours - duskHalfDuration;
@@ -549,9 +550,9 @@ export function createWorldShaderSkyEnvironmentPhaseCacheKey(
export function resolveWorldShaderSkyEnvironmentPhaseBlend( export function resolveWorldShaderSkyEnvironmentPhaseBlend(
state: Pick<WorldShaderSkyRenderState, "time"> state: Pick<WorldShaderSkyRenderState, "time">
): WorldShaderSkyEnvironmentPhaseBlend | null { ): WorldShaderSkyEnvironmentPhaseBlend | null {
const activePhases = ( const activePhases = (["day", "dawn", "dusk", "night"] as const).filter(
["day", "dawn", "dusk", "night"] as const (phase) => state.time.phaseWeights[phase] > 1e-4
).filter((phase) => state.time.phaseWeights[phase] > 1e-4); );
if (activePhases.length === 0) { if (activePhases.length === 0) {
return null; return null;

View File

@@ -180,7 +180,10 @@ import {
buildRuntimeNpcCollider, buildRuntimeNpcCollider,
createRuntimeNpcFromDefinition createRuntimeNpcFromDefinition
} from "./runtime-scene-build"; } from "./runtime-scene-build";
import { resolvePlayerStartLookInput, resolvePlayerStartPauseInput } from "./player-input-bindings"; import {
resolvePlayerStartLookInput,
resolvePlayerStartPauseInput
} from "./player-input-bindings";
interface CachedMaterialTexture { interface CachedMaterialTexture {
signature: string; signature: string;
@@ -584,7 +587,8 @@ export class RuntimeHost {
desiredCameraPosition, desiredCameraPosition,
radius radius
) ?? { ...desiredCameraPosition }, ) ?? { ...desiredCameraPosition },
isCameraDrivenExternally: () => this.resolveActiveRuntimeCameraRig() !== null, isCameraDrivenExternally: () =>
this.resolveActiveRuntimeCameraRig() !== null,
getCameraYawRadians: () => { getCameraYawRadians: () => {
this.camera.getWorldDirection(this.cameraForward); this.camera.getWorldDirection(this.cameraForward);
return Math.atan2(this.cameraForward.x, this.cameraForward.z); return Math.atan2(this.cameraForward.x, this.cameraForward.z);
@@ -1215,7 +1219,8 @@ export class RuntimeHost {
const target = rig.target; const target = rig.target;
const activeNpc = const activeNpc =
this.runtimeScene.npcDefinitions.find( this.runtimeScene.npcDefinitions.find(
(candidate) => candidate.actorId === target.actorId && candidate.active (candidate) =>
candidate.actorId === target.actorId && candidate.active
) ?? ) ??
this.runtimeScene.npcDefinitions.find( this.runtimeScene.npcDefinitions.find(
(candidate) => candidate.actorId === target.actorId (candidate) => candidate.actorId === target.actorId
@@ -1259,12 +1264,15 @@ export class RuntimeHost {
if (this.activeCameraRigOverrideEntityId !== null) { if (this.activeCameraRigOverrideEntityId !== null) {
return ( return (
cameraRigs.find( cameraRigs.find(
(candidate) => candidate.entityId === this.activeCameraRigOverrideEntityId (candidate) =>
candidate.entityId === this.activeCameraRigOverrideEntityId
) ?? null ) ?? null
); );
} }
const eligibleCameraRigs = cameraRigs.filter((candidate) => candidate.defaultActive); const eligibleCameraRigs = cameraRigs.filter(
(candidate) => candidate.defaultActive
);
if (eligibleCameraRigs.length === 0) { if (eligibleCameraRigs.length === 0) {
return null; return null;
@@ -1277,10 +1285,7 @@ export class RuntimeHost {
)[0]!; )[0]!;
} }
private updateRuntimeCameraRigLookState( private updateRuntimeCameraRigLookState(rig: RuntimeCameraRig, dt: number) {
rig: RuntimeCameraRig,
dt: number
) {
if (this.runtimeScene === null) { if (this.runtimeScene === null) {
return; return;
} }
@@ -4514,8 +4519,7 @@ export class RuntimeHost {
deltaY * CAMERA_RIG_POINTER_LOOK_SENSITIVITY, deltaY * CAMERA_RIG_POINTER_LOOK_SENSITIVITY,
(-this.activeRuntimeCameraRig.lookAround.pitchLimitDegrees * Math.PI) / (-this.activeRuntimeCameraRig.lookAround.pitchLimitDegrees * Math.PI) /
180, 180,
(this.activeRuntimeCameraRig.lookAround.pitchLimitDegrees * Math.PI) / (this.activeRuntimeCameraRig.lookAround.pitchLimitDegrees * Math.PI) / 180
180
); );
event.preventDefault(); event.preventDefault();
event.stopImmediatePropagation(); event.stopImmediatePropagation();

View File

@@ -735,14 +735,12 @@ export class ViewportHost {
} }
} }
); );
this.shaderSkyEnvironmentBlendCache = createRendererQuantizedPmremBlendCache( this.shaderSkyEnvironmentBlendCache =
this.renderer, createRendererQuantizedPmremBlendCache(this.renderer, {
{
onTextureReady: () => { onTextureReady: () => {
this.applyWorld(); this.applyWorld();
} }
} });
);
this.shaderSkyEnvironmentCache = this.shaderSkyEnvironmentCache =
createRendererPrecomputedShaderSkyEnvironmentCache( createRendererPrecomputedShaderSkyEnvironmentCache(
this.renderer, this.renderer,

View File

@@ -36,16 +36,24 @@ describe("resolveWorldEnvironmentState", () => {
environmentIntensity: 0.4 environmentIntensity: 0.4
}; };
const earlyTwilight = resolveWorldEnvironmentState(world.background, dayTexture, { const earlyTwilight = resolveWorldEnvironmentState(
texture: nightTexture, world.background,
opacity: 0.25, dayTexture,
environmentIntensity: 0.8 {
}); texture: nightTexture,
const lateTwilight = resolveWorldEnvironmentState(world.background, dayTexture, { opacity: 0.25,
texture: nightTexture, environmentIntensity: 0.8
opacity: 0.75, }
environmentIntensity: 0.8 );
}); const lateTwilight = resolveWorldEnvironmentState(
world.background,
dayTexture,
{
texture: nightTexture,
opacity: 0.75,
environmentIntensity: 0.8
}
);
expect(earlyTwilight.texture).toBe(dayTexture); expect(earlyTwilight.texture).toBe(dayTexture);
expect(earlyTwilight.intensity).toBeCloseTo(0.5); expect(earlyTwilight.intensity).toBeCloseTo(0.5);