├── .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/skalnik/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 cube_bsp = new ThreeBSP( cube_mesh ); 35 | 36 | var sphere_geometry = new THREE.SphereGeometry( 1.8, 32, 32 ); 37 | var sphere_mesh = new THREE.Mesh( sphere_geometry ); 38 | sphere_mesh.position.x = -7; 39 | var sphere_bsp = new ThreeBSP( sphere_mesh ); 40 | 41 | var subtract_bsp = cube_bsp.subtract( sphere_bsp ); 42 | var result = subtract_bsp.toMesh( new THREE.MeshLambertMaterial({ shading: THREE.SmoothShading, map: THREE.ImageUtils.loadTexture('assets/texture.png') }) ); 43 | result.geometry.computeVertexNormals(); 44 | scene.add( result ); 45 | 46 | console.log( 'Example 1: ' + ((new Date()).getTime() - start_time) + 'ms' ); 47 | })(); 48 | 49 | // Example #2 - Sphere (geometry) union Cube (geometry) 50 | (function() { 51 | var start_time = (new Date()).getTime(); 52 | 53 | var sphere_geometry = new THREE.SphereGeometry( 2, 16, 16 ); 54 | var sphere_bsp = new ThreeBSP( sphere_geometry ); 55 | 56 | var cube_geometry = new THREE.CubeGeometry( 7, .5, 3 ); 57 | var cube_bsp = new ThreeBSP( cube_geometry ); 58 | 59 | var union_bsp = sphere_bsp.union( cube_bsp ); 60 | 61 | var result = union_bsp.toMesh( new THREE.MeshLambertMaterial({ shading: THREE.SmoothShading, map: THREE.ImageUtils.loadTexture('assets/texture.png') }) ); 62 | result.geometry.computeVertexNormals(); 63 | scene.add( result ); 64 | 65 | console.log( 'Example 2: ' + ((new Date()).getTime() - start_time) + 'ms' ); 66 | })(); 67 | 68 | 69 | // Example #3 - Sphere (geometry) intersect Sphere (mesh) 70 | (function() { 71 | var start_time = (new Date()).getTime(); 72 | 73 | var sphere_geometry_1 = new THREE.SphereGeometry( 2, 64, 8 ); 74 | var sphere_bsp_1 = new ThreeBSP( sphere_geometry_1 ); 75 | 76 | var sphere_geometry_2 = new THREE.SphereGeometry( 2, 8, 32 ); 77 | var sphere_mesh_2 = new THREE.Mesh( sphere_geometry_2 ); 78 | sphere_mesh_2.position.x = 2; 79 | var sphere_bsp_2 = new ThreeBSP( sphere_mesh_2 ); 80 | 81 | var intersect_bsp = sphere_bsp_1.intersect( sphere_bsp_2 ); 82 | 83 | var result = intersect_bsp.toMesh( new THREE.MeshLambertMaterial({ shading: THREE.SmoothShading, map: THREE.ImageUtils.loadTexture('assets/texture.png') }) ); 84 | result.position.x = 6; 85 | result.geometry.computeVertexNormals(); 86 | scene.add( result ); 87 | 88 | console.log( 'Example 3: ' + ((new Date()).getTime() - start_time) + 'ms' ); 89 | })(); 90 | 91 | (function render() { 92 | requestAnimationFrame( render ); 93 | renderer.render(scene, camera); 94 | })(); 95 | }, 1000)) 96 | -------------------------------------------------------------------------------- /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 | ## 16 | ## ThreBSP Driver 17 | # 18 | # Can be instantiated with THREE.Geometry, 19 | # THREE.Mesh or a ThreeBSP.Node 20 | class window.ThreeBSP 21 | constructor: (treeIsh, @matrix) -> 22 | @matrix ?= new THREE.Matrix4() 23 | @tree = @toTree treeIsh 24 | 25 | toTree: (treeIsh) => 26 | return treeIsh if treeIsh instanceof ThreeBSP.Node 27 | polygons = [] 28 | geometry = 29 | if treeIsh instanceof THREE.Geometry 30 | treeIsh 31 | else if treeIsh instanceof THREE.Mesh 32 | treeIsh.updateMatrix() 33 | @matrix = treeIsh.matrix.clone() 34 | treeIsh.geometry 35 | 36 | for face, i in geometry.faces 37 | do (face, i) => 38 | faceVertexUvs = geometry.faceVertexUvs?[0][i] 39 | faceVertexUvs ?= [new THREE.Vector2(), new THREE.Vector2(), 40 | new THREE.Vector2(), new THREE.Vector2()] 41 | polygon = new ThreeBSP.Polygon() 42 | for vName, vIndex in ['a', 'b', 'c', 'd'] 43 | if (idx = face[vName])? 44 | vertex = geometry.vertices[idx] 45 | vertex = new ThreeBSP.Vertex vertex.x, vertex.y, vertex.z, 46 | face.vertexNormals[0], 47 | new THREE.Vector2(faceVertexUvs[vIndex].x, faceVertexUvs[vIndex].y) 48 | vertex.applyMatrix4 @matrix 49 | polygon.vertices.push vertex 50 | polygons.push polygon.calculateProperties() 51 | new ThreeBSP.Node polygons 52 | 53 | # Converters/Exporters 54 | toMesh: (material=new THREE.MeshNormalMaterial()) => 55 | geometry = @toGeometry() 56 | returning (mesh = new THREE.Mesh geometry, material), => 57 | mesh.position.getPositionFromMatrix @matrix 58 | mesh.rotation.setEulerFromRotationMatrix @matrix 59 | 60 | toGeometry: () => 61 | matrix = new THREE.Matrix4().getInverse @matrix 62 | 63 | returning (geometry = new THREE.Geometry()), => 64 | for polygon in @tree.allPolygons() 65 | polyVerts = (v.clone().applyMatrix4(matrix) for v in polygon.vertices) 66 | for idx in [2...polyVerts.length] 67 | verts = [polyVerts[0], polyVerts[idx-1], polyVerts[idx]] 68 | vertUvs = (new THREE.Vector2(v.uv?.x, v.uv?.y) for v in verts) 69 | 70 | face = new THREE.Face3 (geometry.vertices.push(v) - 1 for v in verts)..., polygon.normal.clone() 71 | geometry.faces.push face 72 | geometry.faceVertexUvs[0].push vertUvs 73 | 74 | # CSG Operations 75 | subtract: (other) => 76 | [us, them] = [@tree.clone(), other.tree.clone()] 77 | us 78 | .invert() 79 | .clipTo(them) 80 | them 81 | .clipTo(us) 82 | .invert() 83 | .clipTo(us) 84 | .invert() 85 | new ThreeBSP us.build(them.allPolygons()).invert(), @matrix 86 | 87 | union: (other) => 88 | [us, them] = [@tree.clone(), other.tree.clone()] 89 | us.clipTo them 90 | them 91 | .clipTo(us) 92 | .invert() 93 | .clipTo(us) 94 | .invert() 95 | new ThreeBSP us.build(them.allPolygons()), @matrix 96 | 97 | intersect: (other) => 98 | [us, them] = [@tree.clone(), other.tree.clone()] 99 | them 100 | .clipTo(us.invert()) 101 | .invert() 102 | .clipTo(us.clipTo(them)) 103 | new ThreeBSP us.build(them.allPolygons()).invert(), @matrix 104 | 105 | 106 | ## 107 | ## ThreeBSP.Vertex 108 | class ThreeBSP.Vertex extends THREE.Vector3 109 | constructor: (x, y, z, @normal=new THREE.Vector3(), @uv=new THREE.Vector2()) -> 110 | super x, y, z 111 | 112 | clone: -> 113 | new ThreeBSP.Vertex @x, @y, @z, @normal.clone(), @uv.clone() 114 | 115 | lerp: (v, alpha) => returning super, => 116 | # @uv is a V2 instead of V3, so we perform the lerp by hand 117 | @uv.add v.uv.clone().sub(@uv).multiplyScalar alpha 118 | @normal.lerp v, alpha 119 | 120 | interpolate: (args...) => 121 | @clone().lerp args... 122 | 123 | ## 124 | ## ThreeBSP.Polygon 125 | class ThreeBSP.Polygon 126 | constructor: (@vertices=[], @normal, @w) -> 127 | @calculateProperties() if @vertices.length 128 | 129 | calculateProperties: () => returning this, => 130 | [a, b, c] = @vertices 131 | @normal = b.clone().sub(a).cross( 132 | c.clone().sub a 133 | ).normalize() 134 | @w = @normal.clone().dot a 135 | 136 | clone: () => 137 | new ThreeBSP.Polygon( 138 | (v.clone() for v in @vertices), 139 | @normal.clone(), 140 | @w 141 | ) 142 | 143 | invert: () => returning this, => 144 | @normal.multiplyScalar -1 145 | @w *= -1 146 | @vertices.reverse() 147 | 148 | classifyVertex: (vertex) => 149 | side = @normal.dot(vertex) - @w 150 | switch 151 | when side < -EPSILON then BACK 152 | when side > EPSILON then FRONT 153 | else COPLANAR 154 | 155 | classifySide: (polygon) => 156 | [front, back] = [0, 0] 157 | tally = (v) => switch @classifyVertex v 158 | when FRONT then front += 1 159 | when BACK then back += 1 160 | (tally v for v in polygon.vertices) 161 | return FRONT if front > 0 and back == 0 162 | return BACK if front == 0 and back > 0 163 | return COPLANAR if front == back == 0 164 | return SPANNING 165 | 166 | # Return a list of polygons from `poly` such 167 | # that no polygons span the plane defined by 168 | # `this`. Should be a list of one or two Polygons 169 | tessellate: (poly) => 170 | {f, b, count} = {f: [], b: [], count: poly.vertices.length} 171 | 172 | return [poly] unless @classifySide(poly) == SPANNING 173 | # vi and vj are the current and next Vertex 174 | # i and j are the indexes of vi and vj 175 | # ti and tj are the classifications of vi and vj 176 | for vi, i in poly.vertices 177 | vj = poly.vertices[(j = (i + 1) % count)] 178 | [ti, tj] = (@classifyVertex v for v in [vi, vj]) 179 | f.push vi if ti != BACK 180 | b.push vi if ti != FRONT 181 | if (ti | tj) == SPANNING 182 | t = (@w - @normal.dot vi) / @normal.dot vj.clone().sub(vi) 183 | v = vi.interpolate vj, t 184 | f.push v 185 | b.push v 186 | 187 | returning (polys = []), => 188 | polys.push new ThreeBSP.Polygon(f) if f.length >= 3 189 | polys.push new ThreeBSP.Polygon(b) if b.length >= 3 190 | 191 | 192 | subdivide: (polygon, coplanar_front, coplanar_back, front, back) => 193 | for poly in @tessellate polygon 194 | side = @classifySide poly 195 | switch side 196 | when FRONT then front.push poly 197 | when BACK then back.push poly 198 | when COPLANAR 199 | if @normal.dot(poly.normal) > 0 200 | coplanar_front.push poly 201 | else 202 | coplanar_back.push poly 203 | else 204 | throw new Error("BUG: Polygon of classification #{side} in subdivision") 205 | 206 | ## 207 | ## ThreeBSP.Node 208 | class ThreeBSP.Node 209 | clone: => returning (node = new ThreeBSP.Node()), => 210 | node.divider = @divider?.clone() 211 | node.polygons = (p.clone() for p in @polygons) 212 | node.front = @front?.clone() 213 | node.back = @back?.clone() 214 | 215 | constructor: (polygons) -> 216 | @polygons = [] 217 | @build(polygons) if polygons? and polygons.length 218 | 219 | build: (polygons) => returning this, => 220 | sides = front: [], back: [] 221 | @divider ?= polygons[0].clone() 222 | for poly in polygons 223 | @divider.subdivide poly, @polygons, @polygons, sides.front, sides.back 224 | 225 | for own side, polys of sides 226 | if polys.length 227 | @[side] ?= new ThreeBSP.Node() 228 | @[side].build polys 229 | 230 | isConvex: (polys) => 231 | for inner in polys 232 | for outer in polys 233 | return false if inner != outer and outer.classifySide(inner) != BACK 234 | true 235 | 236 | allPolygons: => 237 | @polygons.slice() 238 | .concat(@front?.allPolygons() or []) 239 | .concat(@back?.allPolygons() or []) 240 | 241 | invert: => returning this, => 242 | for poly in @polygons 243 | do poly.invert 244 | for flipper in [@divider, @front, @back] 245 | flipper?.invert() 246 | [@front, @back] = [@back, @front] 247 | 248 | clipPolygons: (polygons) => 249 | return polygons.slice() unless @divider 250 | front = [] 251 | back = [] 252 | 253 | for poly in polygons 254 | @divider.subdivide poly, front, back, front, back 255 | 256 | front = @front.clipPolygons front if @front 257 | back = @back.clipPolygons back if @back 258 | 259 | return front.concat if @back then back else [] 260 | 261 | clipTo: (node) => returning this, => 262 | @polygons = node.clipPolygons @polygons 263 | @front?.clipTo node 264 | @back?.clipTo node 265 | --------------------------------------------------------------------------------