littlejsengine 1.25.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -0
- package/FAQ.md +49 -11
- package/README.md +6 -2
- package/REFERENCE.md +120 -18
- package/dist/littlejs.d.ts +464 -99
- package/dist/littlejs.esm.js +2853 -704
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2844 -701
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2658 -512
- package/package.json +4 -4
- package/plugins/box2d.js +110 -34
- package/plugins/gltf.js +74 -22
- package/plugins/lightSystem.js +410 -28
- package/plugins/math3d.js +4 -2
- package/plugins/medalSystem.js +19 -7
- package/plugins/newgrounds.js +4 -2
- package/plugins/parallax.js +1 -0
- package/plugins/particleEffects.js +44 -10
- package/plugins/pathFinder.js +9 -11
- package/plugins/pluginExport.js +9 -0
- package/plugins/postProcess.js +4 -2
- package/plugins/render3d.js +485 -2385
- package/plugins/render3dDebug.js +5 -8
- package/plugins/render3dEditor.js +81 -6
- package/plugins/render3dExtras.js +103 -31
- package/plugins/render3dLevel.js +53 -5
- package/plugins/render3dMesh.js +1158 -0
- package/plugins/render3dObject.js +1451 -0
- package/plugins/render3dVoxels.js +15 -3
- package/plugins/textureSheet.js +9 -8
- package/plugins/threejs.js +1 -1
- package/plugins/tweenSystem.js +76 -51
- package/plugins/uiSystem.js +37 -8
- package/plugins/wavedash.js +229 -0
- package/src/engine.js +184 -36
- package/src/engineAudio.js +37 -21
- package/src/engineBuild.mjs +6 -1
- package/src/engineDebug.js +22 -162
- package/src/engineDraw.js +139 -34
- package/src/engineEditor.js +79 -18
- package/src/engineExport.js +0 -3
- package/src/engineInput.js +27 -18
- package/src/engineMath.js +35 -14
- package/src/engineObject.js +60 -41
- package/src/engineParticles.js +58 -11
- package/src/engineRelease.js +1 -5
- package/src/engineSettings.js +48 -9
- package/src/engineTileLayer.js +11 -4
- package/src/engineUtilities.js +41 -9
- package/src/engineWebGL.js +37 -13
|
@@ -0,0 +1,1158 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* LittleJS 3D Mesh Plugin
|
|
3
|
+
* - Mesh, the geometry the renderer draws: triangle strips or indexed lists, uploaded to the GPU on their first draw
|
|
4
|
+
* - The basic builders, buildBox, buildSphere, buildGrid, buildLathe and buildSky, and the helpers that pack a mesh
|
|
5
|
+
* - Goes after the Render3D plugin, everything here is part of its Render3D namespace
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
11
|
+
|
|
12
|
+
// how many vertices a strip of n points takes with its repeats, and which point vertex k of it is
|
|
13
|
+
function render3DStripCount(n) { return n + 2 + (n & 1); }
|
|
14
|
+
function render3DStripIndex(k, n) { return k < 1 ? 0 : k <= n ? k - 1 : n - 1; }
|
|
15
|
+
|
|
16
|
+
// walk a strip's vertices with the repeats applied, calling back with (point, normal, uv, color)
|
|
17
|
+
// normals, uvs and colors may be one value for all points, an array per point, or undefined
|
|
18
|
+
function render3DForEachStripVertex(points, normals, uvs, colors, callback)
|
|
19
|
+
{
|
|
20
|
+
ASSERT(isArray(points) && points.length > 2, 'strip needs at least 3 points');
|
|
21
|
+
const n = points.length, count = render3DStripCount(n);
|
|
22
|
+
const normalArray = isArray(normals), uvArray = isArray(uvs), colorArray = isArray(colors);
|
|
23
|
+
for (let k = 0; k < count; ++k)
|
|
24
|
+
{
|
|
25
|
+
const i = render3DStripIndex(k, n);
|
|
26
|
+
callback(points[i],
|
|
27
|
+
normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL,
|
|
28
|
+
uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV,
|
|
29
|
+
colorArray ? colors[i] : colors || WHITE);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// write one vertex into a packed buffer at float index j
|
|
34
|
+
function render3DWriteVertex(floats, ints, j, x, y, z, n, u, v, rgba)
|
|
35
|
+
{
|
|
36
|
+
floats[j] = x; floats[j+1] = y; floats[j+2] = z;
|
|
37
|
+
floats[j+3] = n.x; floats[j+4] = n.y; floats[j+5] = n.z;
|
|
38
|
+
floats[j+6] = u; floats[j+7] = v;
|
|
39
|
+
ints[j+8] = rgba;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// turn every triangle of an index list the other way round, in place: the authoring form reads counter clockwise
|
|
43
|
+
// from the front and the pass draws clockwise, as a strip's triangles come out after its leading repeat
|
|
44
|
+
function render3DFlipTriangles(indices)
|
|
45
|
+
{
|
|
46
|
+
for (let t = 0; t < indices.length; t += 3)
|
|
47
|
+
{
|
|
48
|
+
const b = indices[t+1];
|
|
49
|
+
indices[t+1] = indices[t+2], indices[t+2] = b;
|
|
50
|
+
}
|
|
51
|
+
return indices;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// the triangles of a strip of count entries as an index list, given which vertex each entry maps to and which place
|
|
55
|
+
// it is at: the strip's triangle i is (i-2, i-1, i), its odd ones read the other way as the GPU reads a strip, and a
|
|
56
|
+
// triangle with two corners in one place has no area, it is a join between pieces or a sliver at a pole, and is left out
|
|
57
|
+
function render3DStripTriangles(count, remap, place)
|
|
58
|
+
{
|
|
59
|
+
const indices = [];
|
|
60
|
+
for (let i = 2; i < count; ++i)
|
|
61
|
+
{
|
|
62
|
+
const a = i & 1 ? i - 1 : i - 2, b = i & 1 ? i - 2 : i - 1;
|
|
63
|
+
if (place[a] != place[b] && place[b] != place[i] && place[a] != place[i])
|
|
64
|
+
indices.push(remap[a], remap[b], remap[i]);
|
|
65
|
+
}
|
|
66
|
+
return indices;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// a mesh's packed vertex data for the GPU, one vertex per entry of a layout, which is the strip index of each,
|
|
70
|
+
// written into data when given, the buffer an earlier call returned for the same layout; it measures the mesh's
|
|
71
|
+
// radius and box as it goes, as computeRadius does, over the vertices once instead of every strip entry
|
|
72
|
+
function render3DMeshVertexData(mesh, vertices, data=new ArrayBuffer(vertices.length * RENDER3D_VERTEX_BYTES))
|
|
73
|
+
{
|
|
74
|
+
const count = vertices.length;
|
|
75
|
+
const floats = new Float32Array(data), ints = new Uint32Array(data);
|
|
76
|
+
let r = 0, x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
|
|
77
|
+
for (let j = 0; j < count; ++j)
|
|
78
|
+
{
|
|
79
|
+
// a hand built mesh may leave normals, uvs and colors empty
|
|
80
|
+
const i = vertices[j], p = mesh.points[i], uv = mesh.uvs[i] || RENDER3D_DEFAULT_UV;
|
|
81
|
+
const x = p.x, y = p.y, z = p.z;
|
|
82
|
+
render3DWriteVertex(floats, ints, j * RENDER3D_VERTEX_FLOATS, x, y, z,
|
|
83
|
+
mesh.normals[i] || RENDER3D_DEFAULT_NORMAL, uv.x, uv.y, (mesh.colors[i] || WHITE).rgbaInt());
|
|
84
|
+
r = max(r, x*x + y*y + z*z);
|
|
85
|
+
x0 = min(x0, x), y0 = min(y0, y), z0 = min(z0, z);
|
|
86
|
+
x1 = max(x1, x), y1 = max(y1, y), z1 = max(z1, z);
|
|
87
|
+
}
|
|
88
|
+
mesh.radius = r ** .5;
|
|
89
|
+
mesh.bounds = count ? {min: vec3(x0, y0, z0), max: vec3(x1, y1, z1)} : {min: vec3(), max: vec3()};
|
|
90
|
+
return data;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// reorder a convex polygon's points, counter clockwise from outside, into one triangle strip
|
|
94
|
+
function render3DPolygonStrip(points)
|
|
95
|
+
{
|
|
96
|
+
const strip = [points[0]];
|
|
97
|
+
for (let i = 1, j = points.length - 1; i <= j; ++i, --j)
|
|
98
|
+
{
|
|
99
|
+
strip.push(points[i]);
|
|
100
|
+
if (i !== j)
|
|
101
|
+
strip.push(points[j]);
|
|
102
|
+
}
|
|
103
|
+
return strip;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// turn every triangle of a mesh the other way round, the normals left alone: an index list reads each one in the
|
|
107
|
+
// other order, and a strip gets one extra point at each end, which flips every triangle and keeps the count even
|
|
108
|
+
function render3DFlipWinding(mesh)
|
|
109
|
+
{
|
|
110
|
+
if (mesh.indices)
|
|
111
|
+
return void render3DFlipTriangles(mesh.indices);
|
|
112
|
+
for (const key of ['points', 'normals', 'uvs', 'colors'])
|
|
113
|
+
{
|
|
114
|
+
const a = mesh[key];
|
|
115
|
+
if (a.length)
|
|
116
|
+
a.unshift(a[0]), a.push(a[a.length - 1]);
|
|
117
|
+
}
|
|
118
|
+
mesh.vertexKeys = undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
122
|
+
|
|
123
|
+
// frees the GPU buffer of a mesh that is garbage collected without dispose, some time after it goes; it holds the
|
|
124
|
+
// buffer and its context, never the mesh, or the mesh could not be collected, and dispose unregisters the mesh
|
|
125
|
+
const render3DMeshBuffers = typeof FinalizationRegistry == 'undefined' ? undefined :
|
|
126
|
+
new FinalizationRegistry(({buffer, indexBuffer, generation})=>
|
|
127
|
+
{
|
|
128
|
+
if (generation !== render3D?.contextGeneration || !glContext) return;
|
|
129
|
+
glContext.deleteBuffer(buffer);
|
|
130
|
+
glContext.deleteBuffer(indexBuffer);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// the points at one place, to a hundred thousandth, which smooth normals are summed by: each point's group, numbered
|
|
134
|
+
// from 0, found by a number made from its place and checked against the place, so no text is made for a point
|
|
135
|
+
function render3DPlaceGroups(points)
|
|
136
|
+
{
|
|
137
|
+
const groups = new Int32Array(points.length), first = new Map, next = [], places = [];
|
|
138
|
+
for (let i = 0; i < points.length; ++i)
|
|
139
|
+
{
|
|
140
|
+
const p = points[i], x = round(p.x * 1e5), y = round(p.y * 1e5), z = round(p.z * 1e5);
|
|
141
|
+
const hash = Math.imul(x, 73856093) ^ Math.imul(y, 19349663) ^ Math.imul(z, 83492791);
|
|
142
|
+
let group = first.get(hash);
|
|
143
|
+
while (group !== undefined && !(places[group*3] === x && places[group*3+1] === y && places[group*3+2] === z))
|
|
144
|
+
group = next[group];
|
|
145
|
+
if (group === undefined)
|
|
146
|
+
{
|
|
147
|
+
group = next.length;
|
|
148
|
+
next.push(first.get(hash));
|
|
149
|
+
first.set(hash, group);
|
|
150
|
+
places.push(x, y, z);
|
|
151
|
+
}
|
|
152
|
+
groups[i] = group;
|
|
153
|
+
}
|
|
154
|
+
return {groups, count: next.length};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// the face normals meeting at each place added up, each weighted by its corner angle so a cube corner averages its
|
|
158
|
+
// three faces evenly however they are cut into triangles; of a strip, every other triangle turned back, with no
|
|
159
|
+
// indices, or of the index list; in numbers, with no vector made for a triangle or a corner
|
|
160
|
+
function render3DSmoothNormalSums(points, indices)
|
|
161
|
+
{
|
|
162
|
+
const strip = !indices, triangles = strip ? max(points.length - 2, 0) : indices.length / 3 | 0;
|
|
163
|
+
const {groups, count} = render3DPlaceGroups(points);
|
|
164
|
+
const sums = new Float64Array(count * 3), touched = new Uint8Array(count);
|
|
165
|
+
let nx = 0, ny = 0, nz = 0;
|
|
166
|
+
const addCorner = (a, b, c, group)=>
|
|
167
|
+
{
|
|
168
|
+
// the angle at corner a, then the face normal scaled by it added to the sum at a's place
|
|
169
|
+
const ux = b.x - a.x, uy = b.y - a.y, uz = b.z - a.z, vx = c.x - a.x, vy = c.y - a.y, vz = c.z - a.z;
|
|
170
|
+
const dot = ux*vx + uy*vy + uz*vz, lengths = (ux**2 + uy**2 + uz**2)**.5 * (vx**2 + vy**2 + vz**2)**.5;
|
|
171
|
+
const angle = Math.acos(clamp(dot / (lengths || 1), -1, 1)), s = group * 3;
|
|
172
|
+
sums[s] += nx * angle, sums[s+1] += ny * angle, sums[s+2] += nz * angle;
|
|
173
|
+
touched[group] = 1;
|
|
174
|
+
};
|
|
175
|
+
for (let t = 0; t < triangles; ++t)
|
|
176
|
+
{
|
|
177
|
+
const i0 = strip ? t : indices[t*3], i1 = strip ? t + 1 : indices[t*3+1], i2 = strip ? t + 2 : indices[t*3+2];
|
|
178
|
+
const a = points[i0], b = points[i1], c = points[i2];
|
|
179
|
+
const ux = b.x - a.x, uy = b.y - a.y, uz = b.z - a.z, vx = c.x - a.x, vy = c.y - a.y, vz = c.z - a.z;
|
|
180
|
+
const cx = uy*vz - uz*vy, cy = uz*vx - ux*vz, cz = ux*vy - uy*vx;
|
|
181
|
+
const lengthSquared = cx**2 + cy**2 + cz**2;
|
|
182
|
+
if (!lengthSquared) continue; // no area, like a triangle joining two strips
|
|
183
|
+
const k = (strip && !(t & 1) ? -1 : 1) / lengthSquared**.5;
|
|
184
|
+
nx = cx * k, ny = cy * k, nz = cz * k;
|
|
185
|
+
addCorner(a, b, c, groups[i0]);
|
|
186
|
+
addCorner(b, c, a, groups[i1]);
|
|
187
|
+
addCorner(c, a, b, groups[i2]);
|
|
188
|
+
}
|
|
189
|
+
return {groups, sums, touched};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Mesh - Triangles with positions, normals, uvs and colors, uploaded once and drawn by matrix
|
|
194
|
+
* - Build with addStrip, addQuad, combine or the shape builders, then render each frame
|
|
195
|
+
* - Its back faces are skipped unless doubleSided is set, which the open builders like buildGrid do for you
|
|
196
|
+
* - Two forms: a triangle strip, what the builders make, or an indexed list of triangles over their own vertices,
|
|
197
|
+
* what addTriangles and the model loaders make; upload sends the GPU an indexed list either way, see getTriangles,
|
|
198
|
+
* so a strip's joins between its pieces cost nothing to draw, and toIndexed turns a strip mesh into the list form
|
|
199
|
+
* - The GPU buffer is created lazily on first render and dropped by dispose, or freed once the mesh is garbage
|
|
200
|
+
* collected, so dispose is only needed to free it right away, like for a mesh rebuilt often
|
|
201
|
+
* @memberof Render3D
|
|
202
|
+
* @example
|
|
203
|
+
* const mesh = buildLathe([[0, -1], [1, 0], [0, 1]], 4); // octahedron
|
|
204
|
+
* mesh.render(buildMatrix(vec3(0, 1, 0)), undefined, RED);
|
|
205
|
+
*/
|
|
206
|
+
class Mesh
|
|
207
|
+
{
|
|
208
|
+
/** Create an empty mesh */
|
|
209
|
+
constructor()
|
|
210
|
+
{
|
|
211
|
+
/** @property {Array<Vector3>} - Vertex positions, in strip order or one per vertex of an indexed mesh
|
|
212
|
+
* @type {Array<Vector3>} */
|
|
213
|
+
this.points = [];
|
|
214
|
+
/** @property {Array<Vector3>} - Vertex normals
|
|
215
|
+
* @type {Array<Vector3>} */
|
|
216
|
+
this.normals = [];
|
|
217
|
+
/** @property {Array<Vector2>} - Vertex texture coords, 0-1 across the tile
|
|
218
|
+
* @type {Array<Vector2>} */
|
|
219
|
+
this.uvs = [];
|
|
220
|
+
/** @property {Array<Color>} - Vertex colors
|
|
221
|
+
* @type {Array<Color>} */
|
|
222
|
+
this.colors = [];
|
|
223
|
+
/** @property {WebGLBuffer|undefined} - GPU vertex buffer, created by upload
|
|
224
|
+
* @type {WebGLBuffer|undefined} */
|
|
225
|
+
this.buffer = undefined;
|
|
226
|
+
/** @property {WebGLBuffer|undefined} - GPU index buffer, the triangles, created by upload
|
|
227
|
+
* @type {WebGLBuffer|undefined} */
|
|
228
|
+
this.indexBuffer = undefined;
|
|
229
|
+
/** @property {number} - Indices in the GPU index buffer, three per triangle */
|
|
230
|
+
this.bufferCount = 0;
|
|
231
|
+
this.indexType = 0; // gl.UNSIGNED_SHORT, or UNSIGNED_INT past 65535 vertices
|
|
232
|
+
/** @property {boolean} - The mesh changed and needs uploading again, set it yourself if you edit the arrays */
|
|
233
|
+
this.dirty = false;
|
|
234
|
+
/** @property {boolean|undefined} - Draw every use of this mesh in the opaque stage as one instanced call,
|
|
235
|
+
* undefined follows render3D.instancing
|
|
236
|
+
* @type {boolean|undefined} */
|
|
237
|
+
this.instanced = undefined;
|
|
238
|
+
/** @property {boolean} - Draw both sides, each lit as the side that is seen; off skips the faces pointing away,
|
|
239
|
+
* which is faster and right for closed shapes, the open builders like buildGrid and buildRibbon turn it on */
|
|
240
|
+
this.doubleSided = false;
|
|
241
|
+
/** @property {boolean} - The values change often but the shape never does, for a water surface or a cloth: set
|
|
242
|
+
* once, the mesh keeps its GPU layout and a dirty upload only rewrites the vertices into the buffer it has;
|
|
243
|
+
* the strip must keep the same points in the same order, a new point count asserts; the layout is decided by
|
|
244
|
+
* the first upload, so strip entries equal then stay one vertex and triangles with no area then stay dropped,
|
|
245
|
+
* set vertexKeys or give it distinct values at the start, not a flat grid of one color or points all in one place */
|
|
246
|
+
this.dynamicDraw = false;
|
|
247
|
+
/** @property {Array<number>|undefined} - The mesh as an indexed triangle list instead of a strip: the arrays
|
|
248
|
+
* hold each vertex once and this says how they join, three vertex numbers per triangle, counter clockwise seen
|
|
249
|
+
* from the front like a strip's first triangle; addTriangles and the loaders fill it, toIndexed turns a strip
|
|
250
|
+
* mesh into this form
|
|
251
|
+
* @type {Array<number>|undefined} */
|
|
252
|
+
this.indices = undefined;
|
|
253
|
+
/** @property {Int32Array|undefined} - Which strip entries are one vertex, set by a builder that knows, one
|
|
254
|
+
* whole number per entry with equal numbers meaning the same vertex; upload skips its search for them, then
|
|
255
|
+
* drops the keys, since an edit after that may tell the entries apart; adding geometry or recomputing normals
|
|
256
|
+
* drops them too
|
|
257
|
+
* @type {Int32Array|undefined} */
|
|
258
|
+
this.vertexKeys = undefined;
|
|
259
|
+
/** @type {{vertices: Array<number>, pointCount: number, data: ArrayBuffer}|undefined} */
|
|
260
|
+
// the strip index of each GPU vertex, the point count and packed data of the last upload, for a dynamicDraw mesh
|
|
261
|
+
this.vertexLayout = undefined;
|
|
262
|
+
// the layout's data already holds the mesh as it is, with its radius and box, written by a glTF skin as it
|
|
263
|
+
// bent the mesh, so the next upload sends it without packing it again
|
|
264
|
+
this.vertexDataPacked = false;
|
|
265
|
+
this.instanceCount = 0; // draws waiting in this mesh's batch, with their values, texture and draw state
|
|
266
|
+
/** @type {Float32Array|undefined} */
|
|
267
|
+
this.instanceData = undefined;
|
|
268
|
+
/** @property {number} - Bounding sphere radius around the origin, for culling and picking, computed by upload */
|
|
269
|
+
this.radius = 0;
|
|
270
|
+
/** @property {{min: Vector3, max: Vector3}|undefined} - Bounding box, for picking, measured with the radius
|
|
271
|
+
* @type {{min: Vector3, max: Vector3}|undefined} */
|
|
272
|
+
this.bounds = undefined;
|
|
273
|
+
this.contextGeneration = 0; // the context the buffer belongs to, see render3D.contextGeneration
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Number of vertices in the mesh
|
|
277
|
+
* @return {number} */
|
|
278
|
+
get vertexCount() { return this.points.length; }
|
|
279
|
+
|
|
280
|
+
/** Add a triangle strip, joined to the previous one by invisible flat triangles so one mesh holds many strips
|
|
281
|
+
* - Strip order: the first three points make a triangle, then each point makes another with the two before it
|
|
282
|
+
* - List the first three points counter clockwise as seen from the front, or the face points away
|
|
283
|
+
* and may vanish when back faces are culled
|
|
284
|
+
* @param {Array<Vector3>} points - Strip order
|
|
285
|
+
* @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
|
|
286
|
+
* @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, default zero
|
|
287
|
+
* @param {Color|Array<Color>} [colors] - One for all or one per point, default white
|
|
288
|
+
* @return {Mesh} */
|
|
289
|
+
addStrip(points, normals, uvs, colors)
|
|
290
|
+
{
|
|
291
|
+
if (this.indices)
|
|
292
|
+
{
|
|
293
|
+
// an indexed mesh takes the strip as the triangles it makes
|
|
294
|
+
const part = new Mesh().addStrip(points, normals, uvs, colors).toIndexed();
|
|
295
|
+
return this.addTriangles(part.points, part.indices, part.normals, part.uvs, part.colors);
|
|
296
|
+
}
|
|
297
|
+
render3DForEachStripVertex(points, normals, uvs, colors, (p, n, uv, c)=>
|
|
298
|
+
{
|
|
299
|
+
this.points.push(p);
|
|
300
|
+
this.normals.push(n);
|
|
301
|
+
this.uvs.push(uv);
|
|
302
|
+
this.colors.push(c);
|
|
303
|
+
});
|
|
304
|
+
this.vertexKeys = undefined; // the new entries have no keys
|
|
305
|
+
this.dirty = true;
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Add triangles over their own vertices, the indexed form a model file comes in
|
|
310
|
+
* - The mesh becomes indexed: a strip mesh is turned into triangles first, and strips added later join as triangles
|
|
311
|
+
* - List each triangle counter clockwise as seen from the front, like a strip's first triangle
|
|
312
|
+
* @param {Array<Vector3>} points - Each vertex once
|
|
313
|
+
* @param {Array<number>} indices - Three vertex numbers per triangle, into points
|
|
314
|
+
* @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
|
|
315
|
+
* @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, default zero
|
|
316
|
+
* @param {Color|Array<Color>} [colors] - One for all or one per point, default white
|
|
317
|
+
* @return {Mesh} */
|
|
318
|
+
addTriangles(points, indices, normals, uvs, colors)
|
|
319
|
+
{
|
|
320
|
+
ASSERT(isArray(points) && isArray(indices) && indices.length % 3 === 0,
|
|
321
|
+
'addTriangles takes points and three indices per triangle');
|
|
322
|
+
ASSERT(indices.every(i=> i >= 0 && i < points.length && i % 1 === 0), 'an index points past the vertices given');
|
|
323
|
+
this.toIndexed();
|
|
324
|
+
const offset = this.points.length, normalArray = isArray(normals), uvArray = isArray(uvs), colorArray = isArray(colors);
|
|
325
|
+
for (let i = 0; i < points.length; ++i)
|
|
326
|
+
{
|
|
327
|
+
this.points.push(points[i]);
|
|
328
|
+
this.normals.push(normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL);
|
|
329
|
+
this.uvs.push(uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV);
|
|
330
|
+
this.colors.push(colorArray ? colors[i] : colors || WHITE);
|
|
331
|
+
}
|
|
332
|
+
for (const i of indices)
|
|
333
|
+
this.indices.push(i + offset);
|
|
334
|
+
this.dirty = true;
|
|
335
|
+
return this;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Turn a strip mesh into the indexed form, each distinct vertex once and the real triangles over them, in place
|
|
339
|
+
* - An indexed mesh is left as it is; the builders make strips and a loader makes this, and either draws the same
|
|
340
|
+
* @return {Mesh} */
|
|
341
|
+
toIndexed()
|
|
342
|
+
{
|
|
343
|
+
if (this.indices) return this;
|
|
344
|
+
const {vertices, indices} = this.getTriangles();
|
|
345
|
+
this.points = vertices.map(i=> this.points[i]);
|
|
346
|
+
this.normals = vertices.map(i=> this.normals[i] || RENDER3D_DEFAULT_NORMAL);
|
|
347
|
+
this.uvs = vertices.map(i=> this.uvs[i] || RENDER3D_DEFAULT_UV);
|
|
348
|
+
this.colors = vertices.map(i=> this.colors[i] || WHITE);
|
|
349
|
+
// the list upload sends reads clockwise, the pass draws it that way; the authoring form is counter clockwise
|
|
350
|
+
this.indices = render3DFlipTriangles(indices);
|
|
351
|
+
this.vertexKeys = undefined;
|
|
352
|
+
this.dirty = true;
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Add a flat quad from four corners in loop order, counter clockwise seen from the front, a is the top left of the texture
|
|
357
|
+
* @param {Vector3} a
|
|
358
|
+
* @param {Vector3} b
|
|
359
|
+
* @param {Vector3} c
|
|
360
|
+
* @param {Vector3} d
|
|
361
|
+
* @param {Color|Array<Color>} [color] - One for all or one per corner
|
|
362
|
+
* @param {Array<Vector2>} [uvs] - One per corner, default across the tile
|
|
363
|
+
* @return {Mesh} */
|
|
364
|
+
addQuad(a, b, c, d, color, uvs)
|
|
365
|
+
{
|
|
366
|
+
return this.addStrip(render3DQuadStrip(a, b, c, d), render3DFaceNormal(a, b, c, d),
|
|
367
|
+
uvs ? render3DQuadValues(uvs) : RENDER3D_QUAD_UVS, render3DQuadValues(color));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Append another mesh transformed by a matrix, for building one shape out of several
|
|
371
|
+
* @param {Mesh} mesh
|
|
372
|
+
* @param {Matrix4|Vector3} [matrix] - Transform, or just a position to move it to
|
|
373
|
+
* @param {Color} [color] - Multiplies the appended vertex colors
|
|
374
|
+
* @return {Mesh} */
|
|
375
|
+
combine(mesh, matrix=RENDER3D_IDENTITY, color=WHITE)
|
|
376
|
+
{
|
|
377
|
+
matrix = render3DMatrix(matrix); // most parts only need moving into place
|
|
378
|
+
const normalMatrix = render3DNormalMatrix(matrix), mirrors = render3DMirrors(matrix);
|
|
379
|
+
let part = mesh, order;
|
|
380
|
+
if (this.indices || mesh.indices)
|
|
381
|
+
{
|
|
382
|
+
// one of them is indexed, so both are: the part as a copy if it is a strip
|
|
383
|
+
this.toIndexed();
|
|
384
|
+
part = mesh.indices ? mesh : new Mesh().combine(mesh).toIndexed();
|
|
385
|
+
const offset = this.points.length, indices = part.indices;
|
|
386
|
+
// the count read once, a mesh combined with itself grows as it is read; a mirror turns every triangle the
|
|
387
|
+
// other way round
|
|
388
|
+
for (let t = 0, n = indices.length; t < n; t += 3)
|
|
389
|
+
this.indices.push(indices[t] + offset, indices[t + (mirrors ? 2 : 1)] + offset,
|
|
390
|
+
indices[t + (mirrors ? 1 : 2)] + offset);
|
|
391
|
+
}
|
|
392
|
+
else if (mirrors && part.points.length)
|
|
393
|
+
{
|
|
394
|
+
// a strip reads the other way round with one more point at each end, as render3DFlipWinding does
|
|
395
|
+
const last = part.points.length - 1;
|
|
396
|
+
order = [0, ...part.points.keys(), last];
|
|
397
|
+
}
|
|
398
|
+
for (let j = 0, count = order ? order.length : part.points.length; j < count; ++j)
|
|
399
|
+
{
|
|
400
|
+
const i = order ? order[j] : j;
|
|
401
|
+
this.points.push(matrix.transformPoint(part.points[i]));
|
|
402
|
+
this.normals.push(normalMatrix.transformDirection(part.normals[i] || RENDER3D_DEFAULT_NORMAL).normalize());
|
|
403
|
+
this.uvs.push((part.uvs[i] || RENDER3D_DEFAULT_UV).copy());
|
|
404
|
+
this.colors.push((part.colors[i] || WHITE).multiply(color));
|
|
405
|
+
}
|
|
406
|
+
this.doubleSided ||= mesh.doubleSided; // an open part leaves the whole mesh open
|
|
407
|
+
this.vertexKeys = undefined; // the new entries have no keys
|
|
408
|
+
this.dirty = true;
|
|
409
|
+
return this;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Returns a new mesh: this one and its mirror image across the plane through the origin facing axis
|
|
413
|
+
* - For modeling half a shape against that plane, a part that crosses it overlaps its image
|
|
414
|
+
* @param {Vector3} [axis] - Faces the mirror plane, vec3(1,0,0) mirrors across x
|
|
415
|
+
* @return {Mesh} */
|
|
416
|
+
mirror(axis=vec3(1, 0, 0))
|
|
417
|
+
{
|
|
418
|
+
ASSERT(isVector3(axis) && axis.lengthSquared() > 0, 'mirror needs an axis');
|
|
419
|
+
const n = axis.normalize(), a = -2 * n.x, b = -2 * n.y, c = -2 * n.z;
|
|
420
|
+
const reflect = new Matrix4([1 + a * n.x, b * n.x, c * n.x, 0, a * n.y, 1 + b * n.y, c * n.y, 0,
|
|
421
|
+
a * n.z, b * n.z, 1 + c * n.z, 0, 0, 0, 0, 1]);
|
|
422
|
+
return new Mesh().combine(this).combine(this, reflect); // combine turns the image's faces the right way out
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Returns a new mesh of count copies of this one, each turned further around an axis through the origin
|
|
426
|
+
* @param {number} count - Copies, spaced evenly around the whole turn
|
|
427
|
+
* @param {Vector3} [axis] - Up by default
|
|
428
|
+
* @return {Mesh} */
|
|
429
|
+
spin(count, axis=vec3(0, 1, 0))
|
|
430
|
+
{
|
|
431
|
+
ASSERT(count >= 1 && count % 1 === 0, 'spin count must be a whole number, 1 or more');
|
|
432
|
+
ASSERT(isVector3(axis) && axis.lengthSquared() > 0, 'spin needs an axis');
|
|
433
|
+
const mesh = new Mesh, unit = axis.normalize();
|
|
434
|
+
for (let i = 0; i < count; ++i)
|
|
435
|
+
mesh.combine(this, render3DAxisRotation(unit, i / count * 2 * PI));
|
|
436
|
+
return mesh;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Returns a new mesh of everything in this mesh or the other, see subtract
|
|
440
|
+
* @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
|
|
441
|
+
* @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
|
|
442
|
+
* @return {Mesh} */
|
|
443
|
+
union(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 0); }
|
|
444
|
+
|
|
445
|
+
/** Returns a new mesh of this one with the other cut out of it, CSG with BSP trees
|
|
446
|
+
* - Both must be closed, every edge shared by two triangles, as the builders make them apart from the open
|
|
447
|
+
* ones like buildGrid and buildRibbon; the result is closed and indexed, and neither mesh changes
|
|
448
|
+
* - The faces a cut makes come from the other mesh's surface, turned to face out, with its normals, uvs and
|
|
449
|
+
* colors, so a smooth cylinder drills a round hole
|
|
450
|
+
* - Parts that overlap must be joined with union to be one solid, not with combine, mirror or spin, which
|
|
451
|
+
* leave them overlapping, and CSG then gives a wrong shape
|
|
452
|
+
* - Cuts split the triangles near them, so the result has more: a few thousand triangles take tens to a few
|
|
453
|
+
* hundred milliseconds, so build shapes this way at load time, not every frame; joining several cutters
|
|
454
|
+
* with union and cutting once is quicker than cutting with each in turn
|
|
455
|
+
* - Details closer than about 1e-4 are made one, so build a very small part larger and scale it after
|
|
456
|
+
* @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
|
|
457
|
+
* @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
|
|
458
|
+
* @return {Mesh}
|
|
459
|
+
* @example
|
|
460
|
+
* const wall = buildBox(vec3(4, 3, .5)).subtract(buildBox(vec3(1, 2, 1)), vec3(0, -.5, 0)); // a doorway */
|
|
461
|
+
subtract(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 1); }
|
|
462
|
+
|
|
463
|
+
/** Returns a new mesh of only what is in both this mesh and the other, see subtract
|
|
464
|
+
* @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
|
|
465
|
+
* @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
|
|
466
|
+
* @return {Mesh} */
|
|
467
|
+
intersect(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 2); }
|
|
468
|
+
|
|
469
|
+
/** Scale every uv, so a whole texture repeats across the mesh when its TextureInfo wraps
|
|
470
|
+
* @param {Vector2|number} scale - Repeats across and up, a number for both
|
|
471
|
+
* @return {Mesh} */
|
|
472
|
+
scaleUVs(scale)
|
|
473
|
+
{
|
|
474
|
+
const s = render3DSize2(scale);
|
|
475
|
+
// new vectors, builders share uv objects between faces
|
|
476
|
+
this.uvs = this.uvs.map(uv=> vec2(uv.x * s.x, uv.y * s.y));
|
|
477
|
+
this.dirty = true;
|
|
478
|
+
return this;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Move, turn or scale every vertex in place, normals follow along
|
|
482
|
+
* @param {Matrix4|Vector3} matrix - Transform, or just an offset to move by
|
|
483
|
+
* @return {Mesh} */
|
|
484
|
+
transform(matrix)
|
|
485
|
+
{
|
|
486
|
+
matrix = render3DMatrix(matrix);
|
|
487
|
+
const normalMatrix = render3DNormalMatrix(matrix);
|
|
488
|
+
for (let i = 0; i < this.points.length; ++i)
|
|
489
|
+
{
|
|
490
|
+
this.points[i] = matrix.transformPoint(this.points[i]);
|
|
491
|
+
// a mesh built by hand may have no normals yet, and then there is nothing to turn
|
|
492
|
+
this.normals[i] &&= normalMatrix.transformDirection(this.normals[i]).normalize();
|
|
493
|
+
}
|
|
494
|
+
render3DMirrors(matrix) && render3DFlipWinding(this); // a mirror would leave the faces pointing in
|
|
495
|
+
this.dirty = true;
|
|
496
|
+
return this;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Turn the mesh inside out so it is lit and drawn from within, for rooms and domes
|
|
500
|
+
* @return {Mesh} */
|
|
501
|
+
flipNormals()
|
|
502
|
+
{
|
|
503
|
+
render3DFlipWinding(this);
|
|
504
|
+
this.normals = this.normals.map(n=> n.scale(-1));
|
|
505
|
+
this.dirty = true;
|
|
506
|
+
return this;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Set every vertex color
|
|
510
|
+
* @param {Color} color
|
|
511
|
+
* @return {Mesh} */
|
|
512
|
+
setColor(color)
|
|
513
|
+
{
|
|
514
|
+
// one per point, not one per color already there, so a mesh built by hand with no
|
|
515
|
+
// colors gets them instead of quietly staying white
|
|
516
|
+
this.colors = this.points.map(()=> color);
|
|
517
|
+
this.dirty = true;
|
|
518
|
+
return this;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Measure the axis aligned box around the vertices
|
|
522
|
+
* @return {{min: Vector3, max: Vector3}} */
|
|
523
|
+
getBounds()
|
|
524
|
+
{
|
|
525
|
+
if (!this.points.length)
|
|
526
|
+
return {min: vec3(), max: vec3()};
|
|
527
|
+
const lo = vec3(Infinity), hi = vec3(-Infinity);
|
|
528
|
+
for (const p of this.points)
|
|
529
|
+
{
|
|
530
|
+
lo.x = min(lo.x, p.x); lo.y = min(lo.y, p.y); lo.z = min(lo.z, p.z);
|
|
531
|
+
hi.x = max(hi.x, p.x); hi.y = max(hi.y, p.y); hi.z = max(hi.z, p.z);
|
|
532
|
+
}
|
|
533
|
+
return {min: lo, max: hi};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Move the mesh so the center of its bounds is on the origin
|
|
537
|
+
* @return {Mesh} */
|
|
538
|
+
center()
|
|
539
|
+
{
|
|
540
|
+
const bounds = this.getBounds();
|
|
541
|
+
return this.transform(bounds.min.add(bounds.max).scale(-.5));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Scale the mesh evenly so its largest extent is a size, for loaded models of unknown units
|
|
545
|
+
* @param {number} [size]
|
|
546
|
+
* @return {Mesh} */
|
|
547
|
+
fit(size=1)
|
|
548
|
+
{
|
|
549
|
+
const bounds = this.getBounds();
|
|
550
|
+
const extent = bounds.max.subtract(bounds.min);
|
|
551
|
+
const scale = size / (max(extent.x, extent.y, extent.z) || 1);
|
|
552
|
+
return this.transform(Matrix4.scaling(vec3(scale)));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Measure the bounding sphere around the origin into radius, called by upload
|
|
556
|
+
* @return {number} */
|
|
557
|
+
computeRadius()
|
|
558
|
+
{
|
|
559
|
+
let r = 0;
|
|
560
|
+
for (const p of this.points)
|
|
561
|
+
r = max(r, p.lengthSquared());
|
|
562
|
+
this.bounds = this.getBounds(); // measured with it, a pick tests a mesh's box after its sphere
|
|
563
|
+
return this.radius = r ** .5;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Derive normals from the triangles, of the strip or of the index list
|
|
567
|
+
* @param {boolean} [smooth] - Round the lighting across faces instead of giving each face a hard edge; flat
|
|
568
|
+
* normals on an indexed mesh give every corner its own vertex
|
|
569
|
+
* @return {Mesh} */
|
|
570
|
+
computeNormals(smooth=false)
|
|
571
|
+
{
|
|
572
|
+
if (this.indices)
|
|
573
|
+
{
|
|
574
|
+
// the triangles are listed: smooth normals add up around each position, weighted by the corner angle
|
|
575
|
+
// like the strip's, so vertices split apart at one place smooth back together and a mesh can go flat and
|
|
576
|
+
// smooth again; flat ones need a vertex per corner, so the vertices are split up first
|
|
577
|
+
if (smooth)
|
|
578
|
+
{
|
|
579
|
+
const {groups, sums, touched} = render3DSmoothNormalSums(this.points, this.indices);
|
|
580
|
+
this.normals = this.points.map((p, i)=>
|
|
581
|
+
{
|
|
582
|
+
const s = groups[i] * 3, x = sums[s], y = sums[s+1], z = sums[s+2];
|
|
583
|
+
const lengthSquared = x**2 + y**2 + z**2, k = 1 / lengthSquared**.5;
|
|
584
|
+
return touched[groups[i]] && lengthSquared ? vec3(x * k, y * k, z * k) : RENDER3D_DEFAULT_NORMAL;
|
|
585
|
+
});
|
|
586
|
+
this.dirty = true;
|
|
587
|
+
return this;
|
|
588
|
+
}
|
|
589
|
+
const split = (a)=> this.indices.map(i=> a[i]);
|
|
590
|
+
this.points = split(this.points), this.normals = split(this.normals);
|
|
591
|
+
this.uvs = split(this.uvs), this.colors = split(this.colors);
|
|
592
|
+
this.indices = this.indices.map((_, i)=> i);
|
|
593
|
+
const points = this.points, indices = this.indices;
|
|
594
|
+
const normals = points.map(()=> RENDER3D_DEFAULT_NORMAL);
|
|
595
|
+
for (let t = 0; t < indices.length; t += 3)
|
|
596
|
+
{
|
|
597
|
+
const a = points[indices[t]], b = points[indices[t+1]], c = points[indices[t+2]];
|
|
598
|
+
const cross = b.subtract(a).cross(c.subtract(a));
|
|
599
|
+
if (cross.lengthSquared()) // its own three corners
|
|
600
|
+
normals[indices[t]] = normals[indices[t+1]] = normals[indices[t+2]] = cross.normalize();
|
|
601
|
+
}
|
|
602
|
+
this.normals = normals;
|
|
603
|
+
this.dirty = true;
|
|
604
|
+
return this;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// smooth normals are the sums at each place made unit length, or straight up for a place with none
|
|
608
|
+
const points = this.points, n = points.length;
|
|
609
|
+
if (smooth)
|
|
610
|
+
{
|
|
611
|
+
const {groups, sums, touched} = render3DSmoothNormalSums(points);
|
|
612
|
+
this.normals = points.map((p, i)=>
|
|
613
|
+
{
|
|
614
|
+
const s = groups[i] * 3, x = sums[s], y = sums[s+1], z = sums[s+2], l = (x**2 + y**2 + z**2)**.5;
|
|
615
|
+
return !touched[groups[i]] ? vec3(0, 1, 0) : l ? vec3(x * (1/l), y * (1/l), z * (1/l)) : vec3();
|
|
616
|
+
});
|
|
617
|
+
this.vertexKeys = undefined;
|
|
618
|
+
this.dirty = true;
|
|
619
|
+
return this;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// the outward normal of each triangle in the strip
|
|
623
|
+
const faceNormals = [];
|
|
624
|
+
for (let i = 0; i + 2 < n; ++i)
|
|
625
|
+
{
|
|
626
|
+
const a = points[i], b = points[i+1], c = points[i+2];
|
|
627
|
+
const normal = b.subtract(a).cross(c.subtract(a));
|
|
628
|
+
// triangles in a strip alternate which way they wind, so every other one is flipped back
|
|
629
|
+
// a zero normal means a flat triangle joining two strips, so skip it
|
|
630
|
+
faceNormals.push(normal.lengthSquared() ? normal.normalize(i & 1 ? 1 : -1) : undefined);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// then hand those to the vertices: every triangle writes its own three corners, so the only vertices left
|
|
634
|
+
// with the default are the repeats at the ends of a strip, which no triangle with any area uses
|
|
635
|
+
const normals = points.map(()=> RENDER3D_DEFAULT_NORMAL);
|
|
636
|
+
faceNormals.forEach((f, i)=> f && (normals[i] = normals[i+1] = normals[i+2] = f));
|
|
637
|
+
|
|
638
|
+
this.normals = normals;
|
|
639
|
+
this.vertexKeys = undefined; // flat normals tell entries at one place apart
|
|
640
|
+
this.dirty = true;
|
|
641
|
+
return this;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Pack the vertices and create the GPU buffer, called automatically by render
|
|
645
|
+
* @return {Mesh} */
|
|
646
|
+
upload()
|
|
647
|
+
{
|
|
648
|
+
// the packing below measures the radius and box, with nothing to pack to they are measured here
|
|
649
|
+
if (!render3D?.program || !glContext) return this.computeRadius(), this;
|
|
650
|
+
const gl = glContext, layout = this.vertexLayout;
|
|
651
|
+
// no layout when dynamicDraw was turned on after an upload, the next upload makes one
|
|
652
|
+
if (this.dynamicDraw && layout && this.buffer && this.contextGeneration === render3D.contextGeneration)
|
|
653
|
+
{
|
|
654
|
+
// the layout of the last upload stands, only the values are written again into the buffer it has,
|
|
655
|
+
// packed into the same memory each time so a mesh uploaded every frame makes no garbage
|
|
656
|
+
ASSERT(layout.pointCount === this.points.length,
|
|
657
|
+
'a dynamicDraw mesh keeps its shape, the same points in the same order; ' +
|
|
658
|
+
'for a new shape make a new mesh or turn dynamicDraw off', this.points.length);
|
|
659
|
+
if (layout.pointCount === this.points.length)
|
|
660
|
+
{
|
|
661
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
662
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0,
|
|
663
|
+
this.vertexDataPacked ? layout.data : render3DMeshVertexData(this, layout.vertices, layout.data));
|
|
664
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer);
|
|
665
|
+
this.dirty = this.vertexDataPacked = false;
|
|
666
|
+
return this;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
this.dispose();
|
|
670
|
+
const {vertices, indices} = this.getTriangles(), count = vertices.length, wide = count > 65535;
|
|
671
|
+
const data = render3DMeshVertexData(this, vertices);
|
|
672
|
+
this.vertexLayout = this.dynamicDraw ? {vertices, pointCount: this.points.length, data} : undefined;
|
|
673
|
+
this.vertexKeys = undefined; // used once: an edit after this may tell entries apart
|
|
674
|
+
this.buffer = gl.createBuffer();
|
|
675
|
+
this.indexBuffer = gl.createBuffer();
|
|
676
|
+
this.bufferCount = indices.length;
|
|
677
|
+
this.indexType = wide ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
|
|
678
|
+
this.dirty = this.vertexDataPacked = false;
|
|
679
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
680
|
+
gl.bufferData(gl.ARRAY_BUFFER, data, this.dynamicDraw ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW);
|
|
681
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
|
682
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wide ? new Uint32Array(indices) : new Uint16Array(indices), gl.STATIC_DRAW);
|
|
683
|
+
this.contextGeneration = render3D.contextGeneration;
|
|
684
|
+
const buffers = {buffer: this.buffer, indexBuffer: this.indexBuffer, generation: this.contextGeneration};
|
|
685
|
+
render3DMeshBuffers?.register(this, buffers, this);
|
|
686
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer); // the engine's 2D batch writes through this binding
|
|
687
|
+
return this;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** The mesh as an indexed triangle list, what upload sends to the GPU: the strip's real triangles over its
|
|
691
|
+
* distinct vertices, the joins between its pieces dropped and every triangle facing the way it did in the strip
|
|
692
|
+
* - Vertices are compared to a millionth, so two at one place with the same normal, uv and color are one
|
|
693
|
+
* @return {{vertices: Array<number>, indices: Array<number>}} - vertices are strip indices, one per distinct
|
|
694
|
+
* vertex; indices are the triangles, three per triangle, into vertices */
|
|
695
|
+
getTriangles()
|
|
696
|
+
{
|
|
697
|
+
const count = this.points.length, vertices = [];
|
|
698
|
+
if (this.indices)
|
|
699
|
+
{
|
|
700
|
+
// already a list: every vertex as it is, and the triangles read the way the pass draws, clockwise
|
|
701
|
+
for (let i = 0; i < count; ++i)
|
|
702
|
+
vertices.push(i);
|
|
703
|
+
return {vertices, indices: render3DFlipTriangles(this.indices.slice())};
|
|
704
|
+
}
|
|
705
|
+
const remap = new Int32Array(count), place = new Int32Array(count), keys = this.vertexKeys;
|
|
706
|
+
if (keys && keys.length === count)
|
|
707
|
+
{
|
|
708
|
+
// a builder said which entries are one vertex: the first entry with a key stands for every entry with it,
|
|
709
|
+
// and is their place, so nothing is searched
|
|
710
|
+
const first = new Int32Array(count).fill(-1);
|
|
711
|
+
for (let i = 0; i < count; ++i)
|
|
712
|
+
{
|
|
713
|
+
const key = keys[i];
|
|
714
|
+
ASSERT(key >= 0 && key < count, 'vertexKeys must be whole numbers below the entry count', key);
|
|
715
|
+
const j = first[key];
|
|
716
|
+
if (j < 0)
|
|
717
|
+
first[key] = i, remap[i] = vertices.length, vertices.push(i);
|
|
718
|
+
else
|
|
719
|
+
remap[i] = remap[j];
|
|
720
|
+
place[i] = first[key];
|
|
721
|
+
}
|
|
722
|
+
return {vertices, indices: render3DStripTriangles(count, remap, place)};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// each vertex as nine whole numbers, its values in millionths and its color, so vertices hash and compare as
|
|
726
|
+
// numbers; a hash table over all nine finds the distinct vertices and one over the first three the places,
|
|
727
|
+
// each slot holding a vertex index plus one and a taken slot moving on to the next
|
|
728
|
+
const values = new Float64Array(count * 9);
|
|
729
|
+
let size = 1;
|
|
730
|
+
while (size < count * 2) size *= 2;
|
|
731
|
+
const mask = size - 1, seen = new Int32Array(size), places = new Int32Array(size);
|
|
732
|
+
const mix = (h, v)=> Math.imul(h ^ v, 0x9e3779b1) >>> 0;
|
|
733
|
+
for (let i = 0; i < count; ++i)
|
|
734
|
+
{
|
|
735
|
+
const p = this.points[i], n = this.normals[i] || RENDER3D_DEFAULT_NORMAL;
|
|
736
|
+
const uv = this.uvs[i] || RENDER3D_DEFAULT_UV, k = i * 9;
|
|
737
|
+
const x = values[k] = round(p.x * 1e6), y = values[k+1] = round(p.y * 1e6), z = values[k+2] = round(p.z * 1e6);
|
|
738
|
+
values[k+3] = round(n.x * 1e6), values[k+4] = round(n.y * 1e6), values[k+5] = round(n.z * 1e6);
|
|
739
|
+
values[k+6] = round(uv.x * 1e6), values[k+7] = round(uv.y * 1e6);
|
|
740
|
+
values[k+8] = (this.colors[i] || WHITE).rgbaInt();
|
|
741
|
+
let h = mix(mix(mix(0x811c9dc5, x), y), z);
|
|
742
|
+
|
|
743
|
+
// the place, shared with a vertex there that has another normal or uv, as at a lathe's poles
|
|
744
|
+
for (let slot = h & mask;; slot = (slot + 1) & mask)
|
|
745
|
+
{
|
|
746
|
+
const o = places[slot] - 1;
|
|
747
|
+
if (o < 0) { places[slot] = i + 1, place[i] = i; break; }
|
|
748
|
+
if (values[o*9] === x && values[o*9+1] === y && values[o*9+2] === z) { place[i] = o; break; }
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// the whole vertex
|
|
752
|
+
for (let m = 3; m < 9; ++m)
|
|
753
|
+
h = mix(h, values[k+m]);
|
|
754
|
+
for (let slot = h & mask;; slot = (slot + 1) & mask)
|
|
755
|
+
{
|
|
756
|
+
const o = seen[slot] - 1;
|
|
757
|
+
if (o < 0) { seen[slot] = i + 1, remap[i] = vertices.length, vertices.push(i); break; }
|
|
758
|
+
let same = true;
|
|
759
|
+
for (let m = 0; same && m < 9; ++m)
|
|
760
|
+
same = values[o*9+m] === values[k+m];
|
|
761
|
+
if (same) { remap[i] = remap[o]; break; }
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return {vertices, indices: render3DStripTriangles(count, remap, place)};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** Draw the mesh with the current draw state, batched with its other uses in the opaque stage
|
|
768
|
+
* @param {Matrix4|Vector3} [matrix] - Object transform, or just a position to draw it at
|
|
769
|
+
* @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile or the whole texture
|
|
770
|
+
* @param {Color} [color] - Tint */
|
|
771
|
+
render(matrix, tileInfo, color) { render3D?.drawMesh(this, matrix, tileInfo, color); }
|
|
772
|
+
|
|
773
|
+
/** Delete the GPU buffer now, the CPU arrays stay so the mesh can be rendered again
|
|
774
|
+
* - Optional, the buffer is freed anyway once the mesh is garbage collected, this frees it right away */
|
|
775
|
+
dispose()
|
|
776
|
+
{
|
|
777
|
+
if (!this.buffer) return;
|
|
778
|
+
render3DMeshBuffers?.unregister(this); // freed here, so not again when the mesh is collected
|
|
779
|
+
// a buffer from a context that was lost is gone with it, and the new context refuses to delete it
|
|
780
|
+
if (this.contextGeneration === render3D?.contextGeneration)
|
|
781
|
+
glContext?.deleteBuffer(this.buffer), glContext?.deleteBuffer(this.indexBuffer);
|
|
782
|
+
this.buffer = this.indexBuffer = undefined;
|
|
783
|
+
this.bufferCount = 0;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
788
|
+
// Shape builders, all centered on the origin so buildMatrix does placement
|
|
789
|
+
// Sizes are full sizes like buildBox and the 2D drawCircle, sides go around an axis and rings along it
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Spin a flat outline around the Y axis to make a round shape, like a vase or a wheel
|
|
793
|
+
* - profile is [[radius, y], ...] from bottom to top
|
|
794
|
+
* - A profile that ends where it starts makes a closed ring like a donut
|
|
795
|
+
* - An end left open, with a radius and no cap, makes the mesh doubleSided so its inside shows
|
|
796
|
+
* - An end on the axis smooth shades as a round pole like a sphere's when its segment is within 45 degrees of
|
|
797
|
+
* level, and as a point like a cone's tip when it is steeper
|
|
798
|
+
* @param {Array<Array<number>>} profile
|
|
799
|
+
* @param {number} [sides] - Around the axis
|
|
800
|
+
* @param {boolean} [smooth] - Defaults to render3D.smoothShading
|
|
801
|
+
* @param {boolean} [capped] - Close the ends that have a radius with flat discs
|
|
802
|
+
* @return {Mesh}
|
|
803
|
+
* @memberof Render3D
|
|
804
|
+
* @example
|
|
805
|
+
* const vase = buildLathe([[0, -1], [.8, -.3], [.9, .2], [.4, .6], [0, 1]], 12);
|
|
806
|
+
*/
|
|
807
|
+
function buildLathe(profile, sides=16, smooth=render3D?.smoothShading, capped=true)
|
|
808
|
+
{
|
|
809
|
+
ASSERT(isArray(profile) && profile.length > 1, 'lathe profile needs at least 2 points');
|
|
810
|
+
sides |= 0;
|
|
811
|
+
ASSERT(sides > 2, 'lathe needs at least 3 sides');
|
|
812
|
+
const mesh = new Mesh;
|
|
813
|
+
const rings = profile.length;
|
|
814
|
+
const point = (i, a)=> vec3(sin(a) * profile[i][0], profile[i][1], cos(a) * profile[i][0]);
|
|
815
|
+
|
|
816
|
+
// 2D outward normal of each profile segment, in (radius, y) space
|
|
817
|
+
const segmentNormal = (i)=>
|
|
818
|
+
{
|
|
819
|
+
const [r0, y0] = profile[i], [r1, y1] = profile[i+1];
|
|
820
|
+
const n = vec2(y1 - y0, r0 - r1);
|
|
821
|
+
return n.length() ? n.normalize() : vec2(1, 0);
|
|
822
|
+
};
|
|
823
|
+
// vertex normal: average of the adjacent segment normals, across the seam when the profile is closed,
|
|
824
|
+
// and a closed profile needs no caps
|
|
825
|
+
const closed = rings > 2 && abs(profile[0][0] - profile[rings-1][0]) < 1e-9 &&
|
|
826
|
+
abs(profile[0][1] - profile[rings-1][1]) < 1e-9;
|
|
827
|
+
const segmentLength = (i)=> hypot(profile[i+1][0] - profile[i][0], profile[i+1][1] - profile[i][1]);
|
|
828
|
+
const vertexNormal = (i)=>
|
|
829
|
+
{
|
|
830
|
+
// an open end on the axis is a pole and points along it, like a sphere's, when the surface there is
|
|
831
|
+
// within 45 degrees of level; a steeper one is a point like a cone's tip and takes its side's normal
|
|
832
|
+
if (!closed && (!i || i == rings - 1) && abs(profile[i][0]) < 1e-9)
|
|
833
|
+
{
|
|
834
|
+
const up = i ? 1 : -1;
|
|
835
|
+
if (segmentNormal(i ? i - 1 : 0).y * up > Math.SQRT1_2 - 1e-9)
|
|
836
|
+
return vec2(0, up);
|
|
837
|
+
}
|
|
838
|
+
// otherwise the neighbors weighted by their length, so a short band does not tilt a long wall
|
|
839
|
+
let n = vec2();
|
|
840
|
+
const add = (s)=> n = n.add(segmentNormal(s).scale(segmentLength(s)));
|
|
841
|
+
if (i > 0) add(i - 1);
|
|
842
|
+
else if (closed) add(rings - 2);
|
|
843
|
+
if (i < rings - 1) add(i);
|
|
844
|
+
else if (closed) add(0);
|
|
845
|
+
return n.length() ? n.normalize() : vec2(1, 0);
|
|
846
|
+
};
|
|
847
|
+
const normal3D = (n, a)=> vec3(sin(a) * n.x, n.y, cos(a) * n.x);
|
|
848
|
+
|
|
849
|
+
// v runs along the profile by arc length
|
|
850
|
+
const lengths = [0];
|
|
851
|
+
for (let i = 1; i < rings; ++i)
|
|
852
|
+
lengths[i] = lengths[i-1] + hypot(profile[i][0] - profile[i-1][0], profile[i][1] - profile[i-1][1]);
|
|
853
|
+
const total = lengths[rings - 1] || 1;
|
|
854
|
+
const v = (i)=> 1 - lengths[i] / total;
|
|
855
|
+
|
|
856
|
+
for (let i = 0; i + 1 < rings; ++i)
|
|
857
|
+
{
|
|
858
|
+
if (smooth)
|
|
859
|
+
{
|
|
860
|
+
// one ribbon around the ring pair, top point then bottom point per column
|
|
861
|
+
const points = [], normals = [], uvs = [];
|
|
862
|
+
const n0 = vertexNormal(i), n1 = vertexNormal(i + 1);
|
|
863
|
+
// a point on the axis is one per column, each drawn by the face beside it, so its normal turns half
|
|
864
|
+
// a side toward the middle of that face; a pole's points along the axis and does not turn
|
|
865
|
+
const half = PI / sides;
|
|
866
|
+
const turn1 = abs(profile[i + 1][0]) < 1e-9 ? -half : 0, turn0 = abs(profile[i][0]) < 1e-9 ? half : 0;
|
|
867
|
+
for (let j = 0; j <= sides; ++j)
|
|
868
|
+
{
|
|
869
|
+
const a = j / sides * 2 * PI, u = j / sides;
|
|
870
|
+
points.push(point(i + 1, a), point(i, a));
|
|
871
|
+
normals.push(normal3D(n1, a + turn1), normal3D(n0, a + turn0));
|
|
872
|
+
uvs.push(vec2(u, v(i + 1)), vec2(u, v(i)));
|
|
873
|
+
}
|
|
874
|
+
mesh.addStrip(points, normals, uvs);
|
|
875
|
+
}
|
|
876
|
+
else
|
|
877
|
+
{
|
|
878
|
+
// one quad per side with its face normal
|
|
879
|
+
const n = segmentNormal(i);
|
|
880
|
+
for (let j = 0; j < sides; ++j)
|
|
881
|
+
{
|
|
882
|
+
const a0 = j / sides * 2 * PI, a1 = (j + 1) / sides * 2 * PI;
|
|
883
|
+
const u0 = j / sides, u1 = (j + 1) / sides;
|
|
884
|
+
mesh.addStrip(
|
|
885
|
+
[point(i + 1, a0), point(i, a0), point(i + 1, a1), point(i, a1)],
|
|
886
|
+
normal3D(n, (a0 + a1) / 2),
|
|
887
|
+
[vec2(u0, v(i + 1)), vec2(u0, v(i)), vec2(u1, v(i + 1)), vec2(u1, v(i))]);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// flat discs close the ends that have a radius, a hard edge even when the sides are smooth
|
|
893
|
+
if (capped && !closed)
|
|
894
|
+
for (const [i, up] of /** @type {Array<[number, boolean]>} */ ([[0, false], [rings - 1, true]]))
|
|
895
|
+
{
|
|
896
|
+
if (abs(profile[i][0]) < 1e-9) continue; // a pole has no cap
|
|
897
|
+
const points = [], uvs = [];
|
|
898
|
+
for (let j = 0; j < sides; ++j)
|
|
899
|
+
{
|
|
900
|
+
const a = (up ? j : -j) / sides * 2 * PI; // counter clockwise seen from outside
|
|
901
|
+
points.push(point(i, a));
|
|
902
|
+
uvs.push(vec2(sin(a) * .5 + .5, cos(a) * .5 + .5));
|
|
903
|
+
}
|
|
904
|
+
mesh.addStrip(render3DPolygonStrip(points), vec3(0, up ? 1 : -1, 0), render3DPolygonStrip(uvs));
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// an end left open shows the inside, so it is seen from both sides
|
|
908
|
+
mesh.doubleSided = !closed && !capped && (abs(profile[0][0]) > 1e-9 || abs(profile[rings-1][0]) > 1e-9);
|
|
909
|
+
return mesh;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Build a sphere centered on the origin
|
|
914
|
+
* @param {number} [size] - Diameter
|
|
915
|
+
* @param {number} [sides] - Around
|
|
916
|
+
* @param {number} [rings] - Top to bottom
|
|
917
|
+
* @param {boolean} [smooth] - Defaults to render3D.smoothShading
|
|
918
|
+
* @return {Mesh}
|
|
919
|
+
* @memberof Render3D
|
|
920
|
+
*/
|
|
921
|
+
function buildSphere(size=1, sides=16, rings=8, smooth=render3D?.smoothShading)
|
|
922
|
+
{
|
|
923
|
+
ASSERT(rings > 1, 'sphere needs at least 2 rings');
|
|
924
|
+
const profile = [];
|
|
925
|
+
for (let i = 0; i <= rings; ++i)
|
|
926
|
+
{
|
|
927
|
+
const a = i / rings * PI - PI/2;
|
|
928
|
+
profile.push([cos(a) * size / 2, sin(a) * size / 2]);
|
|
929
|
+
}
|
|
930
|
+
return buildLathe(profile, sides, smooth);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// a box with its edges cut by a bevel of size t: each face shrunk by t, a strip along each edge around a quarter
|
|
934
|
+
// circle, and an eighth of a sphere at each corner; one segment makes them flat, a chamfer
|
|
935
|
+
function render3DBevelBox(half, t, segments, faces)
|
|
936
|
+
{
|
|
937
|
+
const mesh = new Mesh, inner = half.subtract(vec3(t)), n = max(1, segments | 0);
|
|
938
|
+
|
|
939
|
+
// a triangle facing out, since the box is convex around the origin, flat shaded or with the normals of the
|
|
940
|
+
// round, and uvs from the face it faces most, as a plain box maps that face
|
|
941
|
+
const triangle = (a, b, c, na, nb, nc)=>
|
|
942
|
+
{
|
|
943
|
+
let normal = b.subtract(a).cross(c.subtract(a));
|
|
944
|
+
if (normal.lengthSquared() < 1e-20) return; // an edge or face with no size
|
|
945
|
+
if (normal.dot(a.add(b).add(c)) < 0)
|
|
946
|
+
[b, c, nb, nc, normal] = [c, b, nc, nb, normal.scale(-1)];
|
|
947
|
+
normal = normal.normalize();
|
|
948
|
+
let face = faces[0];
|
|
949
|
+
for (const f of faces)
|
|
950
|
+
if (f[0].dot(normal) > face[0].dot(normal))
|
|
951
|
+
face = f;
|
|
952
|
+
const [, r, u] = face, hr = abs(half.dot(r)), hu = abs(half.dot(u));
|
|
953
|
+
const uv = (p)=> vec2(.5 + p.dot(r) / (2 * hr), .5 - p.dot(u) / (2 * hu));
|
|
954
|
+
mesh.addStrip([a, b, c], n > 1 && na ? [na, nb, nc] : normal, [uv(a), uv(b), uv(c)]);
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
// the faces, shrunk by the bevel
|
|
958
|
+
for (const [normal, r, u] of faces)
|
|
959
|
+
{
|
|
960
|
+
const [a, b, c, d] = render3DQuadAxes(normal.multiply(half), r.multiply(inner), u.multiply(inner));
|
|
961
|
+
triangle(a, b, c);
|
|
962
|
+
triangle(c, b, d);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// the edges, each a strip around a quarter circle from one face to the next
|
|
966
|
+
const axes = [vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1)];
|
|
967
|
+
for (let k = 0; k < 3; ++k)
|
|
968
|
+
for (const si of [-1, 1])
|
|
969
|
+
for (const sj of [-1, 1])
|
|
970
|
+
{
|
|
971
|
+
const n1 = axes[(k + 1) % 3].scale(si), n2 = axes[(k + 2) % 3].scale(sj);
|
|
972
|
+
const middle = n1.multiply(inner).add(n2.multiply(inner)), along = axes[k].multiply(inner);
|
|
973
|
+
const start = middle.subtract(along), end = middle.add(along);
|
|
974
|
+
const dir = (m)=> n1.scale(cos(m / n * PI / 2)).add(n2.scale(sin(m / n * PI / 2)));
|
|
975
|
+
for (let m = 0; m < n; ++m)
|
|
976
|
+
{
|
|
977
|
+
const d0 = dir(m), d1 = dir(m + 1);
|
|
978
|
+
const a = start.add(d0.scale(t)), b = end.add(d0.scale(t));
|
|
979
|
+
const c = start.add(d1.scale(t)), d = end.add(d1.scale(t));
|
|
980
|
+
triangle(a, b, c, d0, d0, d1);
|
|
981
|
+
triangle(c, b, d, d1, d0, d1);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// the corners, an eighth of a sphere in rows from one face's axis down to the edge between the other two
|
|
986
|
+
for (const sx of [-1, 1])
|
|
987
|
+
for (const sy of [-1, 1])
|
|
988
|
+
for (const sz of [-1, 1])
|
|
989
|
+
{
|
|
990
|
+
const nx = vec3(sx, 0, 0), ny = vec3(0, sy, 0), nz = vec3(0, 0, sz), corner = inner.multiply(vec3(sx, sy, sz));
|
|
991
|
+
const dir = (i, j)=>
|
|
992
|
+
{
|
|
993
|
+
const polar = i / n * PI / 2, around = i ? j / i * PI / 2 : 0;
|
|
994
|
+
return nz.scale(cos(polar)).add(nx.scale(cos(around) * sin(polar))).add(ny.scale(sin(around) * sin(polar)));
|
|
995
|
+
};
|
|
996
|
+
const point = (i, j)=> corner.add(dir(i, j).scale(t));
|
|
997
|
+
for (let i = 0; i < n; ++i)
|
|
998
|
+
{
|
|
999
|
+
for (let j = 0; j <= i; ++j)
|
|
1000
|
+
triangle(point(i, j), point(i + 1, j), point(i + 1, j + 1), dir(i, j), dir(i + 1, j), dir(i + 1, j + 1));
|
|
1001
|
+
for (let j = 0; j < i; ++j)
|
|
1002
|
+
triangle(point(i, j), point(i + 1, j + 1), point(i, j + 1), dir(i, j), dir(i + 1, j + 1), dir(i, j + 1));
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return mesh;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Build a box centered on the origin, six flat faces with uvs covering each face
|
|
1010
|
+
* - bevel cuts its edges and corners: 1 segment is a flat chamfer, more round them, and the biggest bevel, half
|
|
1011
|
+
* the smallest side, rounds a cube into a ball
|
|
1012
|
+
* @param {Vector3|number} [size] - Full size, a number for a cube
|
|
1013
|
+
* @param {number} [bevel] - Size of the cut on each edge, clamped to half the smallest side
|
|
1014
|
+
* @param {number} [bevelSegments] - Steps around each edge, 1 for a flat chamfer
|
|
1015
|
+
* @return {Mesh}
|
|
1016
|
+
* @memberof Render3D
|
|
1017
|
+
*/
|
|
1018
|
+
function buildBox(size=1, bevel=0, bevelSegments=1)
|
|
1019
|
+
{
|
|
1020
|
+
ASSERT(isNumber(bevel) && bevel >= 0, 'bevel must be a number, 0 or more');
|
|
1021
|
+
const half = render3DSize3(size).scale(.5);
|
|
1022
|
+
// each face: normal, right axis, up axis (right cross up = normal)
|
|
1023
|
+
const faces = [
|
|
1024
|
+
[vec3(0, 0, 1), vec3(1, 0, 0), vec3(0, 1, 0)],
|
|
1025
|
+
[vec3(0, 0, -1), vec3(-1, 0, 0), vec3(0, 1, 0)],
|
|
1026
|
+
[vec3(1, 0, 0), vec3(0, 0, -1), vec3(0, 1, 0)],
|
|
1027
|
+
[vec3(-1, 0, 0), vec3(0, 0, 1), vec3(0, 1, 0)],
|
|
1028
|
+
[vec3(0, 1, 0), vec3(1, 0, 0), vec3(0, 0, -1)],
|
|
1029
|
+
[vec3(0, -1, 0), vec3(1, 0, 0), vec3(0, 0, 1)],
|
|
1030
|
+
];
|
|
1031
|
+
const t = min(bevel, half.x, half.y, half.z);
|
|
1032
|
+
if (t > 0)
|
|
1033
|
+
return render3DBevelBox(half, t, bevelSegments, faces);
|
|
1034
|
+
const mesh = new Mesh;
|
|
1035
|
+
for (const [n, r, u] of faces)
|
|
1036
|
+
{
|
|
1037
|
+
const center = n.multiply(half);
|
|
1038
|
+
const right = r.multiply(half), up = u.multiply(half);
|
|
1039
|
+
mesh.addStrip(render3DQuadAxes(center, right, up), n, RENDER3D_QUAD_UVS);
|
|
1040
|
+
}
|
|
1041
|
+
return mesh;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Build a heightfield grid in the XZ plane centered on the origin
|
|
1046
|
+
* - smooth rounds the lighting across cells and colors each corner
|
|
1047
|
+
* - flat lights and colors each cell on its own, so a checkerboard stays crisp
|
|
1048
|
+
* - doubleSided, a sheet seen from both sides; turn it off for ground only ever seen from above
|
|
1049
|
+
* - One cell is a plain square, render3D.planeMesh and planeMeshDoubleSided are shared ones
|
|
1050
|
+
* @param {Vector2|number} [size] - World size along X and Z, a number for a square
|
|
1051
|
+
* @param {Vector2|number} [segments] - Cells along X and Z, a number for both
|
|
1052
|
+
* @param {Color|function(number, number): Color} [color] - One Color for the whole grid, or (x, z) => Color
|
|
1053
|
+
* @param {function(number, number): number} [heightFunction] - (x, z) => y, default flat
|
|
1054
|
+
* @param {boolean} [smooth] - Defaults to render3D.smoothShading with a heightFunction; a flat grid is flat shaded,
|
|
1055
|
+
* since its light is even anyway and smoothing would only blend its cell colors; pass smooth to blend a gradient
|
|
1056
|
+
* @return {Mesh}
|
|
1057
|
+
* @memberof Render3D
|
|
1058
|
+
* @example
|
|
1059
|
+
* const ground = buildGrid(vec2(20), 10, (x, z)=> (floor(x / 2) + floor(z / 2)) & 1 ? GRAY : WHITE); // 2 unit checks
|
|
1060
|
+
*/
|
|
1061
|
+
function buildGrid(size=vec2(1), segments=1, color, heightFunction, smooth=heightFunction && render3D?.smoothShading)
|
|
1062
|
+
{
|
|
1063
|
+
heightFunction ||= ()=> 0;
|
|
1064
|
+
size = render3DSize2(size);
|
|
1065
|
+
segments = render3DSize2(segments);
|
|
1066
|
+
ASSERT(segments.x > 0 && segments.y > 0 && segments.x % 1 === 0 && segments.y % 1 === 0,
|
|
1067
|
+
'grid segments must be whole numbers above zero');
|
|
1068
|
+
const mesh = new Mesh;
|
|
1069
|
+
const segmentsX = segments.x, segmentsZ = segments.y;
|
|
1070
|
+
const cellX = size.x / segmentsX, cellZ = size.y / segmentsZ;
|
|
1071
|
+
const halfX = size.x / 2, halfZ = size.y / 2, ex = cellX / 2, ez = cellZ / 2;
|
|
1072
|
+
const px = (i)=> i * cellX - halfX, pz = (j)=> j * cellZ - halfZ;
|
|
1073
|
+
const colorAt = /** @type {function(number, number): Color} */ (color);
|
|
1074
|
+
const cellColor = (i, j)=> !color ? WHITE : isColor(color) ? /** @type {Color} */ (color) : colorAt(px(i), pz(j));
|
|
1075
|
+
// a big terrain has millions of vertices, so each one is made once, shared by the rows above and below it
|
|
1076
|
+
const row = (j)=>
|
|
1077
|
+
{
|
|
1078
|
+
const points = [], normals = [], uvs = [], colors = [], z = pz(j);
|
|
1079
|
+
for (let i = 0; i <= segmentsX; ++i)
|
|
1080
|
+
{
|
|
1081
|
+
const x = px(i);
|
|
1082
|
+
points.push(vec3(x, heightFunction(x, z), z));
|
|
1083
|
+
uvs.push(vec2(i / segmentsX, j / segmentsZ));
|
|
1084
|
+
if (!smooth) continue;
|
|
1085
|
+
normals.push(render3DSlopeNormal(heightFunction, x, z, ex, ez, halfX, halfZ));
|
|
1086
|
+
colors.push(cellColor(i, j));
|
|
1087
|
+
}
|
|
1088
|
+
return {points, normals, uvs, colors};
|
|
1089
|
+
};
|
|
1090
|
+
let above = row(0);
|
|
1091
|
+
for (let j = 0; j < segmentsZ; ++j)
|
|
1092
|
+
{
|
|
1093
|
+
const below = row(j + 1);
|
|
1094
|
+
if (smooth)
|
|
1095
|
+
{
|
|
1096
|
+
// one ribbon per row with vertex normals from the slope
|
|
1097
|
+
const points = [], normals = [], uvs = [], colors = [];
|
|
1098
|
+
for (let i = 0; i <= segmentsX; ++i)
|
|
1099
|
+
{
|
|
1100
|
+
points.push(above.points[i], below.points[i]);
|
|
1101
|
+
normals.push(above.normals[i], below.normals[i]);
|
|
1102
|
+
uvs.push(above.uvs[i], below.uvs[i]);
|
|
1103
|
+
colors.push(above.colors[i], below.colors[i]);
|
|
1104
|
+
}
|
|
1105
|
+
mesh.addStrip(points, normals, uvs, colors);
|
|
1106
|
+
}
|
|
1107
|
+
else
|
|
1108
|
+
{
|
|
1109
|
+
// one quad per cell with its face normal and one color sampled at its center
|
|
1110
|
+
for (let i = 0; i < segmentsX; ++i)
|
|
1111
|
+
mesh.addQuad(above.points[i], below.points[i], below.points[i+1], above.points[i+1], cellColor(i + .5, j + .5),
|
|
1112
|
+
[above.uvs[i], below.uvs[i], below.uvs[i+1], above.uvs[i+1]]);
|
|
1113
|
+
}
|
|
1114
|
+
above = below;
|
|
1115
|
+
}
|
|
1116
|
+
if (smooth)
|
|
1117
|
+
{
|
|
1118
|
+
// the grid knows which strip entries are one vertex, each shared by the rows above and below it and by the
|
|
1119
|
+
// repeats at the ends of its ribbons, so the upload of a big terrain skips searching millions of entries
|
|
1120
|
+
const n = 2 * (segmentsX + 1) + 2, keys = mesh.vertexKeys = new Int32Array(mesh.points.length);
|
|
1121
|
+
const id = (i, j)=> j * (segmentsX + 1) + i;
|
|
1122
|
+
for (let j = 0; j < segmentsZ; ++j)
|
|
1123
|
+
{
|
|
1124
|
+
const start = j * n;
|
|
1125
|
+
keys[start] = id(0, j); // the leading repeat
|
|
1126
|
+
for (let i = 0; i <= segmentsX; ++i)
|
|
1127
|
+
keys[start + 1 + 2*i] = id(i, j), keys[start + 2 + 2*i] = id(i, j + 1);
|
|
1128
|
+
keys[start + n - 1] = id(segmentsX, j + 1); // the trailing repeat
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
mesh.doubleSided = true; // a sheet, seen from both sides; terrain seen only from above can turn it off
|
|
1132
|
+
return mesh;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Build a sky dome: a sphere colored by direction, wound to be seen from inside
|
|
1137
|
+
* - set it as render3D.sky and the pass draws it around the camera behind everything
|
|
1138
|
+
* @param {Color} [topColor] - Straight up
|
|
1139
|
+
* @param {Color} [horizonColor] - Level with the camera
|
|
1140
|
+
* @param {Color} [bottomColor] - Straight down, what a camera looking at the ground sees past its edge; defaults to the
|
|
1141
|
+
* horizon color
|
|
1142
|
+
* @param {number} [sides] - Around
|
|
1143
|
+
* @param {number} [rings] - Top to bottom
|
|
1144
|
+
* @return {Mesh}
|
|
1145
|
+
* @memberof Render3D
|
|
1146
|
+
*/
|
|
1147
|
+
function buildSky(topColor=hsl(.6, .8, .55), horizonColor=hsl(.6, 1, .9), bottomColor=horizonColor, sides=16, rings=8)
|
|
1148
|
+
{
|
|
1149
|
+
// a sphere turned inside out so it is seen from within, each point colored by how high it is
|
|
1150
|
+
const mesh = buildSphere(2, sides, rings, true).flipNormals();
|
|
1151
|
+
mesh.colors = mesh.points.map(p=> p.y < 0 ? horizonColor.lerp(bottomColor, -p.y) : horizonColor.lerp(topColor, p.y));
|
|
1152
|
+
render3DSkyColors.set(mesh, [topColor.copy(), horizonColor.copy(), bottomColor.copy()]);
|
|
1153
|
+
return mesh;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// the top, horizon and bottom colors of each sky dome buildSky made, which a reflection shows while that dome is
|
|
1157
|
+
// render3D.sky, whether setSky set it or the game did
|
|
1158
|
+
const render3DSkyColors = new WeakMap;
|