Add sound emitter volume control and update runtime host for various controls

This commit is contained in:
2026-04-14 02:38:06 +02:00
parent 982409e83f
commit a995cd172e
3 changed files with 413 additions and 31 deletions

View File

@@ -44,8 +44,11 @@ import {
type ControlEffect,
type InteractionControlTargetRef,
type LightControlTargetRef,
type ModelInstanceControlTargetRef,
type RuntimeResolvedControlChannelValue,
type RuntimeResolvedDiscreteControlState
type RuntimeResolvedDiscreteControlState,
type SceneControlTargetRef,
type SoundEmitterControlTargetRef
} from "../controls/control-surface";
import { buildBoxBrushDerivedMeshData } from "../geometry/box-brush-mesh";
import {
@@ -476,9 +479,9 @@ export class RuntimeHost {
status: "loading",
message: null
});
this.syncResolvedControlStateToRuntime(runtimeScene.control.resolved);
this.applyWorld();
this.rebuildLocalLights(runtimeScene.localLights);
this.syncResolvedControlStateToRuntime(runtimeScene.control.resolved);
this.rebuildBrushMeshes(runtimeScene.brushes);
this.rebuildModelRenderObjects(
runtimeScene.modelInstances,
@@ -971,6 +974,7 @@ export class RuntimeHost {
pointLight.position.y,
pointLight.position.z
);
group.visible = pointLight.enabled;
light.position.set(0, 0, 0);
group.add(light);
@@ -1008,6 +1012,7 @@ export class RuntimeHost {
spotLight.position.z
);
group.quaternion.copy(orientation);
group.visible = spotLight.enabled;
light.position.set(0, 0, 0);
light.target.position.set(0, 1, 0);
group.add(light);
@@ -1035,12 +1040,37 @@ export class RuntimeHost {
state: RuntimeResolvedDiscreteControlState
) {
switch (state.type) {
case "actorPresence":
this.applyActorPresenceControl(state.target.actorId, state.value);
return;
case "modelVisibility":
this.applyModelInstanceVisibilityControl(state.target, state.value);
return;
case "soundPlayback":
this.applySoundPlaybackControl(state.target, state.value);
return;
case "modelAnimationPlayback":
this.applyModelAnimationPlaybackControl(
state.target,
state.clipName,
state.loop
);
return;
case "lightEnabled":
this.applyLightEnabledControl(state.target, state.value);
return;
case "lightColor":
this.applyLightColorControl(state.target, state.value);
return;
case "interactionEnabled":
this.applyInteractionEnabledControl(state.target, state.value);
return;
case "ambientLightColor":
this.applyAmbientLightColorControl(state.target, state.value);
return;
case "sunLightColor":
this.applySunLightColorControl(state.target, state.value);
return;
}
}
@@ -1054,6 +1084,47 @@ export class RuntimeHost {
channelValue.value
);
return;
case "soundVolume":
this.applySoundVolumeControl(
channelValue.descriptor.target,
channelValue.value
);
return;
case "ambientLightIntensity":
this.applyAmbientLightIntensityControl(
channelValue.descriptor.target,
channelValue.value
);
return;
case "sunLightIntensity":
this.applySunLightIntensityControl(
channelValue.descriptor.target,
channelValue.value
);
return;
}
}
private mutateRuntimeLightState(
target: LightControlTargetRef,
mutate: (
light:
| RuntimeSceneDefinition["localLights"]["pointLights"][number]
| RuntimeSceneDefinition["localLights"]["spotLights"][number]
) => void
) {
if (this.runtimeScene === null) {
return;
}
const lights =
target.entityKind === "pointLight"
? this.runtimeScene.localLights.pointLights
: this.runtimeScene.localLights.spotLights;
const light = lights.find((candidate) => candidate.entityId === target.entityId);
if (light !== undefined) {
mutate(light);
}
}
@@ -1061,6 +1132,10 @@ export class RuntimeHost {
target: LightControlTargetRef,
enabled: boolean
) {
this.mutateRuntimeLightState(target, (light) => {
light.enabled = enabled;
});
const renderObjects = this.localLightObjects.get(target.entityId);
if (renderObjects === undefined) {
@@ -1074,6 +1149,10 @@ export class RuntimeHost {
target: LightControlTargetRef,
intensity: number
) {
this.mutateRuntimeLightState(target, (light) => {
light.intensity = intensity;
});
const renderObjects = this.localLightObjects.get(target.entityId);
if (renderObjects === undefined) {
@@ -1083,6 +1162,165 @@ export class RuntimeHost {
renderObjects.light.intensity = intensity;
}
private applyLightColorControl(target: LightControlTargetRef, colorHex: string) {
this.mutateRuntimeLightState(target, (light) => {
light.colorHex = colorHex;
});
const renderObjects = this.localLightObjects.get(target.entityId);
if (renderObjects === undefined) {
return;
}
renderObjects.light.color.set(colorHex);
}
private applyAmbientLightIntensityControl(
_target: SceneControlTargetRef,
intensity: number
) {
if (this.runtimeScene === null || this.currentWorld === null) {
return;
}
this.runtimeScene.world.ambientLight.intensity = intensity;
this.currentWorld.ambientLight.intensity = intensity;
this.applyDayNightLighting();
}
private applyAmbientLightColorControl(
_target: SceneControlTargetRef,
colorHex: string
) {
if (this.runtimeScene === null || this.currentWorld === null) {
return;
}
this.runtimeScene.world.ambientLight.colorHex = colorHex;
this.currentWorld.ambientLight.colorHex = colorHex;
this.applyDayNightLighting();
}
private applySunLightIntensityControl(
_target: SceneControlTargetRef,
intensity: number
) {
if (this.runtimeScene === null || this.currentWorld === null) {
return;
}
this.runtimeScene.world.sunLight.intensity = intensity;
this.currentWorld.sunLight.intensity = intensity;
this.applyDayNightLighting();
}
private applySunLightColorControl(
_target: SceneControlTargetRef,
colorHex: string
) {
if (this.runtimeScene === null || this.currentWorld === null) {
return;
}
this.runtimeScene.world.sunLight.colorHex = colorHex;
this.currentWorld.sunLight.colorHex = colorHex;
this.applyDayNightLighting();
}
private applyModelInstanceVisibilityControl(
target: ModelInstanceControlTargetRef,
visible: boolean
) {
if (this.runtimeScene !== null) {
const modelInstance =
this.runtimeScene.modelInstances.find(
(candidate) => candidate.instanceId === target.modelInstanceId
) ?? null;
if (modelInstance !== null) {
modelInstance.visible = visible;
}
}
const renderGroup = this.modelRenderObjects.get(target.modelInstanceId);
if (renderGroup !== undefined) {
renderGroup.visible = visible;
}
}
private applyModelAnimationPlaybackControl(
target: ModelInstanceControlTargetRef,
clipName: string | null,
loop: boolean | undefined
) {
if (this.runtimeScene !== null) {
const modelInstance =
this.runtimeScene.modelInstances.find(
(candidate) => candidate.instanceId === target.modelInstanceId
) ?? null;
if (modelInstance !== null) {
modelInstance.animationClipName = clipName ?? undefined;
modelInstance.animationAutoplay = clipName !== null;
modelInstance.animationLoop = clipName === null ? undefined : loop;
}
}
if (clipName === null) {
this.applyStopAnimationAction(target.modelInstanceId);
return;
}
this.applyPlayAnimationAction(target.modelInstanceId, clipName, loop);
}
private applySoundPlaybackControl(
target: SoundEmitterControlTargetRef,
playing: boolean,
link: InteractionLink | null = null
) {
if (this.runtimeScene !== null) {
const soundEmitter =
this.runtimeScene.entities.soundEmitters.find(
(candidate) => candidate.entityId === target.entityId
) ?? null;
if (soundEmitter !== null) {
soundEmitter.autoplay = playing;
}
}
if (!this.audioSystem.hasSoundEmitter(target.entityId)) {
return;
}
if (playing) {
this.audioSystem.playSound(target.entityId, link);
} else {
this.audioSystem.stopSound(target.entityId);
}
}
private applySoundVolumeControl(
target: SoundEmitterControlTargetRef,
volume: number
) {
if (this.runtimeScene !== null) {
const soundEmitter =
this.runtimeScene.entities.soundEmitters.find(
(candidate) => candidate.entityId === target.entityId
) ?? null;
if (soundEmitter !== null) {
soundEmitter.volume = volume;
}
}
this.audioSystem.setSoundEmitterVolume(target.entityId, volume);
}
private applyInteractionEnabledControl(
target: InteractionControlTargetRef,
enabled: boolean
@@ -1150,20 +1388,26 @@ export class RuntimeHost {
this.applyActorPresenceControl(effect.target.actorId, effect.active);
break;
case "playModelAnimation":
this.applyPlayAnimationAction(
effect.target.modelInstanceId,
this.applyModelAnimationPlaybackControl(
effect.target,
effect.clipName,
effect.loop
);
break;
case "stopModelAnimation":
this.applyStopAnimationAction(effect.target.modelInstanceId);
this.applyModelAnimationPlaybackControl(effect.target, null, undefined);
break;
case "setModelInstanceVisible":
this.applyModelInstanceVisibilityControl(effect.target, effect.visible);
break;
case "playSound":
this.audioSystem.playSound(effect.target.entityId, link);
this.applySoundPlaybackControl(effect.target, true, link);
break;
case "stopSound":
this.audioSystem.stopSound(effect.target.entityId);
this.applySoundPlaybackControl(effect.target, false);
break;
case "setSoundVolume":
this.applySoundVolumeControl(effect.target, effect.volume);
break;
case "setInteractionEnabled":
this.applyInteractionEnabledControl(effect.target, effect.enabled);
@@ -1174,6 +1418,21 @@ export class RuntimeHost {
case "setLightIntensity":
this.applyLightIntensityControl(effect.target, effect.intensity);
break;
case "setLightColor":
this.applyLightColorControl(effect.target, effect.colorHex);
break;
case "setAmbientLightIntensity":
this.applyAmbientLightIntensityControl(effect.target, effect.intensity);
break;
case "setAmbientLightColor":
this.applyAmbientLightColorControl(effect.target, effect.colorHex);
break;
case "setSunLightIntensity":
this.applySunLightIntensityControl(effect.target, effect.intensity);
break;
case "setSunLightColor":
this.applySunLightColorControl(effect.target, effect.colorHex);
break;
}
if (this.runtimeScene === null) {
@@ -1498,7 +1757,11 @@ export class RuntimeHost {
modelInstance.animationClipName
);
if (clip) {
mixer.clipAction(clip).play();
const action = mixer.clipAction(clip);
action.loop =
modelInstance.animationLoop === false ? LoopOnce : LoopRepeat;
action.clampWhenFinished = modelInstance.animationLoop === false;
action.reset().play();
}
}
}