Skip to content

Commit 12e65fd

Browse files
authored
Upgrade viewer export format to v2 with per-pose FOV and loop mode (#831)
1 parent 6f79e76 commit 12e65fd

23 files changed

Lines changed: 365 additions & 304 deletions

src/camera-pose-gizmos.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,9 @@ class CameraPoseGizmos extends Element {
108108
return;
109109
}
110110

111-
const boundSize = this.scene.bound.halfExtents.length();
112-
const iconScale = boundSize > 0 ? boundSize * 0.04 : 0.2;
113-
114-
const depth = iconScale * 2;
115-
const halfW = iconScale * 1.2;
116-
const halfH = iconScale * 0.9;
111+
const depth = 0.08;
112+
const halfW = 0.06;
113+
const halfH = 0.04;
117114

118115
const numVerts = poses.length * VERTS_PER_CAMERA;
119116
const positions: number[] = [];

src/camera-poses.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ type Pose = {
88
name: string,
99
frame: number,
1010
position: Vec3,
11-
target: Vec3
11+
target: Vec3,
12+
fov?: number
1213
};
1314

1415
/**
@@ -60,12 +61,12 @@ class CameraAnimTrack implements AnimTrack {
6061

6162
const existingIndex = this.poses.findIndex(p => p.frame === frame);
6263

63-
// camera.getPose returns plain {x,y,z} objects, convert to Vec3
6464
const newPose: Pose = {
6565
name: `camera_${this.poses.length}`,
6666
frame,
6767
position: new Vec3(pose.position.x, pose.position.y, pose.position.z),
68-
target: new Vec3(pose.target.x, pose.target.y, pose.target.z)
68+
target: new Vec3(pose.target.x, pose.target.y, pose.target.z),
69+
fov: pose.fov
6970
};
7071

7172
if (existingIndex === -1) {
@@ -121,12 +122,12 @@ class CameraAnimTrack implements AnimTrack {
121122
this.poses.splice(toIndex, 1);
122123
}
123124

124-
// Clone the pose data to the new frame
125125
this.poses.push({
126126
name: `camera_${this.poses.length}`,
127127
frame: toFrame,
128128
position: source.position.clone(),
129-
target: source.target.clone()
129+
target: source.target.clone(),
130+
fov: source.fov
130131
});
131132

132133
this.rebuildSpline();
@@ -149,7 +150,8 @@ class CameraAnimTrack implements AnimTrack {
149150
name: p.name,
150151
frame: p.frame,
151152
position: p.position.clone(),
152-
target: p.target.clone()
153+
target: p.target.clone(),
154+
fov: p.fov
153155
}));
154156
}
155157

@@ -158,7 +160,8 @@ class CameraAnimTrack implements AnimTrack {
158160
name: p.name,
159161
frame: p.frame,
160162
position: p.position.clone(),
161-
target: p.target.clone()
163+
target: p.target.clone(),
164+
fov: p.fov
162165
}));
163166
this.rebuildSpline();
164167
this.events.fire('track.keysLoaded');
@@ -172,7 +175,8 @@ class CameraAnimTrack implements AnimTrack {
172175
return;
173176
}
174177

175-
// If a pose already exists at this frame, update it
178+
pose.fov ??= this.events.invoke('camera.fov') ?? 60;
179+
176180
const idx = this.poses.findIndex(p => p.frame === pose.frame);
177181
if (idx !== -1) {
178182
this.poses[idx] = pose;
@@ -212,24 +216,25 @@ class CameraAnimTrack implements AnimTrack {
212216
.filter(a => a.frame < duration)
213217
.sort((a, b) => a.frame - b.frame);
214218

215-
// construct the spline points to be interpolated
216219
const times = orderedPoses.map(p => p.frame);
217220
const points: number[] = [];
218221
for (let i = 0; i < orderedPoses.length; ++i) {
219222
const p = orderedPoses[i];
220223
points.push(p.position.x, p.position.y, p.position.z);
221224
points.push(p.target.x, p.target.y, p.target.z);
225+
points.push(p.fov);
222226
}
223227

224228
if (orderedPoses.length > 1) {
225229
const spline = CubicSpline.fromPointsLooping(duration, times, points, smoothness);
226230
const result: number[] = [];
227-
const pose = { position: new Vec3(), target: new Vec3() };
231+
const pose = { position: new Vec3(), target: new Vec3(), fov: 0 };
228232

229233
this.onTimelineChange = (frame: number) => {
230234
spline.evaluate(frame, result);
231235
pose.position.set(result[0], result[1], result[2]);
232236
pose.target.set(result[3], result[4], result[5]);
237+
pose.fov = result[6];
233238
this.events.fire('camera.setPose', pose, 0);
234239
};
235240
} else {
@@ -281,25 +286,29 @@ const registerCameraPosesEvents = (events: Events) => {
281286
name: pose.name,
282287
frame: pose.frame,
283288
position: pack3(pose.position),
284-
target: pack3(pose.target)
289+
target: pack3(pose.target),
290+
fov: pose.fov
285291
};
286292
})
287293
}];
288294
});
289295

290-
events.function('docDeserialize.poseSets', (poseSets: any[]) => {
296+
events.function('docDeserialize.poseSets', (poseSets: any[], documentCameraFov?: number) => {
291297
if (!poseSets || poseSets.length === 0) {
292298
return;
293299
}
294300

295301
const fps = events.invoke('timeline.frameRate');
296302

303+
const defaultFov = documentCameraFov ?? events.invoke('camera.fov') ?? 60;
304+
297305
const loadedPoses: Pose[] = poseSets[0].poses.map((docPose: any, index: number) => {
298306
return {
299307
name: docPose.name,
300308
frame: docPose.frame ?? (index * fps),
301309
position: new Vec3(docPose.position),
302-
target: new Vec3(docPose.target)
310+
target: new Vec3(docPose.target),
311+
fov: docPose.fov ?? defaultFov
303312
};
304313
});
305314

src/camera.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ class Camera extends Element {
8181

8282
controlMode: 'orbit' | 'fly' = 'orbit';
8383

84+
// during fly-mode look, stores the camera position that must stay fixed
85+
// while the azim/elev tween smoothly converges
86+
lookCameraPos: Vec3 | null = null;
87+
8488
picker: Picker;
8589

8690
mainCamera: Entity;
@@ -206,9 +210,29 @@ class Camera extends Element {
206210
}
207211

208212
setFocalPoint(point: Vec3, dampingFactorFactor: number = 1) {
213+
this.lookCameraPos = null;
209214
this.focalPointTween.goto(point, dampingFactorFactor * this.scene.config.controls.dampingFactor);
210215
}
211216

217+
// Fly mode: rotate camera around itself, keeping the camera position fixed
218+
look(dx: number, dy: number) {
219+
const sensitivity = this.scene.config.controls.orbitSensitivity;
220+
const d = this.distance * this.sceneRadius / this.fovFactor;
221+
222+
Camera.calcForwardVec(forwardVec, this.azim, this.elevation);
223+
const cameraPos = this.focalPoint.add(forwardVec.clone().mulScalar(d));
224+
225+
const azim = this.azim - dx * sensitivity;
226+
const elev = this.elevation - dy * sensitivity;
227+
228+
Camera.calcForwardVec(forwardVec, azim, elev);
229+
const focalPoint = cameraPos.clone().sub(forwardVec.clone().mulScalar(d));
230+
231+
this.setAzimElev(azim, elev);
232+
this.focalPointTween.goto(focalPoint, this.scene.config.controls.dampingFactor);
233+
this.lookCameraPos = cameraPos;
234+
}
235+
212236
setAzimElev(azim: number, elev: number, dampingFactorFactor: number = 1) {
213237
// clamp
214238
azim = mod(azim, 360);
@@ -229,6 +253,8 @@ class Camera extends Element {
229253
}
230254

231255
setDistance(distance: number, dampingFactorFactor: number = 1) {
256+
this.lookCameraPos = null;
257+
232258
const controls = this.scene.config.controls;
233259

234260
// clamp
@@ -550,9 +576,17 @@ class Camera extends Element {
550576
const distance = this.distanceTween.value;
551577

552578
Camera.calcForwardVec(forwardVec, azimElev.azim, azimElev.elev);
553-
cameraPosition.copy(forwardVec);
554-
cameraPosition.mulScalar(distance.distance * this.sceneRadius / this.fovFactor);
555-
cameraPosition.add(this.focalPointTween.value);
579+
580+
if (this.lookCameraPos) {
581+
cameraPosition.copy(this.lookCameraPos);
582+
if (this.azimElevTween.timer >= this.azimElevTween.transitionTime) {
583+
this.lookCameraPos = null;
584+
}
585+
} else {
586+
cameraPosition.copy(forwardVec);
587+
cameraPosition.mulScalar(distance.distance * this.sceneRadius / this.fovFactor);
588+
cameraPosition.add(this.focalPointTween.value);
589+
}
556590

557591
this.mainCamera.setLocalPosition(cameraPosition);
558592
this.mainCamera.setLocalEulerAngles(azimElev.elev, azimElev.azim, 0);

src/controllers.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ const fromWorldPoint = new Vec3();
66
const toWorldPoint = new Vec3();
77
const worldDiff = new Vec3();
88
const moveVec = new Vec3();
9-
const forwardVec = new Vec3();
109

1110
// calculate the distance between two 2d points
1211
const dist = (x0: number, y0: number, x1: number, y1: number) => Math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2);
@@ -24,29 +23,8 @@ class PointerController {
2423
camera.setAzimElev(azim, elev);
2524
};
2625

27-
// Fly mode: rotate camera around itself (keep camera position fixed)
2826
const look = (dx: number, dy: number) => {
29-
// Use TARGET values to calculate target camera position (not current interpolated)
30-
const distance = camera.distance * camera.sceneRadius / camera.fovFactor;
31-
32-
// Calculate target camera position from target focal point and angles
33-
Camera.calcForwardVec(forwardVec, camera.azim, camera.elevation);
34-
const targetCameraPos = camera.focalPoint.add(forwardVec.clone().mulScalar(distance));
35-
36-
// Calculate new azim/elev
37-
const azim = camera.azim - dx * camera.scene.config.controls.orbitSensitivity;
38-
const elev = camera.elevation - dy * camera.scene.config.controls.orbitSensitivity;
39-
40-
// Calculate the new forward vector based on new angles
41-
Camera.calcForwardVec(forwardVec, azim, elev);
42-
43-
// Calculate new focal point to keep camera at target position
44-
// Camera position = focalPoint + forwardVec * distance
45-
// So: focalPoint = cameraPosition - forwardVec * distance
46-
const newFocalPoint = targetCameraPos.clone().sub(forwardVec.clone().mulScalar(distance));
47-
48-
camera.setAzimElev(azim, elev);
49-
camera.setFocalPoint(newFocalPoint);
27+
camera.look(dx, dy);
5028
};
5129

5230
const pan = (x: number, y: number, dx: number, dy: number) => {

src/doc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ const registerDocEvents = (scene: Scene, events: Events) => {
121121
}
122122

123123
events.invoke('docDeserialize.timeline', document.timeline);
124-
events.invoke('docDeserialize.poseSets', document.poseSets);
124+
events.invoke('docDeserialize.poseSets', document.poseSets, document.camera?.fov);
125125
events.invoke('docDeserialize.view', document.view);
126126
scene.camera.docDeserialize(document.camera);
127127

src/editor.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,12 +708,17 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S
708708
const focalPoint = camera.focalPoint;
709709
return {
710710
position: { x: position.x, y: position.y, z: position.z },
711-
target: { x: focalPoint.x, y: focalPoint.y, z: focalPoint.z }
711+
target: { x: focalPoint.x, y: focalPoint.y, z: focalPoint.z },
712+
fov: camera.fov
712713
};
713714
});
714715

715-
events.on('camera.setPose', (pose: { position: Vec3, target: Vec3 }, speed = 1) => {
716+
events.on('camera.setPose', (pose: { position: Vec3, target: Vec3, fov?: number }, speed = 1) => {
716717
scene.camera.setPose(pose.position, pose.target, speed);
718+
if (pose.fov !== undefined) {
719+
scene.camera.fov = pose.fov;
720+
events.fire('camera.fov', pose.fov);
721+
}
717722
});
718723

719724
// hack: fire events to initialize UI

src/file-handler.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,20 @@ const loadCameraPoses = async (file: ImportFile, events: Events) => {
174174
// Use fixed offset along Z-axis direction instead of variable dot product
175175
vec.copy(z).mulScalar(10).add(p);
176176

177+
// compute max FOV from intrinsics (vertical or horizontal, whichever is larger)
178+
let fov = 60;
179+
if (pose.fx && pose.fy && pose.width && pose.height) {
180+
const fovX = 2 * Math.atan(pose.width / (2 * pose.fx)) * (180 / Math.PI);
181+
const fovY = 2 * Math.atan(pose.height / (2 * pose.fy)) * (180 / Math.PI);
182+
fov = Math.max(fovX, fovY);
183+
}
184+
177185
events.fire('camera.addPose', {
178186
name: pose.img_name ?? `${file.filename}_${i}`,
179187
frame: i,
180188
position: new Vec3(-p.x, -p.y, p.z),
181-
target: new Vec3(-vec.x, -vec.y, vec.z)
189+
target: new Vec3(-vec.x, -vec.y, vec.z),
190+
fov
182191
});
183192
}
184193
});

src/publish.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ type PublishSettings = {
5959
listed: boolean;
6060
serializeSettings: SerializeSettings;
6161
experienceSettings: ExperienceSettings;
62-
overwriteId?: string; // for republishing an existing scene
62+
overwriteId?: string;
6363
};
6464

6565
const origin = location.origin;
@@ -312,7 +312,7 @@ const registerPublishEvents = (events: Events) => {
312312
type: 'info',
313313
header: localize('popup.publish.succeeded'),
314314
message: localize('popup.publish.message'),
315-
link: response.url
315+
link: `${origin}/scene/${response.hash}/edit`
316316
});
317317
}
318318
} catch (error) {

src/splat-overlay.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ class SplatOverlay extends Element {
112112
material.update();
113113

114114
// subscribe to sorter updates for dynamic count
115-
this.onSorterUpdated = (count: number) => {
116-
mesh.primitive[0].count = count;
115+
this.onSorterUpdated = () => {
116+
mesh.primitive[0].count = instance.sorter.pendingSorted?.count ?? mesh.primitive[0].count;
117117
};
118118
instance.sorter.on('updated', this.onSorterUpdated);
119119

0 commit comments

Comments
 (0)