diff --git a/src/00-welcome-and-example/Example.ts b/src/00-welcome-and-example/Example.ts index 858c7edc17b79188832fdcc27ccc646d76eb4331..a5a6f7d4c0488d025800ebdec65f738beab1cd43 100644 --- a/src/00-welcome-and-example/Example.ts +++ b/src/00-welcome-and-example/Example.ts @@ -19,7 +19,7 @@ class Example extends PipelineData { const point2d = new Point2d(.1, 32, new THREE.Color(0xffffff)); const point3d = new Point3d(.1, 32, new THREE.Color(0xffffff)); const line1d = new Line1d(pointsA, new THREE.Color(0xffffff)); - const line2d = new Line2d(pointsB, .001, new THREE.Color(0xffffff)); + const line2d = new Line2d(pointsB, .01, new THREE.Color(0xffffff)); const mesh = new Mesh("../assets/suzanne.obj", new THREE.Color(0xffffff)); this.addShape(point2d, true, true); diff --git a/src/01-bezierCurves/CubicBezierCurve.ts b/src/01-bezierCurves/CubicBezierCurve.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b40aa69fea3f38b4598ac49476b7abe050eca6b --- /dev/null +++ b/src/01-bezierCurves/CubicBezierCurve.ts @@ -0,0 +1,85 @@ +import * as THREE from "three"; +import { Vector3 } from "three"; + +abstract class CubicBezierAlgorithm { + /** + * @param controlPointsPosition an array with vectors that give the positons of the control points. The + * first vector is the start point, the second vector is the first control point, the third vector is the + * second control point and the fourth vector is the end point. + * @param t the time at which the curve should be evaluated (0.0 - 1.0) + */ + abstract evaluatePosition(controlPointsPosition: Array<THREE.Vector3>, t: number): THREE.Vector3; +} + +class BernsteinAlgorithm extends CubicBezierAlgorithm { + evaluatePosition(positions: Array<THREE.Vector3>, t: number): THREE.Vector3 { + const c = BernsteinAlgorithm.calculateCoefficients(t); + const x = c[0] * positions[0].x + c[1] * positions[1].x + c[2] * positions[2].x + c[3] * positions[3].x; + const y = c[0] * positions[0].y + c[1] * positions[1].y + c[2] * positions[2].y + c[3] * positions[3].y; + const z = c[0] * positions[0].z + c[1] * positions[1].z + c[2] * positions[2].z + c[3] * positions[3].z; + return new THREE.Vector3(x, y, z); + } + + /** + * Weight the contribution of controlpoints by calculating the values of the different terms of the + * Bernstein polynome. The terms yield a coefficient of the different points. + * p(t) = (1-t)^3 * p0 + 3 * (1-t)^2 * t * p1 + + * 3 * (1-t) * t^2 * p2 + t^3 * p3 + */ + public static calculateCoefficients(t: number): number[]{ + const u0 = 1.0 * Math.pow(1.0 - t, 3.0) * Math.pow(t, 0.0); + const u1 = 3.0 * Math.pow(1.0 - t, 2.0) * Math.pow(t, 1.0); + const u2 = 3.0 * Math.pow(1.0 - t, 1.0) * Math.pow(t, 2.0); + const u3 = 1.0 * Math.pow(1.0 - t, 0.0) * Math.pow(t, 3.0); + return [u0, u1, u2, u3]; + } +} + +class DeCasteljauAlgorithm extends CubicBezierAlgorithm { + evaluatePosition(positions: Array<THREE.Vector3>, t: number): THREE.Vector3 { + while (positions.length > 1) + positions = DeCasteljauAlgorithm.Iteration(positions, t); + return positions[0]; + } + + public static Iteration(points: Array<THREE.Vector3>, t: number): Array<THREE.Vector3> + { + var result = new Array<THREE.Vector3>(); + for (let i = 0; i < points.length - 1; i++) + result.push(points[i + 1].clone().sub(points[i]).multiplyScalar(t).add(points[i])); + return result; + } +} + +class CubicBezierCurve { + + private _algorithm: CubicBezierAlgorithm; + private _controlPoints: Array<THREE.Vector3>; + + constructor(controlPoints: Array<THREE.Vector3>, algorithm: CubicBezierAlgorithm) { + this._controlPoints = controlPoints; + this._algorithm = algorithm; + } + + public evaluatePosition(t: number): THREE.Vector3 { + return this._algorithm.evaluatePosition(this._controlPoints, t); + } + + public evaluatePositions(resolution: number): Array<THREE.Vector3> { + const positions = new Array<THREE.Vector3>(); + for (let i = 0; i < resolution + 1; i++) + positions.push(this.evaluatePosition(i / resolution)); + return positions; + } + + public get controlPoints(): Array<THREE.Vector3> { + return this._controlPoints; + } + + public set controlPoints(value: Array<THREE.Vector3>) { + this._controlPoints = value; + } + +} + +export {CubicBezierAlgorithm, BernsteinAlgorithm, DeCasteljauAlgorithm, CubicBezierCurve}; \ No newline at end of file diff --git a/src/01-bezierCurves/Curve.ts b/src/01-bezierCurves/Curve.ts new file mode 100644 index 0000000000000000000000000000000000000000..d086d9926d954b9b34c5bf12345893b8270a2b4c --- /dev/null +++ b/src/01-bezierCurves/Curve.ts @@ -0,0 +1,47 @@ +import { Line2d, Point2d } from "../core/Shapes"; +import { CubicBezierCurve, DeCasteljauAlgorithm } from "./CubicBezierCurve"; + +interface CurveParameter { + pointA: Point2d; + tangentA: Point2d; + pointB: Point2d; + tangentB: Point2d; + + offset: number; + resolution: number; + linewidth: number; + color: THREE.Color; +} + +class Curve extends Line2d { + + private _generator: CubicBezierCurve; + private _resolution: number; + private _offset: number; + + constructor(parameter: CurveParameter) { + super([], parameter.linewidth, parameter.color); + + this._resolution = parameter.resolution; + this._offset = parameter.offset; + + const positions = new Array<THREE.Vector3>(); + positions.push(parameter.pointA.position); + positions.push(parameter.tangentA.position); + positions.push(parameter.tangentB.position); + positions.push(parameter.pointB.position); + + this._generator = new CubicBezierCurve(positions, new DeCasteljauAlgorithm()); + } + + public update(_deltaTime: number): void { + this._points = this._generator.evaluatePositions(this._resolution); + this._points.forEach((point) => { + point.z = this._offset; + }); + this._createLine(); + } +} + +export { Curve }; +export type { CurveParameter }; diff --git a/src/01-bezierCurves/DemoBezier.ts b/src/01-bezierCurves/DemoBezier.ts index 7589d58160e880f51021430d0975e510aebb133e..01ff12c93c394c2068368f7917bfb13d9b8e13fd 100644 --- a/src/01-bezierCurves/DemoBezier.ts +++ b/src/01-bezierCurves/DemoBezier.ts @@ -1,13 +1,39 @@ import * as THREE from 'three'; import { PipelineData } from "../core/Pipeline"; import { Point2d } from "../core/Shapes"; +import { Curve } from './Curve'; class BezierDemo extends PipelineData { public constructor() { super(); } data(): void { - const point = new Point2d(.1, 36, new THREE.Color(0x00ff00)); - this.addShape(point, true, true); + + const pointA = new Point2d(.05, 36, new THREE.Color(0x00ff00)); + const tangentA = new Point2d(.025, 36, new THREE.Color(0x00ff00)); + const pointB = new Point2d(.05, 36, new THREE.Color(0xff0000)); + const tangentB = new Point2d(.025, 36, new THREE.Color(0xff0000)); + + pointA.material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: false });; + tangentA.material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: false });; + pointB.material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: false });; + tangentB.material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: false });; + + const curve = new Curve({ + pointA: pointA, + tangentA: tangentA, + pointB: pointB, + tangentB: tangentB, + resolution: 128, + linewidth: .02, + offset: -1, + color: new THREE.Color(0xffffff) + }); + + this.addShape(pointA, true, true); + this.addShape(tangentA, true, true); + this.addShape(pointB, true, true); + this.addShape(tangentB, true, true); + this.addShape(curve, true, false); } } diff --git a/src/Main.ts b/src/Main.ts index 7d2cc2ce0d79b6c912d1de9d73637becc4915775..39ae54112b2f0fb94da3123acc4a912c885a76c1 100644 --- a/src/Main.ts +++ b/src/Main.ts @@ -7,21 +7,28 @@ import { QuaternionDemo } from "./02-quaternion/demoQuaternion"; import { SimulationDemo } from "./03-simulation/demoSimulation"; window.onload = () => { - const pipeline = new Pipeline(); const ref = window.location.href let scenario: PipelineData; + let pipeline: Pipeline; if (ref.includes("bezier")) { + pipeline = new Pipeline(false); + pipeline.orbitControls.enableRotate = false; scenario = new BezierDemo(); } else if (ref.includes("quaternion")) { + pipeline = new Pipeline(); scenario = new QuaternionDemo(); } else if (ref.includes("simulation")) { + pipeline = new Pipeline(); scenario = new SimulationDemo(); } else if (ref.includes("example")) { + pipeline = new Pipeline(); scenario = new Example(); } else { + pipeline = new Pipeline(); scenario = new Demo(); } + pipeline.init(scenario); pipeline.loop(); } diff --git a/src/core/Pipeline.ts b/src/core/Pipeline.ts index 60178015278e3257c10b8127e6fb559f1ea50eff..98ff748bc8897b883f617d36d5f45928e9c748c1 100644 --- a/src/core/Pipeline.ts +++ b/src/core/Pipeline.ts @@ -18,8 +18,6 @@ abstract class PipelineData { draggables: Array<THREE.Object3D>; scene: THREE.Scene; - allowOrbit: boolean = true; - constructor() { this.observers = new Array<PipelineObserver>(); this.draggables = new Array<THREE.Object3D>(); @@ -48,25 +46,39 @@ abstract class PipelineData { class Pipeline { public readonly renderer: THREE.WebGLRenderer; - public readonly camera: THREE.PerspectiveCamera; + public readonly camera: THREE.Camera; - public readonly orbitControls: OrbitControls; - public readonly dragControls: DragControls; + public orbitControls: OrbitControls; + public dragControls: DragControls; private scene?: THREE.Scene; private observer: Array<PipelineObserver>; private startTime: number; - constructor() { + constructor(perspective: boolean = true) { // CREATE CAMERA - this.camera = new THREE.PerspectiveCamera(75, - window.innerWidth / window.innerHeight, 0.1, 1000); - this.camera.position.z = 5; - window.addEventListener('resize', () => { - this.camera.aspect = window.innerWidth / window.innerHeight - this.camera.updateProjectionMatrix() - }, false); + if (perspective) { + this.camera = new THREE.PerspectiveCamera(75, + window.innerWidth / window.innerHeight, 0.1, 1000); + this.camera.position.z = 5; + + window.addEventListener('resize', () => { + (this.camera as THREE.PerspectiveCamera).aspect = window.innerWidth / window.innerHeight; + (this.camera as THREE.PerspectiveCamera).updateProjectionMatrix() + }, false); + } else { + const aspect = window.innerWidth / window.innerHeight; + this.camera = new THREE.OrthographicCamera(-aspect, aspect, 1, -1, 0.1, 1000); + this.camera.position.z = 1; + + window.addEventListener('resize', () => { + const aspect = window.innerWidth / window.innerHeight; + (this.camera as THREE.OrthographicCamera).left = -aspect; + (this.camera as THREE.OrthographicCamera).right = aspect; + (this.camera as THREE.OrthographicCamera).updateProjectionMatrix() + }, false); + } // CREATE RENDERER this.renderer = new THREE.WebGLRenderer({ diff --git a/src/core/Shapes.ts b/src/core/Shapes.ts index dc6485bc70b0bb3ac9924db281be430b9acc2968..8ef39e04ba9cb6dc7100ba33f86ba8adb2e45d33 100644 --- a/src/core/Shapes.ts +++ b/src/core/Shapes.ts @@ -25,34 +25,6 @@ abstract class Shape implements PipelineObserver { update(_deltaTime: number): void {/* INTENTIONAL */ } } -class DebugHelper implements PipelineObserver { - - private _debug: Map<Shape, THREE.BoxHelper>; - - constructor() { - this._debug = new Map<Shape, THREE.BoxHelper>(); - } - - public get debug(): Array<THREE.BoxHelper> { - return Array.from(this._debug.values()); - } - - public add(shape: Shape): void { - const box = new THREE.BoxHelper(shape.object3d); - this._debug.set(shape, box); - } - - public remove(shape: Shape): void { - this._debug.delete(shape); - } - - update(_deltaTime: number): void { - for (let [_, box] of this._debug) { - box.update(); - } - } -} - abstract class Point extends Shape { private _geometry: THREE.BufferGeometry; private _material: THREE.MeshBasicMaterial | THREE.MeshLambertMaterial; @@ -93,38 +65,13 @@ abstract class Point extends Shape { } public set material(material: THREE.MeshBasicMaterial | THREE.MeshLambertMaterial) { - this._material = material; + this._material = material.clone(); this._material.color = this._color; (this.object3d as THREE.Mesh).material = material; } } -class Point2d extends Point { - constructor(radius: number, resolution: number, color: THREE.Color) { - super(radius, resolution, color, new THREE.CircleBufferGeometry(radius, resolution), - new THREE.MeshBasicMaterial({ color: color.getHex(), wireframe: true })); - } - - protected _createMesh(): void { - (this._object3d as THREE.Mesh).geometry.dispose(); - (this._object3d as THREE.Mesh).geometry = new THREE.CircleBufferGeometry(this._radius, this._resolution); - } -} - -class Point3d extends Point { - constructor(radius: number, resolution: number, color: THREE.Color) { - super(radius, resolution, color, new THREE.SphereBufferGeometry(radius, resolution, resolution / 2), - new THREE.MeshBasicMaterial({ color: color.getHex(), wireframe: true })); - } - - protected _createMesh(): void { - (this._object3d as THREE.Mesh).geometry.dispose(); - (this._object3d as THREE.Mesh).geometry = new THREE.SphereBufferGeometry(this._radius, this._resolution, - this._resolution / 2); - } -} - abstract class Line extends Shape { protected _geometry: THREE.BufferGeometry; protected _material: LineMaterial | THREE.LineBasicMaterial; @@ -142,7 +89,11 @@ abstract class Line extends Shape { this._geometry = geometry; this._material = material; - this._object3d = new THREE.Line(this._geometry, this._material); + if (this._material instanceof LineMaterial) { + this._object3d = new THREE.Mesh(this._geometry, this._material); + } else { + this._object3d = new THREE.Line(this._geometry, this._material); + } } protected abstract _createLine(): void; @@ -158,13 +109,69 @@ abstract class Line extends Shape { } public set material(material: LineMaterial | THREE.LineBasicMaterial) { - this._material = material; + this._material = material.clone(); this._material.color = this._color; (this._object3d as THREE.Line).material = material; } } +class DebugHelper implements PipelineObserver { + + private _debug: Map<Shape, THREE.BoxHelper>; + + constructor() { + this._debug = new Map<Shape, THREE.BoxHelper>(); + } + + public get debug(): Array<THREE.BoxHelper> { + return Array.from(this._debug.values()); + } + + public add(shape: Shape): void { + const box = new THREE.BoxHelper(shape.object3d); + this._debug.set(shape, box); + } + + public remove(shape: Shape): void { + this._debug.delete(shape); + } + + update(_deltaTime: number): void { + for (let [_, box] of this._debug) { + box.update(); + } + } +} + +class Point2d extends Point { + constructor(radius: number, resolution: number, color: THREE.Color) { + super(radius, resolution, color, new THREE.CircleBufferGeometry(radius, resolution), + new THREE.MeshBasicMaterial({ color: color.getHex(), wireframe: true })); + } + + protected _createMesh(): void { + (this._object3d as THREE.Mesh).geometry.dispose(); + (this._object3d as THREE.Mesh).geometry = new THREE.CircleBufferGeometry(this._radius, this._resolution); + } +} + +class Point3d extends Point { + constructor(radius: number, resolution: number, color: THREE.Color) { + super(radius, resolution, color, new THREE.SphereBufferGeometry(radius, resolution, resolution / 2), + new THREE.MeshBasicMaterial({ color: color.getHex(), wireframe: true })); + } + + protected _createMesh(): void { + (this._object3d as THREE.Mesh).geometry.dispose(); + (this._object3d as THREE.Mesh).geometry = new THREE.SphereBufferGeometry(this._radius, this._resolution, + this._resolution / 2); + } +} + function pointsToBuffer(points: Array<THREE.Vector3>): LineGeometry { + if (points.length < 2) { + return new LineGeometry(); + } return new LineGeometry().setPositions(points.reduce((acc, cur) => { acc.push(cur.x, cur.y, cur.z); return acc;}