├── .gitignore ├── assets ├── texture.png └── demo.js ├── component.json ├── package.json ├── index.html ├── README.md ├── LICENSE └── src └── ThreeBSP.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | npm-* 2 | node_modules/* -------------------------------------------------------------------------------- /assets/texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sshirokov/ThreeBSP/HEAD/assets/texture.png -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ThreeBSP", 3 | "version": "0.0.1", 4 | "ignore": [ 5 | "**/.*", 6 | "node_modules", 7 | "components" 8 | ], 9 | "main": "src/ThreeBSP.coffee" 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threecsg", 3 | "version": "0.0.0", 4 | 5 | "dependencies": { 6 | "coffee-script": "1.4.0" 7 | }, 8 | 9 | 10 | "directories": { 11 | "lib": "./lib" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CSG plugin for Three.js 2 | 3 | Based almost entirely on [chandlerprall/ThreeCSG](https://github.com/chandlerprall/ThreeCSG) :heart: 4 | but ported to [CoffeeScript](http://coffeescript.org/) and cleaned up a little. 5 | 6 | ### Quickstart and Demo 7 | 8 | ```bash 9 | $ git clone https://github.com/sshirokov/ThreeBSP.git 10 | $ npm install . 11 | $ python -mSimpleHTTPServer 12 | Serving HTTP on 0.0.0.0 port 8000 ... 13 | ``` 14 | 15 | Open your browser to http://localhost:8000 and you should see something 16 | like this 17 | 18 |  19 | 20 | ### Bower 21 | 22 | This package is available in the [Bower](https://github.com/bower/bower) registry. 23 | 24 | ```bash 25 | $ bower install ThreeBSP 26 | bower cloning git://github.com/sshirokov/ThreeBSP.git 27 | bower caching git://github.com/sshirokov/ThreeBSP.git 28 | bower fetching ThreeBSP 29 | 30 | ``` 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012 Chandler Prall. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /assets/demo.js: -------------------------------------------------------------------------------- 1 | var renderer, scene, camera, light; 2 | 3 | console.log("Demo will load 1 second after the DOM"); 4 | 5 | document.addEventListener("DOMContentLoaded", setTimeout(function() { 6 | 7 | renderer = new THREE.WebGLRenderer({antialias: true}); 8 | renderer.setSize( window.innerWidth, window.innerHeight ); 9 | document.getElementById('viewport').appendChild(renderer.domElement); 10 | 11 | scene = new THREE.Scene(); 12 | 13 | light = new THREE.DirectionalLight( 0xffffff ); 14 | light.position.set( 1, 1, 1 ).normalize(); 15 | scene.add( light ); 16 | 17 | camera = new THREE.PerspectiveCamera( 18 | 35, 19 | window.innerWidth / window.innerHeight, 20 | 1, 21 | 1000 22 | ); 23 | camera.position.set( 5, 5, 15 ); 24 | camera.lookAt( scene.position ); 25 | scene.add( camera ); 26 | 27 | // Example #1 - Cube (mesh) subtract Sphere (mesh) 28 | (function() { 29 | var start_time = (new Date()).getTime(); 30 | 31 | var cube_geometry = new THREE.CubeGeometry( 3, 3, 3 ); 32 | var cube_mesh = new THREE.Mesh( cube_geometry ); 33 | cube_mesh.position.x = -7; 34 | var start, stop; 35 | start = Date.now(); 36 | p = {}; 37 | var cube_bsp = new ThreeBSP( cube_mesh, {timeout: 2000, progress: function(c,t) { 38 | percent = Math.round(c/t * 100) 39 | if(p['P' + percent] === undefined) { 40 | p['P' + percent] = true; 41 | console.log(percent + "%"); 42 | } 43 | }}); 44 | stop = Date.now(); 45 | console.log("Elapsed(build cube): ", stop - start); 46 | 47 | 48 | var sphere_geometry = new THREE.SphereGeometry( 1.8, 32, 32 ); 49 | var sphere_mesh = new THREE.Mesh( sphere_geometry ); 50 | sphere_mesh.position.x = -7; 51 | start = Date.now(); 52 | var sphere_bsp = new ThreeBSP( sphere_mesh, {timeout: 3000} ); 53 | stop = Date.now(); 54 | console.log("Elapsed(build sphere): ", stop - start); 55 | 56 | 57 | start = Date.now(); 58 | var subtract_bsp = cube_bsp.subtract( sphere_bsp ); 59 | stop = Date.now(); 60 | console.log("Elapsed(cube - sphere): ", stop - start); 61 | start = Date.now(); 62 | var result = subtract_bsp.toMesh( new THREE.MeshLambertMaterial({ shading: THREE.SmoothShading, map: THREE.ImageUtils.loadTexture('assets/texture.png') }) ); 63 | stop = Date.now(); 64 | console.log("Elapsed(toMesh): ", stop - start); 65 | result.geometry.computeVertexNormals(); 66 | scene.add( result ); 67 | 68 | console.log( 'Example 1: ' + ((new Date()).getTime() - start_time) + 'ms' ); 69 | })(); 70 | 71 | // Example #2 - Sphere (geometry) union Cube (geometry) 72 | (function() { 73 | var start_time = (new Date()).getTime(); 74 | 75 | var sphere_geometry = new THREE.SphereGeometry( 2, 16, 16 ); 76 | var sphere_bsp = new ThreeBSP( sphere_geometry ); 77 | 78 | var cube_geometry = new THREE.CubeGeometry( 7, .5, 3 ); 79 | var cube_bsp = new ThreeBSP( cube_geometry ); 80 | 81 | var union_bsp = sphere_bsp.union( cube_bsp ); 82 | 83 | var result = union_bsp.toMesh( new THREE.MeshLambertMaterial({ shading: THREE.SmoothShading, map: THREE.ImageUtils.loadTexture('assets/texture.png') }) ); 84 | result.geometry.computeVertexNormals(); 85 | scene.add( result ); 86 | 87 | console.log( 'Example 2: ' + ((new Date()).getTime() - start_time) + 'ms' ); 88 | })(); 89 | 90 | 91 | // Example #3 - Sphere (geometry) intersect Sphere (mesh) 92 | (function() { 93 | var start_time = (new Date()).getTime(); 94 | 95 | var sphere_geometry_1 = new THREE.SphereGeometry( 2, 64, 8 ); 96 | var sphere_bsp_1 = new ThreeBSP( sphere_geometry_1 ); 97 | 98 | var sphere_geometry_2 = new THREE.SphereGeometry( 2, 8, 32 ); 99 | var sphere_mesh_2 = new THREE.Mesh( sphere_geometry_2 ); 100 | sphere_mesh_2.position.x = 2; 101 | var sphere_bsp_2 = new ThreeBSP( sphere_mesh_2 ); 102 | 103 | var intersect_bsp = sphere_bsp_1.intersect( sphere_bsp_2 ); 104 | 105 | var result = intersect_bsp.toMesh( new THREE.MeshLambertMaterial({ shading: THREE.SmoothShading, map: THREE.ImageUtils.loadTexture('assets/texture.png') }) ); 106 | result.position.x = 6; 107 | result.geometry.computeVertexNormals(); 108 | scene.add( result ); 109 | 110 | console.log( 'Example 3: ' + ((new Date()).getTime() - start_time) + 'ms' ); 111 | })(); 112 | 113 | (function render() { 114 | requestAnimationFrame( render ); 115 | renderer.render(scene, camera); 116 | })(); 117 | }, 1000)) 118 | -------------------------------------------------------------------------------- /src/ThreeBSP.coffee: -------------------------------------------------------------------------------- 1 | EPSILON = 1e-5 2 | COPLANAR = 0 3 | FRONT = 1 4 | BACK = 2 5 | SPANNING = 3 6 | 7 | # Call the callback with no arguments 8 | # then return the first value. 9 | # Used to construct chainable 10 | # callbacks 11 | returning = (value, fn) -> 12 | fn() 13 | value 14 | 15 | class Timelimit 16 | constructor: (@timeout, @progress) -> "NOTHING" 17 | 18 | check: => 19 | return unless @started? 20 | returning (elapsed = (Date.now() - @started)), => 21 | if elapsed >= @timeout ? Infinity 22 | throw new Error("Timeout reached: #{elapsed}/#{@timeout}, #{@tasks ? 0} tasks unfinished #{@done ? 0} finished.") 23 | 24 | start: => 25 | @started ?= Date.now() 26 | @tasks ?= 0 27 | @total ?= 0 28 | @total += 1 29 | @tasks += 1 30 | do @check 31 | 32 | finish: => 33 | throw new Error("Finished more tasks than started") if @tasks? and @tasks < 1 34 | @tasks -= 1 35 | elapsed = @check() 36 | @done ?= 0 37 | @done += 1 38 | @progress(@done, @total) if @progress? 39 | if @tasks == 0 40 | "Finished #{@done} tasks in #{elapsed}/#{@timeout} ms" 41 | @started = @done = @total = undefined 42 | 43 | doTask: (block) => 44 | do @start 45 | result = block() 46 | do @finish 47 | result 48 | 49 | 50 | ## 51 | ## ThreBSP Driver 52 | # 53 | # Can be instantiated with THREE.Geometry, 54 | # THREE.Mesh or a ThreeBSP.Node 55 | class window.ThreeBSP 56 | constructor: (treeIsh, @matrix, @options={}) -> 57 | if @matrix? and not (@matrix instanceof THREE.Matrix4) 58 | @options = @matrix 59 | @matrix = undefined 60 | 61 | @options ?= {} 62 | @matrix ?= new THREE.Matrix4() 63 | 64 | # Start a timer if one wasn't passed 65 | @options.timer ?= new Timelimit( 66 | @options.timer?.timeout ? @options.timeout 67 | @options.timer?.progress ? @options.progress 68 | ) 69 | 70 | @tree = @toTree treeIsh 71 | 72 | # Evaluate block after replacing @timer with new_timer 73 | # then put @timer back after block returns 74 | withTimer: (new_timer, block) => 75 | old_timer = @options.timer 76 | try 77 | @options.timer = new_timer 78 | do block 79 | finally 80 | @options.timer = old_timer 81 | 82 | toTree: (treeIsh) => 83 | return treeIsh if treeIsh instanceof ThreeBSP.Node 84 | polygons = [] 85 | geometry = 86 | if treeIsh instanceof THREE.Geometry 87 | treeIsh 88 | else if treeIsh instanceof THREE.Mesh 89 | treeIsh.updateMatrix() 90 | @matrix = treeIsh.matrix.clone() 91 | treeIsh.geometry 92 | 93 | for face, i in geometry.faces 94 | do (face, i) => 95 | faceVertexUvs = geometry.faceVertexUvs?[0][i] 96 | faceVertexUvs ?= [new THREE.Vector2(), new THREE.Vector2(), 97 | new THREE.Vector2(), new THREE.Vector2()] 98 | polygon = new ThreeBSP.Polygon() 99 | for vName, vIndex in ['a', 'b', 'c', 'd'] 100 | if (idx = face[vName])? 101 | vertex = geometry.vertices[idx] 102 | vertex = new ThreeBSP.Vertex vertex.x, vertex.y, vertex.z, 103 | face.vertexNormals[0], 104 | new THREE.Vector2(faceVertexUvs[vIndex].x, faceVertexUvs[vIndex].y) 105 | vertex.applyMatrix4 @matrix 106 | polygon.vertices.push vertex 107 | polygons.push polygon.calculateProperties() 108 | new ThreeBSP.Node polygons, @options 109 | 110 | # Converters/Exporters 111 | toMesh: (material=new THREE.MeshNormalMaterial()) => @options.timer.doTask => 112 | geometry = @toGeometry() 113 | returning (mesh = new THREE.Mesh geometry, material), => 114 | mesh.position.getPositionFromMatrix @matrix 115 | mesh.rotation.setEulerFromRotationMatrix @matrix 116 | 117 | toGeometry: () => @options.timer.doTask => 118 | matrix = new THREE.Matrix4().getInverse @matrix 119 | 120 | returning (geometry = new THREE.Geometry()), => 121 | for polygon in @tree.allPolygons() 122 | @options.timer.doTask => 123 | polyVerts = (v.clone().applyMatrix4(matrix) for v in polygon.vertices) 124 | for idx in [2...polyVerts.length] 125 | verts = [polyVerts[0], polyVerts[idx-1], polyVerts[idx]] 126 | vertUvs = (new THREE.Vector2(v.uv?.x, v.uv?.y) for v in verts) 127 | 128 | face = new THREE.Face3 (geometry.vertices.push(v) - 1 for v in verts)..., polygon.normal.clone() 129 | geometry.faces.push face 130 | geometry.faceVertexUvs[0].push vertUvs 131 | 132 | # CSG Operations 133 | subtract: (other) => @options.timer.doTask => other.withTimer @options.timer, => 134 | [us, them] = [@tree.clone(), other.tree.clone()] 135 | us 136 | .invert() 137 | .clipTo(them) 138 | them 139 | .clipTo(us) 140 | .invert() 141 | .clipTo(us) 142 | .invert() 143 | new ThreeBSP us.build(them.allPolygons()).invert(), @matrix, @options 144 | 145 | union: (other) => @options.timer.doTask => other.withTimer @options.timer, => 146 | [us, them] = [@tree.clone(), other.tree.clone()] 147 | us.clipTo them 148 | them 149 | .clipTo(us) 150 | .invert() 151 | .clipTo(us) 152 | .invert() 153 | new ThreeBSP us.build(them.allPolygons()), @matrix, @options 154 | 155 | intersect: (other) => @options.timer.doTask => other.withTimer @options.timer, => 156 | [us, them] = [@tree.clone(), other.tree.clone()] 157 | them 158 | .clipTo(us.invert()) 159 | .invert() 160 | .clipTo(us.clipTo(them)) 161 | new ThreeBSP us.build(them.allPolygons()).invert(), @matrix, @options 162 | 163 | 164 | ## 165 | ## ThreeBSP.Vertex 166 | class ThreeBSP.Vertex extends THREE.Vector3 167 | constructor: (x, y, z, @normal=new THREE.Vector3(), @uv=new THREE.Vector2()) -> 168 | super x, y, z 169 | 170 | clone: -> 171 | new ThreeBSP.Vertex @x, @y, @z, @normal.clone(), @uv.clone() 172 | 173 | lerp: (v, alpha) => returning super, => 174 | # @uv is a V2 instead of V3, so we perform the lerp by hand 175 | @uv.add v.uv.clone().sub(@uv).multiplyScalar alpha 176 | @normal.lerp v, alpha 177 | 178 | interpolate: (args...) => 179 | @clone().lerp args... 180 | 181 | ## 182 | ## ThreeBSP.Polygon 183 | class ThreeBSP.Polygon 184 | constructor: (@vertices=[], @normal, @w) -> 185 | @calculateProperties() if @vertices.length 186 | 187 | calculateProperties: () => returning this, => 188 | [a, b, c] = @vertices 189 | @normal = b.clone().sub(a).cross( 190 | c.clone().sub a 191 | ).normalize() 192 | @w = @normal.clone().dot a 193 | 194 | clone: () => 195 | new ThreeBSP.Polygon( 196 | (v.clone() for v in @vertices), 197 | @normal.clone(), 198 | @w 199 | ) 200 | 201 | invert: () => returning this, => 202 | @normal.multiplyScalar -1 203 | @w *= -1 204 | @vertices.reverse() 205 | 206 | classifyVertex: (vertex) => 207 | side = @normal.dot(vertex) - @w 208 | switch 209 | when side < -EPSILON then BACK 210 | when side > EPSILON then FRONT 211 | else COPLANAR 212 | 213 | classifySide: (polygon) => 214 | [front, back] = [0, 0] 215 | tally = (v) => switch @classifyVertex v 216 | when FRONT then front += 1 217 | when BACK then back += 1 218 | (tally v for v in polygon.vertices) 219 | return FRONT if front > 0 and back == 0 220 | return BACK if front == 0 and back > 0 221 | return COPLANAR if front == back == 0 222 | return SPANNING 223 | 224 | # Return a list of polygons from `poly` such 225 | # that no polygons span the plane defined by 226 | # `this`. Should be a list of one or two Polygons 227 | tessellate: (poly) => 228 | {f, b, count} = {f: [], b: [], count: poly.vertices.length} 229 | 230 | return [poly] unless @classifySide(poly) == SPANNING 231 | # vi and vj are the current and next Vertex 232 | # i and j are the indexes of vi and vj 233 | # ti and tj are the classifications of vi and vj 234 | for vi, i in poly.vertices 235 | vj = poly.vertices[(j = (i + 1) % count)] 236 | [ti, tj] = (@classifyVertex v for v in [vi, vj]) 237 | f.push vi if ti != BACK 238 | b.push vi if ti != FRONT 239 | if (ti | tj) == SPANNING 240 | t = (@w - @normal.dot vi) / @normal.dot vj.clone().sub(vi) 241 | v = vi.interpolate vj, t 242 | f.push v 243 | b.push v 244 | 245 | returning (polys = []), => 246 | polys.push new ThreeBSP.Polygon(f) if f.length >= 3 247 | polys.push new ThreeBSP.Polygon(b) if b.length >= 3 248 | 249 | 250 | subdivide: (polygon, coplanar_front, coplanar_back, front, back) => 251 | for poly in @tessellate polygon 252 | side = @classifySide poly 253 | switch side 254 | when FRONT then front.push poly 255 | when BACK then back.push poly 256 | when COPLANAR 257 | if @normal.dot(poly.normal) > 0 258 | coplanar_front.push poly 259 | else 260 | coplanar_back.push poly 261 | else 262 | throw new Error("BUG: Polygon of classification #{side} in subdivision") 263 | 264 | ## 265 | ## ThreeBSP.Node 266 | class ThreeBSP.Node 267 | clone: => returning (node = new ThreeBSP.Node(@options)), => 268 | node.divider = @divider?.clone() 269 | node.polygons = @options.timer.doTask => (p.clone() for p in @polygons) 270 | node.front = @options.timer.doTask => @front?.clone() 271 | node.back = @options.timer.doTask => @back?.clone() 272 | 273 | constructor: (polygons, @options={}) -> 274 | if polygons? and not (polygons instanceof Array) 275 | @options = polygons 276 | polygons = undefined 277 | 278 | @polygons = [] 279 | @options.timer.doTask => 280 | @build(polygons) if polygons? and polygons.length 281 | 282 | build: (polygons) => returning this, => 283 | sides = front: [], back: [] 284 | @divider ?= polygons[0].clone() 285 | 286 | @options.timer.doTask => 287 | for poly in polygons 288 | @options.timer.doTask => 289 | @divider.subdivide poly, @polygons, @polygons, sides.front, sides.back 290 | 291 | for own side, polys of sides 292 | if polys.length 293 | @[side] ?= new ThreeBSP.Node(@options) 294 | @[side].build polys 295 | 296 | isConvex: (polys) => 297 | for inner in polys 298 | for outer in polys 299 | return false if inner != outer and outer.classifySide(inner) != BACK 300 | true 301 | 302 | allPolygons: => @options.timer.doTask => 303 | @polygons.slice() 304 | .concat(@front?.allPolygons() or []) 305 | .concat(@back?.allPolygons() or []) 306 | 307 | invert: => returning this, => @options.timer.doTask => 308 | for poly in @polygons 309 | @options.timer.doTask => do poly.invert 310 | for flipper in [@divider, @front, @back] 311 | @options.timer.doTask => flipper?.invert() 312 | [@front, @back] = [@back, @front] 313 | 314 | clipPolygons: (polygons) => @options.timer.doTask => 315 | return polygons.slice() unless @divider 316 | front = [] 317 | back = [] 318 | 319 | for poly in polygons 320 | @options.timer.doTask => 321 | @divider.subdivide poly, front, back, front, back 322 | 323 | front = @front.clipPolygons front if @front 324 | back = @back.clipPolygons back if @back 325 | 326 | if @back 327 | return front.concat back 328 | else 329 | return front 330 | 331 | clipTo: (node) => returning this, => @options.timer.doTask => 332 | @polygons = node.clipPolygons @polygons 333 | @front?.clipTo node 334 | @back?.clipTo node 335 | --------------------------------------------------------------------------------