Skip to content

Commit a4b88ac

Browse files
authored
3MFLoader: Basic support of Beam Lattice. (mrdoob#34267)
1 parent 9515872 commit a4b88ac

6 files changed

Lines changed: 223 additions & 1 deletion

File tree

examples/jsm/loaders/3MFLoader.js

Lines changed: 202 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
BufferGeometry,
44
ClampToEdgeWrapping,
55
Color,
6+
CylinderGeometry,
67
FileLoader,
78
Float32BufferAttribute,
89
Group,
@@ -15,14 +16,21 @@ import {
1516
MeshStandardMaterial,
1617
MirroredRepeatWrapping,
1718
NearestFilter,
19+
Quaternion,
1820
RepeatWrapping,
21+
SphereGeometry,
1922
TextureLoader,
20-
SRGBColorSpace
23+
SRGBColorSpace,
24+
Vector3
2125
} from 'three';
2226
import { unzipSync } from '../libs/fflate.module.js';
27+
import { mergeGeometries } from '../utils/BufferGeometryUtils.js';
2328

2429
const COLOR_SPACE_3MF = SRGBColorSpace;
2530

31+
const NS_BEAM_LATTICE = 'http://schemas.microsoft.com/3dmanufacturing/beamlattice/2017/02';
32+
const NS_BEAM_LATTICE_BALLS = 'http://schemas.microsoft.com/3dmanufacturing/beamlattice/balls/2020/07';
33+
2634
/**
2735
* A loader for the [3D Manufacturing Format (3MF)](https://3mf.io/specification/) format.
2836
*
@@ -38,6 +46,7 @@ const COLOR_SPACE_3MF = SRGBColorSpace;
3846
* - Texture 2D Groups
3947
* - Color Groups (Vertex Colors)
4048
* - Metallic Display Properties (PBR)
49+
* - Beam Lattice
4150
*
4251
* ```js
4352
* const loader = new ThreeMFLoader();
@@ -604,10 +613,87 @@ class ThreeMFLoader extends Loader {
604613
meshData[ 'triangleProperties' ] = triangleProperties;
605614
meshData[ 'triangles' ] = new Uint32Array( triangles );
606615

616+
const beamLatticeNode = meshNode.getElementsByTagNameNS( NS_BEAM_LATTICE, 'beamlattice' )[ 0 ];
617+
618+
if ( beamLatticeNode !== undefined ) {
619+
620+
meshData[ 'beamlattice' ] = parseBeamLatticeNode( beamLatticeNode );
621+
622+
}
623+
607624
return meshData;
608625

609626
}
610627

628+
function parseBeamLatticeNode( beamLatticeNode ) {
629+
630+
const beamLatticeData = {
631+
radius: parseFloat( beamLatticeNode.getAttribute( 'radius' ) ), // required
632+
minLength: parseFloat( beamLatticeNode.getAttribute( 'minlength' ) ),
633+
cap: beamLatticeNode.getAttribute( 'cap' ) || 'sphere',
634+
ballMode: beamLatticeNode.getAttributeNS( NS_BEAM_LATTICE_BALLS, 'ballmode' ) || 'none',
635+
ballRadius: parseFloat( beamLatticeNode.getAttributeNS( NS_BEAM_LATTICE_BALLS, 'ballradius' ) ),
636+
beams: [],
637+
balls: []
638+
};
639+
640+
// clipping requires boolean operations which are not supported ('clipping' is a legacy name of 'clippingmode')
641+
642+
const clippingMode = beamLatticeNode.getAttribute( 'clippingmode' ) || beamLatticeNode.getAttribute( 'clipping' );
643+
644+
if ( clippingMode !== null && clippingMode !== 'none' ) {
645+
646+
console.warn( 'THREE.3MFLoader: Beam lattice clipping is not supported. The lattice is rendered unclipped.' );
647+
648+
}
649+
650+
const beamNodes = beamLatticeNode.getElementsByTagNameNS( NS_BEAM_LATTICE, 'beam' );
651+
652+
for ( let i = 0; i < beamNodes.length; i ++ ) {
653+
654+
const beamNode = beamNodes[ i ];
655+
656+
const beamData = {
657+
v1: parseInt( beamNode.getAttribute( 'v1' ), 10 ), // required
658+
v2: parseInt( beamNode.getAttribute( 'v2' ), 10 ) // required
659+
};
660+
661+
// optional
662+
663+
const r1 = beamNode.getAttribute( 'r1' );
664+
const r2 = beamNode.getAttribute( 'r2' );
665+
666+
if ( r1 !== null ) beamData[ 'r1' ] = parseFloat( r1 );
667+
if ( r2 !== null ) beamData[ 'r2' ] = parseFloat( r2 );
668+
669+
beamLatticeData.beams.push( beamData );
670+
671+
}
672+
673+
const ballNodes = beamLatticeNode.getElementsByTagNameNS( NS_BEAM_LATTICE_BALLS, 'ball' );
674+
675+
for ( let i = 0; i < ballNodes.length; i ++ ) {
676+
677+
const ballNode = ballNodes[ i ];
678+
679+
const ballData = {
680+
vindex: parseInt( ballNode.getAttribute( 'vindex' ), 10 ) // required
681+
};
682+
683+
// optional
684+
685+
const r = ballNode.getAttribute( 'r' );
686+
687+
if ( r !== null ) ballData[ 'r' ] = parseFloat( r );
688+
689+
beamLatticeData.balls.push( ballData );
690+
691+
}
692+
693+
return beamLatticeData;
694+
695+
}
696+
611697
function parseComponentsNode( componentsNode ) {
612698

613699
const components = [];
@@ -1211,6 +1297,111 @@ class ThreeMFLoader extends Loader {
12111297

12121298
}
12131299

1300+
function buildLatticeMesh( meshData ) {
1301+
1302+
const beamLatticeData = meshData[ 'beamlattice' ];
1303+
const vertices = meshData[ 'vertices' ];
1304+
1305+
const geometries = [];
1306+
1307+
const p1 = new Vector3();
1308+
const p2 = new Vector3();
1309+
const direction = new Vector3();
1310+
const up = new Vector3( 0, 1, 0 );
1311+
const quaternion = new Quaternion();
1312+
const matrix = new Matrix4();
1313+
1314+
// beams
1315+
1316+
const beams = beamLatticeData.beams;
1317+
const openEnded = beamLatticeData.cap !== 'butt';
1318+
1319+
// spheres for caps and balls are collected per vertex so joints are only built once
1320+
1321+
const sphereRadii = new Map();
1322+
1323+
for ( let i = 0; i < beams.length; i ++ ) {
1324+
1325+
const beam = beams[ i ];
1326+
1327+
p1.fromArray( vertices, beam.v1 * 3 );
1328+
p2.fromArray( vertices, beam.v2 * 3 );
1329+
1330+
const length = p1.distanceTo( p2 );
1331+
1332+
if ( length < beamLatticeData.minLength ) continue; // consumers must ignore beams shorter than "minlength"
1333+
1334+
const r1 = ( beam.r1 !== undefined ) ? beam.r1 : beamLatticeData.radius;
1335+
const r2 = ( beam.r2 !== undefined ) ? beam.r2 : r1;
1336+
1337+
const geometry = new CylinderGeometry( r2, r1, length, 8, 1, openEnded );
1338+
1339+
direction.subVectors( p2, p1 ).divideScalar( length );
1340+
quaternion.setFromUnitVectors( up, direction );
1341+
1342+
matrix.makeRotationFromQuaternion( quaternion );
1343+
matrix.setPosition( p1.add( p2 ).multiplyScalar( 0.5 ) );
1344+
1345+
geometry.applyMatrix4( matrix );
1346+
geometries.push( geometry );
1347+
1348+
if ( openEnded ) {
1349+
1350+
// approximate "sphere" and "hemisphere" caps with a sphere per beam end
1351+
1352+
sphereRadii.set( beam.v1, Math.max( r1, sphereRadii.get( beam.v1 ) || 0 ) );
1353+
sphereRadii.set( beam.v2, Math.max( r2, sphereRadii.get( beam.v2 ) || 0 ) );
1354+
1355+
}
1356+
1357+
if ( beamLatticeData.ballMode === 'all' ) {
1358+
1359+
sphereRadii.set( beam.v1, Math.max( beamLatticeData.ballRadius, sphereRadii.get( beam.v1 ) || 0 ) );
1360+
sphereRadii.set( beam.v2, Math.max( beamLatticeData.ballRadius, sphereRadii.get( beam.v2 ) || 0 ) );
1361+
1362+
}
1363+
1364+
}
1365+
1366+
// balls
1367+
1368+
const balls = beamLatticeData.balls;
1369+
1370+
for ( let i = 0; i < balls.length; i ++ ) {
1371+
1372+
const ball = balls[ i ];
1373+
const r = ( ball.r !== undefined ) ? ball.r : beamLatticeData.ballRadius;
1374+
1375+
sphereRadii.set( ball.vindex, Math.max( r, sphereRadii.get( ball.vindex ) || 0 ) );
1376+
1377+
}
1378+
1379+
for ( const [ vindex, radius ] of sphereRadii ) {
1380+
1381+
const geometry = new SphereGeometry( radius, 8, 6 );
1382+
1383+
p1.fromArray( vertices, vindex * 3 );
1384+
geometry.translate( p1.x, p1.y, p1.z );
1385+
1386+
geometries.push( geometry );
1387+
1388+
}
1389+
1390+
//
1391+
1392+
const geometry = mergeGeometries( geometries );
1393+
1394+
const material = new MeshPhongMaterial( {
1395+
name: Loader.DEFAULT_MATERIAL_NAME,
1396+
color: 0xffffff
1397+
} );
1398+
1399+
const mesh = new Mesh( geometry, material );
1400+
1401+
return mesh;
1402+
1403+
}
1404+
12141405
function buildMeshes( resourceMap, meshData, objects, modelData, textureData, objectData ) {
12151406

12161407
const keys = Object.keys( resourceMap );
@@ -1329,6 +1520,16 @@ class ThreeMFLoader extends Loader {
13291520
const resourceMap = analyzeObject( meshData, objectData );
13301521
const meshes = buildMeshes( resourceMap, meshData, objects, modelData, textureData, objectData );
13311522

1523+
if ( meshData[ 'beamlattice' ] !== undefined ) {
1524+
1525+
const latticeMesh = buildLatticeMesh( meshData );
1526+
1527+
if ( objectData.name ) latticeMesh.name = objectData.name;
1528+
1529+
meshes.push( latticeMesh );
1530+
1531+
}
1532+
13321533
for ( let i = 0, l = meshes.length; i < l; i ++ ) {
13331534

13341535
group.add( meshes[ i ] );

examples/models/3mf/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@ Source: https://github.com/3MFConsortium/3mf-samples
1818

1919
License: BSD 2-Clause "Simplified" License
2020

21+
### pyramid.3mf
22+
23+
Source: https://github.com/3MFConsortium/3mf-samples
24+
25+
License: BSD 2-Clause "Simplified" License
26+
27+
### spinal implant.3mf
28+
29+
Source: https://github.com/3MFConsortium/3mf-samples
30+
31+
License: BSD 2-Clause "Simplified" License
32+
33+
### variable voronoi.3mf
34+
35+
Source: https://github.com/3MFConsortium/3mf-samples
36+
37+
License: BSD 2-Clause "Simplified" License
38+
2139
### vertexcolors.3mf
2240

2341
Source: https://github.com/3MFConsortium/3mf-samples (original name `pyramid_vertexcolor.3mf`)

examples/models/3mf/pyramid.3mf

4.72 KB
Binary file not shown.
256 KB
Binary file not shown.
20.5 KB
Binary file not shown.

examples/webgl_loader_3mf.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
'cube_gears',
5151
'facecolors',
5252
'multipletextures',
53+
'pyramid',
54+
'spinal implant',
55+
'variable voronoi',
5356
'vertexcolors'
5457
];
5558

0 commit comments

Comments
 (0)