Add sunrise Threads automation and improve capture and character interactions
This commit is contained in:
+34
-7
@@ -44,6 +44,11 @@ export function createSheep(model: Group, gear?: { umbrella: Group; raincoat: Gr
|
||||
let foldedHood: Object3D | undefined;
|
||||
if (gear) {
|
||||
model.add(gear.umbrella, gear.raincoat);
|
||||
// Preserve the authored overhead position in the held pose, then let the hand carry it.
|
||||
joints.ArmRight.quaternion.setFromUnitVectors(new Vector3(0, -1, 0),
|
||||
new Vector3(...RAIN_GEAR.umbrellaGripDirection).normalize()).multiply(rest.ArmRight);
|
||||
joints.ArmRight.attach(gear.umbrella);
|
||||
joints.ArmRight.quaternion.copy(rest.ArmRight);
|
||||
hood = gear.raincoat.getObjectByName('CoatHood');
|
||||
foldedHood = gear.raincoat.getObjectByName('CoatFoldedHood');
|
||||
if (!hood || !foldedHood) throw new Error('우비 후드 모델 누락');
|
||||
@@ -112,7 +117,10 @@ export function createSheep(model: Group, gear?: { umbrella: Group; raincoat: Gr
|
||||
const rigHood = copy(hood), rigFoldedHood = copy(foldedHood), rigUmbrella = copy(gear?.umbrella);
|
||||
const rigFlashlight = copy(gear?.flashlight);
|
||||
let flashlightOn = false;
|
||||
let flashlightPose = 0;
|
||||
let previousPoseTime: number | undefined;
|
||||
let umbrellaOpen = false;
|
||||
let umbrellaPose = 0;
|
||||
const rotation = new Quaternion();
|
||||
const down = new Vector3(0, -1, 0);
|
||||
const aim = new Vector3();
|
||||
@@ -128,9 +136,9 @@ export function createSheep(model: Group, gear?: { umbrella: Group; raincoat: Gr
|
||||
clone() { return bindRig(group.clone(true), false); },
|
||||
setEquipment(umbrella: boolean, raincoat: boolean, hoodUp = false, flashlight = false) {
|
||||
flashlightOn = flashlight;
|
||||
if (rigFlashlight) rigFlashlight.visible = flashlight;
|
||||
if (rigFlashlight) rigFlashlight.visible = flashlight || flashlightPose > 0;
|
||||
umbrellaOpen = umbrella;
|
||||
if (rigUmbrella) rigUmbrella.visible = umbrella;
|
||||
if (rigUmbrella) rigUmbrella.visible = umbrella || umbrellaPose > 0;
|
||||
rigCoatParts.forEach(part => { part.visible = raincoat; });
|
||||
if (rigHood) rigHood.visible = raincoat && hoodUp;
|
||||
if (rigFoldedHood) rigFoldedHood.visible = raincoat && !hoodUp;
|
||||
@@ -144,13 +152,24 @@ export function createSheep(model: Group, gear?: { umbrella: Group; raincoat: Gr
|
||||
for (const [material, color] of baseColors) material.color.copy(color).multiplyScalar(brightness);
|
||||
},
|
||||
pose(emote: Emote, elapsed: number, moving: boolean, time: number) {
|
||||
const changed = moving || emote !== 'idle' || previousMoving !== moving || previousEmote !== emote;
|
||||
const delta = Math.max(0, time - (previousPoseTime ?? time));
|
||||
const step = delta / FLASHLIGHT.poseTransitionSeconds;
|
||||
previousPoseTime = time;
|
||||
const previousFlashlightPose = flashlightPose;
|
||||
flashlightPose = flashlightOn ? Math.min(1, flashlightPose + step) : Math.max(0, flashlightPose - step);
|
||||
const previousUmbrellaPose = umbrellaPose;
|
||||
const umbrellaStep = delta / RAIN_GEAR.umbrellaTransitionSeconds;
|
||||
umbrellaPose = umbrellaOpen ? Math.min(1, umbrellaPose + umbrellaStep) : Math.max(0, umbrellaPose - umbrellaStep);
|
||||
const carryingUmbrella = umbrellaOpen || umbrellaPose > 0;
|
||||
const changed = moving || emote !== 'idle' || previousMoving !== moving || previousEmote !== emote
|
||||
|| previousFlashlightPose !== flashlightPose || flashlightPose !== Number(flashlightOn)
|
||||
|| previousUmbrellaPose !== umbrellaPose || umbrellaPose !== Number(umbrellaOpen);
|
||||
previousMoving = moving;
|
||||
previousEmote = emote;
|
||||
for (const [name, joint] of Object.entries(joints)) joint.quaternion.copy(rest[name]);
|
||||
if (emote === 'wave') {
|
||||
pointArm(umbrellaOpen ? 'ArmLeft' : 'ArmRight',
|
||||
aim.set(umbrellaOpen ? -1 : 1, .35 + Math.sin(elapsed * 12) * .3, -.15));
|
||||
pointArm(carryingUmbrella ? 'ArmLeft' : 'ArmRight',
|
||||
aim.set(carryingUmbrella ? -1 : 1, .35 + Math.sin(elapsed * 12) * .3, -.15));
|
||||
} else if (emote === 'pray') {
|
||||
pointArm('ArmLeft', aim.set(.68, -.22, .64));
|
||||
pointArm('ArmRight', aim.set(-.68, -.22, .64));
|
||||
@@ -163,8 +182,16 @@ export function createSheep(model: Group, gear?: { umbrella: Group; raincoat: Gr
|
||||
joints.ArmRight.rotateX(stride * .6);
|
||||
}
|
||||
// Keep the grip stable while the free arm and legs perform their own animations.
|
||||
if (umbrellaOpen) pointArm('ArmRight', aim.set(.05, -.45, .7));
|
||||
if (flashlightOn) pointArm('ArmLeft', aim.set(-.08, -.22, 1));
|
||||
if (umbrellaPose > 0) {
|
||||
rotation.setFromUnitVectors(down, aim.set(...RAIN_GEAR.umbrellaGripDirection).normalize()).multiply(rest.ArmRight);
|
||||
joints.ArmRight.quaternion.slerp(rotation, MathUtils.smoothstep(umbrellaPose, 0, 1));
|
||||
}
|
||||
if (rigUmbrella) rigUmbrella.visible = carryingUmbrella;
|
||||
if (flashlightPose > 0) {
|
||||
rotation.setFromUnitVectors(down, aim.set(-.08, -.22, 1).normalize()).multiply(rest.ArmLeft);
|
||||
joints.ArmLeft.quaternion.slerp(rotation, MathUtils.smoothstep(flashlightPose, 0, 1));
|
||||
}
|
||||
if (rigFlashlight) rigFlashlight.visible = flashlightOn || flashlightPose > 0;
|
||||
return changed;
|
||||
},
|
||||
dispose() {
|
||||
|
||||
+40
-1
@@ -414,6 +414,30 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
});
|
||||
const world = {
|
||||
buildingCounts: buildings?.counts,
|
||||
captureFrame(view?: PerspectiveCamera) {
|
||||
const activeCamera = view ?? (overview ? overviewCamera : sheep ? followCamera : camera);
|
||||
if (composer && !profile?.disablePost) {
|
||||
renderPass.camera = activeCamera;
|
||||
composer.render(0);
|
||||
} else renderer.render(scene, activeCamera);
|
||||
// Copy synchronously before WebGL clears the drawing buffer.
|
||||
const frame = document.createElement('canvas');
|
||||
frame.width = canvas.width;
|
||||
frame.height = canvas.height;
|
||||
frame.getContext('2d')!.drawImage(canvas, 0, 0);
|
||||
return frame;
|
||||
},
|
||||
captureAngle(azimuth: number, pitch: number) {
|
||||
// Orbit only the capture camera; keep the character facing the sunrise.
|
||||
const view = camera.clone();
|
||||
view.lookAt(view.position.clone().add(new Vector3(...directionFromAngles(azimuth, pitch))));
|
||||
if (sheep) {
|
||||
const orbit = view.clone();
|
||||
updateThirdPersonCamera(orbit, view, (x, z) => harbour?.heightAt(x, z) ?? terrain.heightAt(x, z));
|
||||
return world.captureFrame(orbit);
|
||||
}
|
||||
return world.captureFrame(view);
|
||||
},
|
||||
readPlayer(): PlayerUpdate {
|
||||
const yaw = sheep?.group.rotation.y ?? camera.rotation.y;
|
||||
return { x: camera.position.x, z: camera.position.z, yaw: Math.atan2(Math.sin(yaw), Math.cos(yaw)),
|
||||
@@ -440,7 +464,7 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
clearPlayers() { remotePlayers?.clear(); rendering.invalidate(); },
|
||||
setWeather(weather: WeatherSample | null) {
|
||||
weatherTarget = weatherAppearance(weather);
|
||||
rainGear.updateWeather(weatherTarget.rain, weatherTarget.snowfall);
|
||||
rainGear.updateWeather(weather?.rain ?? null, weather?.snowfall ?? null);
|
||||
sheep?.setEquipment(rainGear.umbrella, rainGear.raincoat, rainGear.hood, flashlightOn);
|
||||
refreshGear();
|
||||
// Do not blend an old precipitation type into the newly selected weather.
|
||||
@@ -475,6 +499,21 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited<Retu
|
||||
if (target) camera.lookAt(target.position.clone().add(new Vector3(0, id.includes('mast') ? 14 : id === 'weather-radar' ? 10 : 1, 0)));
|
||||
},
|
||||
lookEast() { restoreView(); camera.lookAt(camera.position.clone().add(new Vector3(1, 0, 0))); },
|
||||
lookAtSunrise(azimuth: number) {
|
||||
restoreView();
|
||||
forward.set(...directionFromAngles(azimuth, 0));
|
||||
camera.lookAt(camera.position.clone().add(forward));
|
||||
if (sheep) {
|
||||
sheep.group.rotation.y = Math.atan2(forward.x, forward.z);
|
||||
updateThirdPersonCamera(followCamera, camera, (x, z) => harbour?.heightAt(x, z) ?? terrain.heightAt(x, z));
|
||||
}
|
||||
},
|
||||
lookAtSun() {
|
||||
if (!solarState) return;
|
||||
restoreView();
|
||||
camera.lookAt(camera.position.clone().add(new Vector3(...directionFromAngles(solarState.azimuth, 0))));
|
||||
if (sheep) updateThirdPersonCamera(followCamera, camera, (x, z) => harbour?.heightAt(x, z) ?? terrain.heightAt(x, z));
|
||||
},
|
||||
setEyeHeight(meters: number) {
|
||||
eyeHeight = meters;
|
||||
placeObserver();
|
||||
|
||||
Reference in New Issue
Block a user