diff --git a/example/floorProjection.html b/example/floorProjection.html new file mode 100644 index 0000000..f5f6e08 --- /dev/null +++ b/example/floorProjection.html @@ -0,0 +1,51 @@ + + + + three-edge-projection - Projected Edge Generation + + + + + +
+ Floor plan silhouette and edge projection from Sketchfab model. +
+
loading...
+ + + diff --git a/example/floorProjection.js b/example/floorProjection.js new file mode 100644 index 0000000..1d66152 --- /dev/null +++ b/example/floorProjection.js @@ -0,0 +1,221 @@ +import { + Box3, + WebGLRenderer, + Scene, + DirectionalLight, + AmbientLight, + Group, + BufferGeometry, + LineSegments, + LineBasicMaterial, + PerspectiveCamera, + MeshBasicMaterial, + Mesh, + DoubleSide, +} from 'three'; +import { MapControls } from 'three/examples/jsm/controls/MapControls.js'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; +import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'; +import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js'; +import { ProjectionGenerator, SilhouetteGenerator } from '../src'; + +const ANGLE_THRESHOLD = 50; +let renderer, camera, scene, gui, controls; +let model, outlines, group, silhouette; +let outputContainer; +let task = null; + +init(); + +async function init() { + + outputContainer = document.getElementById( 'output' ); + + const bgColor = 0x111111; + + // renderer setup + renderer = new WebGLRenderer( { antialias: true } ); + renderer.setPixelRatio( window.devicePixelRatio ); + renderer.setSize( window.innerWidth, window.innerHeight ); + renderer.setClearColor( bgColor, 1 ); + document.body.appendChild( renderer.domElement ); + + // scene setup + scene = new Scene(); + + // lights + const light = new DirectionalLight( 0xffffff, 3.5 ); + light.position.set( 1, 2, 3 ); + scene.add( light ); + + const ambientLight = new AmbientLight( 0xb0bec5, 0.5 ); + scene.add( ambientLight ); + + // load model + group = new Group(); + scene.add( group ); + + const gltf = await new GLTFLoader() + .setMeshoptDecoder( MeshoptDecoder ) + .loadAsync( 'https://raw.githubusercontent.com/gkjohnson/3d-demo-data/main/models/3d-home-layout/scene.glb' ); + model = gltf.scene; + group.updateMatrixWorld( true ); + + // center model + const box = new Box3(); + box.setFromObject( model, true ); + box.getCenter( group.position ).multiplyScalar( - 1 ); + group.position.y = Math.max( 0, - box.min.y ) + 1; + group.add( model ); + model.visible = false; + + // create projection display mesh + silhouette = new Mesh( new BufferGeometry(), new MeshBasicMaterial( { + color: '#eee', + polygonOffset: true, + polygonOffsetFactor: 3, + polygonOffsetUnits: 3, + side: DoubleSide, + } ) ); + outlines = new LineSegments( new BufferGeometry(), new LineBasicMaterial( { color: 0x030303 } ) ); + scene.add( outlines, silhouette ); + + // camera setup + camera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.01, 50 ); + camera.position.setScalar( 5.5 ); + camera.updateProjectionMatrix(); + + // controls + controls = new MapControls( camera, renderer.domElement ); + controls.zoomToCursor = true; + controls.maxPolarAngle = Math.PI / 3; + + task = updateProjection(); + + render(); + + window.addEventListener( 'resize', function () { + + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + + renderer.setSize( window.innerWidth, window.innerHeight ); + + }, false ); + +} + +function* updateProjection() { + + outputContainer.innerText = 'processing: --'; + silhouette.visible = false; + outlines.visible = false; + + // transform and merge geometries to project into a single model + const geometries = []; + model.updateWorldMatrix( true, true ); + model.traverse( c => { + + if ( c.geometry ) { + + const clone = c.geometry.clone(); + clone.applyMatrix4( c.matrixWorld ); + for ( const key in clone.attributes ) { + + if ( key !== 'position' ) { + + clone.deleteAttribute( key ); + + } + + } + + geometries.push( clone ); + + } + + } ); + const mergedGeometry = mergeGeometries( geometries, false ); + + yield; + + // generate the silhouette + let task, result, generator; + generator = new SilhouetteGenerator(); + generator.sortTriangles = true; + task = generator.generate( mergedGeometry, { + + onProgress: ( p, data ) => { + + outputContainer.innerText = `processing: ${ parseFloat( ( p * 100 ).toFixed( 2 ) ) }%`; + silhouette.geometry.dispose(); + silhouette.geometry = data.getGeometry(); + silhouette.visible = true; + + }, + + } ); + + result = task.next(); + while ( ! result.done ) { + + result = task.next(); + yield; + + } + + silhouette.geometry.dispose(); + silhouette.geometry = result.value; + silhouette.visible = true; + outputContainer.innerText = 'generating intersection edges...'; + + // generate the edges + generator = new ProjectionGenerator(); + generator.angleThreshold = ANGLE_THRESHOLD; + task = generator.generate( mergedGeometry, { + + onProgress: ( p, data ) => { + + outputContainer.innerText = `processing: ${ parseFloat( ( p * 100 ).toFixed( 2 ) ) }%`; + outlines.geometry.dispose(); + outlines.geometry = data.getLineGeometry(); + outlines.visible = true; + + }, + + } ); + + result = task.next(); + while ( ! result.done ) { + + result = task.next(); + yield; + + } + + outlines.geometry.dispose(); + outlines.geometry = result.value; + outlines.visible = true; + outputContainer.innerText = ''; + +} + + +function render() { + + requestAnimationFrame( render ); + + if ( task ) { + + const res = task.next(); + if ( res.done ) { + + task = null; + + } + + } + + renderer.render( scene, camera ); + +} diff --git a/src/ProjectionGenerator.js b/src/ProjectionGenerator.js index fc38014..08cd7df 100644 --- a/src/ProjectionGenerator.js +++ b/src/ProjectionGenerator.js @@ -68,7 +68,7 @@ export class ProjectionGenerator { this.sortEdges = true; this.iterationTime = 30; this.angleThreshold = 50; - this.includeIntersectionEdges = false; + this.includeIntersectionEdges = true; } @@ -121,7 +121,8 @@ export class ProjectionGenerator { let edges = generateEdges( geometry, UP_VECTOR, angleThreshold ); if ( includeIntersectionEdges ) { - edges = edges.concat( generateIntersectionEdges( bvh ) ); + const results = yield* generateIntersectionEdges( bvh, iterationTime ); + edges = edges.concat( results ); } diff --git a/src/utils/generateIntersectionEdges.js b/src/utils/generateIntersectionEdges.js index ad0660e..d1fd438 100644 --- a/src/utils/generateIntersectionEdges.js +++ b/src/utils/generateIntersectionEdges.js @@ -5,7 +5,7 @@ import { isLineTriangleEdge } from './triangleLineUtils.js'; const OFFSET_EPSILON = 1e-6; const _tri = new ExtendedTriangle(); const _line = new Line3(); -export function generateIntersectionEdges( bvh ) { +export function* generateIntersectionEdges( bvh, iterationTime = 30 ) { const edges = []; const geometry = bvh.geometry; @@ -13,7 +13,7 @@ export function generateIntersectionEdges( bvh ) { const posAttr = geometry.attributes.position; const vertCount = index ? index.count : posAttr; - + let time = performance.now(); for ( let i = 0; i < vertCount; i += 3 ) { let i0 = i + 0; @@ -78,6 +78,14 @@ export function generateIntersectionEdges( bvh ) { } ); + const delta = performance.now() - time; + if ( delta > iterationTime ) { + + yield; + time = performance.now(); + + } + } return edges;