Skip to content

Commit 87cc59b

Browse files
authored
USDLoader: Improve material and UV support. (#32746)
1 parent 0b75d1d commit 87cc59b

File tree

2 files changed

+189
-9
lines changed

2 files changed

+189
-9
lines changed

examples/jsm/loaders/usd/USDCParser.js

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ function lz4DecompressBlock( input, inputOffset, inputEnd, output, outputOffset,
172172
// USD uses TfFastCompression which wraps LZ4 with chunk headers
173173
function decompressLZ4( input, uncompressedSize ) {
174174

175-
// USD's TfFastCompression format:
175+
// TfFastCompression format (used by OpenUSD):
176176
// Single chunk (byte 0 == 0): [0] + LZ4 data
177177
// Multi chunk (byte 0 > 0): [numChunks] + [compressedSizes...] + [chunkData...]
178178

@@ -1362,7 +1362,64 @@ class USDCParser {
13621362

13631363
}
13641364

1365-
case TypeEnum.Dictionary:
1365+
case TypeEnum.Dictionary: {
1366+
1367+
// Dictionary format:
1368+
// u64 elementCount
1369+
// For each element: u32 keyIndex + i64 valueOffset (relative)
1370+
const elementCount = reader.readUint64();
1371+
const dict = {};
1372+
1373+
for ( let i = 0; i < elementCount; i ++ ) {
1374+
1375+
const keyIdx = reader.readUint32();
1376+
const key = this.tokens[ keyIdx ];
1377+
1378+
// Value offset is relative to current position
1379+
const currentPos = reader.position;
1380+
const valueOffset = reader.readInt64();
1381+
const valuePos = currentPos + valueOffset;
1382+
1383+
// Save position, read value, restore position
1384+
const savedPos = reader.position;
1385+
reader.position = valuePos;
1386+
1387+
// Read the value representation at the offset
1388+
const valueRepData = reader.readUint64();
1389+
const valueRep = new ValueRep( valueRepData );
1390+
1391+
// Read the value based on the representation
1392+
let value = null;
1393+
if ( valueRep.isInlined ) {
1394+
1395+
value = this._readInlinedValue( valueRep );
1396+
1397+
} else if ( valueRep.isArray ) {
1398+
1399+
reader.position = valueRep.payload;
1400+
value = this._readArrayValue( valueRep );
1401+
1402+
} else {
1403+
1404+
reader.position = valueRep.payload;
1405+
value = this._readScalarValue( valueRep );
1406+
1407+
}
1408+
1409+
reader.position = savedPos;
1410+
1411+
if ( key !== undefined && value !== null ) {
1412+
1413+
dict[ key ] = value;
1414+
1415+
}
1416+
1417+
}
1418+
1419+
return dict;
1420+
1421+
}
1422+
13661423
case TypeEnum.TokenListOp:
13671424
case TypeEnum.StringListOp:
13681425
case TypeEnum.IntListOp:

examples/jsm/loaders/usd/USDComposer.js

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
Bone,
2020
SRGBColorSpace,
2121
Texture,
22+
Vector2,
2223
Vector3,
2324
VectorKeyframeTrack
2425
} from 'three';
@@ -1092,6 +1093,28 @@ class USDComposer {
10921093

10931094
}
10941095

1096+
// Second UV set (st1) for lightmaps/AO
1097+
const { uvs2, uv2Indices } = this._findUV2Primvar( fields );
1098+
1099+
if ( uvs2 && uvs2.length > 0 ) {
1100+
1101+
let uv2Data = uvs2;
1102+
1103+
if ( uv2Indices && uv2Indices.length > 0 ) {
1104+
1105+
const triangulatedUv2Indices = this._triangulateIndices( uv2Indices, faceVertexCounts );
1106+
uv2Data = this._expandAttribute( uvs2, triangulatedUv2Indices, 2 );
1107+
1108+
} else if ( indices && uvs2.length / 2 === points.length / 3 ) {
1109+
1110+
uv2Data = this._expandAttribute( uvs2, indices, 2 );
1111+
1112+
}
1113+
1114+
geometry.setAttribute( 'uv1', new BufferAttribute( new Float32Array( uv2Data ), 2 ) );
1115+
1116+
}
1117+
10951118
// Add skinning attributes
10961119
if ( hasSkinning ) {
10971120

@@ -1164,6 +1187,7 @@ class USDComposer {
11641187
if ( ! faceVertexCounts || faceVertexCounts.length === 0 ) return geometry;
11651188

11661189
const { uvs, uvIndices } = this._findUVPrimvar( fields );
1190+
const { uvs2, uv2Indices } = this._findUV2Primvar( fields );
11671191
const normals = fields[ 'normals' ] || fields[ 'primvars:normals' ];
11681192

11691193
const jointIndices = hasSkinning ? fields[ 'primvars:skel:jointIndices' ] : null;
@@ -1261,6 +1285,7 @@ class USDComposer {
12611285
// Triangulate original data
12621286
const origIndices = this._triangulateIndices( faceVertexIndices, faceVertexCounts );
12631287
const origUvIndices = uvIndices ? this._triangulateIndices( uvIndices, faceVertexCounts ) : null;
1288+
const origUv2Indices = uv2Indices ? this._triangulateIndices( uv2Indices, faceVertexCounts ) : null;
12641289

12651290
const numFaceVertices = faceVertexCounts.reduce( ( a, b ) => a + b, 0 );
12661291
const hasFaceVaryingNormals = normals && normals.length / 3 === numFaceVertices;
@@ -1272,6 +1297,7 @@ class USDComposer {
12721297
const vertexCount = triangleCount * 3;
12731298
const positions = new Float32Array( vertexCount * 3 );
12741299
const uvData = uvs ? new Float32Array( vertexCount * 2 ) : null;
1300+
const uv1Data = uvs2 ? new Float32Array( vertexCount * 2 ) : null;
12751301
const normalData = normals ? new Float32Array( vertexCount * 3 ) : null;
12761302
const skinIndexData = jointIndices ? new Uint16Array( vertexCount * 4 ) : null;
12771303
const skinWeightData = jointWeights ? new Float32Array( vertexCount * 4 ) : null;
@@ -1307,6 +1333,23 @@ class USDComposer {
13071333

13081334
}
13091335

1336+
if ( uv1Data && uvs2 ) {
1337+
1338+
if ( origUv2Indices ) {
1339+
1340+
const uv2Idx = origUv2Indices[ origIdx ];
1341+
uv1Data[ newIdx * 2 ] = uvs2[ uv2Idx * 2 ];
1342+
uv1Data[ newIdx * 2 + 1 ] = uvs2[ uv2Idx * 2 + 1 ];
1343+
1344+
} else if ( uvs2.length / 2 === points.length / 3 ) {
1345+
1346+
uv1Data[ newIdx * 2 ] = uvs2[ pointIdx * 2 ];
1347+
uv1Data[ newIdx * 2 + 1 ] = uvs2[ pointIdx * 2 + 1 ];
1348+
1349+
}
1350+
1351+
}
1352+
13101353
if ( normalData && normals ) {
13111354

13121355
if ( origNormalIndices ) {
@@ -1358,6 +1401,12 @@ class USDComposer {
13581401

13591402
}
13601403

1404+
if ( uv1Data ) {
1405+
1406+
geometry.setAttribute( 'uv1', new BufferAttribute( uv1Data, 2 ) );
1407+
1408+
}
1409+
13611410
if ( normalData ) {
13621411

13631412
geometry.setAttribute( 'normal', new BufferAttribute( normalData, 3 ) );
@@ -1410,6 +1459,15 @@ class USDComposer {
14101459

14111460
}
14121461

1462+
_findUV2Primvar( fields ) {
1463+
1464+
// Look for second UV set (st1, commonly used for lightmaps/AO)
1465+
const uvs2 = fields[ 'primvars:st1' ];
1466+
const uv2Indices = fields[ 'primvars:st1:indices' ];
1467+
return { uvs2, uv2Indices };
1468+
1469+
}
1470+
14131471
_triangulateIndices( indices, counts ) {
14141472

14151473
const triangulated = [];
@@ -1754,6 +1812,15 @@ class USDComposer {
17541812
// Normal map
17551813
applyTextureFromConnection( 'inputs:normal', 'normalMap', NoColorSpace, null );
17561814

1815+
// Apply normal map scale from UsdUVTexture scale input
1816+
if ( material.normalMap && material.normalMap.userData.scale ) {
1817+
1818+
const scale = material.normalMap.userData.scale;
1819+
// UsdUVTexture scale is float4 (r,g,b,a), use first two components for normalScale
1820+
material.normalScale = new Vector2( scale[ 0 ], scale[ 1 ] );
1821+
1822+
}
1823+
17571824
// Roughness
17581825
const hasRoughnessMap = applyTextureFromConnection(
17591826
'inputs:roughness',
@@ -1814,10 +1881,26 @@ class USDComposer {
18141881

18151882
}
18161883

1817-
// Opacity
1818-
if ( fields[ 'inputs:opacity' ] !== undefined ) {
1884+
// Opacity and opacity modes
1885+
const opacity = fields[ 'inputs:opacity' ] !== undefined ? fields[ 'inputs:opacity' ] : 1.0;
1886+
const opacityThreshold = fields[ 'inputs:opacityThreshold' ] !== undefined ? fields[ 'inputs:opacityThreshold' ] : 0.0;
1887+
const opacityMode = fields[ 'inputs:opacityMode' ] || 'transparent';
1888+
1889+
if ( opacityMode === 'presence' ) {
1890+
1891+
// Presence mode: use alpha testing (cutout transparency)
1892+
material.alphaTest = opacityThreshold;
1893+
material.transparent = false;
18191894

1820-
const opacity = fields[ 'inputs:opacity' ];
1895+
if ( opacity < 1.0 ) {
1896+
1897+
material.opacity = opacity;
1898+
1899+
}
1900+
1901+
} else if ( opacityMode === 'transparent' ) {
1902+
1903+
// Transparent mode: traditional alpha blending
18211904
if ( opacity < 1.0 ) {
18221905

18231906
material.transparent = true;
@@ -1826,6 +1909,7 @@ class USDComposer {
18261909
}
18271910

18281911
}
1912+
// opacityMode === 'opaque': ignore opacity entirely (default material state)
18291913

18301914
}
18311915

@@ -1845,8 +1929,9 @@ class USDComposer {
18451929
const filePath = attrs[ 'inputs:file' ];
18461930
if ( ! filePath ) return null;
18471931

1848-
// Check for UsdTransform2d connection via inputs:st
1932+
// Check for UsdTransform2d connection via inputs:st and trace to PrimvarReader
18491933
let transformAttrs = null;
1934+
let uvChannel = 0; // Default to first UV set
18501935
const stAttrPath = shaderPath + '.inputs:st';
18511936
const stAttrSpec = this.specsByPath[ stAttrPath ];
18521937

@@ -1865,23 +1950,61 @@ class USDComposer {
18651950

18661951
transformAttrs = stAttrs;
18671952

1953+
// Trace to PrimvarReader to find UV set
1954+
const inAttrPath = stPath + '.inputs:in';
1955+
const inAttrSpec = this.specsByPath[ inAttrPath ];
1956+
1957+
if ( inAttrSpec?.fields?.connectionPaths?.length > 0 ) {
1958+
1959+
const inConnPath = inAttrSpec.fields.connectionPaths[ 0 ];
1960+
const primvarPath = inConnPath.replace( /<|>/g, '' ).split( '.' )[ 0 ];
1961+
const primvarAttrs = this._getAttributes( primvarPath );
1962+
1963+
// Check varname to determine UV channel
1964+
const varname = primvarAttrs[ 'inputs:varname' ];
1965+
if ( varname === 'st1' ) uvChannel = 1;
1966+
else if ( varname === 'st2' ) uvChannel = 2;
1967+
1968+
}
1969+
1970+
} else if ( stInfoId === 'UsdPrimvarReader_float2' ) {
1971+
1972+
// Direct connection to PrimvarReader
1973+
const varname = stAttrs[ 'inputs:varname' ];
1974+
if ( varname === 'st1' ) uvChannel = 1;
1975+
else if ( varname === 'st2' ) uvChannel = 2;
1976+
18681977
}
18691978

18701979
}
18711980

18721981
}
18731982

1874-
if ( this.textureCache[ filePath ] ) {
1983+
// Extract scale and bias for texture value modification
1984+
const scale = attrs[ 'inputs:scale' ];
1985+
const bias = attrs[ 'inputs:bias' ];
1986+
1987+
// Create cache key that includes scale/bias if present
1988+
let cacheKey = filePath;
1989+
if ( scale ) cacheKey += ':s' + scale.join( ',' );
1990+
if ( bias ) cacheKey += ':b' + bias.join( ',' );
1991+
1992+
if ( this.textureCache[ cacheKey ] ) {
18751993

1876-
return this.textureCache[ filePath ];
1994+
return this.textureCache[ cacheKey ];
18771995

18781996
}
18791997

18801998
const texture = this._loadTexture( filePath, attrs, transformAttrs );
18811999

18822000
if ( texture ) {
18832001

1884-
this.textureCache[ filePath ] = texture;
2002+
// Store scale/bias and UV channel in userData
2003+
if ( scale ) texture.userData.scale = scale;
2004+
if ( bias ) texture.userData.bias = bias;
2005+
if ( uvChannel !== 0 ) texture.channel = uvChannel;
2006+
2007+
this.textureCache[ cacheKey ] = texture;
18852008

18862009
}
18872010

0 commit comments

Comments
 (0)