Skip to content

Commit

Permalink
More fidelity tests (google#820)
Browse files Browse the repository at this point in the history
* added two more khronos tests

* added the rest of the khronos tests

* refactored intrinsics

* fixed framing bugs

* workaround for filament camera

* updated goldens

* updated to fit with new camera sync

* refactored

* bumped filament version and removed hacks

* cleanup

* addressing feedback

* fixed culled animations
  • Loading branch information
elalish authored Oct 31, 2019
1 parent dcc9f10 commit 6f48755
Show file tree
Hide file tree
Showing 107 changed files with 225 additions and 70 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"concurrently": "^4.0.1",
"cross-os": "^1.3.0",
"esm": "^3.0.84",
"filament": "1.4.0",
"filament": "1.4.1",
"focus-visible": "^5.0.1",
"fullscreen-polyfill": "^1.0.2",
"intersection-observer": "^0.5.1",
Expand Down
73 changes: 31 additions & 42 deletions src/features/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import ModelViewerElementBase, {$ariaLabel, $loadedTime, $needsRender, $onModelL
import {normalizeUnit} from '../styles/conversions.js';
import {EvaluatedStyle, Intrinsics, SphericalIntrinsics, Vector3Intrinsics} from '../styles/evaluators.js';
import {IdentNode, NumberNode, numberNode, parseExpressions} from '../styles/parsers.js';
import {DEFAULT_FOV_DEG} from '../three-components/Model.js';
import {ChangeEvent, ChangeSource, SmoothControls} from '../three-components/SmoothControls.js';
import {Constructor} from '../utilities.js';
import {timeline} from '../utilities/animation.js';
Expand Down Expand Up @@ -127,7 +126,6 @@ export const cameraTargetIntrinsics = (element: ModelViewerElementBase) => {
};
};

const HALF_FIELD_OF_VIEW_RADIANS = (DEFAULT_FOV_DEG / 2) * Math.PI / 180;
const HALF_PI = Math.PI / 2.0;
const THIRD_PI = Math.PI / 3.0;
const QUARTER_PI = HALF_PI / 2.0;
Expand All @@ -144,11 +142,10 @@ export const $controls = Symbol('controls');
export const $promptElement = Symbol('promptElement');
export const $promptAnimatedContainer = Symbol('promptAnimatedContainer');
export const $idealCameraDistance = Symbol('idealCameraDistance');
const $framedFieldOfView = Symbol('framedFieldOfView');

const $deferInteractionPrompt = Symbol('deferInteractionPrompt');
const $updateAria = Symbol('updateAria');
const $updateCamera = Symbol('updateCamera');
const $updateCameraForRadius = Symbol('updateCameraForRadius');

const $blurHandler = Symbol('blurHandler');
const $focusHandler = Symbol('focusHandler');
Expand Down Expand Up @@ -250,8 +247,7 @@ export const ControlsMixin = <T extends Constructor<ModelViewerElementBase>>(
protected[$controls] = new SmoothControls(
this[$scene].getCamera() as PerspectiveCamera, this[$scene].canvas);

protected[$framedFieldOfView]: number|null = null;
protected[$zoomAdjustedFieldOfView]: number = DEFAULT_FOV_DEG;
protected[$zoomAdjustedFieldOfView] = 0;
protected[$lastSpherical] = new Spherical();
protected[$jumpCamera] = false;

Expand Down Expand Up @@ -350,6 +346,7 @@ export const ControlsMixin = <T extends Constructor<ModelViewerElementBase>>(
}

[$syncCameraOrbit](style: EvaluatedStyle<SphericalIntrinsics>) {
this[$updateCameraForRadius](style[2]);
this[$controls].setOrbit(style[0], style[1], style[2]);
}

Expand Down Expand Up @@ -437,42 +434,16 @@ export const ControlsMixin = <T extends Constructor<ModelViewerElementBase>>(
}

/**
* Set the camera's radius and field of view to properly frame the scene
* based on changes to the model or aspect ratio, and maintains the
* relative camera zoom state.
* Updates the camera's near and far planes to enclose the scene when
* orbiting at the supplied radius.
*/
[$updateCamera]() {
const controls = this[$controls];
const {aspect} = this[$scene];
const {idealCameraDistance, fieldOfViewAspect} = this[$scene].model;

const maximumRadius = idealCameraDistance * 2;
controls.applyOptions({maximumRadius});
[$updateCameraForRadius](radius: number) {
const {idealCameraDistance} = this[$scene].model;
const maximumRadius = Math.max(idealCameraDistance, radius);

const modelRadius =
idealCameraDistance * Math.sin(HALF_FIELD_OF_VIEW_RADIANS);
const near = 0;
const far = maximumRadius + modelRadius;

controls.updateIntrinsics(near, far, aspect);

if (this.fieldOfView === DEFAULT_FIELD_OF_VIEW) {
const zoom = (this[$framedFieldOfView] != null) ?
controls.getFieldOfView() / this[$framedFieldOfView]! :
1;

const vertical = Math.tan(HALF_FIELD_OF_VIEW_RADIANS) *
Math.max(1, fieldOfViewAspect / aspect);
this[$framedFieldOfView] = 2 * Math.atan(vertical) * 180 / Math.PI;

const maximumFieldOfView = this[$framedFieldOfView]!;
controls.applyOptions({maximumFieldOfView});
// TODO(#835): Move computation of this value to Model or ModelScene
this[$zoomAdjustedFieldOfView] = this[$framedFieldOfView]! * zoom;
this.requestUpdate('fieldOfView', this.fieldOfView);
}

controls.jumpToGoal();
const far = 2 * maximumRadius;
this[$controls].updateNearFar(near, far);
}

[$updateAria]() {
Expand Down Expand Up @@ -513,16 +484,34 @@ export const ControlsMixin = <T extends Constructor<ModelViewerElementBase>>(
}

[$onResize](event: any) {
const controls = this[$controls];
const oldFramedFieldOfView = this[$scene].framedFieldOfView;

// The super of $onResize will update the scene's framedFieldOfView, so we
// compare the before and after to calculate the proper zoom.
super[$onResize](event);
this[$updateCamera]();

const newFramedFieldOfView = this[$scene].framedFieldOfView;
const zoom = controls.getFieldOfView() / oldFramedFieldOfView;
this[$zoomAdjustedFieldOfView] = newFramedFieldOfView * zoom;
controls.applyOptions({maximumFieldOfView: newFramedFieldOfView});
controls.updateAspect(this[$scene].aspect);
this.requestUpdate('fieldOfView', this.fieldOfView);
controls.jumpToGoal();
}

[$onModelLoad](event: any) {
super[$onModelLoad](event);
this[$updateCamera]();

const controls = this[$controls];
const {framedFieldOfView} = this[$scene];
this[$zoomAdjustedFieldOfView] = framedFieldOfView;
controls.applyOptions({maximumFieldOfView: framedFieldOfView});

this.requestUpdate('fieldOfView', this.fieldOfView);
this.requestUpdate('cameraOrbit', this.cameraOrbit);
this.requestUpdate('cameraTarget', this.cameraTarget);
this[$controls].jumpToGoal();
controls.jumpToGoal();
}

[$onFocus]() {
Expand Down
5 changes: 2 additions & 3 deletions src/test/features/controls-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,10 @@ suite('ModelViewerElementBase with ControlsMixin', () => {
.to.be.equal('always-allow');
});

test('sets max radius greater than the camera framed distance', () => {
test('sets max radius to at least the camera framed distance', () => {
const cameraDistance = element[$scene].camera.position.distanceTo(
element[$scene].model.position);
expect(controls.options.maximumRadius)
.to.be.greaterThan(cameraDistance);
expect(controls.options.maximumRadius).to.be.at.least(cameraDistance);
});

test('disables interaction if disabled after enabled', async () => {
Expand Down
22 changes: 19 additions & 3 deletions src/test/fidelity/components/renderers/filament-viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ const basepath = (urlString: string): string => {

const IS_BINARY_RE = /\.glb$/;

interface BoundingBox {
min: [number, number, number];
max: [number, number, number];
}

const $engine = Symbol('engine');
const $scene = Symbol('scene');
const $ibl = Symbol('ibl');
Expand All @@ -42,6 +47,7 @@ const $renderer = Symbol('renderer');
const $camera = Symbol('camera');
const $view = Symbol('view');
const $canvas = Symbol('canvas');
const $boundingBox = Symbol('boundingBox');
const $currentAsset = Symbol('currentAsset');

const $initialize = Symbol('initialize');
Expand All @@ -67,6 +73,7 @@ export class FilamentViewer extends LitElement {
private[$currentAsset]: any = null;

private[$canvas]: HTMLCanvasElement|null = null;
private[$boundingBox]: BoundingBox|null = null;

constructor() {
super();
Expand Down Expand Up @@ -182,6 +189,7 @@ export class FilamentViewer extends LitElement {
finalize();
loader.delete();

this[$boundingBox] = this[$currentAsset].getBoundingBox() as BoundingBox;
this[$scene].addEntities(this[$currentAsset].getEntities());

this[$updateSize]();
Expand Down Expand Up @@ -242,14 +250,22 @@ export class FilamentViewer extends LitElement {
orbit.radius * Math.cos(phi) + target.y,
radiusSinPhi * Math.cos(theta) + target.z
];
if (orbit.radius <= 0) {
center[0] = eye[0] - Math.sin(phi) * Math.sin(theta);
center[1] = eye[1] - Math.cos(phi);
center[2] = eye[2] - Math.sin(phi) * Math.cos(theta);
}

const near = orbit.radius / 10.0;
const far = orbit.radius * 10.0;
const {min, max} = this[$boundingBox]!;
const modelRadius =
Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
const far = 2 * Math.max(modelRadius, orbit.radius);
const near = far / 1000;

this[$camera].setProjectionFov(
verticalFoV, aspect, near, far, Fov!.VERTICAL);
const up = [0, 1, 0];
this[$camera].lookAt(eye, center, up);
this[$camera].setExposure(1.0, 1.2, 100);
this[$camera].setExposureDirect(1.0);
}
}
10 changes: 7 additions & 3 deletions src/three-components/CachingGLTFLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,15 @@ export class CachingGLTFLoader {
const gltf = await cache.get(url)!;

if (gltf.scene != null) {
// Animations for objects without names target their UUID instead. When
// objects are cloned, they get new UUIDs which the animation can't find.
// To fix this, we assign their UUID as their name.
gltf.scene.traverse((node: Object3D) => {
node.castShadow = true;
// Three.js seems to cull some animated models incorrectly. Since we
// expect to view our whole scene anyway, we turn off the frustum
// culling optimization here.
node.frustumCulled = false;
// Animations for objects without names target their UUID instead. When
// objects are cloned, they get new UUIDs which the animation can't
// find. To fix this, we assign their UUID as their name.
if (!node.name) {
node.name = node.uuid;
}
Expand Down
23 changes: 16 additions & 7 deletions src/three-components/ModelScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {BackSide, BoxBufferGeometry, Camera, Color, Event as ThreeEvent, Mesh, O
import ModelViewerElementBase, {$needsRender} from '../model-viewer-base.js';
import {resolveDpr} from '../utilities.js';

import Model from './Model.js';
import Model, {DEFAULT_FOV_DEG} from './Model.js';
import {Renderer} from './Renderer.js';
import {cubeUVChunk} from './shader-chunk/cube_uv_reflection_fragment.glsl.js';
import {Shadow} from './Shadow.js';
Expand All @@ -42,6 +42,8 @@ export const IlluminationRole: {[index: string]: IlluminationRole} = {
Secondary: 'secondary'
};

const DEFAULT_TAN_FOV = Math.tan((DEFAULT_FOV_DEG / 2) * Math.PI / 180);

const $paused = Symbol('paused');

/**
Expand All @@ -68,6 +70,7 @@ export class ModelScene extends Scene {
public context: CanvasRenderingContext2D;
public exposure = 1;
public model: Model;
public framedFieldOfView = DEFAULT_FOV_DEG;
public skyboxMesh: Mesh;
public activeCamera: Camera;
// These default camera values are never used, as they are reset once the
Expand Down Expand Up @@ -149,6 +152,7 @@ export class ModelScene extends Scene {
this.canvas.style.width = `${this.width}px`;
this.canvas.style.height = `${this.height}px`;
this.aspect = this.width / this.height;
this.frameModel();

// Immediately queue a render to happen at microtask timing. This is
// necessary because setting the width and height of the canvas has the
Expand All @@ -166,19 +170,23 @@ export class ModelScene extends Scene {
}
}

/**
* Set's the framedFieldOfView based on the aspect ratio of the window in
* order to keep the model fully visible at any camera orientation.
*/
frameModel() {
const vertical = DEFAULT_TAN_FOV *
Math.max(1, this.model.fieldOfViewAspect / this.aspect);
this.framedFieldOfView = 2 * Math.atan(vertical) * 180 / Math.PI;
}

/**
* Returns the size of the corresponding canvas element.
*/
getSize(): {width: number, height: number} {
return {width: this.width, height: this.height};
}

resetModelPose() {
this.model.position.set(0, 0, 0);
this.model.rotation.set(0, 0, 0);
this.model.scale.set(1, 1, 1);
}

/**
* Returns the current camera.
*/
Expand Down Expand Up @@ -219,6 +227,7 @@ export class ModelScene extends Scene {
* Called when the model's contents have loaded, or changed.
*/
onModelLoad(event: {url: string}) {
this.frameModel();
this.setShadowIntensity(this.shadowIntensity);
if (this.shadow != null) {
this.shadow.updateModel(this.model, this.shadowSoftness);
Expand Down
13 changes: 10 additions & 3 deletions src/three-components/SmoothControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export interface SmoothControlsOptions {

export const DEFAULT_OPTIONS = Object.freeze<SmoothControlsOptions>({
minimumRadius: 0,
maximumRadius: 2,
maximumRadius: Infinity,
minimumPolarAngle: Math.PI / 8,
maximumPolarAngle: Math.PI - Math.PI / 8,
minimumAzimuthalAngle: -Infinity,
Expand Down Expand Up @@ -366,11 +366,18 @@ export class SmoothControls extends EventDispatcher {
}

/**
* Sets the non-interpolated camera parameters
* Sets the near and far planes of the camera.
*/
updateIntrinsics(nearPlane: number, farPlane: number, aspect: number) {
updateNearFar(nearPlane: number, farPlane: number) {
this.camera.near = Math.max(nearPlane, farPlane / 1000);
this.camera.far = farPlane;
this.camera.updateProjectionMatrix();
}

/**
* Sets the aspect ratio of the camera
*/
updateAspect(aspect: number) {
this.camera.aspect = aspect;
this.camera.updateProjectionMatrix();
}
Expand Down
Loading

0 comments on commit 6f48755

Please sign in to comment.