Summary Files Commits Branches @commit @diff @stats
diff options
context:
space:
mode:
Commite98fbbd145b6ff3d32f9a1e72708c52d54404f3a (patch)
Author
 Syndamia <kamen@syndamia.com>
2022-11-03 08:30:59 +0200
Committer
 Syndamia <kamen@syndamia.com>
2022-11-03 08:32:42 +0200
Filesd57a2a32299a0b9b6588ed8a03e9e87186d2d56e
uni-projects-e98fbbd145b6ff3d32f9a1e72708c52d54404f3a.tar
uni-projects-e98fbbd145b6ff3d32f9a1e72708c52d54404f3a.tar.gz
uni-projects-e98fbbd145b6ff3d32f9a1e72708c52d54404f3a.zip
Parent422c4067c35e8a33fdb2ac349b746b4492fa21b5 (diff)
[OKG] Added CSG library
1 files changed, 673 insertions, 0 deletions
-rw-r--r--Year 2/Computer Graphics Fundamentals/CSG.js673
diff --git a/Year 2/Computer Graphics Fundamentals/CSG.js b/Year 2/Computer Graphics Fundamentals/CSG.js
new file mode 100644
index 0000000..930b2e8
--- /dev/null
+++ b/Year 2/Computer Graphics Fundamentals/CSG.js
@@ -0,0 +1,673 @@
+// Taken from:
+// https://github.com/looeee/threejs-csg
+//
+// Demodulized. Merged into a single file.
+
+
+
+// Holds a node in adaptive partitioning BSP tree, built from a collection of polygons
+// by picking a polygon to split along. That polygon (and all other coplanar
+// polygons) are added directly to that node and the other polygons are added to
+// the front and/or back subtrees.
+
+class BSPNode {
+ constructor(polygons) {
+ this.plane = null;
+ this.front = null;
+ this.back = null;
+ this.polygons = [];
+ if (polygons) this.build(polygons);
+ }
+
+ clone() {
+ const node = new BSPNode();
+ node.plane = this.plane && this.plane.clone();
+ node.front = this.front && this.front.clone();
+ node.back = this.back && this.back.clone();
+ node.polygons = this.polygons.map(function (p) {
+ return p.clone();
+ });
+ return node;
+ }
+
+ // Convert solid space to empty space and empty space to solid space.
+ invert() {
+ for (let i = 0; i < this.polygons.length; i++) {
+ this.polygons[i].negate();
+ }
+ this.plane.negate();
+ if (this.front) this.front.invert();
+ if (this.back) this.back.invert();
+ const temp = this.front;
+ this.front = this.back;
+ this.back = temp;
+ }
+
+ // Recursively remove all polygons in `polygons` that are inside this BSP
+ // tree.
+ clipPolygons(polygons) {
+ if (!this.plane) return polygons.slice();
+ let front = [];
+ let back = [];
+ for (let i = 0; i < polygons.length; i++) {
+ this.plane.splitPolygon(polygons[i], front, back, front, back);
+ }
+ if (this.front) front = this.front.clipPolygons(front);
+ if (this.back) back = this.back.clipPolygons(back);
+ else back = [];
+ return front.concat(back);
+ }
+
+ // Remove all polygons in this BSP tree that are inside the other BSP tree
+ // `bsp`.
+ clipTo(bsp) {
+ this.polygons = bsp.clipPolygons(this.polygons);
+ if (this.front) this.front.clipTo(bsp);
+ if (this.back) this.back.clipTo(bsp);
+ }
+
+ // Return a list of all polygons in this BSP tree.
+ allPolygons() {
+ let polygons = this.polygons.slice();
+ if (this.front)
+ polygons = polygons.concat(this.front.allPolygons());
+ if (this.back)
+ polygons = polygons.concat(this.back.allPolygons());
+ return polygons;
+ }
+
+ // Build a BSP tree out of `polygons`. When called on an existing tree, the
+ // new polygons are filtered down to the bottom of the tree and become new
+ // nodes there. Each set of polygons is partitioned using the first polygon
+ // (no heuristic is used to pick a good split).
+ build(polygons) {
+ if (!polygons.length) return;
+ if (!this.plane) this.plane = polygons[0].plane;
+ const front = [];
+ const back = [];
+ for (let i = 0; i < polygons.length; i++) {
+ this.plane.splitPolygon(
+ polygons[i],
+ this.polygons,
+ this.polygons,
+ front,
+ back,
+ );
+ }
+ if (front.length) {
+ if (!this.front) this.front = new BSPNode();
+ this.front.build(front);
+ }
+ if (back.length) {
+ if (!this.back) this.back = new BSPNode();
+ this.back.build(back);
+ }
+ }
+}
+
+
+var _vector1 = new THREE.Vector3();
+var _vector2 = new THREE.Vector3();
+
+class CuttingPlane extends THREE.Plane {
+ constructor(normal, constant) {
+ super(normal, constant);
+ }
+
+ fromPoints(a, b, c) {
+ const normal = b
+ .subVectors(b, a)
+ .cross(_vector2.subVectors(c, a))
+ .normalize();
+
+ this.normal = normal;
+ this.constant = normal.dot(a);
+
+ return this;
+ }
+
+ setFromCoplanarPoints(a, b, c) {
+ var normal = _vector1
+ .subVectors(c, b)
+ .cross(_vector2.subVectors(a, b))
+ .normalize();
+
+ this.setFromNormalAndCoplanarPoint(normal, a);
+
+ return this;
+ }
+
+ setFromNormalAndCoplanarPoint(normal, point) {
+ this.normal.copy(normal);
+ this.constant = -point.dot(this.normal);
+
+ return this;
+ }
+
+ clone() {
+ return new CuttingPlane().copy(this);
+ }
+
+ copy(plane) {
+ this.normal.copy(plane.normal);
+ this.constant = plane.constant;
+
+ return this;
+ }
+
+ negate() {
+ this.constant *= -1;
+ this.normal.negate();
+
+ return this;
+ }
+
+ // Split `polygon` by this plane if needed, then put the polygon or polygon
+ // fragments in the appropriate lists. Coplanar polygons go into either
+ // `coplanarFront` or `coplanarBack` depending on their orientation with
+ // respect to this plane. Polygons in front or in back of this plane go into
+ // either `front` or `back`.
+ splitPolygon(polygon, coplanarFront, coplanarBack, front, back) {
+ const COPLANAR = 0;
+ const FRONT = 1;
+ const BACK = 2;
+ const SPANNING = 3;
+
+ // tolerance used to decide if a point is on the plane.
+ const EPSILON = 1e-5;
+
+ // Classify each point as well as the entire polygon into one of the above
+ // four classes.
+ let polygonType = 0;
+ const types = [];
+ for (let i = 0; i < polygon.vertices.length; i++) {
+ const t =
+ this.normal.dot(polygon.vertices[i].pos) - this.constant;
+ const type =
+ t < -EPSILON ? BACK : t > EPSILON ? FRONT : COPLANAR;
+ polygonType |= type;
+ types.push(type);
+ }
+
+ // Put the polygon in the correct list, splitting it when necessary.
+ switch (polygonType) {
+ case COPLANAR:
+ (this.normal.dot(polygon.plane.normal) > 0
+ ? coplanarFront
+ : coplanarBack
+ ).push(polygon);
+ break;
+ case FRONT:
+ front.push(polygon);
+ break;
+ case BACK:
+ back.push(polygon);
+ break;
+ case SPANNING:
+ const f = [];
+ const b = [];
+ for (let i = 0; i < polygon.vertices.length; i++) {
+ const j = (i + 1) % polygon.vertices.length;
+ const ti = types[i];
+ const tj = types[j];
+ const vi = polygon.vertices[i];
+ const vj = polygon.vertices[j];
+ if (ti != BACK) f.push(vi);
+ if (ti != FRONT) b.push(ti != BACK ? vi.clone() : vi);
+ if ((ti | tj) == SPANNING) {
+ const t =
+ (this.constant - this.normal.dot(vi.pos)) /
+ this.normal.dot(vj.pos.clone().sub(vi.pos));
+ const v = vi.interpolate(vj, t);
+ f.push(v);
+ b.push(v.clone());
+ }
+ }
+ if (f.length >= 3) {
+ const p = new Polygon(f);
+ front.push(p);
+ }
+
+ if (b.length >= 3) {
+ const p = new Polygon(b);
+ back.push(p);
+ }
+ break;
+ }
+ }
+}
+
+
+// # class Polygon
+
+// Represents a convex polygon. The vertices used to initialize a polygon must
+// be coplanar and form a convex loop. They do not have to be `Vertex`
+// instances but they must behave similarly (duck typing can be used for
+// customization).
+
+class Polygon {
+ constructor(vertices) {
+ this.vertices = vertices;
+ this.plane = new CuttingPlane().fromPoints(
+ vertices[0].pos.clone(),
+ vertices[1].pos.clone(),
+ vertices[2].pos.clone(),
+ );
+ }
+
+ clone() {
+ const vertices = this.vertices.map(function (v) {
+ return v.clone();
+ });
+ return new Polygon(vertices);
+ }
+
+ negate() {
+ this.vertices.reverse().map(function (v) {
+ v.negate();
+ });
+ this.plane.negate();
+ }
+}
+
+
+// # class Vertex
+
+// Represents a vertex of a polygon. Use your own vertex class instead of this
+// one to provide additional features like texture coordinates and vertex
+// colors. Custom vertex classes need to provide a `pos` property and `clone()`,
+// `flip()`, and `interpolate()` methods that behave analogous to the ones
+// defined by `Vertex`. This class provides `normal` so convenience
+// functions like `CSG.sphere()` can return a smooth vertex normal, but `normal`
+// is not used anywhere else.
+
+class Vertex {
+ constructor(pos, normal, uv) {
+ this.pos = pos;
+ if (normal) this.normal = normal;
+ if (uv) this.uv = uv;
+ }
+ clone() {
+ return new Vertex(
+ this.pos.clone(),
+ this.normal && this.normal.clone(),
+ this.uv && this.uv.clone(),
+ );
+ }
+
+ // Invert all orientation-specific data (e.g. vertex normal). Called when the
+ // orientation of a polygon is flipped.
+ negate() {
+ this.normal = this.normal && this.normal.negate();
+ }
+
+ // Create a new vertex between this vertex and `other` by linearly
+ // interpolating all properties using a parameter of `t`. Subclasses should
+ // override this to interpolate additional properties.
+ interpolate(other, t) {
+ let normal = null;
+ if (this.normal && other.normal) {
+ normal = this.normal.clone().lerp(other.normal, t);
+ }
+ let uv = null;
+ if (this.uv && other.uv) {
+ uv = this.uv.clone().lerp(other.uv, t);
+ }
+
+ return new Vertex(
+ this.pos.clone().lerp(other.pos, t),
+ normal,
+ uv,
+ );
+ }
+}
+
+// Constructive Solid Geometry (CSG) is a modeling technique that uses Boolean
+// operations like union and intersection to combine 3D solids. This library
+// implements CSG operations on meshes elegantly and concisely using BSP trees,
+// and is meant to serve as an easily understandable implementation of the
+// algorithm. All edge cases involving overlapping coplanar polygons in both
+// solids are correctly handled.
+//
+// ## Implementation Details
+//
+// All CSG operations are implemented in terms of two functions, `clipTo()` and
+// `invert()`, which remove parts of a BSP tree inside another BSP tree and swap
+// solid and empty space, respectively. To find the union of `a` and `b`, we
+// want to remove everything in `a` inside `b` and everything in `b` inside `a`,
+// then combine polygons from `a` and `b` into one solid:
+//
+// a.clipTo(b);
+// b.clipTo(a);
+// a.build(b.allPolygons());
+//
+// The only tricky part is handling overlapping coplanar polygons in both trees.
+// The code above keeps both copies, but we need to keep them in one tree and
+// remove them in the other tree. To remove them from `b` we can clip the
+// inverse of `b` against `a`. The code for union now looks like this:
+//
+// a.clipTo(b);
+// b.clipTo(a);
+// b.invert();
+// b.clipTo(a);
+// b.invert();
+// a.build(b.allPolygons());
+//
+// Subtraction and intersection naturally follow from set operations. If
+// union is `A | B`, subtraction is `A - B = ~(~A | B)` and intersection is
+// `A & B = ~(~A | ~B)` where `~` is the complement operator.
+//
+// ## License
+//
+// Copyright (c) 2011 Evan Wallace (http://madebyevan.com/), under the MIT license.
+
+// # class CSG
+
+// Holds a binary space partition tree representing a 3D solid. Two solids can
+// be combined using the `union()`, `subtract()`, and `intersect()` methods.
+
+class CSG {
+ constructor() {
+ this.polygons = [];
+ this.material = [];
+ }
+
+ setFromGeometry(geometry) {
+ if (!(geometry instanceof THREE.BufferGeometry)) {
+ console.error("This library only works with three.js BufferGeometry");
+ return;
+ }
+
+ if (geometry.index !== null) {
+ geometry = geometry.toNonIndexed();
+ }
+
+ const positions = geometry.attributes.position;
+ const normals = geometry.attributes.normal;
+ const uvs = geometry.attributes.uv;
+
+ // TODO
+ // const colors = geometry.attributes.color;
+
+ function createVertex(index) {
+ const position = new THREE.Vector3(
+ positions.getX(index),
+ positions.getY(index),
+ positions.getZ(index)
+ );
+ const normal = normals
+ ? new THREE.Vector3(
+ normals.getX(index),
+ normals.getY(index),
+ normals.getZ(index)
+ )
+ : null;
+
+ const uv = uvs ? new THREE.Vector2(uvs.getX(index), uvs.getY(index)) : null;
+
+ return new Vertex(position, normal, uv);
+ }
+
+ for (let i = 0; i < positions.count; i += 3) {
+ const v1 = createVertex(i);
+ const v2 = createVertex(i + 1);
+ const v3 = createVertex(i + 2);
+
+ this.polygons.push(new Polygon([v1, v2, v3]));
+ }
+
+ return this;
+ }
+
+ setFromMesh(mesh) {
+ mesh.updateWorldMatrix();
+
+ const transformedGeometry = mesh.geometry.clone();
+
+ transformedGeometry.applyMatrix4(mesh.matrix);
+
+ this.material.push(mesh.material);
+
+ this.setFromGeometry(transformedGeometry);
+ return this;
+ }
+
+ setPolygons(polygons) {
+ this.polygons = polygons;
+ return this;
+ }
+
+ toMesh() {
+ return new THREE.Mesh(this.toGeometry(), this.material[0]);
+ }
+
+ toGeometry() {
+ const geometry = new THREE.BufferGeometry();
+
+ const positions = [];
+ const normals = [];
+ const uvs = [];
+
+ const createFace = (a, b, c) => {
+ positions.push(
+ a.pos.x,
+ a.pos.y,
+ a.pos.z,
+ b.pos.x,
+ b.pos.y,
+ b.pos.z,
+ c.pos.x,
+ c.pos.y,
+ c.pos.z
+ );
+
+ // TODO: should not assume that all vertices have the same attributes
+ if (a.normal) {
+ normals.push(
+ a.normal.x,
+ a.normal.y,
+ a.normal.z,
+ b.normal.x,
+ b.normal.y,
+ b.normal.z,
+ c.normal.x,
+ c.normal.y,
+ c.normal.z
+ );
+ }
+
+ if (a.uv) {
+ uvs.push(a.uv.x, a.uv.y, b.uv.x, b.uv.y, c.uv.x, c.uv.y);
+ }
+ };
+
+ for (const polygon of this.polygons) {
+ // triangulate the polygon
+ for (let i = 0; i <= polygon.vertices.length - 3; i++) {
+ createFace(
+ polygon.vertices[0],
+ polygon.vertices[i + 1],
+ polygon.vertices[i + 2]
+ );
+ }
+ }
+
+ geometry.setAttribute(
+ "position",
+ new THREE.BufferAttribute(new Float32Array(positions), 3)
+ );
+ if (normals.length) {
+ geometry.setAttribute(
+ "normal",
+ new THREE.BufferAttribute(new Float32Array(normals), 3)
+ );
+ }
+ if (uvs.length) {
+ geometry.setAttribute(
+ "uv",
+ new THREE.BufferAttribute(new Float32Array(uvs), 2)
+ );
+ }
+
+ return geometry;
+ }
+
+ clone() {
+ const csg = new CSG();
+ csg.polygons = this.polygons.map(function (p) {
+ return p.clone();
+ });
+ return csg;
+ }
+
+ // Return a new CSG solid representing space in either this solid or in the
+ // solid `csg`
+ //
+ // A.union(B)
+ //
+ // +-------+ +-------+
+ // | | | |
+ // | A | | |
+ // | +--+----+ = | +----+
+ // +----+--+ | +----+ |
+ // | B | | |
+ // | | | |
+ // +-------+ +-------+
+ //
+ // A || B
+ union(operands) {
+ for (const operand of operands) {
+ // console.log('operand: ', operand);
+ if (!this.polygons.length) {
+ this.setFromMesh(operand);
+ } else {
+ // todo: support multimaterial
+ this.material.push(operand.material);
+ this.unionOperand(new CSG().setFromMesh(operand));
+ }
+ }
+
+ return this;
+ }
+
+ unionOperand(operand) {
+ const a = new BSPNode(this.polygons);
+ const b = new BSPNode(operand.polygons);
+ a.clipTo(b);
+ b.clipTo(a);
+ b.invert();
+ b.clipTo(a);
+ b.invert();
+ a.build(b.allPolygons());
+ this.polygons = a.allPolygons();
+ return this;
+ }
+
+ // Return a new CSG solid representing space in this solid but not in the
+ // solid `csg`
+ //
+ // A.subtract(B)
+ //
+ // +-------+ +-------+
+ // | | | |
+ // | A | | |
+ // | +--+----+ = | +--+
+ // +----+--+ | +----+
+ // | B |
+ // | |
+ // +-------+
+ //
+ // A && !B
+ subtract(operands) {
+ for (const operand of operands) {
+ if (!this.polygons.length) {
+ this.setFromMesh(operand);
+ } else {
+ this.material.push(operand.material);
+ this.subtractOperand(new CSG().setFromMesh(operand));
+ }
+ }
+
+ return this;
+ }
+
+ subtractOperand(operand) {
+ this.complement().unionOperand(operand).complement();
+ }
+ // subtractOperand(operand) {
+ // const a = new BSPNode(this.polygons);
+ // const b = new BSPNode(operand.polygons);
+ // a.invert();
+ // a.clipTo(b);
+ // b.clipTo(a);
+ // b.invert();
+ // b.clipTo(a);
+ // b.invert();
+ // a.build(b.allPolygons());
+ // a.invert();
+ // this.polygons = a.allPolygons();
+ // }
+
+ // Return a new CSG solid representing space both this solid and in the
+ // solid `csg`
+ //
+ // A.intersect(B)
+ //
+ // +-------+
+ // | |
+ // | A |
+ // | +--+----+ = +--+
+ // +----+--+ | +--+
+ // | B |
+ // | |
+ // +-------+
+ //
+ // A && B
+ intersect(operands) {
+ for (const operand of operands) {
+ if (!this.polygons.length) {
+ this.setFromMesh(operand);
+ } else {
+ this.material.push(operand.material);
+ this.intersectOperand(new CSG().setFromMesh(operand));
+ }
+ }
+
+ return this;
+ }
+
+ intersectOperand(operand) {
+ const a = new BSPNode(this.polygons);
+ const b = new BSPNode(operand.polygons);
+
+ const d = new BSPNode(this.clone().polygons);
+ const c = new BSPNode(operand.clone().polygons);
+
+ a.invert();
+ b.clipTo(a);
+ b.invert();
+ a.clipTo(b);
+ b.clipTo(a);
+ a.build(b.allPolygons());
+ a.invert();
+
+ c.invert();
+ d.clipTo(c);
+ d.invert();
+ c.clipTo(d);
+ d.clipTo(c);
+ c.build(d.allPolygons());
+ c.invert();
+
+ this.polygons = c.allPolygons().concat(a.allPolygons());
+ }
+
+ // Switch solid and empty space
+ // !A
+ complement() {
+ this.polygons.map((p) => {
+ p.negate();
+ });
+ return this;
+ }
+}