Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions examples/jsm/loaders/usd/USDComposer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
MirroredRepeatWrapping,
NoColorSpace,
Object3D,
OrthographicCamera,
PerspectiveCamera,
Quaternion,
QuaternionKeyframeTrack,
RepeatWrapping,
Expand Down Expand Up @@ -44,6 +46,19 @@ const SpecType = {
VariantSet: 11
};

// UsdGeomCamera fallback values (OpenUSD schema)
const USD_CAMERA_DEFAULTS = {
projection: 'perspective',
clippingRange: [ 1, 1000000 ],
horizontalAperture: 20.955,
verticalAperture: 15.2908,
horizontalApertureOffset: 0,
verticalApertureOffset: 0,
focalLength: 50,
focusDistance: 0,
fStop: 0
};

/**
* USDComposer handles scene composition from parsed USD data.
* This includes reference resolution, variant selection, transform handling,
Expand Down Expand Up @@ -657,6 +672,15 @@ class USDComposer {

}

} else if ( typeName === 'Camera' ) {

const obj = this._buildCamera( path );
obj.name = name;
const attrs = this._getAttributes( path );
this.applyTransform( obj, spec.fields, attrs );
parent.add( obj );
this._buildHierarchy( obj, path );

} else if ( typeName === 'Material' || typeName === 'Shader' || typeName === 'GeomSubset' ) {

// Skip materials/shaders/subsets, they're referenced by meshes
Expand Down Expand Up @@ -1191,6 +1215,99 @@ class USDComposer {

}

/**
* Build a camera from a Camera spec.
*/
_buildCamera( path ) {

const attrs = this._getAttributes( path );
const projectionToken = attrs[ 'projection' ];
const projection = typeof projectionToken === 'string'
? projectionToken.toLowerCase()
: USD_CAMERA_DEFAULTS.projection;
const clippingRange = attrs[ 'clippingRange' ] || USD_CAMERA_DEFAULTS.clippingRange;
const near = Math.max(
Number.EPSILON,
this._parseNumber( clippingRange[ 0 ], USD_CAMERA_DEFAULTS.clippingRange[ 0 ] )
);
const far = Math.max(
near + Number.EPSILON,
this._parseNumber( clippingRange[ 1 ], USD_CAMERA_DEFAULTS.clippingRange[ 1 ] )
);
const horizontalAperture = this._parseNumber(
attrs[ 'horizontalAperture' ],
USD_CAMERA_DEFAULTS.horizontalAperture
);
const verticalAperture = this._parseNumber(
attrs[ 'verticalAperture' ],
USD_CAMERA_DEFAULTS.verticalAperture
);
const horizontalApertureOffset = this._parseNumber(
attrs[ 'horizontalApertureOffset' ],
USD_CAMERA_DEFAULTS.horizontalApertureOffset
);
const verticalApertureOffset = this._parseNumber(
attrs[ 'verticalApertureOffset' ],
USD_CAMERA_DEFAULTS.verticalApertureOffset
);
const focalLength = this._parseNumber( attrs[ 'focalLength' ], USD_CAMERA_DEFAULTS.focalLength );
const focusDistance = this._parseNumber( attrs[ 'focusDistance' ], USD_CAMERA_DEFAULTS.focusDistance );
const fStop = this._parseNumber( attrs[ 'fStop' ], USD_CAMERA_DEFAULTS.fStop );

let camera;

if ( projection === 'orthographic' ) {

// USD orthographic apertures are in tenths of a world unit.
const width = horizontalAperture / 10;
const height = verticalAperture / 10;
const offsetX = horizontalApertureOffset / 10;
const offsetY = verticalApertureOffset / 10;

camera = new OrthographicCamera(
offsetX - width * 0.5,
offsetX + width * 0.5,
offsetY + height * 0.5,
offsetY - height * 0.5,
near,
far
);

} else {

const safeVerticalAperture = Math.max( Number.EPSILON, verticalAperture );
const safeFocalLength = Math.max( Number.EPSILON, focalLength );
const aspect = horizontalAperture / safeVerticalAperture;
const fov = 2 * Math.atan( safeVerticalAperture / ( 2 * safeFocalLength ) ) * 180 / Math.PI;

camera = new PerspectiveCamera( fov, aspect, near, far );
camera.filmGauge = Math.max( horizontalAperture, verticalAperture );
camera.filmOffset = horizontalApertureOffset;
camera.focus = focusDistance;
camera.setFocalLength( safeFocalLength );

if ( verticalApertureOffset !== 0 ) {

// Three.js supports only horizontal film offset directly.
camera.userData.verticalApertureOffset = verticalApertureOffset;

}

}

camera.userData.fStop = fStop;
camera.userData.usdProjection = projection;
return camera;

}

_parseNumber( value, fallback ) {

const n = Number( value );
return Number.isFinite( n ) ? n : fallback;

}

