auto-git:
[add] src/runtime-three/dialogue-attention-camera.ts
This commit is contained in:
257
src/runtime-three/dialogue-attention-camera.ts
Normal file
257
src/runtime-three/dialogue-attention-camera.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import type { Vec3 } from "../core/vector";
|
||||
|
||||
export type DialogueAttentionSideSign = -1 | 1;
|
||||
|
||||
export interface DialogueAttentionCameraSolution {
|
||||
position: Vec3;
|
||||
lookTarget: Vec3;
|
||||
sideSign: DialogueAttentionSideSign;
|
||||
subjectDistance: number;
|
||||
}
|
||||
|
||||
export interface ResolveDialogueAttentionCameraOptions {
|
||||
playerFocusPoint: Vec3;
|
||||
npcFocusPoint: Vec3;
|
||||
referenceCameraPosition: Vec3;
|
||||
referenceLookTarget: Vec3;
|
||||
previousSideSign?: DialogueAttentionSideSign | null;
|
||||
preferredConversationDistance?: number;
|
||||
preferredConversationHeight?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_DIALOGUE_ATTENTION_DISTANCE = 3.8;
|
||||
const DEFAULT_DIALOGUE_ATTENTION_HEIGHT = 0.48;
|
||||
const MIN_DIALOGUE_ATTENTION_DISTANCE = 3.2;
|
||||
const MAX_DIALOGUE_ATTENTION_DISTANCE = 7.5;
|
||||
const MIN_DIALOGUE_ATTENTION_LATERAL_OFFSET = 1.05;
|
||||
const MAX_DIALOGUE_ATTENTION_LATERAL_OFFSET = 2.3;
|
||||
const MIN_DIALOGUE_ATTENTION_LOOK_AHEAD = 0.08;
|
||||
const MAX_DIALOGUE_ATTENTION_LOOK_AHEAD = 0.34;
|
||||
const MIN_DIALOGUE_ATTENTION_COMPOSITION_OFFSET = 0.04;
|
||||
const MAX_DIALOGUE_ATTENTION_COMPOSITION_OFFSET = 0.18;
|
||||
const CAMERA_SIDE_EPSILON = 1e-4;
|
||||
|
||||
function addVec3(left: Vec3, right: Vec3): Vec3 {
|
||||
return {
|
||||
x: left.x + right.x,
|
||||
y: left.y + right.y,
|
||||
z: left.z + right.z
|
||||
};
|
||||
}
|
||||
|
||||
function subtractVec3(left: Vec3, right: Vec3): Vec3 {
|
||||
return {
|
||||
x: left.x - right.x,
|
||||
y: left.y - right.y,
|
||||
z: left.z - right.z
|
||||
};
|
||||
}
|
||||
|
||||
function scaleVec3(vector: Vec3, scalar: number): Vec3 {
|
||||
return {
|
||||
x: vector.x * scalar,
|
||||
y: vector.y * scalar,
|
||||
z: vector.z * scalar
|
||||
};
|
||||
}
|
||||
|
||||
function dotVec3(left: Vec3, right: Vec3): number {
|
||||
return left.x * right.x + left.y * right.y + left.z * right.z;
|
||||
}
|
||||
|
||||
function lengthVec3(vector: Vec3): number {
|
||||
return Math.sqrt(dotVec3(vector, vector));
|
||||
}
|
||||
|
||||
function normalizeVec3(vector: Vec3): Vec3 | null {
|
||||
const length = lengthVec3(vector);
|
||||
|
||||
if (length <= CAMERA_SIDE_EPSILON) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return scaleVec3(vector, 1 / length);
|
||||
}
|
||||
|
||||
function clampScalar(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function lerpVec3(start: Vec3, end: Vec3, t: number): Vec3 {
|
||||
return {
|
||||
x: start.x + (end.x - start.x) * t,
|
||||
y: start.y + (end.y - start.y) * t,
|
||||
z: start.z + (end.z - start.z) * t
|
||||
};
|
||||
}
|
||||
|
||||
function toHorizontal(vector: Vec3): Vec3 {
|
||||
return {
|
||||
x: vector.x,
|
||||
y: 0,
|
||||
z: vector.z
|
||||
};
|
||||
}
|
||||
|
||||
function rotateHorizontalRight(vector: Vec3): Vec3 {
|
||||
return {
|
||||
x: vector.z,
|
||||
y: 0,
|
||||
z: -vector.x
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDialogueAttentionSideSign(
|
||||
options: ResolveDialogueAttentionCameraOptions,
|
||||
conversationMidpoint: Vec3,
|
||||
pairRight: Vec3
|
||||
): DialogueAttentionSideSign {
|
||||
if (options.previousSideSign !== undefined && options.previousSideSign !== null) {
|
||||
return options.previousSideSign;
|
||||
}
|
||||
|
||||
const referenceOffset = subtractVec3(
|
||||
options.referenceCameraPosition,
|
||||
conversationMidpoint
|
||||
);
|
||||
const sideMetric = dotVec3(toHorizontal(referenceOffset), pairRight);
|
||||
|
||||
if (Math.abs(sideMetric) > CAMERA_SIDE_EPSILON) {
|
||||
return sideMetric >= 0 ? 1 : -1;
|
||||
}
|
||||
|
||||
const referenceForward =
|
||||
normalizeVec3(
|
||||
toHorizontal(
|
||||
subtractVec3(
|
||||
options.referenceLookTarget,
|
||||
options.referenceCameraPosition
|
||||
)
|
||||
)
|
||||
) ??
|
||||
normalizeVec3(toHorizontal(subtractVec3(options.npcFocusPoint, options.playerFocusPoint))) ??
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 1
|
||||
};
|
||||
const referenceRight = rotateHorizontalRight(referenceForward);
|
||||
const facingMetric = dotVec3(referenceRight, pairRight);
|
||||
|
||||
if (Math.abs(facingMetric) > CAMERA_SIDE_EPSILON) {
|
||||
return facingMetric >= 0 ? 1 : -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
export function resolveDialogueAttentionCameraSolution(
|
||||
options: ResolveDialogueAttentionCameraOptions
|
||||
): DialogueAttentionCameraSolution {
|
||||
const pairVector = subtractVec3(options.npcFocusPoint, options.playerFocusPoint);
|
||||
const subjectDistance = lengthVec3(pairVector);
|
||||
const pairDirection =
|
||||
normalizeVec3(toHorizontal(pairVector)) ??
|
||||
normalizeVec3(
|
||||
toHorizontal(
|
||||
subtractVec3(
|
||||
options.referenceLookTarget,
|
||||
options.referenceCameraPosition
|
||||
)
|
||||
)
|
||||
) ?? {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 1
|
||||
};
|
||||
const pairRight = rotateHorizontalRight(pairDirection);
|
||||
const conversationMidpoint = lerpVec3(
|
||||
options.playerFocusPoint,
|
||||
options.npcFocusPoint,
|
||||
0.5
|
||||
);
|
||||
const sideSign = resolveDialogueAttentionSideSign(
|
||||
options,
|
||||
conversationMidpoint,
|
||||
pairRight
|
||||
);
|
||||
const preferredConversationDistance =
|
||||
options.preferredConversationDistance ??
|
||||
DEFAULT_DIALOGUE_ATTENTION_DISTANCE;
|
||||
const preferredConversationHeight =
|
||||
options.preferredConversationHeight ?? DEFAULT_DIALOGUE_ATTENTION_HEIGHT;
|
||||
const desiredDistance = clampScalar(
|
||||
preferredConversationDistance + subjectDistance * 0.42,
|
||||
MIN_DIALOGUE_ATTENTION_DISTANCE,
|
||||
MAX_DIALOGUE_ATTENTION_DISTANCE
|
||||
);
|
||||
const lateralOffset = clampScalar(
|
||||
MIN_DIALOGUE_ATTENTION_LATERAL_OFFSET + subjectDistance * 0.18,
|
||||
MIN_DIALOGUE_ATTENTION_LATERAL_OFFSET,
|
||||
MAX_DIALOGUE_ATTENTION_LATERAL_OFFSET
|
||||
);
|
||||
const verticalOffset =
|
||||
preferredConversationHeight + Math.min(0.42, subjectDistance * 0.05);
|
||||
const backOffset = Math.max(
|
||||
desiredDistance * 0.7,
|
||||
Math.sqrt(
|
||||
Math.max(
|
||||
desiredDistance * desiredDistance -
|
||||
lateralOffset * lateralOffset -
|
||||
verticalOffset * verticalOffset,
|
||||
desiredDistance * desiredDistance * 0.45
|
||||
)
|
||||
)
|
||||
);
|
||||
const cameraAnchor = lerpVec3(
|
||||
options.playerFocusPoint,
|
||||
options.npcFocusPoint,
|
||||
0.4
|
||||
);
|
||||
const lookTarget = addVec3(
|
||||
addVec3(
|
||||
lerpVec3(options.playerFocusPoint, options.npcFocusPoint, 0.56),
|
||||
scaleVec3(
|
||||
pairDirection,
|
||||
clampScalar(
|
||||
subjectDistance * 0.1,
|
||||
MIN_DIALOGUE_ATTENTION_LOOK_AHEAD,
|
||||
MAX_DIALOGUE_ATTENTION_LOOK_AHEAD
|
||||
)
|
||||
)
|
||||
),
|
||||
addVec3(
|
||||
scaleVec3(
|
||||
pairRight,
|
||||
-sideSign *
|
||||
clampScalar(
|
||||
subjectDistance * 0.05,
|
||||
MIN_DIALOGUE_ATTENTION_COMPOSITION_OFFSET,
|
||||
MAX_DIALOGUE_ATTENTION_COMPOSITION_OFFSET
|
||||
)
|
||||
),
|
||||
{
|
||||
x: 0,
|
||||
y: 0.05,
|
||||
z: 0
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
position: addVec3(
|
||||
addVec3(
|
||||
cameraAnchor,
|
||||
scaleVec3(pairDirection, -backOffset)
|
||||
),
|
||||
{
|
||||
x: pairRight.x * lateralOffset * sideSign,
|
||||
y: verticalOffset,
|
||||
z: pairRight.z * lateralOffset * sideSign
|
||||
}
|
||||
),
|
||||
lookTarget,
|
||||
sideSign,
|
||||
subjectDistance
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user