Improve runtime visibility checks using multi-sample target sampling

This commit is contained in:
2026-04-25 18:21:02 +02:00
parent 37b50d7e8a
commit 85e0bc65c1

View File

@@ -5561,17 +5561,20 @@ export class RuntimeHost {
switch (npc.collider.mode) {
case "capsule":
return (
Math.max(npc.collider.radius, npc.collider.height * 0.5) +
Math.max(npc.collider.radius, TARGETING_VISIBILITY_TARGET_CLEARANCE) +
TARGETING_VISIBILITY_TARGET_CLEARANCE_PADDING
);
case "box":
return (
Math.hypot(
npc.collider.size.x,
npc.collider.size.y,
npc.collider.size.z
) *
0.5 +
clampScalar(
Math.max(
npc.collider.size.x,
npc.collider.size.y,
npc.collider.size.z
) * 0.25,
0.35,
0.75
) +
TARGETING_VISIBILITY_TARGET_CLEARANCE_PADDING
);
case "none":
@@ -5597,6 +5600,107 @@ export class RuntimeHost {
);
}
private resolveRuntimeTargetVisibilitySamples(target: {
kind?: string;
entityId?: string;
center: { x: number; y: number; z: number };
range: number;
}): Array<{
point: { x: number; y: number; z: number };
targetClearance: number;
}> {
const targetClearance = this.resolveRuntimeTargetVisibilityClearance(target);
if (this.runtimeScene !== null && target.kind === "npc") {
const npc =
this.runtimeScene.entities.npcs.find(
(candidate) => candidate.entityId === target.entityId
) ?? null;
if (npc !== null) {
switch (npc.collider.mode) {
case "capsule": {
const sampleClearance =
Math.max(
npc.collider.radius,
TARGETING_VISIBILITY_TARGET_CLEARANCE
) + TARGETING_VISIBILITY_TARGET_CLEARANCE_PADDING;
const yAt = (factor: number) =>
npc.position.y + npc.collider.height * factor;
return [
{
point: { x: npc.position.x, y: yAt(0.82), z: npc.position.z },
targetClearance: sampleClearance
},
{
point: { x: npc.position.x, y: yAt(0.62), z: npc.position.z },
targetClearance: sampleClearance
},
{ point: target.center, targetClearance: sampleClearance },
{
point: { x: npc.position.x, y: yAt(0.38), z: npc.position.z },
targetClearance: sampleClearance
}
];
}
case "box": {
const sampleClearance =
clampScalar(
Math.max(
npc.collider.size.x,
npc.collider.size.y,
npc.collider.size.z
) * 0.25,
0.35,
0.75
) + TARGETING_VISIBILITY_TARGET_CLEARANCE_PADDING;
const yAt = (factor: number) =>
npc.position.y + npc.collider.size.y * factor;
return [
{
point: { x: npc.position.x, y: yAt(0.82), z: npc.position.z },
targetClearance: sampleClearance
},
{
point: { x: npc.position.x, y: yAt(0.62), z: npc.position.z },
targetClearance: sampleClearance
},
{ point: target.center, targetClearance: sampleClearance },
{
point: { x: npc.position.x, y: yAt(0.38), z: npc.position.z },
targetClearance: sampleClearance
}
];
}
case "none":
return [
{
point: {
x: npc.position.x,
y: npc.position.y + 1.45,
z: npc.position.z
},
targetClearance
},
{ point: target.center, targetClearance },
{
point: {
x: npc.position.x,
y: npc.position.y + 0.65,
z: npc.position.z
},
targetClearance
}
];
}
}
}
return [{ point: target.center, targetClearance }];
}
private isRuntimeTargetVisibleFrom(
origin: { x: number; y: number; z: number },
target: {
@@ -5610,9 +5714,13 @@ export class RuntimeHost {
return true;
}
return this.collisionWorld.isLineSegmentClear(origin, target.center, {
targetClearance: this.resolveRuntimeTargetVisibilityClearance(target)
});
const collisionWorld = this.collisionWorld;
return this.resolveRuntimeTargetVisibilitySamples(target).some((sample) =>
collisionWorld.isLineSegmentClear(origin, sample.point, {
targetClearance: sample.targetClearance
})
);
}
private isRuntimeTargetCameraVisible(target: {