UNPKG

p5

Version:

[![npm version](https://badge.fury.io/js/p5.svg)](https://www.npmjs.com/package/p5)

614 lines (569 loc) 20.3 kB
import { z as PATH, V as QUADS, Q as QUAD_STRIP, T as TRIANGLE_STRIP, c as TRIANGLES, K as TRIANGLE_FAN, ae as IMAGE, X as LINES } from '../constants-DwbuOBz3.js'; import { Geometry } from './p5.Geometry.js'; import libtess from 'libtess'; import { Vector } from '../math/p5.Vector.js'; import { RenderBuffer } from './p5.RenderBuffer.js'; import './p5.DataArray.js'; import '../io/utilities.js'; const INITIAL_BUFFER_STRIDES = { vertices: 1, vertexNormals: 1, vertexColors: 4, vertexStrokeColors: 4, uvs: 2 }; // The total number of properties per vertex, before additional // user attributes are added. const INITIAL_VERTEX_SIZE = Object.values(INITIAL_BUFFER_STRIDES).reduce((acc, next) => acc + next); class ShapeBuilder { constructor(renderer) { this.renderer = renderer; this.shapeMode = PATH; this.geometry = new Geometry( undefined, undefined, undefined, this.renderer ); this.geometry.gid = '__IMMEDIATE_MODE_GEOMETRY__'; this.contourIndices = []; this._useUserVertexProperties = undefined; this._bezierVertex = []; this._quadraticVertex = []; this._curveVertex = []; // Used to distinguish between user calls to vertex() and internal calls this.isProcessingVertices = false; // Used for converting shape outlines into triangles for rendering this._tessy = this._initTessy(); this.tessyVertexSize = INITIAL_VERTEX_SIZE; this.bufferStrides = { ...INITIAL_BUFFER_STRIDES }; } friendlyErrorsDisabled() { return false; } constructFromContours(shape, contours) { if (this._useUserVertexProperties){ this._resetUserVertexProperties(); } this.geometry.reset(); this.contourIndices = []; // TODO: handle just some contours having non-PATH mode this.shapeMode = shape.contours[0].kind; const shouldProcessEdges = !!this.renderer.states.strokeColor; const userVertexPropertyHelpers = {}; if (shape.userVertexProperties) { this._useUserVertexProperties = true; for (const key in shape.userVertexProperties) { const name = shape.vertexPropertyName(key); const prop = this.geometry._userVertexPropertyHelper( name, [], shape.userVertexProperties[key] ); userVertexPropertyHelpers[key] = prop; this.tessyVertexSize += prop.getDataSize(); this.bufferStrides[prop.getSrcName()] = prop.getDataSize(); this.renderer.buffers.user.push( new RenderBuffer( prop.getDataSize(), prop.getSrcName(), prop.getDstName(), name, this.renderer ) ); } } else { this._useUserVertexProperties = false; } for (const contour of contours) { this.contourIndices.push(this.geometry.vertices.length); for (const vertex of contour) { // WebGL doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra // work to convert QUAD_STRIP here, since the only difference is in how edges // are rendered.) if (this.shapeMode === QUADS) { // A finished quad turned into triangles should leave 6 vertices in the // buffer: // 0--3 0 3--5 // | | --> | \ \ | // 1--2 1--2 4 // When vertex index 3 is being added, add the necessary duplicates. if (this.geometry.vertices.length % 6 === 3) { for (const key in this.bufferStrides) { const stride = this.bufferStrides[key]; const buffer = this.geometry[key]; buffer.push( ...buffer.slice( buffer.length - 3 * stride, buffer.length - 2 * stride ), ...buffer.slice(buffer.length - stride, buffer.length) ); } } } this.geometry.vertices.push(vertex.position); this.geometry.vertexNormals.push(vertex.normal || new Vector(0, 0, 0)); this.geometry.uvs.push( vertex.textureCoordinates.x, vertex.textureCoordinates.y ); if (this.renderer.states.fillColor) { this.geometry.vertexColors.push(...vertex.fill.array()); } else { this.geometry.vertexColors.push(0, 0, 0, 0); } if (this.renderer.states.strokeColor) { this.geometry.vertexStrokeColors.push(...vertex.stroke.array()); } else { this.geometry.vertexStrokeColors.push(0, 0, 0, 0); } for (const key in userVertexPropertyHelpers) { const prop = userVertexPropertyHelpers[key]; if (key in vertex) { prop.setCurrentData(vertex[key]); } prop.pushCurrentData(); } } } if (shouldProcessEdges) { this.geometry.edges = this._calculateEdges( this.shapeMode, this.geometry.vertices ); } if (shouldProcessEdges && !this.renderer.geometryBuilder) { this.geometry._edgesToVertices(); } if (this.shapeMode === PATH) { const vertexCount = this.geometry.vertices.length; const MAX_SAFE_TESSELLATION_VERTICES = 50000; if ( vertexCount > MAX_SAFE_TESSELLATION_VERTICES && !this.friendlyErrorsDisabled() && !this.renderer._largeTessellationAcknowledged ) { const proceed = window.confirm( '🌸 p5.js says:\n\n' + `This shape has ${vertexCount} vertices. Tessellating shapes with this ` + 'many vertices can be very slow and may cause your browser to become ' + 'unresponsive.\n\n' + 'Do you want to continue tessellating this shape?' ); if (!proceed) { return; } this.renderer._largeTessellationAcknowledged = true; } this.isProcessingVertices = true; this._tesselateShape(); this.isProcessingVertices = false; } else if (this.shapeMode === QUAD_STRIP) { // The only difference between these two modes is which edges are // displayed, so after we've updated the edges, we switch the mode // to one that native WebGL knows how to render. this.shapeMode = TRIANGLE_STRIP; } else if (this.shapeMode === QUADS) { // We translate QUADS to TRIANGLES when vertices are being added, // since QUADS is just a p5 mode, whereas TRIANGLES is also a mode // that native WebGL knows how to render. Once we've processed edges, // everything should be set up for TRIANGLES mode. this.shapeMode = TRIANGLES; } if ( !this.renderer.geometryBuilder && this.shapeMode === TRIANGLE_FAN && !this.renderer.supportsTriangleFan() ) { this._convertFanToTriangles(); this.shapeMode = TRIANGLES; } if ( this.renderer.states.textureMode === IMAGE && this.renderer.states._tex !== null && this.renderer.states._tex.width > 0 && this.renderer.states._tex.height > 0 ) { this.geometry.uvs = this.geometry.uvs.map((val, i) => { if (i % 2 === 0) { return val / this.renderer.states._tex.width; } else { return val / this.renderer.states._tex.height; } }); } } _remapVertices(newIndices) { this.geometry.vertices = newIndices.map(i => this.geometry.vertices[i]); this.geometry.vertexNormals = newIndices.map(i => this.geometry.vertexNormals[i]); const remapFlat = (arr, stride) => { const result = []; for (const i of newIndices) { for (let j = 0; j < stride; j++) { result.push(arr[i * stride + j]); } } return result; }; this.geometry.uvs = remapFlat(this.geometry.uvs, 2); this.geometry.vertexColors = remapFlat(this.geometry.vertexColors, 4); this.geometry.vertexStrokeColors = remapFlat(this.geometry.vertexStrokeColors, 4); for (const propName in this.geometry.userVertexProperties) { const prop = this.geometry.userVertexProperties[propName]; const size = prop.getDataSize(); const oldData = prop.getSrcArray(); prop.resetSrcArray(); for (const i of newIndices) { prop.setCurrentData(oldData.slice(i * size, i * size + size)); prop.pushCurrentData(); } } } _convertFanToTriangles() { const n = this.geometry.vertices.length; if (n < 3) return; const newIndices = []; for (let i = 2; i < n; i++) { newIndices.push(0, i - 1, i); } this._remapVertices(newIndices); } _resetUserVertexProperties() { const properties = this.geometry.userVertexProperties; for (const propName in properties){ const prop = properties[propName]; delete this.bufferStrides[propName]; prop.delete(); } this._useUserVertexProperties = false; this.tessyVertexSize = INITIAL_VERTEX_SIZE; this.geometry.userVertexProperties = {}; } /** * Called from _processVertices(). This function calculates the stroke vertices for custom shapes and * tesselates shapes when applicable. * @private * @returns {Number[]} indices for custom shape vertices indicating edges. */ _calculateEdges( shapeMode, verts ) { const res = []; let i = 0; const contourIndices = this.contourIndices.slice(); let contourStart = -1; switch (shapeMode) { case TRIANGLE_STRIP: for (i = 0; i < verts.length - 2; i++) { res.push([i, i + 1]); res.push([i, i + 2]); } res.push([i, i + 1]); break; case TRIANGLE_FAN: for (i = 1; i < verts.length - 1; i++) { res.push([0, i]); res.push([i, i + 1]); } res.push([0, verts.length - 1]); break; case TRIANGLES: for (i = 0; i < verts.length - 2; i = i + 3) { res.push([i, i + 1]); res.push([i + 1, i + 2]); res.push([i + 2, i]); } break; case LINES: for (i = 0; i < verts.length - 1; i = i + 2) { res.push([i, i + 1]); } break; case QUADS: // Quads have been broken up into two triangles by `vertex()`: // 0 3--5 // | \ \ | // 1--2 4 for (i = 0; i < verts.length - 5; i += 6) { res.push([i, i + 1]); res.push([i + 1, i + 2]); res.push([i + 2, i + 5]); res.push([i + 5, i]); } break; case QUAD_STRIP: // 0---2---4 // | | | // 1---3---5 for (i = 0; i < verts.length - 2; i += 2) { res.push([i, i + 1]); res.push([i + 1, i + 3]); res.push([i, i + 2]); } res.push([i, i + 1]); break; default: // TODO: handle contours in other modes too for (i = 0; i < verts.length; i++) { if (i === contourIndices[0]) { contourStart = contourIndices.shift(); } else if ( verts[contourStart] && verts[i].equals(verts[contourStart]) ) { res.push([i - 1, contourStart]); } else { res.push([i - 1, i]); } } break; } return res; } /** * Called from _processVertices() when applicable. This function tesselates immediateMode.geometry. * @private */ _tesselateShape() { // const contours = [[]]; const contours = []; for (let i = 0; i < this.geometry.vertices.length; i++) { if ( this.contourIndices.length > 0 && this.contourIndices[0] === i ) { this.contourIndices.shift(); contours.push([]); } contours[contours.length-1].push( this.geometry.vertices[i].x, this.geometry.vertices[i].y, this.geometry.vertices[i].z, this.geometry.uvs[i * 2], this.geometry.uvs[i * 2 + 1], this.geometry.vertexColors[i * 4], this.geometry.vertexColors[i * 4 + 1], this.geometry.vertexColors[i * 4 + 2], this.geometry.vertexColors[i * 4 + 3], this.geometry.vertexNormals[i].x, this.geometry.vertexNormals[i].y, this.geometry.vertexNormals[i].z ); for (const propName in this.geometry.userVertexProperties) { const prop = this.geometry.userVertexProperties[propName]; const start = i * prop.getDataSize(); const end = start + prop.getDataSize(); const vals = prop.getSrcArray().slice(start, end); contours[contours.length-1].push(...vals); } } // Normalize nearly identical consecutive vertices to prevent tessellation artifacts // This addresses numerical precision issues in libtess when consecutive vertices // have coordinates that are almost (but not exactly) equal (e.g., differing by ~1e-8) const epsilon = 1e-6; for (const contour of contours) { const stride = this.tessyVertexSize; for (let i = stride; i < contour.length; i += stride) { const prevX = contour[i - stride]; const prevY = contour[i - stride + 1]; const currX = contour[i]; const currY = contour[i + 1]; if (Math.abs(currX - prevX) < epsilon) { contour[i] = prevX; } if (Math.abs(currY - prevY) < epsilon) { contour[i + 1] = prevY; } } } const polyTriangles = this._triangulate(contours); // If there were no valid faces, we still want to use the original vertices // for strokes, so we'll stop here. if (polyTriangles.length === 0) { return; } // TODO: handle non-PATH shape modes that have contours this.shapeMode = TRIANGLES; const originalVertices = this.geometry.vertices; this.geometry.vertices = []; this.geometry.vertexNormals = []; this.geometry.uvs = []; for (const propName in this.geometry.userVertexProperties){ const prop = this.geometry.userVertexProperties[propName]; prop.resetSrcArray(); } const colors = []; for ( let j = 0, polyTriLength = polyTriangles.length; j < polyTriLength; j = j + this.tessyVertexSize ) { colors.push(...polyTriangles.slice(j + 5, j + 9)); this.geometry.vertexNormals.push( new Vector(...polyTriangles.slice(j + 9, j + 12)) ); { let offset = 12; for (const propName in this.geometry.userVertexProperties){ const prop = this.geometry.userVertexProperties[propName]; const size = prop.getDataSize(); const start = j + offset; const end = start + size; prop.setCurrentData(polyTriangles.slice(start, end)); prop.pushCurrentData(); offset += size; } } this.geometry.vertices.push(new Vector(...polyTriangles.slice(j, j + 3))); this.geometry.uvs.push(...polyTriangles.slice(j + 3, j + 5)); } if (this.renderer.geometryBuilder) { // Tesselating the face causes the indices of edge vertices to stop being // correct. When rendering, this is not a problem, since _edgesToVertices // will have been called before this, and edge vertex indices are no longer // needed. However, the geometry builder still needs this information, so // when one is active, we need to update the indices. // // We record index mappings in a Map so that once we have found a // corresponding vertex, we don't need to loop to find it again. const newIndex = new Map(); this.geometry.edges = this.geometry.edges.map(edge => edge.map(origIdx => { if (!newIndex.has(origIdx)) { const orig = originalVertices[origIdx]; let newVertIndex = this.geometry.vertices.findIndex( v => orig.x === v.x && orig.y === v.y && orig.z === v.z ); if (newVertIndex === -1) { // The tesselation process didn't output a vertex with the exact // coordinate as before, potentially due to numerical issues. This // doesn't happen often, but in this case, pick the closest point let closestDist = Infinity; let closestIndex = 0; for ( let i = 0; i < this.geometry.vertices.length; i++ ) { const vert = this.geometry.vertices[i]; const dX = orig.x - vert.x; const dY = orig.y - vert.y; const dZ = orig.z - vert.z; const dist = dX*dX + dY*dY + dZ*dZ; if (dist < closestDist) { closestDist = dist; closestIndex = i; } } newVertIndex = closestIndex; } newIndex.set(origIdx, newVertIndex); } return newIndex.get(origIdx); })); } this.geometry.vertexColors = colors; } _initTessy() { // function called for each vertex of tesselator output function vertexCallback(data, polyVertArray) { for (const element of data) { polyVertArray.push(element); } } function begincallback(type) { if (type !== libtess.primitiveType.GL_TRIANGLES) { console.log(`expected TRIANGLES but got type: ${type}`); } } function errorcallback(errno) { console.log('error callback'); console.log(`error number: ${errno}`); } // callback for when segments intersect and must be split const combinecallback = (coords, data, weight) => { const result = new Array(this.tessyVertexSize).fill(0); for (let i = 0; i < weight.length; i++) { for (let j = 0; j < result.length; j++) { if (weight[i] === 0 || !data[i]) continue; result[j] += data[i][j] * weight[i]; } } return result; }; function edgeCallback(flag) { // don't really care about the flag, but need no-strip/no-fan behavior } const tessy = new libtess.GluTesselator(); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); tessy.gluTessProperty( libtess.gluEnum.GLU_TESS_WINDING_RULE, libtess.windingRule.GLU_TESS_WINDING_NONZERO ); return tessy; } /** * Runs vertices through libtess to convert them into triangles * @private */ _triangulate(contours) { // libtess will take 3d verts and flatten to a plane for tesselation. // libtess is capable of calculating a plane to tesselate on, but // if all of the vertices have the same z values, we'll just // assume the face is facing the camera, letting us skip any performance // issues or bugs in libtess's automatic calculation. const z = contours[0] ? contours[0][2] : undefined; let allSameZ = true; for (const contour of contours) { for ( let j = 0; j < contour.length; j += this.tessyVertexSize ) { if (contour[j + 2] !== z) { allSameZ = false; break; } } } if (allSameZ) { this._tessy.gluTessNormal(0, 0, 1); } else { // Let libtess pick a plane for us this._tessy.gluTessNormal(0, 0, 0); } const triangleVerts = []; this._tessy.gluTessBeginPolygon(triangleVerts); for (const contour of contours) { this._tessy.gluTessBeginContour(); for ( let j = 0; j < contour.length; j += this.tessyVertexSize ) { const coords = contour.slice( j, j + this.tessyVertexSize ); this._tessy.gluTessVertex(coords, coords); } this._tessy.gluTessEndContour(); } // finish polygon this._tessy.gluTessEndPolygon(); return triangleVerts; } } export { ShapeBuilder };