_getGeomSubsets( meshPath ) {

const subsets = [];
Expand Down
1 change: 1 addition & 0 deletions src/animation/AnimationAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,7 @@ class AnimationAction {

} else {

this._loopCount = loopCount;
this.time = time;

}
Expand Down
3 changes: 1 addition & 2 deletions src/renderers/WebGLRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
NoToneMapping,
LinearMipmapLinearFilter,
SRGBColorSpace,
LinearSRGBColorSpace,
RGBAIntegerFormat,
RGIntegerFormat,
RedIntegerFormat,
Expand Down Expand Up @@ -2272,7 +2271,7 @@ class WebGLRenderer {

const fog = scene.fog;
const environment = ( material.isMeshStandardMaterial || material.isMeshLambertMaterial || material.isMeshPhongMaterial ) ? scene.environment : null;
const colorSpace = ( _currentRenderTarget === null ) ? _this.outputColorSpace : ( _currentRenderTarget.isXRRenderTarget === true ? _currentRenderTarget.texture.colorSpace : LinearSRGBColorSpace );
const colorSpace = ( _currentRenderTarget === null ) ? _this.outputColorSpace : ( _currentRenderTarget.isXRRenderTarget === true ? _currentRenderTarget.texture.colorSpace : ColorManagement.workingColorSpace );
const usePMREM = material.isMeshStandardMaterial || ( material.isMeshLambertMaterial && ! material.envMap ) || ( material.isMeshPhongMaterial && ! material.envMap );
const envMap = environments.get( material.envMap || environment, usePMREM );
const vertexAlphas = material.vertexColors === true && !! geometry.attributes.color && geometry.attributes.color.itemSize === 4;
Expand Down
4 changes: 2 additions & 2 deletions src/renderers/webgl/WebGLPrograms.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BackSide, DoubleSide, CubeUVReflectionMapping, ObjectSpaceNormalMap, TangentSpaceNormalMap, NoToneMapping, NormalBlending, LinearSRGBColorSpace, SRGBTransfer } from '../../constants.js';
import { BackSide, DoubleSide, CubeUVReflectionMapping, ObjectSpaceNormalMap, TangentSpaceNormalMap, NoToneMapping, NormalBlending, SRGBTransfer } from '../../constants.js';
import { Layers } from '../../core/Layers.js';
import { WebGLProgram } from './WebGLProgram.js';
import { WebGLShaderCache } from './WebGLShaderCache.js';
Expand Down Expand Up @@ -200,7 +200,7 @@ function WebGLPrograms( renderer, environments, extensions, capabilities, bindin
instancingColor: IS_INSTANCEDMESH && object.instanceColor !== null,
instancingMorph: IS_INSTANCEDMESH && object.morphTexture !== null,

outputColorSpace: ( currentRenderTarget === null ) ? renderer.outputColorSpace : ( currentRenderTarget.isXRRenderTarget === true ? currentRenderTarget.texture.colorSpace : LinearSRGBColorSpace ),
outputColorSpace: ( currentRenderTarget === null ) ? renderer.outputColorSpace : ( currentRenderTarget.isXRRenderTarget === true ? currentRenderTarget.texture.colorSpace : ColorManagement.workingColorSpace ),
alphaToCoverage: !! material.alphaToCoverage,

map: HAS_MAP,
Expand Down
56 changes: 56 additions & 0 deletions test/unit/src/animation/AnimationAction.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,62 @@ export default QUnit.module( 'Animation', () => {

} );

QUnit.test( 'LoopRepeat with timeScale reversal during first loop', ( assert ) => {

// Regression test for #19151
const root = new Object3D();
const mixer = new AnimationMixer( root );
const track = new NumberKeyframeTrack( '.rotation[x]', [ 0, 1000 ], [ 0, 360 ] );
const clip = new AnimationClip( 'clip1', 1000, [ track ] );

const animationAction = mixer.clipAction( clip );
animationAction.setLoop( LoopRepeat, Infinity );
animationAction.play();

// Advance partway into the first loop
mixer.update( 500 );
assert.equal( root.rotation.x, 180, 'At 500ms, rotation is 180 (halfway).' );

// Reverse timeScale
mixer.timeScale = - 1;

// Step backward — should smoothly reverse, not jump
mixer.update( 250 );
assert.equal( root.rotation.x, 90, 'After reversing and stepping 250ms back, rotation is 90.' );

mixer.update( 250 );
assert.equal( root.rotation.x, 0, 'After reversing and stepping another 250ms back, rotation is 0 (start).' );

} );

QUnit.test( 'LoopPingPong with timeScale reversal during first loop', ( assert ) => {

// Regression test for #19151
const root = new Object3D();
const mixer = new AnimationMixer( root );
const track = new NumberKeyframeTrack( '.rotation[x]', [ 0, 1000 ], [ 0, 360 ] );
const clip = new AnimationClip( 'clip1', 1000, [ track ] );

const animationAction = mixer.clipAction( clip );
animationAction.setLoop( LoopPingPong, Infinity );
animationAction.play();

// Advance partway into the first loop
mixer.update( 500 );
assert.equal( root.rotation.x, 180, 'At 500ms, rotation is 180 (halfway).' );

// Reverse timeScale
mixer.timeScale = - 1;

// Step backward — should smoothly reverse, not jump
mixer.update( 250 );
assert.equal( root.rotation.x, 90, 'After reversing and stepping 250ms back, rotation is 90.' );

mixer.update( 250 );
assert.equal( root.rotation.x, 0, 'After reversing and stepping another 250ms back, rotation is 0 (start).' );

} );

} );

} );