Add sunrise Threads automation and improve capture and character interactions

This commit is contained in:
2026-11-02 03:28:58 +09:00
parent ff86bdb5b5
commit 850ac84ef7
34 changed files with 900 additions and 34 deletions
+34 -7
View File
@@ -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
View File
@@ -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();