p5
Version:
[](https://www.npmjs.com/package/p5)
1,693 lines (1,621 loc) • 75.8 kB
JavaScript
import { F as FLAT, y as SMOOTH } from '../constants-BRcElHU3.js';
import { DataArray } from './p5.DataArray.js';
import { Vector } from '../math/p5.Vector.js';
/**
* @module Shape
* @submodule 3D Primitives
* @for p5
* @requires core
* @requires p5.Geometry
*/
class Geometry {
constructor(detailX, detailY, callback, renderer) {
this.renderer = renderer;
this.vertices = [];
this.boundingBoxCache = null;
//an array containing every vertex for stroke drawing
this.lineVertices = new DataArray();
// The tangents going into or out of a vertex on a line. Along a straight
// line segment, both should be equal. At an endpoint, one or the other
// will not exist and will be all 0. In joins between line segments, they
// may be different, as they will be the tangents on either side of the join.
this.lineTangentsIn = new DataArray();
this.lineTangentsOut = new DataArray();
// When drawing lines with thickness, entries in this buffer represent which
// side of the centerline the vertex will be placed. The sign of the number
// will represent the side of the centerline, and the absolute value will be
// used as an enum to determine which part of the cap or join each vertex
// represents. See the doc comments for _addCap and _addJoin for diagrams.
this.lineSides = new DataArray();
this.vertexNormals = [];
this.faces = [];
this.uvs = [];
// a 2D array containing edge connectivity pattern for create line vertices
//based on faces for most objects;
this.edges = [];
this.vertexColors = [];
// One color per vertex representing the stroke color at that vertex
this.vertexStrokeColors = [];
this.userVertexProperties = {};
// One color per line vertex, generated automatically based on
// vertexStrokeColors in _edgesToVertices()
this.lineVertexColors = new DataArray();
this.detailX = detailX !== undefined ? detailX : 1;
this.detailY = detailY !== undefined ? detailY : 1;
this.dirtyFlags = {};
this._hasFillTransparency = undefined;
this._hasStrokeTransparency = undefined;
this.gid = `_p5_Geometry_${Geometry.nextId}`;
Geometry.nextId++;
if (callback instanceof Function) {
callback.call(this);
}
}
/**
* Calculates the position and size of the smallest box that contains the geometry.
*
* A bounding box is the smallest rectangular prism that contains the entire
* geometry. It's defined by the box's minimum and maximum coordinates along
* each axis, as well as the size (length) and offset (center).
*
* Calling `myGeometry.calculateBoundingBox()` returns an object with four
* properties that describe the bounding box:
*
* ```js
* // Get myGeometry's bounding box.
* let bbox = myGeometry.calculateBoundingBox();
*
* // Print the bounding box to the console.
* console.log(bbox);
*
* // {
* // // The minimum coordinate along each axis.
* // min: { x: -1, y: -2, z: -3 },
* //
* // // The maximum coordinate along each axis.
* // max: { x: 1, y: 2, z: 3},
* //
* // // The size (length) along each axis.
* // size: { x: 2, y: 4, z: 6},
* //
* // // The offset (center) along each axis.
* // offset: { x: 0, y: 0, z: 0}
* // }
* ```
*
* @returns {Object} bounding box of the geometry.
*
* @example
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let particles;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create a new p5.Geometry object with random spheres.
* particles = buildGeometry(createParticles);
*
* describe('Ten white spheres placed randomly against a gray background. A box encloses the spheres.');
* }
*
* function draw() {
* background(50);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Turn on the lights.
* lights();
*
* // Style the particles.
* noStroke();
* fill(255);
*
* // Draw the particles.
* model(particles);
*
* // Calculate the bounding box.
* let bbox = particles.calculateBoundingBox();
*
* // Translate to the bounding box's center.
* translate(bbox.offset.x, bbox.offset.y, bbox.offset.z);
*
* // Style the bounding box.
* stroke(255);
* noFill();
*
* // Draw the bounding box.
* box(bbox.size.x, bbox.size.y, bbox.size.z);
* }
*
* function createParticles() {
* for (let i = 0; i < 10; i += 1) {
* // Calculate random coordinates.
* let x = randomGaussian(0, 15);
* let y = randomGaussian(0, 15);
* let z = randomGaussian(0, 15);
*
* push();
* // Translate to the particle's coordinates.
* translate(x, y, z);
* // Draw the particle.
* sphere(3);
* pop();
* }
* }
* </code>
* </div>
*/
calculateBoundingBox() {
if (this.boundingBoxCache) {
return this.boundingBoxCache; // Return cached result if available
}
let minVertex = new Vector(
Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
let maxVertex = new Vector(
Number.MIN_VALUE, Number.MIN_VALUE, Number.MIN_VALUE);
for (let i = 0; i < this.vertices.length; i++) {
let vertex = this.vertices[i];
minVertex.x = Math.min(minVertex.x, vertex.x);
minVertex.y = Math.min(minVertex.y, vertex.y);
minVertex.z = Math.min(minVertex.z, vertex.z);
maxVertex.x = Math.max(maxVertex.x, vertex.x);
maxVertex.y = Math.max(maxVertex.y, vertex.y);
maxVertex.z = Math.max(maxVertex.z, vertex.z);
}
// Calculate size and offset properties
let size = new Vector(maxVertex.x - minVertex.x,
maxVertex.y - minVertex.y, maxVertex.z - minVertex.z);
let offset = new Vector((minVertex.x + maxVertex.x) / 2,
(minVertex.y + maxVertex.y) / 2, (minVertex.z + maxVertex.z) / 2);
// Cache the result for future access
this.boundingBoxCache = {
min: minVertex,
max: maxVertex,
size: size,
offset: offset
};
return this.boundingBoxCache;
}
reset() {
this._hasFillTransparency = undefined;
this._hasStrokeTransparency = undefined;
this.lineVertices.clear();
this.lineTangentsIn.clear();
this.lineTangentsOut.clear();
this.lineSides.clear();
this.vertices.length = 0;
this.edges.length = 0;
this.vertexColors.length = 0;
this.vertexStrokeColors.length = 0;
this.lineVertexColors.clear();
this.vertexNormals.length = 0;
this.uvs.length = 0;
for (const propName in this.userVertexProperties){
this.userVertexProperties[propName].delete();
}
this.userVertexProperties = {};
this.dirtyFlags = {};
}
hasFillTransparency() {
if (this._hasFillTransparency === undefined) {
this._hasFillTransparency = false;
for (let i = 0; i < this.vertexColors.length; i += 4) {
if (this.vertexColors[i + 3] < 1) {
this._hasFillTransparency = true;
break;
}
}
}
return this._hasFillTransparency;
}
hasStrokeTransparency() {
if (this._hasStrokeTransparency === undefined) {
this._hasStrokeTransparency = false;
for (let i = 0; i < this.lineVertexColors.length; i += 4) {
if (this.lineVertexColors[i + 3] < 1) {
this._hasStrokeTransparency = true;
break;
}
}
}
return this._hasStrokeTransparency;
}
/**
* Removes the geometry’s internal colors.
*
* `p5.Geometry` objects can be created with "internal colors" assigned to
* vertices or the entire shape. When a geometry has internal colors,
* <a href="#/p5/fill">fill()</a> has no effect. Calling
* `myGeometry.clearColors()` allows the
* <a href="#/p5/fill">fill()</a> function to apply color to the geometry.
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100, WEBGL);
*
* background(200);
*
* // Create a p5.Geometry object.
* // Set its internal color to red.
* let myGeometry = buildGeometry(function() {
* fill(255, 0, 0);
* plane(20);
* });
*
* // Style the shape.
* noStroke();
*
* // Draw the p5.Geometry object (center).
* model(myGeometry);
*
* // Translate the origin to the bottom-right.
* translate(25, 25, 0);
*
* // Try to fill the geometry with green.
* fill(0, 255, 0);
*
* // Draw the geometry again (bottom-right).
* model(myGeometry);
*
* // Clear the geometry's colors.
* myGeometry.clearColors();
*
* // Fill the geometry with blue.
* fill(0, 0, 255);
*
* // Translate the origin up.
* translate(0, -50, 0);
*
* // Draw the geometry again (top-right).
* model(myGeometry);
*
* describe(
* 'Three squares drawn against a gray background. Red squares are at the center and the bottom-right. A blue square is at the top-right.'
* );
* }
* </code>
* </div>
*/
clearColors() {
this.vertexColors = [];
return this;
}
/**
* The `saveObj()` function exports `p5.Geometry` objects as
* 3D models in the Wavefront .obj file format.
* This way, you can use the 3D shapes you create in p5.js in other software
* for rendering, animation, 3D printing, or more.
*
* The exported .obj file will include the faces and vertices of the `p5.Geometry`,
* as well as its texture coordinates and normals, if it has them.
*
* @method saveObj
* @param {String} [fileName='model.obj'] The name of the file to save the model as.
* If not specified, the default file name will be 'model.obj'.
* @example
* <div>
* <code>
* let myModel;
* let saveBtn;
* function setup() {
* createCanvas(200, 200, WEBGL);
* myModel = buildGeometry(function()) {
* for (let i = 0; i < 5; i++) {
* push();
* translate(
* random(-75, 75),
* random(-75, 75),
* random(-75, 75)
* );
* sphere(random(5, 50));
* pop();
* }
* });
*
* saveBtn = createButton('Save .obj');
* saveBtn.mousePressed(() => myModel.saveObj());
*
* describe('A few spheres rotating in space');
* }
*
* function draw() {
* background(0);
* noStroke();
* lights();
* rotateX(millis() * 0.001);
* rotateY(millis() * 0.002);
* model(myModel);
* }
* </code>
* </div>
*/
saveObj(fileName = 'model.obj') {
let objStr= '';
// Vertices
this.vertices.forEach(v => {
objStr += `v ${v.x} ${v.y} ${v.z}\n`;
});
// Texture Coordinates (UVs)
if (this.uvs && this.uvs.length > 0) {
for (let i = 0; i < this.uvs.length; i += 2) {
objStr += `vt ${this.uvs[i]} ${this.uvs[i + 1]}\n`;
}
}
// Vertex Normals
if (this.vertexNormals && this.vertexNormals.length > 0) {
this.vertexNormals.forEach(n => {
objStr += `vn ${n.x} ${n.y} ${n.z}\n`;
});
}
// Faces, obj vertex indices begin with 1 and not 0
// texture coordinate (uvs) and vertexNormal indices
// are indicated with trailing ints vertex/normal/uv
// ex 1/1/1 or 2//2 for vertices without uvs
this.faces.forEach(face => {
let faceStr = 'f';
face.forEach(index =>{
faceStr += ' ';
faceStr += index + 1;
if (this.vertexNormals.length > 0 || this.uvs.length > 0) {
faceStr += '/';
if (this.uvs.length > 0) {
faceStr += index + 1;
}
faceStr += '/';
if (this.vertexNormals.length > 0) {
faceStr += index + 1;
}
}
});
objStr += faceStr + '\n';
});
const blob = new Blob([objStr], { type: 'text/plain' });
fn.downloadFile(blob, fileName , 'obj');
}
/**
* The `saveStl()` function exports `p5.Geometry` objects as
* 3D models in the STL stereolithography file format.
* This way, you can use the 3D shapes you create in p5.js in other software
* for rendering, animation, 3D printing, or more.
*
* The exported .stl file will include the faces, vertices, and normals of the `p5.Geometry`.
*
* By default, this method saves a text-based .stl file. Alternatively, you can save a more compact
* but less human-readable binary .stl file by passing `{ binary: true }` as a second parameter.
*
* @method saveStl
* @param {String} [fileName='model.stl'] The name of the file to save the model as.
* If not specified, the default file name will be 'model.stl'.
* @param {Object} [options] Optional settings. Options can include a boolean `binary` property, which
* controls whether or not a binary .stl file is saved. It defaults to false.
* @example
* <div>
* <code>
* let myModel;
* let saveBtn1;
* let saveBtn2;
* function setup() {
* createCanvas(200, 200, WEBGL);
* myModel = buildGeometry(function() {
* for (let i = 0; i < 5; i++) {
* push();
* translate(
* random(-75, 75),
* random(-75, 75),
* random(-75, 75)
* );
* sphere(random(5, 50));
* pop();
* }
* });
*
* saveBtn1 = createButton('Save .stl');
* saveBtn1.mousePressed(function() {
* myModel.saveStl();
* });
* saveBtn2 = createButton('Save binary .stl');
* saveBtn2.mousePressed(function() {
* myModel.saveStl('model.stl', { binary: true });
* });
*
* describe('A few spheres rotating in space');
* }
*
* function draw() {
* background(0);
* noStroke();
* lights();
* rotateX(millis() * 0.001);
* rotateY(millis() * 0.002);
* model(myModel);
* }
* </code>
* </div>
*/
saveStl(fileName = 'model.stl', { binary = false } = {}){
let modelOutput;
let name = fileName.substring(0, fileName.lastIndexOf('.'));
let faceNormals = [];
for (let f of this.faces) {
const U = Vector.sub(this.vertices[f[1]], this.vertices[f[0]]);
const V = Vector.sub(this.vertices[f[2]], this.vertices[f[0]]);
const nx = U.y * V.z - U.z * V.y;
const ny = U.z * V.x - U.x * V.z;
const nz = U.x * V.y - U.y * V.x;
faceNormals.push(new Vector(nx, ny, nz).normalize());
}
if (binary) {
let offset = 80;
const bufferLength =
this.faces.length * 2 + this.faces.length * 3 * 4 * 4 + 80 + 4;
const arrayBuffer = new ArrayBuffer(bufferLength);
modelOutput = new DataView(arrayBuffer);
modelOutput.setUint32(offset, this.faces.length, true);
offset += 4;
for (const [key, f] of Object.entries(this.faces)) {
const norm = faceNormals[key];
modelOutput.setFloat32(offset, norm.x, true);
offset += 4;
modelOutput.setFloat32(offset, norm.y, true);
offset += 4;
modelOutput.setFloat32(offset, norm.z, true);
offset += 4;
for (let vertexIndex of f) {
const vert = this.vertices[vertexIndex];
modelOutput.setFloat32(offset, vert.x, true);
offset += 4;
modelOutput.setFloat32(offset, vert.y, true);
offset += 4;
modelOutput.setFloat32(offset, vert.z, true);
offset += 4;
}
modelOutput.setUint16(offset, 0, true);
offset += 2;
}
} else {
modelOutput = 'solid ' + name + '\n';
for (const [key, f] of Object.entries(this.faces)) {
const norm = faceNormals[key];
modelOutput +=
' facet norm ' + norm.x + ' ' + norm.y + ' ' + norm.z + '\n';
modelOutput += ' outer loop' + '\n';
for (let vertexIndex of f) {
const vert = this.vertices[vertexIndex];
modelOutput +=
' vertex ' + vert.x + ' ' + vert.y + ' ' + vert.z + '\n';
}
modelOutput += ' endloop' + '\n';
modelOutput += ' endfacet' + '\n';
}
modelOutput += 'endsolid ' + name + '\n';
}
const blob = new Blob([modelOutput], { type: 'text/plain' });
fn.downloadFile(blob, fileName, 'stl');
}
/**
* Flips the geometry’s texture u-coordinates.
*
* In order for <a href="#/p5/texture">texture()</a> to work, the geometry
* needs a way to map the points on its surface to the pixels in a rectangular
* image that's used as a texture. The geometry's vertex at coordinates
* `(x, y, z)` maps to the texture image's pixel at coordinates `(u, v)`.
*
* The <a href="#/p5.Geometry/uvs">myGeometry.uvs</a> array stores the
* `(u, v)` coordinates for each vertex in the order it was added to the
* geometry. Calling `myGeometry.flipU()` flips a geometry's u-coordinates
* so that the texture appears mirrored horizontally.
*
* For example, a plane's four vertices are added clockwise starting from the
* top-left corner. Here's how calling `myGeometry.flipU()` would change a
* plane's texture coordinates:
*
* ```js
* // Print the original texture coordinates.
* // Output: [0, 0, 1, 0, 0, 1, 1, 1]
* console.log(myGeometry.uvs);
*
* // Flip the u-coordinates.
* myGeometry.flipU();
*
* // Print the flipped texture coordinates.
* // Output: [1, 0, 0, 0, 1, 1, 0, 1]
* console.log(myGeometry.uvs);
*
* // Notice the swaps:
* // Top vertices: [0, 0, 1, 0] --> [1, 0, 0, 0]
* // Bottom vertices: [0, 1, 1, 1] --> [1, 1, 0, 1]
* ```
*
* @for p5.Geometry
*
* @example
* <div>
* <code>
* let img;
*
* async function setup() {
* img = await loadImage('assets/laDefense.jpg');
* createCanvas(100, 100, WEBGL);
*
* background(200);
*
* // Create p5.Geometry objects.
* let geom1 = buildGeometry(createShape);
* let geom2 = buildGeometry(createShape);
*
* // Flip geom2's U texture coordinates.
* geom2.flipU();
*
* // Left (original).
* push();
* translate(-25, 0, 0);
* texture(img);
* noStroke();
* model(geom1);
* pop();
*
* // Right (flipped).
* push();
* translate(25, 0, 0);
* texture(img);
* noStroke();
* model(geom2);
* pop();
*
* describe(
* 'Two photos of a ceiling on a gray background. The photos are mirror images of each other.'
* );
* }
*
* function createShape() {
* plane(40);
* }
* </code>
* </div>
*/
flipU() {
this.uvs = this.uvs.flat().map((val, index) => {
if (index % 2 === 0) {
return 1 - val;
} else {
return val;
}
});
}
/**
* Flips the geometry’s texture v-coordinates.
*
* In order for <a href="#/p5/texture">texture()</a> to work, the geometry
* needs a way to map the points on its surface to the pixels in a rectangular
* image that's used as a texture. The geometry's vertex at coordinates
* `(x, y, z)` maps to the texture image's pixel at coordinates `(u, v)`.
*
* The <a href="#/p5.Geometry/uvs">myGeometry.uvs</a> array stores the
* `(u, v)` coordinates for each vertex in the order it was added to the
* geometry. Calling `myGeometry.flipV()` flips a geometry's v-coordinates
* so that the texture appears mirrored vertically.
*
* For example, a plane's four vertices are added clockwise starting from the
* top-left corner. Here's how calling `myGeometry.flipV()` would change a
* plane's texture coordinates:
*
* ```js
* // Print the original texture coordinates.
* // Output: [0, 0, 1, 0, 0, 1, 1, 1]
* console.log(myGeometry.uvs);
*
* // Flip the v-coordinates.
* myGeometry.flipV();
*
* // Print the flipped texture coordinates.
* // Output: [0, 1, 1, 1, 0, 0, 1, 0]
* console.log(myGeometry.uvs);
*
* // Notice the swaps:
* // Left vertices: [0, 0] <--> [1, 0]
* // Right vertices: [1, 0] <--> [1, 1]
* ```
*
* @method flipV
* @for p5.Geometry
*
* @example
* <div>
* <code>
* let img;
*
* async function setup() {
* img = await loadImage('assets/laDefense.jpg');
* createCanvas(100, 100, WEBGL);
*
* background(200);
*
* // Create p5.Geometry objects.
* let geom1 = buildGeometry(createShape);
* let geom2 = buildGeometry(createShape);
*
* // Flip geom2's V texture coordinates.
* geom2.flipV();
*
* // Left (original).
* push();
* translate(-25, 0, 0);
* texture(img);
* noStroke();
* model(geom1);
* pop();
*
* // Right (flipped).
* push();
* translate(25, 0, 0);
* texture(img);
* noStroke();
* model(geom2);
* pop();
*
* describe(
* 'Two photos of a ceiling on a gray background. The photos are mirror images of each other.'
* );
* }
*
* function createShape() {
* plane(40);
* }
* </code>
* </div>
*/
flipV() {
this.uvs = this.uvs.flat().map((val, index) => {
if (index % 2 === 0) {
return val;
} else {
return 1 - val;
}
});
}
/**
* Computes the geometry's faces using its vertices.
*
* All 3D shapes are made by connecting sets of points called *vertices*. A
* geometry's surface is formed by connecting vertices to form triangles that
* are stitched together. Each triangular patch on the geometry's surface is
* called a *face*. `myGeometry.computeFaces()` performs the math needed to
* define each face based on the distances between vertices.
*
* The geometry's vertices are stored as <a href="#/p5.Vector">p5.Vector</a>
* objects in the <a href="#/p5.Geometry/vertices">myGeometry.vertices</a>
* array. The geometry's first vertex is the
* <a href="#/p5.Vector">p5.Vector</a> object at `myGeometry.vertices[0]`,
* its second vertex is `myGeometry.vertices[1]`, its third vertex is
* `myGeometry.vertices[2]`, and so on.
*
* Calling `myGeometry.computeFaces()` fills the
* <a href="#/p5.Geometry/faces">myGeometry.faces</a> array with three-element
* arrays that list the vertices that form each face. For example, a geometry
* made from a rectangle has two faces because a rectangle is made by joining
* two triangles. <a href="#/p5.Geometry/faces">myGeometry.faces</a> for a
* rectangle would be the two-dimensional array
* `[[0, 1, 2], [2, 1, 3]]`. The first face, `myGeometry.faces[0]`, is the
* array `[0, 1, 2]` because it's formed by connecting
* `myGeometry.vertices[0]`, `myGeometry.vertices[1]`,and
* `myGeometry.vertices[2]`. The second face, `myGeometry.faces[1]`, is the
* array `[2, 1, 3]` because it's formed by connecting
* `myGeometry.vertices[2]`, `myGeometry.vertices[1]`, and
* `myGeometry.vertices[3]`.
*
* Note: `myGeometry.computeFaces()` only works when geometries have four or more vertices.
*
* @chainable
*
* @example
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let myGeometry;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create a p5.Geometry object.
* myGeometry = new p5.Geometry();
*
* // Create p5.Vector objects to position the vertices.
* let v0 = createVector(-40, 0, 0);
* let v1 = createVector(0, -40, 0);
* let v2 = createVector(0, 40, 0);
* let v3 = createVector(40, 0, 0);
*
* // Add the vertices to myGeometry's vertices array.
* myGeometry.vertices.push(v0, v1, v2, v3);
*
* // Compute myGeometry's faces array.
* myGeometry.computeFaces();
*
* describe('A red square drawn on a gray background.');
* }
*
* function draw() {
* background(200);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Turn on the lights.
* lights();
*
* // Style the shape.
* noStroke();
* fill(255, 0, 0);
*
* // Draw the p5.Geometry object.
* model(myGeometry);
* }
* </code>
* </div>
*
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let myGeometry;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create a p5.Geometry object using a callback function.
* myGeometry = new p5.Geometry(1, 1, createShape);
*
* describe('A red square drawn on a gray background.');
* }
*
* function draw() {
* background(200);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Turn on the lights.
* lights();
*
* // Style the shape.
* noStroke();
* fill(255, 0, 0);
*
* // Draw the p5.Geometry object.
* model(myGeometry);
* }
*
* function createShape() {
* // Create p5.Vector objects to position the vertices.
* let v0 = createVector(-40, 0, 0);
* let v1 = createVector(0, -40, 0);
* let v2 = createVector(0, 40, 0);
* let v3 = createVector(40, 0, 0);
*
* // Add the vertices to the p5.Geometry object's vertices array.
* this.vertices.push(v0, v1, v2, v3);
*
* // Compute the faces array.
* this.computeFaces();
* }
* </code>
* </div>
*/
computeFaces() {
this.faces.length = 0;
const sliceCount = this.detailX + 1;
let a, b, c, d;
for (let i = 0; i < this.detailY; i++) {
for (let j = 0; j < this.detailX; j++) {
a = i * sliceCount + j; // + offset;
b = i * sliceCount + j + 1; // + offset;
c = (i + 1) * sliceCount + j + 1; // + offset;
d = (i + 1) * sliceCount + j; // + offset;
this.faces.push([a, b, d]);
this.faces.push([d, b, c]);
}
}
return this;
}
_getFaceNormal(faceId) {
//This assumes that vA->vB->vC is a counter-clockwise ordering
const face = this.faces[faceId];
const vA = this.vertices[face[0]];
const vB = this.vertices[face[1]];
const vC = this.vertices[face[2]];
const ab = Vector.sub(vB, vA);
const ac = Vector.sub(vC, vA);
const n = Vector.cross(ab, ac);
const ln = Vector.mag(n);
let sinAlpha = ln / (Vector.mag(ab) * Vector.mag(ac));
if (sinAlpha === 0 || isNaN(sinAlpha)) {
console.warn(
'p5.Geometry.prototype._getFaceNormal:',
'face has colinear sides or a repeated vertex'
);
return n;
}
if (sinAlpha > 1) sinAlpha = 1; // handle float rounding error
return n.mult(Math.asin(sinAlpha) / ln);
}
/**
* Calculates the normal vector for each vertex on the geometry.
*
* All 3D shapes are made by connecting sets of points called *vertices*. A
* geometry's surface is formed by connecting vertices to create triangles
* that are stitched together. Each triangular patch on the geometry's
* surface is called a *face*. `myGeometry.computeNormals()` performs the
* math needed to orient each face. Orientation is important for lighting
* and other effects.
*
* A face's orientation is defined by its *normal vector* which points out
* of the face and is normal (perpendicular) to the surface. Calling
* `myGeometry.computeNormals()` first calculates each face's normal vector.
* Then it calculates the normal vector for each vertex by averaging the
* normal vectors of the faces surrounding the vertex. The vertex normals
* are stored as <a href="#/p5.Vector">p5.Vector</a> objects in the
* <a href="#/p5.Geometry/vertexNormals">myGeometry.vertexNormals</a> array.
*
* The first parameter, `shadingType`, is optional. Passing the constant
* `FLAT`, as in `myGeometry.computeNormals(FLAT)`, provides neighboring
* faces with their own copies of the vertices they share. Surfaces appear
* tiled with flat shading. Passing the constant `SMOOTH`, as in
* `myGeometry.computeNormals(SMOOTH)`, makes neighboring faces reuse their
* shared vertices. Surfaces appear smoother with smooth shading. By
* default, `shadingType` is `FLAT`.
*
* The second parameter, `options`, is also optional. If an object with a
* `roundToPrecision` property is passed, as in
* `myGeometry.computeNormals(SMOOTH, { roundToPrecision: 5 })`, it sets the
* number of decimal places to use for calculations. By default,
* `roundToPrecision` uses 3 decimal places.
*
* @param {(FLAT|SMOOTH)} [shadingType=FLAT] shading type. either FLAT or SMOOTH. Defaults to `FLAT`.
* @param {Object} [options] shading options.
* @chainable
*
* @example
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let myGeometry;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create a p5.Geometry object.
* myGeometry = buildGeometry(function() {
* torus();
* });
*
* // Compute the vertex normals.
* myGeometry.computeNormals();
*
* describe(
* "A white torus drawn on a dark gray background. Red lines extend outward from the torus' vertices."
* );
* }
*
* function draw() {
* background(50);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Turn on the lights.
* lights();
*
* // Rotate the coordinate system.
* rotateX(1);
*
* // Style the helix.
* stroke(0);
*
* // Display the helix.
* model(myGeometry);
*
* // Style the normal vectors.
* stroke(255, 0, 0);
*
* // Iterate over the vertices and vertexNormals arrays.
* for (let i = 0; i < myGeometry.vertices.length; i += 1) {
*
* // Get the vertex p5.Vector object.
* let v = myGeometry.vertices[i];
*
* // Get the vertex normal p5.Vector object.
* let n = myGeometry.vertexNormals[i];
*
* // Calculate a point along the vertex normal.
* let p = p5.Vector.mult(n, 5);
*
* // Draw the vertex normal as a red line.
* push();
* translate(v);
* line(0, 0, 0, p.x, p.y, p.z);
* pop();
* }
* }
* </code>
* </div>
*
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let myGeometry;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create a p5.Geometry object using a callback function.
* myGeometry = new p5.Geometry();
*
* // Create p5.Vector objects to position the vertices.
* let v0 = createVector(-40, 0, 0);
* let v1 = createVector(0, -40, 0);
* let v2 = createVector(0, 40, 0);
* let v3 = createVector(40, 0, 0);
*
* // Add the vertices to the p5.Geometry object's vertices array.
* myGeometry.vertices.push(v0, v1, v2, v3);
*
* // Compute the faces array.
* myGeometry.computeFaces();
*
* // Compute the surface normals.
* myGeometry.computeNormals();
*
* describe('A red square drawn on a gray background.');
* }
*
* function draw() {
* background(200);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Add a white point light.
* pointLight(255, 255, 255, 0, 0, 10);
*
* // Style the p5.Geometry object.
* noStroke();
* fill(255, 0, 0);
*
* // Draw the p5.Geometry object.
* model(myGeometry);
* }
* </code>
* </div>
*
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let myGeometry;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create a p5.Geometry object.
* myGeometry = buildGeometry(createShape);
*
* // Compute normals using default (FLAT) shading.
* myGeometry.computeNormals(FLAT);
*
* describe('A white, helical structure drawn on a dark gray background. Its faces appear faceted.');
* }
*
* function draw() {
* background(50);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Turn on the lights.
* lights();
*
* // Rotate the coordinate system.
* rotateX(1);
*
* // Style the helix.
* noStroke();
*
* // Display the helix.
* model(myGeometry);
* }
*
* function createShape() {
* // Create a helical shape.
* beginShape();
* for (let i = 0; i < TWO_PI * 3; i += 0.5) {
* let x = 30 * cos(i);
* let y = 30 * sin(i);
* let z = map(i, 0, TWO_PI * 3, -40, 40);
* vertex(x, y, z);
* }
* endShape();
* }
* </code>
* </div>
*
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let myGeometry;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create a p5.Geometry object.
* myGeometry = buildGeometry(createShape);
*
* // Compute normals using smooth shading.
* myGeometry.computeNormals(SMOOTH);
*
* describe('A white, helical structure drawn on a dark gray background.');
* }
*
* function draw() {
* background(50);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Turn on the lights.
* lights();
*
* // Rotate the coordinate system.
* rotateX(1);
*
* // Style the helix.
* noStroke();
*
* // Display the helix.
* model(myGeometry);
* }
*
* function createShape() {
* // Create a helical shape.
* beginShape();
* for (let i = 0; i < TWO_PI * 3; i += 0.5) {
* let x = 30 * cos(i);
* let y = 30 * sin(i);
* let z = map(i, 0, TWO_PI * 3, -40, 40);
* vertex(x, y, z);
* }
* endShape();
* }
* </code>
* </div>
*
* <div>
* <code>
* // Click and drag the mouse to view the scene from different angles.
*
* let myGeometry;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create a p5.Geometry object.
* myGeometry = buildGeometry(createShape);
*
* // Create an options object.
* let options = { roundToPrecision: 5 };
*
* // Compute normals using smooth shading.
* myGeometry.computeNormals(SMOOTH, options);
*
* describe('A white, helical structure drawn on a dark gray background.');
* }
*
* function draw() {
* background(50);
*
* // Enable orbiting with the mouse.
* orbitControl();
*
* // Turn on the lights.
* lights();
*
* // Rotate the coordinate system.
* rotateX(1);
*
* // Style the helix.
* noStroke();
*
* // Display the helix.
* model(myGeometry);
* }
*
* function createShape() {
* // Create a helical shape.
* beginShape();
* for (let i = 0; i < TWO_PI * 3; i += 0.5) {
* let x = 30 * cos(i);
* let y = 30 * sin(i);
* let z = map(i, 0, TWO_PI * 3, -40, 40);
* vertex(x, y, z);
* }
* endShape();
* }
* </code>
* </div>
*/
computeNormals(shadingType = FLAT, { roundToPrecision = 3 } = {}) {
const vertexNormals = this.vertexNormals;
let vertices = this.vertices;
const faces = this.faces;
let iv;
if (shadingType === SMOOTH) {
const vertexIndices = {};
const uniqueVertices = [];
const power = Math.pow(10, roundToPrecision);
const rounded = val => Math.round(val * power) / power;
const getKey = vert =>
`${rounded(vert.x)},${rounded(vert.y)},${rounded(vert.z)}`;
// loop through each vertex and add uniqueVertices
for (let i = 0; i < vertices.length; i++) {
const vertex = vertices[i];
const key = getKey(vertex);
if (vertexIndices[key] === undefined) {
vertexIndices[key] = uniqueVertices.length;
uniqueVertices.push(vertex);
}
}
// update face indices to use the deduplicated vertex indices
faces.forEach(face => {
for (let fv = 0; fv < 3; ++fv) {
const originalVertexIndex = face[fv];
const originalVertex = vertices[originalVertexIndex];
const key = getKey(originalVertex);
face[fv] = vertexIndices[key];
}
});
// update edge indices to use the deduplicated vertex indices
this.edges.forEach(edge => {
for (let ev = 0; ev < 2; ++ev) {
const originalVertexIndex = edge[ev];
const originalVertex = vertices[originalVertexIndex];
const key = getKey(originalVertex);
edge[ev] = vertexIndices[key];
}
});
// update the deduplicated vertices
this.vertices = vertices = uniqueVertices;
}
// initialize the vertexNormals array with empty vectors
vertexNormals.length = 0;
for (iv = 0; iv < vertices.length; ++iv) {
vertexNormals.push(new Vector());
}
// loop through all the faces adding its normal to the normal
// of each of its vertices
faces.forEach((face, f) => {
const faceNormal = this._getFaceNormal(f);
// all three vertices get the normal added
for (let fv = 0; fv < 3; ++fv) {
const vertexIndex = face[fv];
vertexNormals[vertexIndex].add(faceNormal);
}
});
// normalize the normals
for (iv = 0; iv < vertices.length; ++iv) {
vertexNormals[iv].normalize();
}
return this;
}
/**
* Averages the vertex normals. Used in curved
* surfaces
* @private
* @chainable
*/
averageNormals() {
for (let i = 0; i <= this.detailY; i++) {
const offset = this.detailX + 1;
let temp = Vector.add(
this.vertexNormals[i * offset],
this.vertexNormals[i * offset + this.detailX]
);
temp = Vector.div(temp, 2);
this.vertexNormals[i * offset] = temp;
this.vertexNormals[i * offset + this.detailX] = temp;
}
return this;
}
/**
* Averages pole normals. Used in spherical primitives
* @private
* @chainable
*/
averagePoleNormals() {
//average the north pole
let sum = new Vector(0, 0, 0);
for (let i = 0; i < this.detailX; i++) {
sum.add(this.vertexNormals[i]);
}
sum = Vector.div(sum, this.detailX);
for (let i = 0; i < this.detailX; i++) {
this.vertexNormals[i] = sum;
}
//average the south pole
sum = new Vector(0, 0, 0);
for (
let i = this.vertices.length - 1;
i > this.vertices.length - 1 - this.detailX;
i--
) {
sum.add(this.vertexNormals[i]);
}
sum = Vector.div(sum, this.detailX);
for (
let i = this.vertices.length - 1;
i > this.vertices.length - 1 - this.detailX;
i--
) {
this.vertexNormals[i] = sum;
}
return this;
}
/**
* Create a 2D array for establishing stroke connections
* @private
* @chainable
*/
_makeTriangleEdges() {
this.edges.length = 0;
for (let j = 0; j < this.faces.length; j++) {
this.edges.push([this.faces[j][0], this.faces[j][1]]);
this.edges.push([this.faces[j][1], this.faces[j][2]]);
this.edges.push([this.faces[j][2], this.faces[j][0]]);
}
return this;
}
/**
* @example
* <div>
* <code>
* let tetrahedron;
* function setup() {
* createCanvas(200, 200, WEBGL);
* describe('A rotating tetrahedron');
*
* tetrahedron = new p5.Geometry();
*
* // Give each geometry a unique gid
* tetrahedron.gid = 'tetrahedron';
*
* // Add four points of the tetrahedron
*
* let radius = 50;
* // A 2D triangle:
* tetrahedron.vertices.push(createVector(radius, 0, 0));
* tetrahedron.vertices.push(createVector(radius, 0, 0).rotate(TWO_PI / 3));
* tetrahedron.vertices.push(createVector(radius, 0, 0).rotate(TWO_PI * 2 / 3));
* // Add a tip in the z axis:
* tetrahedron.vertices.push(createVector(0, 0, radius));
*
* // Create the four faces by connecting the sets of three points
* tetrahedron.faces.push([0, 1, 2]);
* tetrahedron.faces.push([0, 1, 3]);
* tetrahedron.faces.push([0, 2, 3]);
* tetrahedron.faces.push([1, 2, 3]);
* tetrahedron.makeEdgesFromFaces();
* }
* function draw() {
* background(200);
* strokeWeight(2);
* orbitControl();
* rotateY(millis() * 0.001);
* model(tetrahedron);
* }
* </code>
* </div>
*/
makeEdgesFromFaces() {
this._makeTriangleEdges();
}
/**
* Converts each line segment into the vertices and vertex attributes needed
* to turn the line into a polygon on screen. This will include:
* - Two triangles line segment to create a rectangle
* - Two triangles per endpoint to create a stroke cap rectangle. A fragment
* shader is responsible for displaying the appropriate cap style within
* that rectangle.
* - Four triangles per join between adjacent line segments, creating a quad on
* either side of the join, perpendicular to the lines. A vertex shader will
* discard the quad in the "elbow" of the join, and a fragment shader will
* display the appropriate join style within the remaining quad.
*
* @private
* @chainable
*/
_edgesToVertices() {
this.lineVertices.clear();
this.lineTangentsIn.clear();
this.lineTangentsOut.clear();
this.lineSides.clear();
const potentialCaps = new Map();
const connected = new Set();
let lastValidDir;
for (let i = 0; i < this.edges.length; i++) {
const prevEdge = this.edges[i - 1];
const currEdge = this.edges[i];
const begin = this.vertices[currEdge[0]];
const end = this.vertices[currEdge[1]];
const prevColor = (this.vertexStrokeColors.length > 0 && prevEdge)
? this.vertexStrokeColors.slice(
prevEdge[1] * 4,
(prevEdge[1] + 1) * 4
)
: [0, 0, 0, 0];
const fromColor = this.vertexStrokeColors.length > 0
? this.vertexStrokeColors.slice(
currEdge[0] * 4,
(currEdge[0] + 1) * 4
)
: [0, 0, 0, 0];
const toColor = this.vertexStrokeColors.length > 0
? this.vertexStrokeColors.slice(
currEdge[1] * 4,
(currEdge[1] + 1) * 4
)
: [0, 0, 0, 0];
const dir = end
.copy()
.sub(begin)
.normalize();
const dirOK = dir.magSq() > 0;
if (dirOK) {
this._addSegment(begin, end, fromColor, toColor, dir);
}
if (!this.renderer?._simpleLines) {
if (i > 0 && prevEdge[1] === currEdge[0]) {
if (!connected.has(currEdge[0])) {
connected.add(currEdge[0]);
potentialCaps.delete(currEdge[0]);
// Add a join if this segment shares a vertex with the previous. Skip
// actually adding join vertices if either the previous segment or this
// one has a length of 0.
//
// Don't add a join if the tangents point in the same direction, which
// would mean the edges line up exactly, and there is no need for a join.
if (lastValidDir && dirOK && dir.dot(lastValidDir) < 1 - 1e-8) {
this._addJoin(begin, lastValidDir, dir, fromColor);
}
}
} else {
// Start a new line
if (dirOK && !connected.has(currEdge[0])) {
const existingCap = potentialCaps.get(currEdge[0]);
if (existingCap) {
this._addJoin(
begin,
existingCap.dir,
dir,
fromColor
);
potentialCaps.delete(currEdge[0]);
connected.add(currEdge[0]);
} else {
potentialCaps.set(currEdge[0], {
point: begin,
dir: dir.copy().mult(-1),
color: fromColor
});
}
}
if (lastValidDir && !connected.has(prevEdge[1])) {
const existingCap = potentialCaps.get(prevEdge[1]);
if (existingCap) {
this._addJoin(
this.vertices[prevEdge[1]],
lastValidDir,
existingCap.dir.copy().mult(-1),
prevColor
);
potentialCaps.delete(prevEdge[1]);
connected.add(prevEdge[1]);
} else {
// Close off the last segment with a cap
potentialCaps.set(prevEdge[1], {
point: this.vertices[prevEdge[1]],
dir: lastValidDir,
color: prevColor
});
}
lastValidDir = undefined;
}
}
if (i === this.edges.length - 1 && !connected.has(currEdge[1])) {
const existingCap = potentialCaps.get(currEdge[1]);
if (existingCap) {
this._addJoin(
end,
dir,
existingCap.dir.copy().mult(-1),
toColor
);
potentialCaps.delete(currEdge[1]);
connected.add(currEdge[1]);
} else {
potentialCaps.set(currEdge[1], {
point: end,
dir,
color: toColor
});
}
}
if (dirOK) {
lastValidDir = dir;
}
}
}
for (const { point, dir, color } of potentialCaps.values()) {
this._addCap(point, dir, color);
}
return this;
}
/**
* Adds the vertices and vertex attributes for two triangles making a rectangle
* for a straight line segment. A vertex shader is responsible for picking
* proper coordinates on the screen given the centerline positions, the tangent,
* and the side of the centerline each vertex belongs to. Sides follow the
* following scheme:
*
* -1 -1
* o-------------o
* | |
* o-------------o
* 1 1
*
* @private
* @chainable
*/
_addSegment(
begin,
end,
fromColor,
toColor,
dir
) {
const a = begin.array();
const b = end.array();
const dirArr = dir.array();
this.lineSides.push(1, 1, -1, 1, -1, -1);
for (const tangents of [this.lineTangentsIn, this.lineTangentsOut]) {
for (let i = 0; i < 6; i++) {
tangents.push(...dirArr);
}
}
this.lineVertices.push(...a, ...b, ...a, ...b, ...b, ...a);
if (!this.renderer?._simpleLines) {
this.lineVertexColors.push(
...fromColor,
...toColor,
...fromColor,
...toColor,
...toColor,
...fromColor
);
}
return this;
}
/**
* Adds the vertices and vertex attributes for two triangles representing the
* stroke cap of a line. A fragment shader is responsible for displaying the
* appropriate cap style within the rectangle they make.
*
* The lineSides buffer will include the following values for the points on
* the cap rectangle:
*
* -1 -2
* -----------o---o
* | |
* -----------o---o
* 1 2
* @private
* @chainable
*/
_addCap(point, tangent, color) {
const ptArray = point.array();
const tanInArray = tangent.array();
const tanOutArray = [0, 0, 0];
for (let i = 0; i < 6; i++) {
this.lineVertices.push(...ptArray);
this.lineTangentsIn.push(...tanInArray);
this.lineTangentsOut.push(...tanOutArray);
this.lineVertexColors.push(...color);
}
this.lineSides.push(-1, 2, -2, 1, 2, -1);
return this;
}
/**
* Adds the vertices and vertex attributes for four triangles representing a
* join between two adjacent line segments. This creates a quad on either side
* of the shared vertex of the two line segments, with each quad perpendicular
* to the lines. A vertex shader will discard all but the quad in the "elbow" of
* the join, and a fragment shader will display the appropriate join style
* within the remaining quad.
*
* The lineSides buffer will include the following values for the points on
* the join rectangles:
*
* -1 -2
* -------------o----o
* | |
* 1 o----o----o -3
* | | 0 |
* --------o----o |
* 2| 3 |
* | |
* | |
* @private
* @chainable
*/
_addJoin(
point,
fromTangent,
toTangent,
color
) {
const ptArray = point.array();
const tanInArray = fromTangent.array();
const tanOutArray = toTangent.array();
for (let i = 0; i < 12; i++) {
this.lineVertices.push(...ptArray);
this.lineTangentsIn.push(...tanInArray);
this.lineTangentsOut.push(...tanOutArray);
this.lineVertexColors.push(...color);
}
this.lineSides.push(-1, -3, -2, -1, 0, -3);
this.lineSides.push(3, 1, 2, 3, 0, 1);
return this;
}
/**
* Transforms the geometry's vertices to fit snugly within a 100×100×100 box
* centered at the origin.
*
* Calling `myGeometry.normalize()` translates the geometry's vertices so that
* they're centered at the origin `(0, 0, 0)`. Then it scales the vertices so
* that they fill a 100×100×100 box. As a result, small geometries will grow
* and large geometries will shrink.
*
* Note: `myGeometry.normalize()` only works when called in the
* <a href="#/p5/setup">setup()</a> function.
*
* @chainable
*
* @example
* <div>
* <code>
* let myGeometry;
*
* function setup() {
* createCanvas(100, 100, WEBGL);
*
* // Create a very small torus.
* myGeometry = buildGeometry(function() {;
* torus(1, 0.25);
* });
*
* // Normalize the torus so its vertices fill
* // the range [-100, 100].
* myGeometry.normalize();
*
* describe('A white torus rotates slowly against a dark gray background.');
* }
*
* function draw() {
* bac