littlejsengine 1.19.3 → 1.20.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/FAQ.md +841 -820
- package/README.md +29 -25
- package/dist/littlejs.d.ts +2455 -935
- package/dist/littlejs.esm.js +30344 -21110
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +30259 -21052
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +26567 -20455
- package/package.json +10 -5
- package/plugins/audioEffects.js +28 -24
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +740 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -332
- package/plugins/math3d.js +58 -17
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +15 -0
- package/plugins/postProcess.js +56 -13
- package/plugins/render3d.js +1643 -1577
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +263 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +191 -75
- package/src/engineAudio.js +229 -125
- package/src/engineBuild.mjs +289 -256
- package/src/engineDebug.js +912 -742
- package/src/engineDraw.js +295 -193
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +12 -0
- package/src/engineInput.js +1462 -1293
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +194 -80
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +77 -29
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +19 -13
- package/src/engineWebGL.js +239 -82
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
package/plugins/render3d.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - The 3D scene draws under the 2D sprites, so HUD and text land on top
|
|
7
7
|
* - Lighting is the sun plus ambient, with optional extra lights, fog and shadows
|
|
8
8
|
* - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to
|
|
9
|
-
* - Build shapes with buildBox, buildSphere and
|
|
9
|
+
* - Build shapes with buildBox, buildSphere, buildGrid and buildLathe; the other builders, terrain, particles,
|
|
10
|
+
* camera controls and the OBJ loader are in the Render3D Extras plugin, which goes after this one
|
|
10
11
|
* - Requires the Math3D plugin
|
|
11
12
|
* @namespace Render3D
|
|
12
13
|
*/
|
|
@@ -24,29 +25,26 @@ let render3D;
|
|
|
24
25
|
const RENDER3D_VERTEX_FLOATS = 9;
|
|
25
26
|
const RENDER3D_VERTEX_BYTES = RENDER3D_VERTEX_FLOATS * 4;
|
|
26
27
|
|
|
27
|
-
// per draw values the shaders read as vertex attributes: the model matrix columns (4-7), the
|
|
28
|
-
//
|
|
29
|
-
const RENDER3D_INSTANCE_FLOATS =
|
|
28
|
+
// per draw values the shaders read as vertex attributes: the model matrix columns (4-7), the tint (11) and the
|
|
29
|
+
// uv rect (12); constants for one draw, one per instance for a batch; the shader derives the normal matrix
|
|
30
|
+
const RENDER3D_INSTANCE_FLOATS = 24;
|
|
30
31
|
const RENDER3D_INSTANCE_BYTES = RENDER3D_INSTANCE_FLOATS * 4;
|
|
31
|
-
const RENDER3D_INSTANCE_ATTRIBS = [[4, 4, 0], [5, 4, 16], [6, 4, 32], [7, 4, 48], [
|
|
32
|
+
const RENDER3D_INSTANCE_ATTRIBS = [[4, 4, 0], [5, 4, 16], [6, 4, 32], [7, 4, 48], [11, 4, 64], [12, 4, 80]];
|
|
32
33
|
const RENDER3D_VERTEX_INPUTS =
|
|
33
34
|
'layout(location=0) in vec3 p;layout(location=1) in vec3 n;layout(location=2) in vec2 t;layout(location=3) in vec4 c;' +
|
|
34
35
|
'layout(location=4) in vec4 m0;layout(location=5) in vec4 m1;layout(location=6) in vec4 m2;layout(location=7) in vec4 m3;' +
|
|
35
|
-
'layout(location=8) in vec3 n0;layout(location=9) in vec3 n1;layout(location=10) in vec3 n2;' +
|
|
36
36
|
'layout(location=11) in vec4 tint;layout(location=12) in vec4 uvRect;';
|
|
37
37
|
const RENDER3D_MAX_STREAM_VERTS = 32768;
|
|
38
38
|
const RENDER3D_MAX_LIGHTS = 8; // Light3D objects per frame, the shader loops over this many
|
|
39
|
-
|
|
39
|
+
// strip order, frozen, and typed as the plain array the uv parameters take
|
|
40
|
+
const RENDER3D_QUAD_UVS = /** @type {Array<Vector2>} */
|
|
41
|
+
(Object.freeze([vec2(0, 0), vec2(0, 1), vec2(1, 0), vec2(1, 1)].map(uv=> Object.freeze(uv))));
|
|
40
42
|
const RENDER3D_FULL_UV_RECT = Object.freeze({x:0, y:0, w:1, h:1});
|
|
41
43
|
const RENDER3D_DEFAULT_NORMAL = Object.freeze(vec3(0, 1, 0));
|
|
42
44
|
const RENDER3D_DEFAULT_UV = Object.freeze(vec2());
|
|
43
45
|
const RENDER3D_SHADOW_COLOR = Object.freeze(hsl(0, 0, 0, .5));
|
|
44
46
|
const RENDER3D_IDENTITY = new Matrix4; // never modified
|
|
45
|
-
|
|
46
|
-
// gap between lines of 3D text, as a share of the character height; flat text can let lines touch
|
|
47
|
-
// the way the 2D font does, but extruded glyphs seen from an angle then overlap the line below
|
|
48
|
-
const RENDER3D_TEXT_LEADING = 1.3;
|
|
49
|
-
|
|
47
|
+
let render3DShadowCut; // the cut last sent to the shadow shader, see render3DSetDrawUniforms
|
|
50
48
|
///////////////////////////////////////////////////////////////////////////////
|
|
51
49
|
// Private helpers
|
|
52
50
|
|
|
@@ -68,29 +66,56 @@ function render3DQuadValues(v) { return isArray(v) ? render3DQuadStrip(...v) : v
|
|
|
68
66
|
function render3DCanDraw()
|
|
69
67
|
{
|
|
70
68
|
if (!render3D.program) return false;
|
|
71
|
-
ASSERT(render3D.isRendering,
|
|
69
|
+
ASSERT(render3D.isRendering,
|
|
70
|
+
'3D draws are only valid during the 3D pass, draw from an EngineObject3D or render3D.onRenderOpaque');
|
|
72
71
|
return render3D.isRendering;
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
// the draw state fields a batch is drawn under; lights and fog are not captured, they are read live at flush
|
|
76
|
-
|
|
75
|
+
// the three functions below write them out by hand for speed, so a new field goes in all four
|
|
76
|
+
const RENDER3D_STATE_FIELDS = ['blend', 'additive', 'depthTest', 'depthWrite', 'cullBackFaces', 'mirrored', 'lighting',
|
|
77
|
+
'emissive', 'receiveShadow', 'specular', 'pixelated', 'shader'];
|
|
77
78
|
|
|
78
|
-
// a copy of the
|
|
79
|
+
// a copy of the draw state in one fixed shape, the fields of RENDER3D_STATE_FIELDS written out so the
|
|
80
|
+
// compare below stays a handful of direct reads, it runs for every instance drawn
|
|
79
81
|
function render3DCaptureBatchState()
|
|
80
82
|
{
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const r = render3D;
|
|
84
|
+
return {blend: r.blend, additive: r.additive, depthTest: r.depthTest, depthWrite: r.depthWrite,
|
|
85
|
+
cullBackFaces: r.cullBackFaces, mirrored: r.mirrored, lighting: r.lighting, emissive: r.emissive,
|
|
86
|
+
receiveShadow: r.receiveShadow, specular: r.specular, pixelated: r.pixelated, shader: r.shader};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// put a captured draw state back, written out the same way; the transparent stage does this for every queued draw
|
|
90
|
+
function render3DApplyBatchState(s)
|
|
91
|
+
{
|
|
92
|
+
const r = render3D;
|
|
93
|
+
r.blend = s.blend, r.additive = s.additive, r.depthTest = s.depthTest, r.depthWrite = s.depthWrite,
|
|
94
|
+
r.cullBackFaces = s.cullBackFaces, r.mirrored = s.mirrored, r.lighting = s.lighting, r.emissive = s.emissive,
|
|
95
|
+
r.receiveShadow = s.receiveShadow, r.specular = s.specular, r.pixelated = s.pixelated, r.shader = s.shader;
|
|
85
96
|
}
|
|
86
97
|
|
|
87
98
|
// true when the current draw state differs from a captured one, so a pending batch must flush first
|
|
88
|
-
function render3DStateChanged(
|
|
99
|
+
function render3DStateChanged(s)
|
|
100
|
+
{
|
|
101
|
+
const r = render3D;
|
|
102
|
+
return r.blend !== s.blend || r.additive !== s.additive || r.depthTest !== s.depthTest
|
|
103
|
+
|| r.depthWrite !== s.depthWrite || r.cullBackFaces !== s.cullBackFaces || r.mirrored !== s.mirrored
|
|
104
|
+
|| r.lighting !== s.lighting || r.emissive !== s.emissive || r.receiveShadow !== s.receiveShadow
|
|
105
|
+
|| r.specular !== s.specular || r.pixelated !== s.pixelated || r.shader !== s.shader;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// whether a sphere is inside the view, or the shadow map's box during the shadow pass, without a vector
|
|
109
|
+
function render3DSphereVisible(x, y, z, radius)
|
|
89
110
|
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
111
|
+
const planes = render3D.shadowPass ? render3D.shadowPlanes : render3D.frustumPlanes;
|
|
112
|
+
for (let i = 0; i < planes.length; ++i)
|
|
113
|
+
{
|
|
114
|
+
const p = planes[i];
|
|
115
|
+
if (p[0]*x + p[1]*y + p[2]*z + p[3] < -radius)
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
// run a function with some draw state fields overridden, restored afterward even on a throw
|
|
@@ -107,9 +132,20 @@ function render3DWithState(fields, fn)
|
|
|
107
132
|
function render3DIsAfter2D(o) { return !!(o.renderAfter2D ?? render3D.renderAfter2D); }
|
|
108
133
|
|
|
109
134
|
// a size given as a number or a vec3
|
|
110
|
-
|
|
135
|
+
/** @param {Vector3|number} size
|
|
136
|
+
* @return {Vector3} */
|
|
137
|
+
function render3DSize3(size)
|
|
138
|
+
{ return isNumber(size) ? vec3(/** @type {number} */ (size)) : /** @type {Vector3} */ (size); }
|
|
139
|
+
|
|
140
|
+
// a size given as a number or a vec2
|
|
141
|
+
/** @param {Vector2|number} size
|
|
142
|
+
* @return {Vector2} */
|
|
143
|
+
function render3DSize2(size)
|
|
144
|
+
{ return isNumber(size) ? vec2(/** @type {number} */ (size)) : /** @type {Vector2} */ (size); }
|
|
111
145
|
|
|
112
146
|
// a transform given as a matrix, or as a vec3 for one that only moves there
|
|
147
|
+
/** @param {Matrix4|Vector3} matrix
|
|
148
|
+
* @return {Matrix4} */
|
|
113
149
|
function render3DMatrix(matrix)
|
|
114
150
|
{
|
|
115
151
|
if (matrix instanceof Vector3)
|
|
@@ -121,16 +157,18 @@ function render3DMatrix(matrix)
|
|
|
121
157
|
// the matrix that keeps normals pointing out when an object is scaled unevenly
|
|
122
158
|
function render3DNormalMatrix(matrix) { return matrix.copy().invert().transpose(); }
|
|
123
159
|
|
|
160
|
+
// whether a matrix mirrors, its determinant negative, so what it moves reads the other way round
|
|
161
|
+
function render3DMirrors(matrix) { return matrix.determinant() < 0; }
|
|
162
|
+
|
|
163
|
+
|
|
124
164
|
// a column of a matrix as a direction: 0 is the right axis, 4 up, 8 back
|
|
125
165
|
function render3DAxis(m, i) { return vec3(m[i], m[i+1], m[i+2]); }
|
|
126
166
|
|
|
127
|
-
//
|
|
128
|
-
function
|
|
167
|
+
// a matrix turning by angle around a unit axis through the origin, counter clockwise when the axis points at you
|
|
168
|
+
function render3DAxisRotation(axis, angle)
|
|
129
169
|
{
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
const dz = (heightFunction(x, z1) - heightFunction(x, z0)) / (z1 - z0 || 1);
|
|
133
|
-
return vec3(-dx, 1, -dz).normalize();
|
|
170
|
+
const x = vec3(1, 0, 0).rotate(axis, angle), y = vec3(0, 1, 0).rotate(axis, angle), z = vec3(0, 0, 1).rotate(axis, angle);
|
|
171
|
+
return new Matrix4([x.x, x.y, x.z, 0, y.x, y.y, y.z, 0, z.x, z.y, z.z, 0, 0, 0, 0, 1]);
|
|
134
172
|
}
|
|
135
173
|
|
|
136
174
|
// the largest axis scale of a matrix, how much it grows a bounding sphere
|
|
@@ -139,6 +177,20 @@ function render3DMaxScale(m)
|
|
|
139
177
|
return max(m[0]*m[0] + m[1]*m[1] + m[2]*m[2], m[4]*m[4] + m[5]*m[5] + m[6]*m[6], m[8]*m[8] + m[9]*m[9] + m[10]*m[10]) ** .5;
|
|
140
178
|
}
|
|
141
179
|
|
|
180
|
+
// how far a matrix at offset k can move a point that is one unit from its origin, for a bounding sphere: the
|
|
181
|
+
// longest axis when the axes are square to each other, more when they are not, as a turned child under a parent
|
|
182
|
+
// scaled on one axis leaves them; the axes' dot products bound the largest stretch by their largest row sum
|
|
183
|
+
function render3DMaxStretch(m, k=0)
|
|
184
|
+
{
|
|
185
|
+
const xx = m[k]*m[k] + m[k+1]*m[k+1] + m[k+2]*m[k+2];
|
|
186
|
+
const yy = m[k+4]*m[k+4] + m[k+5]*m[k+5] + m[k+6]*m[k+6];
|
|
187
|
+
const zz = m[k+8]*m[k+8] + m[k+9]*m[k+9] + m[k+10]*m[k+10];
|
|
188
|
+
const xy = abs(m[k]*m[k+4] + m[k+1]*m[k+5] + m[k+2]*m[k+6]);
|
|
189
|
+
const xz = abs(m[k]*m[k+8] + m[k+1]*m[k+9] + m[k+2]*m[k+10]);
|
|
190
|
+
const yz = abs(m[k+4]*m[k+8] + m[k+5]*m[k+9] + m[k+6]*m[k+10]);
|
|
191
|
+
return max(xx + xy + xz, yy + xy + yz, zz + xz + yz) ** .5;
|
|
192
|
+
}
|
|
193
|
+
|
|
142
194
|
// a quad as a strip from its center and half axes, the same corner order as render3DQuadStrip
|
|
143
195
|
function render3DQuadAxes(center, right, up)
|
|
144
196
|
{
|
|
@@ -162,6 +214,8 @@ function render3DSetObjectState(o)
|
|
|
162
214
|
ASSERT(!o?.shader || o.shader instanceof Shader, 'shader must be a Shader, not the snippet itself');
|
|
163
215
|
r.shader = o?.shader || undefined; // null is no shader too, so it batches with none
|
|
164
216
|
r.depthTest = true;
|
|
217
|
+
if (r.shadowPass)
|
|
218
|
+
r.blend = !!o?.transparent; // the depth shader cuts a see through caster by the alpha it would blend with
|
|
165
219
|
}
|
|
166
220
|
|
|
167
221
|
// draw objects each with the draw state set from its own flags, then reset to the defaults
|
|
@@ -175,20 +229,23 @@ function render3DDrawObjects(objects)
|
|
|
175
229
|
render3DSetObjectState();
|
|
176
230
|
}
|
|
177
231
|
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
function render3DDetach(o)
|
|
232
|
+
// add a draw of a mesh to its batch; a batch is one mesh under one texture and draw state, so a change flushes it
|
|
233
|
+
function render3DInstance(mesh, matrix, tileInfo, color)
|
|
181
234
|
{
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
235
|
+
const k = render3DInstanceSlot(mesh, render3DTextureOf(tileInfo)), data = mesh.instanceData;
|
|
236
|
+
data.set(matrix.m, k);
|
|
237
|
+
// the tint; this batch is never blended, so in the shadow pass the depth shader cuts it by its texture alpha
|
|
238
|
+
// alone, a see through caster draws blended instead and is cut by its tint alpha too
|
|
239
|
+
data[k+16] = color.r, data[k+17] = color.g, data[k+18] = color.b, data[k+19] = color.a;
|
|
240
|
+
const uv = render3DGetTileUVs(tileInfo);
|
|
241
|
+
data[k+20] = uv.x; data[k+21] = uv.y; data[k+22] = uv.w; data[k+23] = uv.h;
|
|
186
242
|
}
|
|
187
243
|
|
|
188
|
-
//
|
|
189
|
-
|
|
244
|
+
// make room for one more instance of a mesh under a texture and the current draw state, flushing a batch that
|
|
245
|
+
// differs first, and return where its 24 floats go in mesh.instanceData: the matrix, the tint and the uv rect
|
|
246
|
+
function render3DInstanceSlot(mesh, textureInfo)
|
|
190
247
|
{
|
|
191
|
-
const r = render3D
|
|
248
|
+
const r = render3D;
|
|
192
249
|
if (mesh.instanceCount && (mesh.instanceTextureInfo !== textureInfo || render3DStateChanged(mesh.instanceState)))
|
|
193
250
|
render3DFlushInstances(mesh);
|
|
194
251
|
if (!mesh.instanceCount)
|
|
@@ -199,19 +256,14 @@ function render3DInstance(mesh, matrix, tileInfo, color)
|
|
|
199
256
|
}
|
|
200
257
|
|
|
201
258
|
// room for one more, doubling as the batch grows
|
|
202
|
-
|
|
203
|
-
const k = mesh.instanceCount++ * RENDER3D_INSTANCE_FLOATS;
|
|
259
|
+
const data = mesh.instanceData, k = mesh.instanceCount++ * RENDER3D_INSTANCE_FLOATS;
|
|
204
260
|
if (!data || data.length < k + RENDER3D_INSTANCE_FLOATS)
|
|
205
261
|
{
|
|
206
262
|
const grown = new Float32Array(max(64 * RENDER3D_INSTANCE_FLOATS, data ? data.length * 2 : 0));
|
|
207
263
|
data && grown.set(data);
|
|
208
|
-
mesh.instanceData =
|
|
264
|
+
mesh.instanceData = grown;
|
|
209
265
|
}
|
|
210
|
-
|
|
211
|
-
render3DNormalMatrix3(matrix.m, data, k + 16);
|
|
212
|
-
data[k+25] = color.r; data[k+26] = color.g; data[k+27] = color.b; data[k+28] = color.a;
|
|
213
|
-
const uv = render3DGetTileUVs(tileInfo);
|
|
214
|
-
data[k+29] = uv.x; data[k+30] = uv.y; data[k+31] = uv.w; data[k+32] = uv.h;
|
|
266
|
+
return k;
|
|
215
267
|
}
|
|
216
268
|
|
|
217
269
|
// draw the pending batches, or just one mesh's, each as a single instanced call
|
|
@@ -226,21 +278,10 @@ function render3DFlushInstances(only)
|
|
|
226
278
|
|
|
227
279
|
// the per instance values on top of the constant attributes, then the mesh under them
|
|
228
280
|
// a ring of buffers with fresh storage each time, so the driver never waits for a draw still reading one
|
|
229
|
-
const buffers = r.instanceBuffers;
|
|
230
|
-
gl.bindBuffer(gl.ARRAY_BUFFER,
|
|
281
|
+
const buffers = r.instanceBuffers, buffer = buffers[r.instanceBufferIndex = (r.instanceBufferIndex + 1) % buffers.length];
|
|
282
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
231
283
|
gl.bufferData(gl.ARRAY_BUFFER, mesh.instanceData, gl.DYNAMIC_DRAW, 0, count * RENDER3D_INSTANCE_FLOATS);
|
|
232
|
-
|
|
233
|
-
{
|
|
234
|
-
gl.vertexAttribPointer(location, size, gl.FLOAT, false, RENDER3D_INSTANCE_BYTES, offset);
|
|
235
|
-
gl.enableVertexAttribArray(location);
|
|
236
|
-
}
|
|
237
|
-
render3DSetDrawUniforms(RENDER3D_IDENTITY, mesh.instanceTextureInfo, WHITE, RENDER3D_FULL_UV_RECT, mesh.instanceState);
|
|
238
|
-
render3DBindVertexBuffer(mesh.buffer);
|
|
239
|
-
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, mesh.bufferCount, count);
|
|
240
|
-
for (const [location] of RENDER3D_INSTANCE_ATTRIBS)
|
|
241
|
-
gl.disableVertexAttribArray(location);
|
|
242
|
-
++drawCount;
|
|
243
|
-
primitiveCount += mesh.bufferCount * count;
|
|
284
|
+
render3DDrawInstanced(mesh, buffer, count, mesh.instanceTextureInfo, mesh.instanceState);
|
|
244
285
|
}
|
|
245
286
|
if (!only)
|
|
246
287
|
r.instanceMeshes.length = 0;
|
|
@@ -251,6 +292,32 @@ function render3DFlushInstances(only)
|
|
|
251
292
|
}
|
|
252
293
|
}
|
|
253
294
|
|
|
295
|
+
// draw a mesh count times from a buffer that holds the per instance values, under a draw state
|
|
296
|
+
// the arrays are turned on with their instance divisor for this one call and both are turned off after: a single
|
|
297
|
+
// draw reads these slots as constant attributes, and in Firefox a draw that reads a constant through a slot whose
|
|
298
|
+
// divisor is set leaves the next batch on that slot reading the wrong values
|
|
299
|
+
function render3DDrawInstanced(mesh, buffer, count, textureInfo, state)
|
|
300
|
+
{
|
|
301
|
+
const gl = glContext;
|
|
302
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
303
|
+
for (const [location, size, offset] of RENDER3D_INSTANCE_ATTRIBS)
|
|
304
|
+
{
|
|
305
|
+
gl.vertexAttribPointer(location, size, gl.FLOAT, false, RENDER3D_INSTANCE_BYTES, offset);
|
|
306
|
+
gl.enableVertexAttribArray(location);
|
|
307
|
+
gl.vertexAttribDivisor(location, 1);
|
|
308
|
+
}
|
|
309
|
+
render3DSetDrawUniforms(RENDER3D_IDENTITY, textureInfo, WHITE, RENDER3D_FULL_UV_RECT, state);
|
|
310
|
+
render3DBindMesh(mesh);
|
|
311
|
+
gl.drawElementsInstanced(gl.TRIANGLES, mesh.bufferCount, mesh.indexType, 0, count);
|
|
312
|
+
for (const [location] of RENDER3D_INSTANCE_ATTRIBS)
|
|
313
|
+
{
|
|
314
|
+
gl.disableVertexAttribArray(location);
|
|
315
|
+
gl.vertexAttribDivisor(location, 0);
|
|
316
|
+
}
|
|
317
|
+
++drawCount;
|
|
318
|
+
primitiveCount += mesh.bufferCount / 3 * count;
|
|
319
|
+
}
|
|
320
|
+
|
|
254
321
|
// forget the pending batches, for a frame that threw or a lost context
|
|
255
322
|
function render3DClearInstances()
|
|
256
323
|
{
|
|
@@ -261,15 +328,18 @@ function render3DClearInstances()
|
|
|
261
328
|
|
|
262
329
|
// the live objects drawn on one side of the 2D scene
|
|
263
330
|
function render3DLayerObjects(after2D)
|
|
264
|
-
{
|
|
331
|
+
{
|
|
332
|
+
return /** @type {Array<EngineObject3D>} */ (engineObjects.filter(o=>
|
|
333
|
+
!o.destroyed && o instanceof EngineObject3D && render3DIsAfter2D(o) === after2D));
|
|
334
|
+
}
|
|
265
335
|
|
|
266
336
|
// the Light3D objects the shader gets this frame: directional lights light the whole scene so they come first,
|
|
267
337
|
// then the point lights nearest the camera
|
|
268
338
|
function render3DCollectLights()
|
|
269
339
|
{
|
|
270
340
|
// a light switched off by its radius or its alpha is left out, so it cannot take one of the few slots
|
|
271
|
-
const lights = engineObjects.filter(o=> !o.destroyed && o instanceof Light3D &&
|
|
272
|
-
o.color.a > 0 && o.intensity > 0 && (o.directional || o.radius > 0));
|
|
341
|
+
const lights = /** @type {Array<Light3D>} */ (engineObjects.filter(o=> !o.destroyed && o instanceof Light3D &&
|
|
342
|
+
o.color.a > 0 && o.intensity > 0 && (o.directional || o.radius > 0)));
|
|
273
343
|
if (lights.length > RENDER3D_MAX_LIGHTS)
|
|
274
344
|
{
|
|
275
345
|
// distances cached once, getWorldPos3D walks the parent chain and the sort asks many times
|
|
@@ -301,20 +371,6 @@ function render3DCircle(sides)
|
|
|
301
371
|
return circle;
|
|
302
372
|
}
|
|
303
373
|
|
|
304
|
-
// a soft white dot for untextured particles, made once from a canvas, undefined headless or without a canvas
|
|
305
|
-
let render3DSoftDotTexture;
|
|
306
|
-
function render3DSoftDot()
|
|
307
|
-
{
|
|
308
|
-
if (render3DSoftDotTexture || !glContext || typeof OffscreenCanvas == 'undefined') return render3DSoftDotTexture;
|
|
309
|
-
const size = 32, context = createCanvasContext(size);
|
|
310
|
-
const gradient = context.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
|
311
|
-
for (const [stop, alpha] of [[0, 1], [.33, .9], [.67, .7], [1, 0]]) // the same falloff as a soft disc
|
|
312
|
-
gradient.addColorStop(stop, 'rgba(255,255,255,' + alpha + ')');
|
|
313
|
-
context.fillStyle = gradient;
|
|
314
|
-
context.fillRect(0, 0, size, size);
|
|
315
|
-
return render3DSoftDotTexture = new TextureInfo(context.canvas);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
374
|
// the rotation that points -Z along a direction, as vec3(pitch, yaw, 0); a zero direction keeps the current one
|
|
319
375
|
function render3DLookRotation(direction, current)
|
|
320
376
|
{
|
|
@@ -347,6 +403,8 @@ class Render3DPlugin
|
|
|
347
403
|
{
|
|
348
404
|
ASSERT(!render3D, 'Render3D plugin already initialized');
|
|
349
405
|
render3D = this;
|
|
406
|
+
ASSERT(Object.keys(render3DCaptureBatchState()).join() === RENDER3D_STATE_FIELDS.join(),
|
|
407
|
+
'the batch state functions must list RENDER3D_STATE_FIELDS');
|
|
350
408
|
|
|
351
409
|
/** @property {Camera3D} - The camera */
|
|
352
410
|
this.camera = new Camera3D;
|
|
@@ -354,12 +412,17 @@ class Render3DPlugin
|
|
|
354
412
|
// lights and fog
|
|
355
413
|
/** @property {Vector3} - Direction toward the sun, where its light comes from, like a directional Light3D;
|
|
356
414
|
* read at each draw, and any length will do, the shading and the shadows normalize it themselves;
|
|
357
|
-
* the sun is the one light that casts shadows
|
|
415
|
+
* the sun is the one light that casts shadows */
|
|
358
416
|
this.sunDirection = vec3(-.3, 1, .5);
|
|
359
417
|
/** @property {Color} - Sunlight color */
|
|
360
418
|
this.sunColor = WHITE.copy();
|
|
361
|
-
/** @property {Color} - Ambient light color */
|
|
419
|
+
/** @property {Color} - Ambient light color, from above when ambientGroundColor is set */
|
|
362
420
|
this.ambientColor = hsl(0, 0, .3);
|
|
421
|
+
/** @property {Color|undefined} - Ambient light from below: set, the ambient blends from this on faces pointing
|
|
422
|
+
* down to ambientColor on faces pointing up, the way a sky and a ground light a scene; setSky sets both from
|
|
423
|
+
* its colors
|
|
424
|
+
* @type {Color|undefined} */
|
|
425
|
+
this.ambientGroundColor = undefined;
|
|
363
426
|
/** @property {Color|undefined} - Fog color, uses canvasClearColor when undefined
|
|
364
427
|
* @type {Color|undefined} */
|
|
365
428
|
this.fogColor = undefined;
|
|
@@ -367,12 +430,15 @@ class Render3DPlugin
|
|
|
367
430
|
this.fogStart = 0;
|
|
368
431
|
/** @property {number} - Distance from the camera where fog is total, 0 disables fog */
|
|
369
432
|
this.fogEnd = 0;
|
|
370
|
-
/** @property {Vector3} - Added to the velocity3D of every object with a mass each frame, scaled by its
|
|
433
|
+
/** @property {Vector3} - Added to the velocity3D of every object with a mass each frame, scaled by its
|
|
434
|
+
* gravityScale; sync2D objects use the 2D gravity */
|
|
371
435
|
this.gravity = vec3();
|
|
372
|
-
/** @property {number|HeightMap|
|
|
373
|
-
*
|
|
436
|
+
/** @property {number|HeightMap|function(number, number): number} - Floor for objects with a softShadow: a
|
|
437
|
+
* height, a HeightMap, or (x, z) => y
|
|
438
|
+
* @type {number|HeightMap|function(number, number): number} */
|
|
374
439
|
this.softShadowHeight = 0;
|
|
375
|
-
/** @property {boolean} - Default for every builder's smooth argument: true for smooth vertex normals, false for
|
|
440
|
+
/** @property {boolean} - Default for every builder's smooth argument: true for smooth vertex normals, false for
|
|
441
|
+
* flat faces */
|
|
376
442
|
this.smoothShading = false;
|
|
377
443
|
|
|
378
444
|
// shadows
|
|
@@ -380,8 +446,9 @@ class Render3DPlugin
|
|
|
380
446
|
this.shadows = false;
|
|
381
447
|
/** @property {number} - Size of the shadow map in pixels, bigger is sharper and slower */
|
|
382
448
|
this.shadowMapSize = 1024;
|
|
383
|
-
/** @property {number} - World size the shadow map covers around shadowCenter, smaller is sharper; it is a
|
|
384
|
-
* facing the light, so it turns as the light does, and about 1.5 times an area's width covers it from
|
|
449
|
+
/** @property {number} - World size the shadow map covers around shadowCenter, smaller is sharper; it is a
|
|
450
|
+
* square facing the light, so it turns as the light does, and about 1.5 times an area's width covers it from
|
|
451
|
+
* any angle */
|
|
385
452
|
this.shadowRange = 40;
|
|
386
453
|
/** @property {Vector3|undefined} - Center of the shadowed area, read each frame, undefined follows the camera
|
|
387
454
|
* @type {Vector3|undefined} */
|
|
@@ -399,7 +466,9 @@ class Render3DPlugin
|
|
|
399
466
|
this.emissive = 0;
|
|
400
467
|
/** @property {boolean} - Additive blending instead of alpha, in the transparent stage */
|
|
401
468
|
this.additive = false;
|
|
402
|
-
/** @property {boolean} - Test against the depth buffer, reset to true before each object and callback
|
|
469
|
+
/** @property {boolean} - Test against the depth buffer, reset to true before each object and callback; a draw
|
|
470
|
+
* with it off goes over what was drawn before it and under what is drawn after, by render order, which
|
|
471
|
+
* ends the batch of meshes before it, so one per object costs a draw per object */
|
|
403
472
|
this.depthTest = true;
|
|
404
473
|
/** @property {boolean} - Write to the depth buffer, owned by the stages: on for opaque, off for transparent */
|
|
405
474
|
this.depthWrite = true;
|
|
@@ -407,12 +476,15 @@ class Render3DPlugin
|
|
|
407
476
|
// show from both sides, and whether its transform mirrors it so the other winding is the front
|
|
408
477
|
this.cullBackFaces = false;
|
|
409
478
|
this.mirrored = false;
|
|
410
|
-
/** @property {number} - Strength of the highlight where the
|
|
479
|
+
/** @property {number} - Strength of the highlight where the sun and the Light3D objects reflect, 0 is none and
|
|
480
|
+
* 1 adds a light's full color at its brightest; its size is fixed */
|
|
411
481
|
this.specular = 0;
|
|
412
|
-
/** @property {Shader|undefined} - Custom Shader for the next draws, set from each object's shader; undefined
|
|
482
|
+
/** @property {Shader|undefined} - Custom Shader for the next draws, set from each object's shader; undefined
|
|
483
|
+
* draws with the plugin's own
|
|
413
484
|
* @type {Shader|undefined} */
|
|
414
485
|
this.shader = undefined;
|
|
415
|
-
/** @property {boolean} - Darken by the shadow map when shadows are on, turn it off for things that should stay
|
|
486
|
+
/** @property {boolean} - Darken by the shadow map when shadows are on, turn it off for things that should stay
|
|
487
|
+
* lit inside a shadow */
|
|
416
488
|
this.receiveShadow = true;
|
|
417
489
|
|
|
418
490
|
// the pass
|
|
@@ -431,11 +503,14 @@ class Render3DPlugin
|
|
|
431
503
|
this.sortTransparent = true;
|
|
432
504
|
/** @property {boolean} - Skip meshes whose bounding sphere is outside the view */
|
|
433
505
|
this.frustumCulling = true;
|
|
434
|
-
/** @property {boolean} - Draw every use of a mesh in the opaque stage as one instanced call, mesh.instanced
|
|
506
|
+
/** @property {boolean} - Draw every use of a mesh in the opaque stage as one instanced call, mesh.instanced
|
|
507
|
+
* overrides it per mesh */
|
|
435
508
|
this.instancing = true;
|
|
436
|
-
/** @property {boolean} - Sample textures through mipmaps so they do not shimmer in the distance, false uses
|
|
509
|
+
/** @property {boolean} - Sample textures through mipmaps so they do not shimmer in the distance, false uses
|
|
510
|
+
* each texture's own filtering like 2D */
|
|
437
511
|
this.mipmaps = true;
|
|
438
|
-
/** @property {boolean} - Draw state: keep texture pixels hard edged, no mipmaps and no blending between them,
|
|
512
|
+
/** @property {boolean} - Draw state: keep texture pixels hard edged, no mipmaps and no blending between them,
|
|
513
|
+
* set per object by pixelated */
|
|
439
514
|
this.pixelated = false;
|
|
440
515
|
/** @property {number} - Anisotropic filtering for textures seen at an angle, 1 to 16, 1 is off; needs mipmaps */
|
|
441
516
|
this.anisotropy = 4;
|
|
@@ -452,6 +527,11 @@ class Render3DPlugin
|
|
|
452
527
|
this.planeMesh.doubleSided = false;
|
|
453
528
|
/** @property {Mesh} - The same square seen and lit from both sides, for signs, cards and leaves */
|
|
454
529
|
this.planeMeshDoubleSided = buildGrid();
|
|
530
|
+
/** @property {Mesh} - A square of size 1 facing +Z with the tile across it, the corners in the order
|
|
531
|
+
* drawBillboard writes them; a ParticleEmitter3D draws its particles as instances of it, each with its own matrix */
|
|
532
|
+
this.billboardMesh = new Mesh().addStrip([vec3(-.5, .5, 0), vec3(-.5, -.5, 0), vec3(.5, .5, 0), vec3(.5, -.5, 0)],
|
|
533
|
+
vec3(0, 0, 1), RENDER3D_QUAD_UVS);
|
|
534
|
+
this.billboardMesh.doubleSided = true;
|
|
455
535
|
|
|
456
536
|
// read only
|
|
457
537
|
/** @property {boolean} - True while the 3D pass is running, 3D draws are only valid then */
|
|
@@ -476,22 +556,33 @@ class Render3DPlugin
|
|
|
476
556
|
|
|
477
557
|
// internal state
|
|
478
558
|
this.blend = false; // blending on, set by the stages
|
|
559
|
+
/** @type {Array<Array<number>>} */
|
|
479
560
|
this.frustumPlanes = []; // the view as six inward planes [x, y, z, w]
|
|
561
|
+
/** @type {Array<Array<number>>} */
|
|
480
562
|
this.shadowPlanes = []; // the shadow map's box as six planes
|
|
563
|
+
/** @type {WebGLProgram|undefined} */
|
|
481
564
|
this.program = undefined; // the main program, undefined when not available
|
|
565
|
+
/** @type {WebGLProgram|undefined} */
|
|
482
566
|
this.currentProgram = undefined; // the program in use during a pass, a Shader's or the main one
|
|
483
567
|
this.lightCount = 0; // Light3D objects sent this pass
|
|
568
|
+
/** @type {WebGLProgram|undefined} */
|
|
484
569
|
this.shadowShader = undefined;
|
|
570
|
+
/** @type {WebGLVertexArrayObject|undefined} */
|
|
485
571
|
this.vao = undefined;
|
|
572
|
+
/** @type {WebGLTexture|undefined} */
|
|
486
573
|
this.whiteTexture = undefined; // 1x1 white for untextured draws
|
|
487
|
-
|
|
574
|
+
/** @type {Map<number, WebGLSampler>} */
|
|
575
|
+
this.samplers = new Map; // how textures are filtered in 3D, by wrap and hard edge, see render3DSampler
|
|
576
|
+
/** @type {string|undefined} */
|
|
488
577
|
this.samplerKey = undefined; // the settings the samplers were made for, they are rebuilt when it changes
|
|
489
|
-
|
|
578
|
+
/** @type {WebGLTexture|undefined} */
|
|
490
579
|
this.shadowTexture = undefined;
|
|
580
|
+
/** @type {WebGLFramebuffer|undefined} */
|
|
491
581
|
this.shadowFramebuffer = undefined;
|
|
492
582
|
this.shadowTextureSize = 0;
|
|
493
583
|
this.contextGeneration = 0; // counts context losses, a mesh uploaded under an older one uploads again
|
|
494
584
|
this.uniforms = new Map; // uniform locations by program
|
|
585
|
+
/** @type {Object<string, Array<number>>} */
|
|
495
586
|
this.uniformValues = {}; // last values sent for the cached vec4 uniforms
|
|
496
587
|
this.shadowMapDrawn = false; // the shadow map is drawn by the first pass of the frame
|
|
497
588
|
this.passIsDefault = true; // the running pass is the default layer, the only one shadowed
|
|
@@ -499,18 +590,25 @@ class Render3DPlugin
|
|
|
499
590
|
this.lightColors = new Float32Array(RENDER3D_MAX_LIGHTS * 4);
|
|
500
591
|
|
|
501
592
|
// the stream of immediate mode draws
|
|
593
|
+
/** @type {WebGLBuffer|undefined} */
|
|
502
594
|
this.streamBuffer = undefined;
|
|
595
|
+
/** @type {Array<WebGLBuffer>} */
|
|
503
596
|
this.instanceBuffers = []; // the per instance values of the batches being drawn, used in turn
|
|
504
597
|
this.instanceBufferIndex = 0;
|
|
598
|
+
/** @type {Array<Mesh>} */
|
|
505
599
|
this.instanceMeshes = []; // meshes with a batch pending this stage
|
|
600
|
+
/** @type {Array<Array<number>>} */
|
|
506
601
|
this.attribValues = []; // last values sent for the cached constant attributes
|
|
507
602
|
this.streamData = new ArrayBuffer(RENDER3D_MAX_STREAM_VERTS * RENDER3D_VERTEX_BYTES);
|
|
508
603
|
this.streamFloats = new Float32Array(this.streamData);
|
|
509
604
|
this.streamInts = new Uint32Array(this.streamData);
|
|
510
605
|
this.streamCount = 0;
|
|
606
|
+
/** @type {TextureInfo|undefined} */
|
|
511
607
|
this.streamTileInfo = undefined;
|
|
512
608
|
this.streamState = undefined; // captured state the pending batch was drawn under
|
|
609
|
+
/** @type {Mesh|undefined} */
|
|
513
610
|
this.capture = undefined; // the mesh a bake is filling
|
|
611
|
+
/** @type {Array<{distance: number, state: Object, draw: function(): void}>|undefined} */
|
|
514
612
|
this.transparentQueue = undefined; // draws queued during the transparent stage, replayed far to near
|
|
515
613
|
|
|
516
614
|
render3DInitGL();
|
|
@@ -574,10 +672,11 @@ class Render3DPlugin
|
|
|
574
672
|
* - It brings the view matrices up to date for that canvas, so worldToScreen stays its exact opposite
|
|
575
673
|
* @param {Vector2} screenPos - Same space as mousePosScreen
|
|
576
674
|
* @param {Vector2} [canvasSize] - Defaults to the main canvas size
|
|
577
|
-
* @return {Ray3D} - Starts at the camera with a unit direction, or on the
|
|
675
|
+
* @return {Ray3D} - Starts at the camera with a unit direction, or on the near plane when orthographic */
|
|
578
676
|
screenToRay(screenPos, canvasSize=mainCanvasSize)
|
|
579
677
|
{
|
|
580
|
-
|
|
678
|
+
// a canvas with no size stands in as 1x1, rather than dividing by zero
|
|
679
|
+
const width = canvasSize.x || 1, height = canvasSize.y || 1;
|
|
581
680
|
const aspect = width / height, camera = this.camera;
|
|
582
681
|
// bring the matrices up to date for this canvas, so worldToScreen and this agree on where things are
|
|
583
682
|
this.updateMatrices(aspect);
|
|
@@ -586,8 +685,9 @@ class Render3DPlugin
|
|
|
586
685
|
// the screen offset moves a parallel ray's origin, or bends a perspective ray's direction
|
|
587
686
|
const h = camera.orthographic ? camera.orthographic / 2 : tan(camera.fov / 2);
|
|
588
687
|
const offset = this.cameraRight.scale(clipX * h * aspect).add(this.cameraUp.scale(clipY * h));
|
|
688
|
+
// a parallel ray starts on the near plane, which an orthographic camera may put behind it, like three.js
|
|
589
689
|
return camera.orthographic
|
|
590
|
-
? new Ray3D(camera.pos.add(offset), this.cameraForward.copy())
|
|
690
|
+
? new Ray3D(camera.pos.add(offset).add(this.cameraForward.scale(camera.near)), this.cameraForward.copy())
|
|
591
691
|
: new Ray3D(camera.pos.copy(), this.cameraForward.add(offset).normalize());
|
|
592
692
|
}
|
|
593
693
|
|
|
@@ -604,7 +704,8 @@ class Render3DPlugin
|
|
|
604
704
|
}
|
|
605
705
|
|
|
606
706
|
/** Find the nearest object under a screen position or along a ray, for clicking on things
|
|
607
|
-
* - Each object is tested as
|
|
707
|
+
* - Each object is tested as the box around its mesh in its own space, or a sprite as the quad it draws,
|
|
708
|
+
* not triangle by triangle
|
|
608
709
|
* - engineObjectsRaycast3D is the other half of this, every object along a ray instead of the nearest
|
|
609
710
|
* @param {Vector2|Ray3D} from - A screen position like mousePosScreen, or a ray to look along
|
|
610
711
|
* @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
|
|
@@ -617,37 +718,39 @@ class Render3DPlugin
|
|
|
617
718
|
{
|
|
618
719
|
const distance = render3DRaycastObject(ray, o);
|
|
619
720
|
if (distance !== undefined && (!nearest || distance < nearest.distance))
|
|
620
|
-
nearest = {object: o, distance};
|
|
721
|
+
nearest = {object: /** @type {EngineObject3D} */ (o), distance}; // only a 3D object has a distance
|
|
621
722
|
}
|
|
622
723
|
return nearest;
|
|
623
724
|
}
|
|
624
725
|
|
|
625
|
-
/** Play a sound at a 3D position, quieter with distance from the camera and panned by its side, like Sound.play
|
|
726
|
+
/** Play a sound at a 3D position, quieter with distance from the camera and panned by its side, like Sound.play
|
|
727
|
+
* with a 2D position
|
|
626
728
|
* @param {Sound} sound
|
|
627
729
|
* @param {Vector3} pos3D
|
|
628
730
|
* @param {number} [volume]
|
|
629
731
|
* @param {number} [pitch]
|
|
630
732
|
* @param {number} [randomnessScale] - How much to scale pitch randomness
|
|
631
733
|
* @param {boolean} [loop]
|
|
734
|
+
* @param {boolean} [paused] - Start it paused
|
|
632
735
|
* @return {SoundInstance|undefined} - undefined when out of range or sound is off */
|
|
633
|
-
playSound(sound, pos3D, volume=1, pitch=1, randomnessScale=1, loop=false)
|
|
736
|
+
playSound(sound, pos3D, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
|
|
634
737
|
{
|
|
635
|
-
//
|
|
738
|
+
// the 3D range fade and pan, Sound.play with no position does the rest
|
|
636
739
|
ASSERT(sound instanceof Sound, 'sound must be a Sound');
|
|
637
740
|
ASSERT(isVector3(pos3D), 'pos3D must be a vec3');
|
|
638
|
-
if (!soundEnable || headlessMode) return;
|
|
639
|
-
if (!sound.sampleBuffer && !sound._sampleChannels) return; // still loading
|
|
640
741
|
const offset = pos3D.subtract(this.camera.pos), range = sound.range;
|
|
641
742
|
if (range)
|
|
642
743
|
{
|
|
643
744
|
const distance = offset.length();
|
|
644
745
|
if (distance > range)
|
|
645
746
|
return; // out of range
|
|
646
|
-
|
|
747
|
+
const taperRange = range * sound.taper;
|
|
748
|
+
if (distance > taperRange)
|
|
749
|
+
volume *= percent(distance, range, taperRange);
|
|
647
750
|
}
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
return
|
|
751
|
+
const instance = sound.play(undefined, volume, pitch, randomnessScale, loop, paused);
|
|
752
|
+
instance?.setPan(offset.normalize().dot(this.cameraRight));
|
|
753
|
+
return instance;
|
|
651
754
|
}
|
|
652
755
|
|
|
653
756
|
/** Play a sound on a loop at a 3D position, the same as playSound with loop on
|
|
@@ -666,13 +769,7 @@ class Render3DPlugin
|
|
|
666
769
|
* @param {Vector3} center
|
|
667
770
|
* @param {number} radius
|
|
668
771
|
* @return {boolean} */
|
|
669
|
-
isSphereVisible(center, radius)
|
|
670
|
-
{
|
|
671
|
-
for (const p of this.shadowPass ? this.shadowPlanes : this.frustumPlanes)
|
|
672
|
-
if (p[0] * center.x + p[1] * center.y + p[2] * center.z + p[3] < -radius)
|
|
673
|
-
return false;
|
|
674
|
-
return true;
|
|
675
|
-
}
|
|
772
|
+
isSphereVisible(center, radius) { return render3DSphereVisible(center.x, center.y, center.z, radius); }
|
|
676
773
|
|
|
677
774
|
///////////////////////////////////////////////////////////////////////////
|
|
678
775
|
// Meshes and the stream
|
|
@@ -681,38 +778,46 @@ class Render3DPlugin
|
|
|
681
778
|
* @param {Mesh} mesh
|
|
682
779
|
* @param {Matrix4|Vector3} [matrix] - Object transform, or just a position to draw it at
|
|
683
780
|
* @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile or the whole texture
|
|
684
|
-
* @param {Color} [color] - Tint
|
|
781
|
+
* @param {Color} [color] - Tint
|
|
782
|
+
* @return {void} */
|
|
685
783
|
drawMesh(mesh, matrix=RENDER3D_IDENTITY, tileInfo, color=WHITE)
|
|
686
784
|
{
|
|
687
785
|
matrix = render3DMatrix(matrix);
|
|
688
|
-
ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
|
|
786
|
+
ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
|
|
787
|
+
'tileInfo must be a TileInfo or TextureInfo, it comes before color');
|
|
689
788
|
ASSERT(isColor(color), 'color must be a Color');
|
|
690
789
|
if (this.capture)
|
|
691
790
|
return void this.capture.combine(mesh, matrix, color);
|
|
692
791
|
if (this.transparentQueue)
|
|
693
|
-
|
|
792
|
+
{
|
|
793
|
+
// the queue replays later, so it keeps copies of what a caller may reuse, like a scratch matrix
|
|
794
|
+
const m = matrix.copy(), c = color.copy();
|
|
795
|
+
return this.queueTransparent(m.getTranslation(), ()=> this.drawMesh(mesh, m, tileInfo, c));
|
|
796
|
+
}
|
|
694
797
|
if (!render3DCanDraw()) return;
|
|
695
798
|
if (this.shadowPass && !this.lighting) return; // unlit things cast no shadow
|
|
696
|
-
|
|
697
|
-
mesh.upload();
|
|
799
|
+
render3DMeshUpload(mesh);
|
|
698
800
|
if (!mesh.bufferCount) return;
|
|
699
|
-
|
|
801
|
+
const m = matrix.m;
|
|
802
|
+
if (this.frustumCulling && !render3DSphereVisible(m[12], m[13], m[14], mesh.radius * render3DMaxStretch(m)))
|
|
700
803
|
return;
|
|
701
804
|
// the mesh says whether its back faces can be skipped, and a mirroring transform, one with a negative
|
|
702
805
|
// determinant, turns the winding around so the other one is its front
|
|
703
|
-
const
|
|
806
|
+
const cullBackFaces = this.cullBackFaces, mirrored = this.mirrored;
|
|
704
807
|
this.cullBackFaces = !mesh.doubleSided;
|
|
705
|
-
this.mirrored =
|
|
706
|
-
|
|
808
|
+
this.mirrored = render3DMirrors(matrix);
|
|
809
|
+
// the stage draws the batch at its end
|
|
810
|
+
if (!this.blend && this.depthTest && (mesh.instanced ?? this.instancing))
|
|
707
811
|
render3DInstance(mesh, matrix, tileInfo, color);
|
|
708
812
|
else
|
|
709
813
|
{
|
|
710
814
|
this.flush();
|
|
815
|
+
render3DFlushBeforeOverlay();
|
|
711
816
|
render3DSetDrawUniforms(matrix, tileInfo, color);
|
|
712
|
-
|
|
713
|
-
glContext.
|
|
817
|
+
render3DBindMesh(mesh);
|
|
818
|
+
glContext.drawElements(glContext.TRIANGLES, mesh.bufferCount, mesh.indexType, 0);
|
|
714
819
|
++drawCount;
|
|
715
|
-
primitiveCount += mesh.bufferCount;
|
|
820
|
+
primitiveCount += mesh.bufferCount / 3;
|
|
716
821
|
}
|
|
717
822
|
this.cullBackFaces = cullBackFaces, this.mirrored = mirrored;
|
|
718
823
|
}
|
|
@@ -722,11 +827,13 @@ class Render3DPlugin
|
|
|
722
827
|
* - List the first three points counter clockwise as seen from the front, or the face points away
|
|
723
828
|
* and may vanish when back faces are culled
|
|
724
829
|
* - inside a bake the strip goes into the mesh instead, in the transparent stage it is queued for sorting
|
|
830
|
+
* and the arrays are read when the queue replays, so leave them unchanged until the stage ends
|
|
725
831
|
* @param {Array<Vector3>} points - In strip order
|
|
726
832
|
* @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
|
|
727
833
|
* @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, 0-1 across the tile
|
|
728
834
|
* @param {Color|Array<Color>} [colors] - One for all or one per point, vertex colors come before the texture
|
|
729
|
-
* @param {TileInfo|TextureInfo} [tileInfo] - Texture for this strip
|
|
835
|
+
* @param {TileInfo|TextureInfo} [tileInfo] - Texture for this strip
|
|
836
|
+
* @return {void} */
|
|
730
837
|
drawStrip(points, normals, uvs, colors, tileInfo)
|
|
731
838
|
{
|
|
732
839
|
if (this.capture)
|
|
@@ -740,7 +847,8 @@ class Render3DPlugin
|
|
|
740
847
|
let x = 0, y = 0, z = 0;
|
|
741
848
|
for (const p of points)
|
|
742
849
|
x += p.x, y += p.y, z += p.z;
|
|
743
|
-
return this.queueTransparent(vec3(x, y, z).scale(1 / points.length),
|
|
850
|
+
return this.queueTransparent(vec3(x, y, z).scale(1 / points.length),
|
|
851
|
+
()=> this.drawStrip(points, normals, uvs, colors, tileInfo));
|
|
744
852
|
}
|
|
745
853
|
ASSERT(isArray(points) && points.length > 2, 'strip needs at least 3 points');
|
|
746
854
|
const n = points.length, count = render3DStripCount(n);
|
|
@@ -753,9 +861,9 @@ class Render3DPlugin
|
|
|
753
861
|
const rgba = colorArray ? 0 : (colors || WHITE).rgbaInt();
|
|
754
862
|
for (let k = 0; k < count; ++k)
|
|
755
863
|
{
|
|
756
|
-
const i = render3DStripIndex(k, n);
|
|
864
|
+
const i = render3DStripIndex(k, n), p = points[i];
|
|
757
865
|
const uv = uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV;
|
|
758
|
-
render3DWriteVertex(floats, ints, this.streamCount++ * RENDER3D_VERTEX_FLOATS,
|
|
866
|
+
render3DWriteVertex(floats, ints, this.streamCount++ * RENDER3D_VERTEX_FLOATS, p.x, p.y, p.z,
|
|
759
867
|
normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL,
|
|
760
868
|
uvRect.x + uv.x * uvRect.w, uvRect.y + uv.y * uvRect.h, colorArray ? colors[i].rgbaInt() : rgba);
|
|
761
869
|
}
|
|
@@ -839,7 +947,7 @@ class Render3DPlugin
|
|
|
839
947
|
{
|
|
840
948
|
// the shadow grows with the object, by the same scale picking and culling
|
|
841
949
|
// measure it at, so one size set once holds however the object is scaled
|
|
842
|
-
const m = o
|
|
950
|
+
const m = render3DObjectMatrix(o);
|
|
843
951
|
this.drawSoftShadow(m.getTranslation(), o.softShadow * render3DMaxScale(m.m), this.softShadowHeight);
|
|
844
952
|
}
|
|
845
953
|
isDefault && this.onRenderTransparent?.();
|
|
@@ -854,25 +962,41 @@ class Render3DPlugin
|
|
|
854
962
|
this.depthWrite = true;
|
|
855
963
|
}
|
|
856
964
|
|
|
857
|
-
/** Queue a draw for the transparent stage, replayed far to near with the current draw state, or draw it now when
|
|
965
|
+
/** Queue a draw for the transparent stage, replayed far to near with the current draw state, or draw it now when
|
|
966
|
+
* sorting is off
|
|
967
|
+
* - The draw runs later, so it should hold copies of any values the caller may change before then
|
|
858
968
|
* @param {Vector3} pos - Where the draw is, for sorting
|
|
859
|
-
* @param {
|
|
969
|
+
* @param {function(): void} draw
|
|
970
|
+
* @return {void} */
|
|
860
971
|
queueTransparent(pos, draw)
|
|
861
972
|
{
|
|
862
973
|
if (!this.transparentQueue)
|
|
863
|
-
|
|
864
|
-
|
|
974
|
+
{
|
|
975
|
+
draw();
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
// sort by depth along the view, not distance, so an orthographic camera orders them right too
|
|
979
|
+
const f = this.cameraForward, c = this.camera.pos;
|
|
980
|
+
const distance = (pos.x - c.x)*f.x + (pos.y - c.y)*f.y + (pos.z - c.z)*f.z;
|
|
981
|
+
this.transparentQueue.push({distance, state: render3DCaptureBatchState(), draw});
|
|
865
982
|
}
|
|
866
983
|
|
|
867
|
-
/** Draw the queued transparent draws far to near with the state each was drawn under, called automatically at the
|
|
984
|
+
/** Draw the queued transparent draws far to near with the state each was drawn under, called automatically at the
|
|
985
|
+
* end of the transparent stage */
|
|
868
986
|
flushTransparentQueue()
|
|
869
987
|
{
|
|
870
988
|
const queue = this.transparentQueue;
|
|
871
989
|
if (!queue) return;
|
|
872
990
|
this.transparentQueue = undefined;
|
|
873
991
|
queue.sort((a, b)=> b.distance - a.distance);
|
|
874
|
-
|
|
875
|
-
|
|
992
|
+
// each draw under the state it was queued with, and the state left as it was found
|
|
993
|
+
const state = render3DCaptureBatchState();
|
|
994
|
+
try
|
|
995
|
+
{
|
|
996
|
+
for (const item of queue)
|
|
997
|
+
render3DApplyBatchState(item.state), item.draw();
|
|
998
|
+
}
|
|
999
|
+
finally { render3DApplyBatchState(state); }
|
|
876
1000
|
}
|
|
877
1001
|
|
|
878
1002
|
/** Draw render3D.sky around the camera, unlit, unfogged and behind everything, called automatically by the pass */
|
|
@@ -903,16 +1027,20 @@ class Render3DPlugin
|
|
|
903
1027
|
this.shadowPlanes = render3DFrustumPlanes(this.shadowMatrix);
|
|
904
1028
|
}
|
|
905
1029
|
|
|
906
|
-
/** Build a sky dome, set it as the sky and
|
|
1030
|
+
/** Build a sky dome, set it as the sky, and light the scene by it: the fog takes the horizon color, and the
|
|
1031
|
+
* ambient light comes from the top color above and the bottom color below, both at the ambient strength
|
|
907
1032
|
* @param {Color} [topColor] - Straight up
|
|
908
1033
|
* @param {Color} [horizonColor] - Level with the camera
|
|
909
1034
|
* @param {Color} [bottomColor] - Straight down, defaults to the horizon color
|
|
1035
|
+
* @param {number} [ambient] - How much of the sky colors lights the scene as ambient, 0 for none
|
|
910
1036
|
* @return {Mesh} - The dome, also in render3D.sky */
|
|
911
|
-
setSky(topColor, horizonColor=hsl(.6, 1, .9), bottomColor)
|
|
1037
|
+
setSky(topColor=hsl(.6, .8, .55), horizonColor=hsl(.6, 1, .9), bottomColor=horizonColor, ambient=.5)
|
|
912
1038
|
{
|
|
913
1039
|
this.sky?.dispose();
|
|
914
1040
|
this.sky = buildSky(topColor, horizonColor, bottomColor);
|
|
915
1041
|
this.fogColor = horizonColor.copy();
|
|
1042
|
+
this.ambientColor = topColor.scale(ambient, 1);
|
|
1043
|
+
this.ambientGroundColor = bottomColor.scale(ambient, 1);
|
|
916
1044
|
return this.sky;
|
|
917
1045
|
}
|
|
918
1046
|
|
|
@@ -957,27 +1085,41 @@ class Render3DPlugin
|
|
|
957
1085
|
* @param {TileInfo|TextureInfo} [tileInfo]
|
|
958
1086
|
* @param {Color} [color]
|
|
959
1087
|
* @param {number} [angle] - Rotation in the camera plane, counter clockwise
|
|
960
|
-
* @param {boolean} [upright] - Stand on world up and only turn to face the camera, for sprites on the ground
|
|
1088
|
+
* @param {boolean} [upright] - Stand on world up and only turn to face the camera, for sprites on the ground
|
|
1089
|
+
* @return {void} */
|
|
961
1090
|
drawBillboard(pos, size=vec2(1), tileInfo, color=WHITE, angle=0, upright=false)
|
|
962
1091
|
{
|
|
963
|
-
if (this.capture)
|
|
964
|
-
return this.drawStripUnlit(render3DBillboardCorners(pos, size, angle, upright), this.cameraBack,
|
|
1092
|
+
if (this.capture) // the mesh keeps the color, so it gets its own, the particles reuse theirs
|
|
1093
|
+
return this.drawStripUnlit(render3DBillboardCorners(pos, size, angle, upright), this.cameraBack,
|
|
1094
|
+
RENDER3D_QUAD_UVS, color.copy(), tileInfo);
|
|
965
1095
|
if (this.transparentQueue) // sort by the exact position, a shadow under it sorts by the floor
|
|
966
|
-
return this.queueTransparent(pos, ()=> this.drawBillboard(pos, size, tileInfo, color, angle, upright));
|
|
967
|
-
|
|
968
|
-
// the particle path: the quad's six stream vertices written straight in, unlit
|
|
969
|
-
const count = render3DStripCount(4);
|
|
970
|
-
const lighting = this.shadowPass && this.lighting; // unlit on screen, in the shadow map the object's flag decides
|
|
971
|
-
const uvRect = render3DWithState({lighting}, ()=> render3DBeginStrip(count, tileInfo));
|
|
972
|
-
if (!uvRect) return;
|
|
973
|
-
const corners = render3DBillboardCorners(pos, size, angle, upright), rgba = color.rgbaInt();
|
|
974
|
-
const floats = this.streamFloats, ints = this.streamInts;
|
|
975
|
-
for (let k = 0; k < count; ++k)
|
|
976
1096
|
{
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
uvRect.x + uv.x * uvRect.w, uvRect.y + uv.y * uvRect.h, rgba);
|
|
1097
|
+
const p = pos.copy(), s = size.copy(), c = color.copy(); // copies, the queue replays later
|
|
1098
|
+
return this.queueTransparent(p, ()=> this.drawBillboard(p, s, tileInfo, c, angle, upright));
|
|
980
1099
|
}
|
|
1100
|
+
|
|
1101
|
+
// the quad's six stream vertices written straight in with no vectors made, unlit, for sprites, and for
|
|
1102
|
+
// particles when instancing is off
|
|
1103
|
+
const lit = this.lighting;
|
|
1104
|
+
this.lighting = this.shadowPass && lit; // unlit on screen, in the shadow map the object's flag decides
|
|
1105
|
+
let uvRect;
|
|
1106
|
+
try { uvRect = render3DBeginStrip(6, tileInfo); }
|
|
1107
|
+
finally { this.lighting = lit; }
|
|
1108
|
+
if (!uvRect) return;
|
|
1109
|
+
const a = render3DBillboardAxes(size, angle, upright);
|
|
1110
|
+
const rx = a[0], ry = a[1], rz = a[2], ux = a[3], uy = a[4], uz = a[5];
|
|
1111
|
+
const x = pos.x, y = pos.y, z = pos.z, n = this.cameraBack, rgba = color.rgbaInt();
|
|
1112
|
+
const u0 = uvRect.x, v0 = uvRect.y, u1 = u0 + uvRect.w, v1 = v0 + uvRect.h;
|
|
1113
|
+
const floats = this.streamFloats, ints = this.streamInts, stride = RENDER3D_VERTEX_FLOATS;
|
|
1114
|
+
let j = this.streamCount * stride;
|
|
1115
|
+
this.streamCount += 6;
|
|
1116
|
+
// the corners in strip order with the repeats: top left twice, bottom left, top right, bottom right twice
|
|
1117
|
+
render3DWriteVertex(floats, ints, j, x - rx + ux, y - ry + uy, z - rz + uz, n, u0, v0, rgba);
|
|
1118
|
+
render3DWriteVertex(floats, ints, j += stride, x - rx + ux, y - ry + uy, z - rz + uz, n, u0, v0, rgba);
|
|
1119
|
+
render3DWriteVertex(floats, ints, j += stride, x - rx - ux, y - ry - uy, z - rz - uz, n, u0, v1, rgba);
|
|
1120
|
+
render3DWriteVertex(floats, ints, j += stride, x + rx + ux, y + ry + uy, z + rz + uz, n, u1, v0, rgba);
|
|
1121
|
+
render3DWriteVertex(floats, ints, j += stride, x + rx - ux, y + ry - uy, z + rz - uz, n, u1, v1, rgba);
|
|
1122
|
+
render3DWriteVertex(floats, ints, j += stride, x + rx - ux, y + ry - uy, z + rz - uz, n, u1, v1, rgba);
|
|
981
1123
|
}
|
|
982
1124
|
|
|
983
1125
|
/** Draw a quad from four corners in loop order, counter clockwise seen from the front, a is the top left of the texture
|
|
@@ -989,7 +1131,11 @@ class Render3DPlugin
|
|
|
989
1131
|
* @param {Color|Array<Color>} [color] - One for all or one per corner */
|
|
990
1132
|
drawQuad(a, b, c, d, tileInfo, color=WHITE)
|
|
991
1133
|
{
|
|
992
|
-
this.
|
|
1134
|
+
if (this.transparentQueue) // the queue replays later, so it keeps copies of what a caller may reuse
|
|
1135
|
+
a = a.copy(), b = b.copy(), c = c.copy(), d = d.copy();
|
|
1136
|
+
color = isArray(color) ? color.map(k=> k.copy()) : color.copy();
|
|
1137
|
+
this.drawStrip(render3DQuadStrip(a, b, c, d), render3DFaceNormal(a, b, c, d), RENDER3D_QUAD_UVS,
|
|
1138
|
+
render3DQuadValues(color), tileInfo);
|
|
993
1139
|
}
|
|
994
1140
|
|
|
995
1141
|
/** Draw a triangle, counter clockwise from outside is the front
|
|
@@ -999,6 +1145,8 @@ class Render3DPlugin
|
|
|
999
1145
|
* @param {Color} [color] */
|
|
1000
1146
|
drawTriangle(a, b, c, color=WHITE)
|
|
1001
1147
|
{
|
|
1148
|
+
if (this.transparentQueue) // the queue replays later, so it keeps copies of what a caller may reuse
|
|
1149
|
+
a = a.copy(), b = b.copy(), c = c.copy(), color = color.copy();
|
|
1002
1150
|
this.drawStrip([a, b, c], render3DFaceNormal(a, b, c), undefined, color);
|
|
1003
1151
|
}
|
|
1004
1152
|
|
|
@@ -1019,12 +1167,14 @@ class Render3DPlugin
|
|
|
1019
1167
|
* @param {number|Array<number>} [width] - Full width, one for all or one per point
|
|
1020
1168
|
* @param {TileInfo|TextureInfo} [tileInfo]
|
|
1021
1169
|
* @param {Color|Array<Color>} [color] - One for all or one per point
|
|
1022
|
-
* @param {Vector3|Array<Vector3>} [side] - Direction across the ribbon, one for all or one per point, default
|
|
1170
|
+
* @param {Vector3|Array<Vector3>} [side] - Direction across the ribbon, one for all or one per point, default
|
|
1171
|
+
* faces the camera */
|
|
1023
1172
|
drawRibbon(points, width=.1, tileInfo, color=WHITE, side)
|
|
1024
1173
|
{
|
|
1025
1174
|
const count = points.length;
|
|
1026
1175
|
ASSERT(count > 1, 'a ribbon needs at least two points');
|
|
1027
|
-
ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
|
|
1176
|
+
ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
|
|
1177
|
+
'tileInfo must be a TileInfo or TextureInfo, it comes before color');
|
|
1028
1178
|
const strip = [], uvs = tileInfo ? [] : undefined, colors = [], forward = this.cameraForward;
|
|
1029
1179
|
let across = vec3(1, 0, 0); // kept from the last point where the direction vanishes
|
|
1030
1180
|
// a loop's two ends take their direction across the join, so they meet edge to edge
|
|
@@ -1033,7 +1183,9 @@ class Render3DPlugin
|
|
|
1033
1183
|
{
|
|
1034
1184
|
const p = points[i];
|
|
1035
1185
|
const w = isArray(width) ? width[i] : width;
|
|
1036
|
-
|
|
1186
|
+
let c = isArray(color) ? color[i] : color;
|
|
1187
|
+
if (this.transparentQueue) // the queue replays later, so it keeps a copy of a color a caller may reuse
|
|
1188
|
+
c = c.copy();
|
|
1037
1189
|
const s = side && (isArray(side) ? side[i] : side);
|
|
1038
1190
|
// across the path in the camera plane unless a side is given
|
|
1039
1191
|
const next = points[i < count - 1 ? i + 1 : loop ? 1 : i];
|
|
@@ -1046,7 +1198,8 @@ class Render3DPlugin
|
|
|
1046
1198
|
uvs?.push(vec2(i / (count - 1), 0), vec2(i / (count - 1), 1));
|
|
1047
1199
|
colors.push(c, c);
|
|
1048
1200
|
}
|
|
1049
|
-
render3DWithState({lighting: false, cullBackFaces: false},
|
|
1201
|
+
render3DWithState({lighting: false, cullBackFaces: false},
|
|
1202
|
+
()=> this.drawStrip(strip, forward.scale(-1), uvs, colors, tileInfo));
|
|
1050
1203
|
}
|
|
1051
1204
|
|
|
1052
1205
|
/** Draw a disc that fades to transparent at the rim, unlit, for glows, puffs and sky dots
|
|
@@ -1054,12 +1207,16 @@ class Render3DPlugin
|
|
|
1054
1207
|
* @param {number} [size] - Diameter
|
|
1055
1208
|
* @param {Color} [color]
|
|
1056
1209
|
* @param {Vector3} [normal] - Facing direction, faces the camera by default
|
|
1057
|
-
* @param {number} [sides]
|
|
1210
|
+
* @param {number} [sides]
|
|
1211
|
+
* @return {void} */
|
|
1058
1212
|
drawSoftDisc(pos, size=1, color=WHITE, normal=this.cameraBack, sides=16)
|
|
1059
1213
|
{
|
|
1060
1214
|
render3DAssertBlending();
|
|
1061
1215
|
if (this.transparentQueue && !this.capture)
|
|
1062
|
-
|
|
1216
|
+
{
|
|
1217
|
+
const p = pos.copy(), c = color.copy(), n = normal.copy(); // copies, the queue replays later
|
|
1218
|
+
return this.queueTransparent(p, ()=> this.drawSoftDisc(p, size, c, n, sides));
|
|
1219
|
+
}
|
|
1063
1220
|
// basis in the disc's plane
|
|
1064
1221
|
const n = normal.normalize();
|
|
1065
1222
|
const helper = abs(n.y) < .9 ? vec3(0, 1, 0) : vec3(1, 0, 0);
|
|
@@ -1072,16 +1229,24 @@ class Render3DPlugin
|
|
|
1072
1229
|
* - Draw it from onRenderTransparent or from a transparent object
|
|
1073
1230
|
* @param {Vector3} pos - Position of the thing casting the shadow
|
|
1074
1231
|
* @param {number} [size] - Diameter
|
|
1075
|
-
* @param {number|HeightMap|
|
|
1232
|
+
* @param {number|HeightMap|function(number, number): number} [floorHeight] - Height of the ground, a HeightMap, or
|
|
1233
|
+
* (x, z) => y to follow terrain
|
|
1076
1234
|
* @param {Color} [color]
|
|
1077
|
-
* @param {number} [lift] - How far above the ground to draw, raise it if the shadow cuts into rough ground
|
|
1235
|
+
* @param {number} [lift] - How far above the ground to draw, raise it if the shadow cuts into rough ground
|
|
1236
|
+
* @return {void} */
|
|
1078
1237
|
drawSoftShadow(pos, size=1, floorHeight=0, color=RENDER3D_SHADOW_COLOR, lift=.02)
|
|
1079
1238
|
{
|
|
1080
1239
|
render3DAssertBlending();
|
|
1081
|
-
|
|
1082
|
-
|
|
1240
|
+
// a HeightMap is in the extras plugin, so it is known by its getHeight rather than its class
|
|
1241
|
+
const heightMap = /** @type {HeightMap} */ (floorHeight);
|
|
1242
|
+
const height = /** @type {function(number, number): number} */ (isNumber(floorHeight) ? ()=> floorHeight
|
|
1243
|
+
: heightMap.getHeight ? (x, z)=> heightMap.getHeight(x, z) : floorHeight);
|
|
1083
1244
|
if (this.transparentQueue && !this.capture) // sort from the floor, under whatever casts it
|
|
1084
|
-
|
|
1245
|
+
{
|
|
1246
|
+
const p = pos.copy(), c = color.copy(); // copies, the queue replays later
|
|
1247
|
+
return this.queueTransparent(vec3(p.x, height(p.x, p.z) + lift, p.z),
|
|
1248
|
+
()=> this.drawSoftShadow(p, size, floorHeight, c, lift));
|
|
1249
|
+
}
|
|
1085
1250
|
render3DDrawSoftDisc(size / 2, color, 16, RENDER3D_DEFAULT_NORMAL, (c, s, r)=>
|
|
1086
1251
|
{
|
|
1087
1252
|
const x = pos.x + c * r, z = pos.z + s * r;
|
|
@@ -1093,7 +1258,8 @@ class Render3DPlugin
|
|
|
1093
1258
|
function render3DAssertBlending()
|
|
1094
1259
|
{
|
|
1095
1260
|
const r = render3D;
|
|
1096
|
-
ASSERT(r.blend || r.capture || r.shadowPass || !r.isRendering,
|
|
1261
|
+
ASSERT(r.blend || r.capture || r.shadowPass || !r.isRendering,
|
|
1262
|
+
'soft discs and shadows need blending: set the object transparent or draw from onRenderTransparent');
|
|
1097
1263
|
}
|
|
1098
1264
|
|
|
1099
1265
|
// draw the three rings of a soft disc as unlit strips, pointAt(cos, sin, radius) gives the world point
|
|
@@ -1115,98 +1281,6 @@ function render3DDrawSoftDisc(radius, color, sides, normal, pointAt)
|
|
|
1115
1281
|
}
|
|
1116
1282
|
}
|
|
1117
1283
|
|
|
1118
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
1119
|
-
// Debug primitives, drawn on top of the 3D scene like the 2D debug functions, only in debug builds
|
|
1120
|
-
|
|
1121
|
-
let render3DDebugPrimitives = [];
|
|
1122
|
-
|
|
1123
|
-
// draw the live debug primitives with depth test off so they show through walls, drop the expired ones
|
|
1124
|
-
function render3DRenderDebug()
|
|
1125
|
-
{
|
|
1126
|
-
if (!render3DDebugPrimitives.length) return;
|
|
1127
|
-
render3DWithState({lighting: false, depthTest: false, receiveShadow: false, additive: false, shader: undefined}, ()=>
|
|
1128
|
-
{
|
|
1129
|
-
for (const p of render3DDebugPrimitives)
|
|
1130
|
-
p.draw();
|
|
1131
|
-
});
|
|
1132
|
-
render3DDebugPrimitives = render3DDebugPrimitives.filter(p=> p.timer < 0); // a Timer compares as negative until it elapses
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
// record a debug draw for a time
|
|
1136
|
-
function render3DDebugPush(duration, draw)
|
|
1137
|
-
{
|
|
1138
|
-
ASSERT(isNumber(duration), 'duration must be a number');
|
|
1139
|
-
debug && render3D?.program && render3DDebugPrimitives.push({timer: new Timer(duration), draw});
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
/** Draw a debug wireframe box
|
|
1143
|
-
* @param {Vector3} pos - Center
|
|
1144
|
-
* @param {Vector3|number} [size] - Full size, a number for a cube
|
|
1145
|
-
* @param {Color} [color]
|
|
1146
|
-
* @param {number} [time] - How long to show it, 0 is one frame
|
|
1147
|
-
* @param {Vector3} [rotation] - vec3(pitch, yaw, roll)
|
|
1148
|
-
* @memberof Render3D */
|
|
1149
|
-
function debugBox3D(pos, size=1, color=WHITE, time=0, rotation)
|
|
1150
|
-
{
|
|
1151
|
-
const matrix = buildMatrix(pos, rotation, render3DSize3(size));
|
|
1152
|
-
const corner = (i)=> matrix.transformPoint(vec3(i & 1 ? .5 : -.5, i & 2 ? .5 : -.5, i & 4 ? .5 : -.5));
|
|
1153
|
-
render3DDebugPush(time, ()=>
|
|
1154
|
-
{
|
|
1155
|
-
for (let i = 0; i < 8; ++i)
|
|
1156
|
-
for (const bit of [1, 2, 4])
|
|
1157
|
-
if (!(i & bit))
|
|
1158
|
-
render3D.drawLine(corner(i), corner(i | bit), RENDER3D_DEBUG_WIDTH, color);
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
/** Draw a debug wireframe sphere as three rings
|
|
1163
|
-
* @param {Vector3} pos - Center
|
|
1164
|
-
* @param {number} [size] - Diameter
|
|
1165
|
-
* @param {Color} [color]
|
|
1166
|
-
* @param {number} [time] - How long to show it, 0 is one frame
|
|
1167
|
-
* @memberof Render3D */
|
|
1168
|
-
function debugSphere3D(pos, size=1, color=WHITE, time=0)
|
|
1169
|
-
{
|
|
1170
|
-
const circle = render3DCircle(24), r = size / 2;
|
|
1171
|
-
render3DDebugPush(time, ()=>
|
|
1172
|
-
{
|
|
1173
|
-
for (const ring of [(c, s)=> vec3(c, s, 0), (c, s)=> vec3(c, 0, s), (c, s)=> vec3(0, c, s)])
|
|
1174
|
-
{
|
|
1175
|
-
const points = [];
|
|
1176
|
-
for (let i = 0; i <= 24; ++i)
|
|
1177
|
-
points.push(pos.add(ring(circle[i*2], circle[i*2 + 1]).scale(r)));
|
|
1178
|
-
render3D.drawRibbon(points, RENDER3D_DEBUG_WIDTH, undefined, color);
|
|
1179
|
-
}
|
|
1180
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
/** Draw a debug line
|
|
1184
|
-
* @param {Vector3} posA
|
|
1185
|
-
* @param {Vector3} posB
|
|
1186
|
-
* @param {Color} [color]
|
|
1187
|
-
* @param {number} [width]
|
|
1188
|
-
* @param {number} [time] - How long to show it, 0 is one frame
|
|
1189
|
-
* @memberof Render3D */
|
|
1190
|
-
function debugLine3D(posA, posB, color=WHITE, width=RENDER3D_DEBUG_WIDTH, time=0)
|
|
1191
|
-
{
|
|
1192
|
-
render3DDebugPush(time, ()=> render3D.drawLine(posA, posB, width, color));
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
/** Draw a debug point as a small cross of three lines
|
|
1196
|
-
* @param {Vector3} pos
|
|
1197
|
-
* @param {Color} [color]
|
|
1198
|
-
* @param {number} [time] - How long to show it, 0 is one frame
|
|
1199
|
-
* @param {number} [size] - Length of the cross
|
|
1200
|
-
* @memberof Render3D */
|
|
1201
|
-
function debugPoint3D(pos, color=WHITE, time=0, size=.2)
|
|
1202
|
-
{
|
|
1203
|
-
render3DDebugPush(time, ()=>
|
|
1204
|
-
{
|
|
1205
|
-
for (const axis of [vec3(size / 2, 0, 0), vec3(0, size / 2, 0), vec3(0, 0, size / 2)])
|
|
1206
|
-
render3D.drawLine(pos.subtract(axis), pos.add(axis), RENDER3D_DEBUG_WIDTH, color);
|
|
1207
|
-
});
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
1284
|
///////////////////////////////////////////////////////////////////////////////
|
|
1211
1285
|
/**
|
|
1212
1286
|
* Camera3D - Position, rotation and lens for the 3D view
|
|
@@ -1248,7 +1322,8 @@ class Camera3D
|
|
|
1248
1322
|
getProjectionMatrix(aspect)
|
|
1249
1323
|
{
|
|
1250
1324
|
const h = this.orthographic / 2, w = h * aspect;
|
|
1251
|
-
return h ? Matrix4.orthographic(-w, w, -h, h, this.near, this.far) :
|
|
1325
|
+
return h ? Matrix4.orthographic(-w, w, -h, h, this.near, this.far) :
|
|
1326
|
+
Matrix4.perspective(this.fov, aspect, this.near, this.far);
|
|
1252
1327
|
}
|
|
1253
1328
|
|
|
1254
1329
|
/** Returns the direction the camera looks
|
|
@@ -1276,7 +1351,8 @@ class Camera3D
|
|
|
1276
1351
|
{
|
|
1277
1352
|
const r = cos(pitch) * distance;
|
|
1278
1353
|
this.pos = target.add(vec3(sin(yaw) * r, sin(pitch) * distance, cos(yaw) * r));
|
|
1279
|
-
|
|
1354
|
+
// straight down has no yaw of its own to look along, so the orbit's yaw is used, and a top down view turns
|
|
1355
|
+
this.rotation = render3DLookRotation(target.subtract(this.pos), vec3(0, yaw, 0));
|
|
1280
1356
|
}
|
|
1281
1357
|
|
|
1282
1358
|
/** Chase a target from an offset, easing toward it, and look at it
|
|
@@ -1309,12 +1385,16 @@ class Camera3D
|
|
|
1309
1385
|
// GL setup, shaders and the frame hooks
|
|
1310
1386
|
|
|
1311
1387
|
// the four attributes of a 36 byte vertex at the locations the shaders declare: location, size, type, normalize, byte offset
|
|
1388
|
+
/** @type {Array<[number, number, number, boolean, number]>} */
|
|
1312
1389
|
const RENDER3D_ATTRIBS = [[0, 3, 5126, false, 0], [1, 3, 5126, false, 12], [2, 2, 5126, false, 24], [3, 4, 5121, true, 32]];
|
|
1313
1390
|
|
|
1314
1391
|
// the vertex shader, shared by the plugin's program and every Shader's
|
|
1315
1392
|
// attributes: p position, n normal, t uv, c color, at fixed slots the depth shader also uses
|
|
1316
|
-
// uniforms: viewProj, lightViewProj; the model matrix, the
|
|
1317
|
-
//
|
|
1393
|
+
// uniforms: viewProj, lightViewProj; the model matrix, the tint and the uv rect are vertex attributes, see
|
|
1394
|
+
// RENDER3D_VERTEX_INPUTS; L is the mesh's own uv for a Shader's localUV
|
|
1395
|
+
// the normal matrix comes from the model matrix here: each column over its squared length, which is the inverse
|
|
1396
|
+
// transpose of any rotation and scale, mirrored or not, and skips a 3x3 inverse per draw on the CPU; a sheared
|
|
1397
|
+
// matrix, one built by multiplying rotations with scales between them, gets normals that are only close
|
|
1318
1398
|
const RENDER3D_VERTEX_SOURCE =
|
|
1319
1399
|
'#version 300 es\n' +
|
|
1320
1400
|
'precision highp float;' +
|
|
@@ -1325,7 +1405,8 @@ const RENDER3D_VERTEX_SOURCE =
|
|
|
1325
1405
|
'vec4 w=mat4(m0,m1,m2,m3)*vec4(p,1.);' +
|
|
1326
1406
|
'gl_Position=viewProj*w;' +
|
|
1327
1407
|
'P=w.xyz;' +
|
|
1328
|
-
'
|
|
1408
|
+
'vec3 c0=m0.xyz,c1=m1.xyz,c2=m2.xyz;' +
|
|
1409
|
+
'N=mat3(c0/dot(c0,c0),c1/dot(c1,c1),c2/dot(c2,c2))*n;' +
|
|
1329
1410
|
'T=uvRect.xy+t*uvRect.zw;' +
|
|
1330
1411
|
'L=t;' +
|
|
1331
1412
|
'C=c*tint;' +
|
|
@@ -1342,6 +1423,7 @@ const RENDER3D_SNIPPET_NAMES =
|
|
|
1342
1423
|
'#define sunDirection (-lightDir.xyz)\n' +
|
|
1343
1424
|
'#define sunColor lightColor.rgb\n' +
|
|
1344
1425
|
'#define ambientColor ambientFog.rgb\n' +
|
|
1426
|
+
'#define ambientGroundColor ambientGround.rgb\n' +
|
|
1345
1427
|
'#define lightCount extraLightCount\n' +
|
|
1346
1428
|
'#define lights extraLights\n' +
|
|
1347
1429
|
'#define lightColors extraLightColors\n';
|
|
@@ -1355,11 +1437,12 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1355
1437
|
{
|
|
1356
1438
|
return '#version 300 es\n' +
|
|
1357
1439
|
'precision highp float;' +
|
|
1358
|
-
'uniform vec4 lightDir,lightColor,ambientFog,fogColor,shadowParams;' +
|
|
1440
|
+
'uniform vec4 lightDir,lightColor,ambientFog,ambientGround,fogColor,shadowParams;' +
|
|
1359
1441
|
'uniform vec4 extraLights[' + RENDER3D_MAX_LIGHTS + '],extraLightColors[' + RENDER3D_MAX_LIGHTS + '];' +
|
|
1360
1442
|
'uniform int extraLightCount;' +
|
|
1361
1443
|
'uniform vec3 cameraPos;' +
|
|
1362
1444
|
'uniform sampler2D tex;' +
|
|
1445
|
+
'uniform bool premultipliedTexture;' + // is the texture a render target, which holds premultiplied color
|
|
1363
1446
|
'uniform highp sampler2DShadow shadowMap;' +
|
|
1364
1447
|
'in vec3 P,N;in vec2 T,L;in vec4 C,S;' +
|
|
1365
1448
|
'out vec4 o;' +
|
|
@@ -1376,6 +1459,7 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1376
1459
|
(fragmentCode ? RENDER3D_SNIPPET_NAMES + fragmentCode + '\n' : '') +
|
|
1377
1460
|
'void main(){' +
|
|
1378
1461
|
(fragmentCode ? 'vec4 t;mainImage(t,T);' : 'vec4 t=texture(tex,T);') +
|
|
1462
|
+
'if(premultipliedTexture&&t.a>0.)t.rgb/=t.a;' + // back to straight color, what the lighting and blend expect
|
|
1379
1463
|
'if(shadowParams.w>0.&&t.a<.5)discard;' + // an opaque draw drops see through texels, as the shadow map does
|
|
1380
1464
|
'vec4 c=C*t;' +
|
|
1381
1465
|
'float e=lightDir.w;' +
|
|
@@ -1384,9 +1468,11 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1384
1468
|
'if(!gl_FrontFacing)n=-n;' + // only a double sided mesh shows a back face, light it on the side that is seen
|
|
1385
1469
|
'float nl=dot(n,-lightDir.xyz);' +
|
|
1386
1470
|
'float s=shadow();' +
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
//
|
|
1471
|
+
// the ambient: one color, or blended from the ground color below to the sky color above by the way the face points
|
|
1472
|
+
'vec3 l=(ambientGround.a>0.?mix(ambientGround.rgb,ambientFog.rgb,n.y*.5+.5):ambientFog.rgb)+lightColor.rgb*max(nl,0.)*s;' +
|
|
1473
|
+
// the Light3D objects: a point light falls off with distance, a directional one does not and carries the
|
|
1474
|
+
// direction toward it in xyz, marked by a negative radius; each adds its own highlight when there is a strength
|
|
1475
|
+
'vec3 eye=lightColor.a>0.?normalize(cameraPos-P):vec3(0),sp=vec3(0);' +
|
|
1390
1476
|
'for(int i=0;i<' + RENDER3D_MAX_LIGHTS + ';++i){' +
|
|
1391
1477
|
'if(i>=extraLightCount)break;' +
|
|
1392
1478
|
'vec4 L=extraLights[i];' +
|
|
@@ -1394,14 +1480,18 @@ function render3DFragmentSource(fragmentCode)
|
|
|
1394
1480
|
'vec3 v=directional?L.xyz:L.xyz-P;' +
|
|
1395
1481
|
'float d=length(v);' +
|
|
1396
1482
|
'float a=directional?1.:max(0.,1.-d/L.w);' +
|
|
1397
|
-
'
|
|
1483
|
+
'v/=max(d,1e-6);' +
|
|
1484
|
+
'float ln=dot(n,v);' +
|
|
1485
|
+
'vec3 lc=extraLightColors[i].rgb*extraLightColors[i].a*a*a;' +
|
|
1486
|
+
'l+=lc*max(0.,ln);' +
|
|
1487
|
+
'if(lightColor.a>0.)sp+=lc*pow(max(dot(reflect(-v,n),eye),0.),16.)*step(0.,ln);' +
|
|
1398
1488
|
'}' +
|
|
1399
1489
|
'c.rgb*=l*(1.-e)+e;' + // lit, blended toward its own color by how emissive it is
|
|
1400
|
-
// specular: only where
|
|
1490
|
+
// specular: the sun's only where its light hits and out of shadow, then the Light3D highlights,
|
|
1491
|
+
// skipped entirely when the strength is zero
|
|
1401
1492
|
'if(lightColor.a>0.){' +
|
|
1402
|
-
'vec3 v=normalize(cameraPos-P);' +
|
|
1403
1493
|
'vec3 r=reflect(lightDir.xyz,n);' +
|
|
1404
|
-
'c.rgb+=lightColor.rgb*pow(max(dot(r,
|
|
1494
|
+
'c.rgb+=lightColor.rgb*pow(max(dot(r,eye),0.),16.)*lightColor.a*step(0.,nl)*s*(1.-e)+sp*lightColor.a*(1.-e);' +
|
|
1405
1495
|
'}}else c.rgb*=e;' + // fully emissive: its own color, or brighter, with no lighting to work out
|
|
1406
1496
|
'if(ambientFog.a>0.){' +
|
|
1407
1497
|
'float z=distance(cameraPos,P);' +
|
|
@@ -1453,37 +1543,40 @@ function render3DInitGL()
|
|
|
1453
1543
|
return;
|
|
1454
1544
|
}
|
|
1455
1545
|
const gl = glContext, r = render3D;
|
|
1546
|
+
glFlush(); // a pending 2D batch draws now, while the engine's own buffer, vertex array and program are bound
|
|
1456
1547
|
r.uniforms = new Map;
|
|
1457
1548
|
r.uniformValues = {};
|
|
1549
|
+
render3DShadowCut = undefined; // the shadow shader is new too
|
|
1458
1550
|
r.attribValues = []; // a fresh context has its own attribute defaults, so nothing sent before it counts
|
|
1459
1551
|
|
|
1460
1552
|
// the shader, see RENDER3D_VERTEX_SOURCE and render3DFragmentSource
|
|
1461
1553
|
r.program = glCreateProgram(RENDER3D_VERTEX_SOURCE, render3DFragmentSource());
|
|
1462
1554
|
|
|
1463
|
-
// the depth only shader for the shadow map, same vertex layout; see through pixels cast nothing,
|
|
1464
|
-
//
|
|
1555
|
+
// the depth only shader for the shadow map, same vertex layout; see through pixels cast nothing, so sprites and
|
|
1556
|
+
// cut out textures cast their outline, and a blended object faded below half its alpha casts nothing, as it
|
|
1557
|
+
// draws; an opaque one draws solid whatever its tint alpha, and casts so
|
|
1465
1558
|
r.shadowShader = glCreateProgram(
|
|
1466
1559
|
'#version 300 es\n' +
|
|
1467
1560
|
'precision highp float;' +
|
|
1468
1561
|
'uniform mat4 viewProj;' +
|
|
1469
1562
|
RENDER3D_VERTEX_INPUTS +
|
|
1470
|
-
'out vec2 T;' +
|
|
1471
|
-
'void main(){T=uvRect.xy+t*uvRect.zw;gl_Position=viewProj*mat4(m0,m1,m2,m3)*vec4(p,1.);}'
|
|
1563
|
+
'out vec2 T;out float A;' +
|
|
1564
|
+
'void main(){T=uvRect.xy+t*uvRect.zw;A=c.a*tint.a;gl_Position=viewProj*mat4(m0,m1,m2,m3)*vec4(p,1.);}'
|
|
1472
1565
|
,
|
|
1473
1566
|
'#version 300 es\n' +
|
|
1474
1567
|
'precision highp float;' +
|
|
1475
1568
|
'uniform sampler2D tex;' +
|
|
1476
|
-
'
|
|
1477
|
-
'
|
|
1569
|
+
'uniform float cut;' + // 1 for a blended batch, cut by its tint and vertex alpha too, 0 by the texture only
|
|
1570
|
+
'in vec2 T;in float A;' +
|
|
1571
|
+
'void main(){if(texture(tex,T).a*mix(1.,A,cut)<.5)discard;}'
|
|
1478
1572
|
);
|
|
1479
1573
|
|
|
1480
1574
|
// the vertex array object with the attributes enabled once, pointers are set per buffer by render3DBindVertexBuffer
|
|
1575
|
+
// the per instance attributes get their divisor only while a batch has them on, see render3DDrawInstanced
|
|
1481
1576
|
r.vao = gl.createVertexArray();
|
|
1482
1577
|
gl.bindVertexArray(r.vao);
|
|
1483
1578
|
for (const [location] of RENDER3D_ATTRIBS)
|
|
1484
1579
|
gl.enableVertexAttribArray(location);
|
|
1485
|
-
for (const [location] of RENDER3D_INSTANCE_ATTRIBS)
|
|
1486
|
-
gl.vertexAttribDivisor(location, 1); // one value per instance whenever a batch turns these arrays on
|
|
1487
1580
|
|
|
1488
1581
|
// the stream buffer
|
|
1489
1582
|
r.streamBuffer = gl.createBuffer();
|
|
@@ -1492,15 +1585,15 @@ function render3DInitGL()
|
|
|
1492
1585
|
r.streamCount = 0;
|
|
1493
1586
|
r.instanceBuffers = [gl.createBuffer(), gl.createBuffer(), gl.createBuffer()];
|
|
1494
1587
|
|
|
1495
|
-
// white texture for untextured draws, and a one texel shadow map that keeps the shadow sampler valid until shadows
|
|
1588
|
+
// white texture for untextured draws, and a one texel shadow map that keeps the shadow sampler valid until shadows
|
|
1589
|
+
// are on
|
|
1496
1590
|
r.whiteTexture = glCreateTexture();
|
|
1497
|
-
r.mipmapped = new WeakSet;
|
|
1498
1591
|
|
|
1499
|
-
r.samplers =
|
|
1592
|
+
r.samplers = new Map;
|
|
1500
1593
|
r.samplerKey = undefined;
|
|
1501
1594
|
render3DUpdateShadowMap(1);
|
|
1502
1595
|
|
|
1503
|
-
// hand the engine back its own buffer
|
|
1596
|
+
// hand the engine back its own buffer, vertex array and program, the 2D batch was flushed before they changed
|
|
1504
1597
|
gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer);
|
|
1505
1598
|
glSetInstancedMode(true);
|
|
1506
1599
|
}
|
|
@@ -1512,7 +1605,8 @@ function render3DContextLost()
|
|
|
1512
1605
|
for (const shader of glShaderObjects)
|
|
1513
1606
|
shader.program3D = undefined; // compiled again by the next draw
|
|
1514
1607
|
r.lightCount = 0;
|
|
1515
|
-
r.instanceBuffers =
|
|
1608
|
+
r.instanceBuffers = [];
|
|
1609
|
+
r.samplers = new Map;
|
|
1516
1610
|
r.samplerKey = undefined;
|
|
1517
1611
|
render3DClearInstances();
|
|
1518
1612
|
r.shadowFramebuffer = r.shadowTexture = undefined;
|
|
@@ -1535,8 +1629,7 @@ function render3DUniform(name, program=render3D.currentProgram)
|
|
|
1535
1629
|
return cache[name] ??= glContext.getUniformLocation(program, name);
|
|
1536
1630
|
}
|
|
1537
1631
|
|
|
1538
|
-
// the model matrix,
|
|
1539
|
-
const render3DNormalScratch = new Float32Array(9);
|
|
1632
|
+
// the model matrix, the tint and the uv rect as constant attributes for one draw
|
|
1540
1633
|
function render3DDrawAttribs(m, tint, uvRect)
|
|
1541
1634
|
{
|
|
1542
1635
|
const gl = glContext;
|
|
@@ -1544,13 +1637,6 @@ function render3DDrawAttribs(m, tint, uvRect)
|
|
|
1544
1637
|
gl.vertexAttrib4f(5, m[4], m[5], m[6], m[7]);
|
|
1545
1638
|
gl.vertexAttrib4f(6, m[8], m[9], m[10], m[11]);
|
|
1546
1639
|
gl.vertexAttrib4f(7, m[12], m[13], m[14], m[15]);
|
|
1547
|
-
if (!render3D.shadowPass) // the shadow map has no lighting
|
|
1548
|
-
{
|
|
1549
|
-
const n = render3DNormalMatrix3(m, render3DNormalScratch, 0);
|
|
1550
|
-
gl.vertexAttrib3f(8, n[0], n[1], n[2]);
|
|
1551
|
-
gl.vertexAttrib3f(9, n[3], n[4], n[5]);
|
|
1552
|
-
gl.vertexAttrib3f(10, n[6], n[7], n[8]);
|
|
1553
|
-
}
|
|
1554
1640
|
render3DAttrib4f(11, tint.r, tint.g, tint.b, tint.a);
|
|
1555
1641
|
render3DAttrib4f(12, uvRect.x, uvRect.y, uvRect.w, uvRect.h);
|
|
1556
1642
|
}
|
|
@@ -1558,62 +1644,48 @@ function render3DDrawAttribs(m, tint, uvRect)
|
|
|
1558
1644
|
// set a constant vec4 attribute only when its value changed since the last time
|
|
1559
1645
|
function render3DAttrib4f(location, x, y, z, w)
|
|
1560
1646
|
{
|
|
1561
|
-
const values = render3D.attribValues, last = values[location];
|
|
1562
|
-
if (last
|
|
1647
|
+
const values = render3D.attribValues, last = values[location] ||= [NaN, NaN, NaN, NaN]; // kept, not made per draw
|
|
1648
|
+
if (last[0] === x && last[1] === y && last[2] === z && last[3] === w)
|
|
1563
1649
|
return;
|
|
1564
|
-
|
|
1650
|
+
last[0] = x, last[1] = y, last[2] = z, last[3] = w;
|
|
1565
1651
|
glContext.vertexAttrib4f(location, x, y, z, w);
|
|
1566
1652
|
}
|
|
1567
1653
|
|
|
1568
|
-
// write the 3x3 matrix that keeps normals pointing out when the model matrix scales unevenly, the inverse transpose
|
|
1569
|
-
// of its top left 3x3 by cofactors; a flat model with no inverse keeps its own axes
|
|
1570
|
-
function render3DNormalMatrix3(m, out, offset)
|
|
1571
|
-
{
|
|
1572
|
-
const a = m[0], b = m[1], c = m[2], d = m[4], e = m[5], f = m[6], g = m[8], h = m[9], i = m[10];
|
|
1573
|
-
const c00 = e*i - h*f, c01 = h*c - b*i, c02 = b*f - e*c;
|
|
1574
|
-
const det = a*c00 + d*c01 + g*c02;
|
|
1575
|
-
if (abs(det) < 1e-12)
|
|
1576
|
-
{
|
|
1577
|
-
out[offset] = a; out[offset+1] = b; out[offset+2] = c;
|
|
1578
|
-
out[offset+3] = d; out[offset+4] = e; out[offset+5] = f;
|
|
1579
|
-
out[offset+6] = g; out[offset+7] = h; out[offset+8] = i;
|
|
1580
|
-
return out;
|
|
1581
|
-
}
|
|
1582
|
-
const s = 1 / det;
|
|
1583
|
-
out[offset] = c00 * s; out[offset+1] = (g*f - d*i) * s; out[offset+2] = (d*h - g*e) * s;
|
|
1584
|
-
out[offset+3] = c01 * s; out[offset+4] = (a*i - g*c) * s; out[offset+5] = (g*b - a*h) * s;
|
|
1585
|
-
out[offset+6] = c02 * s; out[offset+7] = (d*c - a*f) * s; out[offset+8] = (a*e - d*b) * s;
|
|
1586
|
-
return out;
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
1654
|
// textures in 3D shrink into the distance far more than sprites do, so the pass samples them through their mipmaps;
|
|
1590
|
-
// a sampler sets the filtering for the 3D pass only and leaves the engine's textures as they are for 2D
|
|
1591
|
-
//
|
|
1655
|
+
// a sampler sets the filtering for the 3D pass only and leaves the engine's textures as they are for 2D; they are
|
|
1656
|
+
// made as textures need them, and all dropped when the settings change
|
|
1592
1657
|
function render3DUpdateSamplers()
|
|
1593
1658
|
{
|
|
1594
|
-
const
|
|
1659
|
+
const r = render3D, key = tilesPixelated + ' ' + r.anisotropy;
|
|
1595
1660
|
if (r.samplerKey === key) return;
|
|
1596
1661
|
r.samplerKey = key;
|
|
1597
|
-
for (const sampler of r.samplers)
|
|
1598
|
-
|
|
1662
|
+
for (const sampler of r.samplers.values())
|
|
1663
|
+
glContext.deleteSampler(sampler); // the set being replaced, a lost context empties this first
|
|
1664
|
+
r.samplers.clear();
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// the sampler for a texture's wrap modes, smooth or hard edged, made the first time it is needed
|
|
1668
|
+
function render3DSampler(wrap, pixelated)
|
|
1669
|
+
{
|
|
1670
|
+
const gl = glContext, r = render3D, [wrapS, wrapT] = glWrapModes(wrap);
|
|
1671
|
+
const key = wrapS * 1e5 + wrapT * 2 + (pixelated ? 1 : 0); // the modes are 5 digit numbers
|
|
1672
|
+
let sampler = r.samplers.get(key);
|
|
1673
|
+
if (sampler) return sampler;
|
|
1674
|
+
sampler = gl.createSampler();
|
|
1675
|
+
const sharp = pixelated || tilesPixelated;
|
|
1676
|
+
gl.samplerParameteri(sampler, gl.TEXTURE_MAG_FILTER, sharp ? gl.NEAREST : gl.LINEAR);
|
|
1677
|
+
gl.samplerParameteri(sampler, gl.TEXTURE_MIN_FILTER, pixelated ? gl.NEAREST
|
|
1678
|
+
: tilesPixelated ? gl.NEAREST_MIPMAP_LINEAR : gl.LINEAR_MIPMAP_LINEAR);
|
|
1679
|
+
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_S, wrapS);
|
|
1680
|
+
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_T, wrapT);
|
|
1599
1681
|
const anisotropy = gl.getExtension('EXT_texture_filter_anisotropic');
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
: tilesPixelated ? gl.NEAREST_MIPMAP_LINEAR : gl.LINEAR_MIPMAP_LINEAR);
|
|
1608
|
-
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_S, wrap);
|
|
1609
|
-
gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_T, wrap);
|
|
1610
|
-
if (anisotropy && !pixelated)
|
|
1611
|
-
{
|
|
1612
|
-
const most = gl.getParameter(anisotropy.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
|
|
1613
|
-
gl.samplerParameterf(sampler, anisotropy.TEXTURE_MAX_ANISOTROPY_EXT, clamp(r.anisotropy, 1, most));
|
|
1614
|
-
}
|
|
1615
|
-
return sampler;
|
|
1616
|
-
}));
|
|
1682
|
+
if (anisotropy && !pixelated)
|
|
1683
|
+
{
|
|
1684
|
+
const most = gl.getParameter(anisotropy.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
|
|
1685
|
+
gl.samplerParameterf(sampler, anisotropy.TEXTURE_MAX_ANISOTROPY_EXT, clamp(r.anisotropy, 1, most));
|
|
1686
|
+
}
|
|
1687
|
+
r.samplers.set(key, sampler);
|
|
1688
|
+
return sampler;
|
|
1617
1689
|
}
|
|
1618
1690
|
|
|
1619
1691
|
// bind the texture of a tile or texture, white when there is none or it is not loaded, with the 3D sampler that
|
|
@@ -1621,15 +1693,17 @@ function render3DUpdateSamplers()
|
|
|
1621
1693
|
function render3DBindTexture(tileInfo, state=render3D)
|
|
1622
1694
|
{
|
|
1623
1695
|
const gl = glContext, r = render3D;
|
|
1624
|
-
const textureInfo = tileInfo
|
|
1696
|
+
const textureInfo = render3DTextureOf(tileInfo);
|
|
1625
1697
|
const texture = textureInfo?.glTexture || r.whiteTexture;
|
|
1626
1698
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
1627
|
-
if (!r.mipmaps && !state.pixelated)
|
|
1628
|
-
return gl.bindSampler(0, null); // the texture's own filtering, as in 2D
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1699
|
+
if (texture === r.whiteTexture || !r.mipmaps && !state.pixelated)
|
|
1700
|
+
return gl.bindSampler(0, null); // the texture's own filtering, as in 2D; the white texel needs no mipmaps
|
|
1701
|
+
// or anisotropy, and filtering it that way costs every untextured fragment
|
|
1702
|
+
gl.bindSampler(0, render3DSampler(textureInfo?.wrap, state.pixelated));
|
|
1703
|
+
glUpdateMipmaps(texture); // drawn into since its mipmaps were made
|
|
1704
|
+
if (!state.pixelated && !glMipmappedTextures.has(texture)) // a hard edged draw never reads them
|
|
1705
|
+
{
|
|
1706
|
+
glMipmappedTextures.add(texture); // the core makes them again when the texture changes
|
|
1633
1707
|
gl.generateMipmap(gl.TEXTURE_2D);
|
|
1634
1708
|
}
|
|
1635
1709
|
}
|
|
@@ -1637,13 +1711,23 @@ function render3DBindTexture(tileInfo, state=render3D)
|
|
|
1637
1711
|
// send a vec4 uniform of the main shader only when its value changed since the last send
|
|
1638
1712
|
function render3DUniform4f(name, x, y, z, w)
|
|
1639
1713
|
{
|
|
1640
|
-
const values = render3D.uniformValues, last = values[name];
|
|
1641
|
-
if (last
|
|
1714
|
+
const values = render3D.uniformValues, last = values[name] ||= [NaN, NaN, NaN, NaN]; // kept, not made per draw
|
|
1715
|
+
if (last[0] === x && last[1] === y && last[2] === z && last[3] === w)
|
|
1642
1716
|
return;
|
|
1643
|
-
|
|
1717
|
+
last[0] = x, last[1] = y, last[2] = z, last[3] = w;
|
|
1644
1718
|
glContext.uniform4f(render3DUniform(name), x, y, z, w);
|
|
1645
1719
|
}
|
|
1646
1720
|
|
|
1721
|
+
// send an int uniform of the main shader only when its value changed since the last send
|
|
1722
|
+
function render3DUniform1i(name, x)
|
|
1723
|
+
{
|
|
1724
|
+
const values = render3D.uniformValues;
|
|
1725
|
+
if (values[name] === x)
|
|
1726
|
+
return;
|
|
1727
|
+
values[name] = x;
|
|
1728
|
+
glContext.uniform1i(render3DUniform(name), x);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1647
1731
|
// bind a vertex buffer and point the attributes at it
|
|
1648
1732
|
function render3DBindVertexBuffer(buffer)
|
|
1649
1733
|
{
|
|
@@ -1653,12 +1737,43 @@ function render3DBindVertexBuffer(buffer)
|
|
|
1653
1737
|
gl.vertexAttribPointer(a[0], a[1], a[2], a[3], RENDER3D_VERTEX_BYTES, a[4]);
|
|
1654
1738
|
}
|
|
1655
1739
|
|
|
1740
|
+
// a mesh's vertices and its triangle indices, ready for drawElements
|
|
1741
|
+
function render3DBindMesh(mesh)
|
|
1742
|
+
{
|
|
1743
|
+
render3DBindVertexBuffer(mesh.buffer);
|
|
1744
|
+
glContext.bindBuffer(glContext.ELEMENT_ARRAY_BUFFER, mesh.indexBuffer);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// the texture a draw samples: a tile's texture, a TextureInfo as it is, or undefined for white
|
|
1748
|
+
function render3DTextureOf(tileInfo) { return tileInfo instanceof TileInfo ? tileInfo.textureInfo : tileInfo; }
|
|
1749
|
+
|
|
1656
1750
|
// where a tile sits in its texture, pulled in slightly at the edges so neighbors do not bleed in
|
|
1751
|
+
// the level's solid geometry in 3D, the height maps and voxel maps that objects with collideLevel collide with, as
|
|
1752
|
+
// tileCollisionLayers is in 2D; each joins when made and leaves when destroyed
|
|
1753
|
+
const render3DLevel = [];
|
|
1754
|
+
|
|
1755
|
+
// take one of the level's parts out of it, when it is destroyed
|
|
1756
|
+
function render3DLevelLeave(o)
|
|
1757
|
+
{
|
|
1758
|
+
const i = render3DLevel.indexOf(o);
|
|
1759
|
+
i >= 0 && render3DLevel.splice(i, 1);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// the level's parts are looked up in world space, so they stay upright and unscaled at the root
|
|
1763
|
+
function render3DLevelAssertPlaced(o)
|
|
1764
|
+
{
|
|
1765
|
+
ASSERT(!o.parent && !o.rotation3D.lengthSquared() && o.scale3D.x === 1 && o.scale3D.y === 1 && o.scale3D.z === 1,
|
|
1766
|
+
'a height map or voxel map stays upright and unscaled at the root, its lookups are in world space');
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1657
1769
|
// this returns one shared object, so read it before calling again
|
|
1658
1770
|
const render3DTileUVRect = {x:0, y:0, w:1, h:1};
|
|
1771
|
+
// the tiles of objects made from a whole TextureInfo, they cover all of it even after the texture is resized
|
|
1772
|
+
const render3DWholeTiles = new WeakSet;
|
|
1659
1773
|
function render3DGetTileUVs(tileInfo)
|
|
1660
1774
|
{
|
|
1661
|
-
|
|
1775
|
+
// a headless tile has no texture, and a whole texture's tile covers it at any size
|
|
1776
|
+
if (!(tileInfo instanceof TileInfo) || !tileInfo.textureInfo || render3DWholeTiles.has(tileInfo))
|
|
1662
1777
|
return RENDER3D_FULL_UV_RECT;
|
|
1663
1778
|
const inv = tileInfo.textureInfo.sizeInverse, rect = render3DTileUVRect;
|
|
1664
1779
|
const bleedX = inv.x * tileInfo.bleed, bleedY = inv.y * tileInfo.bleed;
|
|
@@ -1669,7 +1784,8 @@ function render3DGetTileUVs(tileInfo)
|
|
|
1669
1784
|
return rect;
|
|
1670
1785
|
}
|
|
1671
1786
|
|
|
1672
|
-
// set the per draw uniforms and gl state for a draw, in the shadow pass only the
|
|
1787
|
+
// set the per draw uniforms and gl state for a draw, in the shadow pass only the per draw attributes, the texture
|
|
1788
|
+
// and the depth shader's cut
|
|
1673
1789
|
// tileInfo may be a TileInfo, a TextureInfo, or undefined for the white texture
|
|
1674
1790
|
// state is the plugin's current fields, or the captured state of a stream batch
|
|
1675
1791
|
function render3DSetDrawUniforms(matrix, tileInfo, tint, uvRect, state=render3D)
|
|
@@ -1680,7 +1796,14 @@ function render3DSetDrawUniforms(matrix, tileInfo, tint, uvRect, state=render3D)
|
|
|
1680
1796
|
uvRect ||= render3DGetTileUVs(tileInfo);
|
|
1681
1797
|
render3DDrawAttribs(matrix.m, tint, uvRect);
|
|
1682
1798
|
render3DBindTexture(tileInfo, state);
|
|
1683
|
-
if (r.shadowPass)
|
|
1799
|
+
if (r.shadowPass)
|
|
1800
|
+
{
|
|
1801
|
+
// the shadow map needs only how to cut: by the alpha it blends with, or the texture's as an opaque draw
|
|
1802
|
+
const cut = state.blend ? 1 : 0;
|
|
1803
|
+
if (render3DShadowCut !== cut)
|
|
1804
|
+
glContext.uniform1f(render3DUniform('cut', r.shadowShader), render3DShadowCut = cut);
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1684
1807
|
|
|
1685
1808
|
// the program: a Shader's own, compiled by its first draw, or the plugin's; switching sends the pass uniforms
|
|
1686
1809
|
const program = state.shader ? render3DShaderProgram(state.shader) : r.program;
|
|
@@ -1708,11 +1831,20 @@ function render3DSetDrawUniforms(matrix, tileInfo, tint, uvRect, state=render3D)
|
|
|
1708
1831
|
render3DUniform4f('lightDir', s.x / sl, s.y / sl, s.z / sl, state.lighting ? state.emissive : 1);
|
|
1709
1832
|
render3DUniform4f('lightColor', lc.r, lc.g, lc.b, state.specular);
|
|
1710
1833
|
render3DUniform4f('ambientFog', ac.r, ac.g, ac.b, r.fogEnd);
|
|
1834
|
+
const gc = r.ambientGroundColor;
|
|
1835
|
+
// its alpha says whether the ground color is on
|
|
1836
|
+
gc ? render3DUniform4f('ambientGround', gc.r, gc.g, gc.b, 1) : render3DUniform4f('ambientGround', 0, 0, 0, 0);
|
|
1837
|
+
|
|
1711
1838
|
render3DUniform4f('fogColor', fc.r, fc.g, fc.b, r.fogStart);
|
|
1712
1839
|
// how the fragment shader finishes: 1 drops see through texels and keeps the draw opaque,
|
|
1713
1840
|
// 0 blends them away instead, and -1 is additive, which has to fade into fog differently
|
|
1714
1841
|
const blendMode = state.blend ? (state.additive ? -1 : 0) : 1;
|
|
1715
|
-
|
|
1842
|
+
const receives = r.shadows && r.passIsDefault && state.receiveShadow ? 1 : 0;
|
|
1843
|
+
render3DUniform4f('shadowParams', receives, r.shadowBias, r.shadowSoftness / r.shadowTextureSize, blendMode);
|
|
1844
|
+
|
|
1845
|
+
// a render target's texture holds premultiplied color, the blend writes it that way, so the shader undoes it
|
|
1846
|
+
const textureInfo = render3DTextureOf(tileInfo);
|
|
1847
|
+
render3DUniform1i('premultipliedTexture', +!!(textureInfo?.glTexture && glPremultipliedTextures.has(textureInfo.glTexture)));
|
|
1716
1848
|
}
|
|
1717
1849
|
|
|
1718
1850
|
// the six flat sides of the camera's visible box, each as [x, y, z, w] facing inward
|
|
@@ -1745,48 +1877,57 @@ function render3DRender()
|
|
|
1745
1877
|
render3DRenderPass(true);
|
|
1746
1878
|
}
|
|
1747
1879
|
|
|
1748
|
-
// one 3D pass for the objects of a layer: take over the gl state, draw the shadow map once a frame and the stages, hand
|
|
1880
|
+
// one 3D pass for the objects of a layer: take over the gl state, draw the shadow map once a frame and the stages, hand
|
|
1881
|
+
// the state back
|
|
1749
1882
|
// the layer matching render3D.renderAfter2D is the default and always runs, the other only when an object asks for it
|
|
1750
1883
|
function render3DRenderPass(after2D)
|
|
1751
1884
|
{
|
|
1752
1885
|
const gl = glContext, r = render3D;
|
|
1753
|
-
if (!r.program) return; // headless, gl disabled, or context lost
|
|
1886
|
+
if (!r.program || !glEnable) return; // headless, gl disabled, or context lost
|
|
1754
1887
|
render3DUpdateSamplers();
|
|
1755
1888
|
ASSERT(!r.fogEnd || r.fogStart < r.fogEnd, 'fogStart must be less than fogEnd');
|
|
1756
1889
|
ASSERT(!glRenderTarget, 'the 3D pass needs the canvas depth buffer, it can not draw into a render target');
|
|
1757
1890
|
const isDefault = after2D === !!r.renderAfter2D, objects = render3DLayerObjects(after2D);
|
|
1758
1891
|
if (!isDefault && !objects.length) return;
|
|
1759
1892
|
r.passIsDefault = isDefault;
|
|
1760
|
-
|
|
1893
|
+
// the 2D sprites drawn so far go under this layer, and a batch a plugin left pending before the layer under
|
|
1894
|
+
// the 2D scene draws now, with the engine's own gl state, not after the pass with its own
|
|
1895
|
+
glFlush();
|
|
1761
1896
|
|
|
1762
1897
|
// a previous frame that threw must not leave anything pending
|
|
1763
1898
|
r.streamCount = 0;
|
|
1764
1899
|
r.capture = r.transparentQueue = undefined;
|
|
1765
1900
|
render3DClearInstances();
|
|
1766
1901
|
|
|
1767
|
-
// take over the gl state
|
|
1768
|
-
gl.bindVertexArray(r.vao);
|
|
1769
|
-
// the leading repeat on every strip shifts the triangles by one, which flips
|
|
1770
|
-
// which way they read, so tell WebGL that clockwise is the front here
|
|
1771
|
-
gl.frontFace(gl.CW);
|
|
1772
|
-
gl.activeTexture(gl.TEXTURE0);
|
|
1773
|
-
gl.depthMask(true);
|
|
1774
|
-
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
1775
|
-
|
|
1776
1902
|
// the Light3D objects, a directional one sends the direction toward it, from the origin, and a negative radius
|
|
1903
|
+
// gathered before the gl state is taken over, so an assert here leaves nothing to hand back
|
|
1777
1904
|
const lights = render3DCollectLights();
|
|
1778
1905
|
r.lightCount = lights.length;
|
|
1779
1906
|
const positions = r.lightPositions, colors = r.lightColors;
|
|
1780
1907
|
lights.forEach((light, i)=>
|
|
1781
1908
|
{
|
|
1782
1909
|
const p = light.directional ? light.getWorldPos3D().normalize() : light.getWorldPos3D();
|
|
1783
|
-
ASSERT(!light.directional || p.lengthSquared(),
|
|
1910
|
+
ASSERT(!light.directional || p.lengthSquared(),
|
|
1911
|
+
'a directional light shines from its position toward the origin, so it cannot sit on the origin');
|
|
1784
1912
|
const c = light.color, k = i * 4;
|
|
1785
1913
|
positions[k] = p.x, positions[k+1] = p.y, positions[k+2] = p.z;
|
|
1786
1914
|
positions[k+3] = light.directional ? -1 : max(0, light.radius); // a negative radius marks a direction
|
|
1787
1915
|
colors[k] = c.r, colors[k+1] = c.g, colors[k+2] = c.b, colors[k+3] = c.a * light.intensity;
|
|
1788
1916
|
});
|
|
1789
1917
|
|
|
1918
|
+
// take over the gl state
|
|
1919
|
+
gl.bindVertexArray(r.vao);
|
|
1920
|
+
// the leading repeat on every strip shifts the triangles by one, which flips
|
|
1921
|
+
// which way they read, so tell WebGL that clockwise is the front here
|
|
1922
|
+
gl.frontFace(gl.CW);
|
|
1923
|
+
// every program's shadow sampler is on unit 1, where a 2D plugin like PostProcessPlugin or LightSystemPlugin
|
|
1924
|
+
// may have left its own texture, which fails every draw; the shadow map goes back there even with shadows off
|
|
1925
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1926
|
+
gl.bindTexture(gl.TEXTURE_2D, r.shadowTexture || null);
|
|
1927
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1928
|
+
gl.depthMask(true);
|
|
1929
|
+
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
1930
|
+
|
|
1790
1931
|
r.isRendering = true;
|
|
1791
1932
|
try
|
|
1792
1933
|
{
|
|
@@ -1843,7 +1984,8 @@ function render3DUpdateShadowMap(size)
|
|
|
1843
1984
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texture, 0);
|
|
1844
1985
|
gl.drawBuffers([gl.NONE]); // depth only
|
|
1845
1986
|
gl.readBuffer(gl.NONE);
|
|
1846
|
-
ASSERT(gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE,
|
|
1987
|
+
ASSERT(gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE,
|
|
1988
|
+
'shadow map framebuffer is incomplete, try a smaller shadowMapSize');
|
|
1847
1989
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1848
1990
|
r.shadowTextureSize = size;
|
|
1849
1991
|
}
|
|
@@ -1873,7 +2015,8 @@ function render3DRenderShadowMap()
|
|
|
1873
2015
|
try
|
|
1874
2016
|
{
|
|
1875
2017
|
// see through objects cast only when textured, their alpha cuts the shadow out
|
|
1876
|
-
const casters = render3DLayerObjects(!!r.renderAfter2D).filter(o=>
|
|
2018
|
+
const casters = render3DLayerObjects(!!r.renderAfter2D).filter(o=>
|
|
2019
|
+
o.castShadow && !o.additive && (!o.transparent || o.tileInfo));
|
|
1877
2020
|
render3DDrawObjects(casters);
|
|
1878
2021
|
r.onRenderOpaque?.();
|
|
1879
2022
|
r.flush();
|
|
@@ -1909,30 +2052,80 @@ function render3DBeginStrip(count, tileInfo)
|
|
|
1909
2052
|
if (r.shadowPass && !r.lighting) return; // unlit things cast no shadow
|
|
1910
2053
|
ASSERT(count <= RENDER3D_MAX_STREAM_VERTS, 'strip is too large for the stream, bake it into a mesh');
|
|
1911
2054
|
if (count > RENDER3D_MAX_STREAM_VERTS) return;
|
|
1912
|
-
const textureInfo = tileInfo
|
|
2055
|
+
const textureInfo = render3DTextureOf(tileInfo);
|
|
1913
2056
|
if (r.streamCount && (textureInfo !== r.streamTileInfo || render3DStateChanged(r.streamState)
|
|
1914
2057
|
|| r.streamCount + count > RENDER3D_MAX_STREAM_VERTS))
|
|
1915
2058
|
r.flush();
|
|
2059
|
+
render3DFlushBeforeOverlay();
|
|
1916
2060
|
if (!r.streamCount)
|
|
1917
2061
|
r.streamState = render3DCaptureBatchState();
|
|
1918
2062
|
r.streamTileInfo = textureInfo;
|
|
1919
2063
|
return render3DGetTileUVs(tileInfo);
|
|
1920
2064
|
}
|
|
1921
2065
|
|
|
1922
|
-
//
|
|
1923
|
-
|
|
2066
|
+
// a draw that is not depth tested goes over what is already drawn, so the batches drawn before it go first, or they
|
|
2067
|
+
// would draw at the end of the stage and cover it whatever its render order; a stream open with the same state is
|
|
2068
|
+
// drawn too, so each such draw splits the batches: one draw per object for meshes each with an overlay
|
|
2069
|
+
function render3DFlushBeforeOverlay()
|
|
2070
|
+
{
|
|
2071
|
+
const r = render3D;
|
|
2072
|
+
if (r.depthTest || r.shadowPass || !r.instanceMeshes.length) return;
|
|
2073
|
+
r.flush();
|
|
2074
|
+
render3DFlushInstances();
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// upload a mesh that is new, changed, or from before the context was lost
|
|
2078
|
+
function render3DMeshUpload(mesh)
|
|
2079
|
+
{
|
|
2080
|
+
if (!mesh.buffer || mesh.dirty || mesh.contextGeneration !== render3D.contextGeneration)
|
|
2081
|
+
mesh.upload();
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// an object's size3D in the world, grown by its scale and its parents', the size its sprite is drawn at and that
|
|
2085
|
+
// collecting, picking and collision measure it at
|
|
2086
|
+
function render3DWorldSize(o, m)
|
|
2087
|
+
{
|
|
2088
|
+
const s = o.size3D;
|
|
2089
|
+
return vec3(s.x * hypot(m[0], m[1], m[2]), s.y * hypot(m[4], m[5], m[6]), s.z * hypot(m[8], m[9], m[10]));
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// the surface normal of a height function at x, z, from the slope across a cell each way, clamped to the edges so a
|
|
2093
|
+
// border sample leans the same as its neighbor; buildGrid and HeightMap both light their slopes by it, and it makes
|
|
2094
|
+
// one vector, a big terrain has millions of vertices
|
|
2095
|
+
function render3DSlopeNormal(heightFunction, x, z, ex, ez, halfX, halfZ)
|
|
2096
|
+
{
|
|
2097
|
+
const x0 = max(x - ex, -halfX), x1 = min(x + ex, halfX), z0 = max(z - ez, -halfZ), z1 = min(z + ez, halfZ);
|
|
2098
|
+
const dx = (heightFunction(x1, z) - heightFunction(x0, z)) / (x1 - x0 || 1);
|
|
2099
|
+
const dz = (heightFunction(x, z1) - heightFunction(x, z0)) / (z1 - z0 || 1);
|
|
2100
|
+
const s = 1 / hypot(dx, 1, dz);
|
|
2101
|
+
return vec3(-dx * s, s, -dz * s);
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// the half axes of a camera facing quad of a size turned by an angle, right then up, in one shared array
|
|
2105
|
+
// so a particle costs no vectors; read it before calling again
|
|
2106
|
+
const render3DBillboardAxesScratch = new Float64Array(6);
|
|
2107
|
+
function render3DBillboardAxes(size, angle, upright)
|
|
1924
2108
|
{
|
|
1925
|
-
|
|
1926
|
-
let r =
|
|
2109
|
+
const r = render3D.cameraRight, u = render3D.cameraUp;
|
|
2110
|
+
let rx = r.x, ry = r.y, rz = r.z, ux = u.x, uy = u.y, uz = u.z;
|
|
1927
2111
|
if (upright)
|
|
1928
2112
|
{
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2113
|
+
// an upright quad stands on world up and only turns to face the camera
|
|
2114
|
+
const l = hypot(rx, rz); // a rolled camera has no flat right
|
|
2115
|
+
rx = l ? rx / l : 1, ry = 0, rz = l ? rz / l : 0;
|
|
2116
|
+
ux = 0, uy = 1, uz = 0;
|
|
1932
2117
|
}
|
|
1933
|
-
const c = cos(angle), s = sin(angle), w = size.x / 2, h = size.y / 2;
|
|
1934
|
-
|
|
1935
|
-
|
|
2118
|
+
const c = cos(angle), s = sin(angle), w = size.x / 2, h = size.y / 2, a = render3DBillboardAxesScratch;
|
|
2119
|
+
a[0] = (rx * c + ux * s) * w, a[1] = (ry * c + uy * s) * w, a[2] = (rz * c + uz * s) * w;
|
|
2120
|
+
a[3] = (ux * c - rx * s) * h, a[4] = (uy * c - ry * s) * h, a[5] = (uz * c - rz * s) * h;
|
|
2121
|
+
return a;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// the four corners of a camera facing quad in strip order
|
|
2125
|
+
function render3DBillboardCorners(pos, size, angle, upright)
|
|
2126
|
+
{
|
|
2127
|
+
const a = render3DBillboardAxes(size, angle, upright);
|
|
2128
|
+
const rx = a[0], ry = a[1], rz = a[2], ux = a[3], uy = a[4], uz = a[5];
|
|
1936
2129
|
return [
|
|
1937
2130
|
vec3(pos.x - rx + ux, pos.y - ry + uy, pos.z - rz + uz), vec3(pos.x - rx - ux, pos.y - ry - uy, pos.z - rz - uz),
|
|
1938
2131
|
vec3(pos.x + rx + ux, pos.y + ry + uy, pos.z + rz + uz), vec3(pos.x + rx - ux, pos.y + ry - uy, pos.z + rz - uz)];
|
|
@@ -1960,14 +2153,57 @@ function render3DForEachStripVertex(points, normals, uvs, colors, callback)
|
|
|
1960
2153
|
}
|
|
1961
2154
|
|
|
1962
2155
|
// write one vertex into a packed buffer at float index j
|
|
1963
|
-
function render3DWriteVertex(floats, ints, j,
|
|
2156
|
+
function render3DWriteVertex(floats, ints, j, x, y, z, n, u, v, rgba)
|
|
1964
2157
|
{
|
|
1965
|
-
floats[j] =
|
|
2158
|
+
floats[j] = x; floats[j+1] = y; floats[j+2] = z;
|
|
1966
2159
|
floats[j+3] = n.x; floats[j+4] = n.y; floats[j+5] = n.z;
|
|
1967
2160
|
floats[j+6] = u; floats[j+7] = v;
|
|
1968
2161
|
ints[j+8] = rgba;
|
|
1969
2162
|
}
|
|
1970
2163
|
|
|
2164
|
+
// turn every triangle of an index list the other way round, in place: the authoring form reads counter clockwise
|
|
2165
|
+
// from the front and the pass draws clockwise, as a strip's triangles come out after its leading repeat
|
|
2166
|
+
function render3DFlipTriangles(indices)
|
|
2167
|
+
{
|
|
2168
|
+
for (let t = 0; t < indices.length; t += 3)
|
|
2169
|
+
{
|
|
2170
|
+
const b = indices[t+1];
|
|
2171
|
+
indices[t+1] = indices[t+2], indices[t+2] = b;
|
|
2172
|
+
}
|
|
2173
|
+
return indices;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
// the triangles of a strip of count entries as an index list, given which vertex each entry maps to and which place
|
|
2177
|
+
// 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
|
|
2178
|
+
// 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
|
|
2179
|
+
function render3DStripTriangles(count, remap, place)
|
|
2180
|
+
{
|
|
2181
|
+
const indices = [];
|
|
2182
|
+
for (let i = 2; i < count; ++i)
|
|
2183
|
+
{
|
|
2184
|
+
const a = i & 1 ? i - 1 : i - 2, b = i & 1 ? i - 2 : i - 1;
|
|
2185
|
+
if (place[a] != place[b] && place[b] != place[i] && place[a] != place[i])
|
|
2186
|
+
indices.push(remap[a], remap[b], remap[i]);
|
|
2187
|
+
}
|
|
2188
|
+
return indices;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// a mesh's packed vertex data for the GPU, one vertex per entry of a layout, which is the strip index of each,
|
|
2192
|
+
// written into data when given, the buffer an earlier call returned for the same layout
|
|
2193
|
+
function render3DMeshVertexData(mesh, vertices, data=new ArrayBuffer(vertices.length * RENDER3D_VERTEX_BYTES))
|
|
2194
|
+
{
|
|
2195
|
+
const count = vertices.length;
|
|
2196
|
+
const floats = new Float32Array(data), ints = new Uint32Array(data);
|
|
2197
|
+
for (let j = 0; j < count; ++j)
|
|
2198
|
+
{
|
|
2199
|
+
// a hand built mesh may leave normals, uvs and colors empty
|
|
2200
|
+
const i = vertices[j], p = mesh.points[i], uv = mesh.uvs[i] || RENDER3D_DEFAULT_UV;
|
|
2201
|
+
render3DWriteVertex(floats, ints, j * RENDER3D_VERTEX_FLOATS, p.x, p.y, p.z,
|
|
2202
|
+
mesh.normals[i] || RENDER3D_DEFAULT_NORMAL, uv.x, uv.y, (mesh.colors[i] || WHITE).rgbaInt());
|
|
2203
|
+
}
|
|
2204
|
+
return data;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
1971
2207
|
// reorder a convex polygon's points, counter clockwise from outside, into one triangle strip
|
|
1972
2208
|
function render3DPolygonStrip(points)
|
|
1973
2209
|
{
|
|
@@ -1981,18 +2217,53 @@ function render3DPolygonStrip(points)
|
|
|
1981
2217
|
return strip;
|
|
1982
2218
|
}
|
|
1983
2219
|
|
|
2220
|
+
// turn every triangle of a mesh the other way round, the normals left alone: an index list reads each one in the
|
|
2221
|
+
// other order, and a strip gets one extra point at each end, which flips every triangle and keeps the count even
|
|
2222
|
+
function render3DFlipWinding(mesh)
|
|
2223
|
+
{
|
|
2224
|
+
if (mesh.indices)
|
|
2225
|
+
return void render3DFlipTriangles(mesh.indices);
|
|
2226
|
+
for (const key of ['points', 'normals', 'uvs', 'colors'])
|
|
2227
|
+
{
|
|
2228
|
+
const a = mesh[key];
|
|
2229
|
+
if (a.length)
|
|
2230
|
+
a.unshift(a[0]), a.push(a[a.length - 1]);
|
|
2231
|
+
}
|
|
2232
|
+
mesh.vertexKeys = undefined;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
1984
2235
|
///////////////////////////////////////////////////////////////////////////////
|
|
1985
2236
|
|
|
1986
2237
|
// frees the GPU buffer of a mesh that is garbage collected without dispose, some time after it goes; it holds the
|
|
1987
2238
|
// buffer and its context, never the mesh, or the mesh could not be collected, and dispose unregisters the mesh
|
|
1988
2239
|
const render3DMeshBuffers = typeof FinalizationRegistry == 'undefined' ? undefined :
|
|
1989
|
-
new FinalizationRegistry(({buffer, generation})=>
|
|
1990
|
-
|
|
2240
|
+
new FinalizationRegistry(({buffer, indexBuffer, generation})=>
|
|
2241
|
+
{
|
|
2242
|
+
if (generation !== render3D?.contextGeneration || !glContext) return;
|
|
2243
|
+
glContext.deleteBuffer(buffer);
|
|
2244
|
+
glContext.deleteBuffer(indexBuffer);
|
|
2245
|
+
});
|
|
2246
|
+
|
|
2247
|
+
// the place a point is at, to a hundred thousandth, which smooth normals are summed by
|
|
2248
|
+
function render3DPlaceKey(p) { return `${round(p.x * 1e5)},${round(p.y * 1e5)},${round(p.z * 1e5)}`; }
|
|
2249
|
+
|
|
2250
|
+
// add a face normal to the sum at a triangle's corner a, weighted by the angle there, so a cube corner averages its
|
|
2251
|
+
// three faces evenly however it is cut into triangles
|
|
2252
|
+
function render3DAddCornerNormal(sums, a, b, c, normal)
|
|
2253
|
+
{
|
|
2254
|
+
const u = b.subtract(a), v = c.subtract(a);
|
|
2255
|
+
const angle = Math.acos(clamp(u.dot(v) / (u.length() * v.length() || 1), -1, 1));
|
|
2256
|
+
const k = render3DPlaceKey(a);
|
|
2257
|
+
sums.set(k, (sums.get(k) || vec3()).add(normal.scale(angle)));
|
|
2258
|
+
}
|
|
1991
2259
|
|
|
1992
2260
|
/**
|
|
1993
|
-
* Mesh -
|
|
2261
|
+
* Mesh - Triangles with positions, normals, uvs and colors, uploaded once and drawn by matrix
|
|
1994
2262
|
* - Build with addStrip, addQuad, combine or the shape builders, then render each frame
|
|
1995
2263
|
* - Its back faces are skipped unless doubleSided is set, which the open builders like buildGrid do for you
|
|
2264
|
+
* - Two forms: a triangle strip, what the builders make, or an indexed list of triangles over their own vertices,
|
|
2265
|
+
* what addTriangles and the model loaders make; upload sends the GPU an indexed list either way, see getTriangles,
|
|
2266
|
+
* so a strip's joins between its pieces cost nothing to draw, and toIndexed turns a strip mesh into the list form
|
|
1996
2267
|
* - The GPU buffer is created lazily on first render and dropped by dispose, or freed once the mesh is garbage
|
|
1997
2268
|
* collected, so dispose is only needed to free it right away, like for a mesh rebuilt often
|
|
1998
2269
|
* @memberof Render3D
|
|
@@ -2005,7 +2276,7 @@ class Mesh
|
|
|
2005
2276
|
/** Create an empty mesh */
|
|
2006
2277
|
constructor()
|
|
2007
2278
|
{
|
|
2008
|
-
/** @property {Array<Vector3>} - Vertex positions in strip order
|
|
2279
|
+
/** @property {Array<Vector3>} - Vertex positions, in strip order or one per vertex of an indexed mesh
|
|
2009
2280
|
* @type {Array<Vector3>} */
|
|
2010
2281
|
this.points = [];
|
|
2011
2282
|
/** @property {Array<Vector3>} - Vertex normals
|
|
@@ -2017,23 +2288,53 @@ class Mesh
|
|
|
2017
2288
|
/** @property {Array<Color>} - Vertex colors
|
|
2018
2289
|
* @type {Array<Color>} */
|
|
2019
2290
|
this.colors = [];
|
|
2020
|
-
/** @property {WebGLBuffer|undefined} - GPU buffer, created by upload
|
|
2291
|
+
/** @property {WebGLBuffer|undefined} - GPU vertex buffer, created by upload
|
|
2021
2292
|
* @type {WebGLBuffer|undefined} */
|
|
2022
2293
|
this.buffer = undefined;
|
|
2023
|
-
/** @property {
|
|
2294
|
+
/** @property {WebGLBuffer|undefined} - GPU index buffer, the triangles, created by upload
|
|
2295
|
+
* @type {WebGLBuffer|undefined} */
|
|
2296
|
+
this.indexBuffer = undefined;
|
|
2297
|
+
/** @property {number} - Indices in the GPU index buffer, three per triangle */
|
|
2024
2298
|
this.bufferCount = 0;
|
|
2299
|
+
this.indexType = 0; // gl.UNSIGNED_SHORT, or UNSIGNED_INT past 65535 vertices
|
|
2025
2300
|
/** @property {boolean} - The mesh changed and needs uploading again, set it yourself if you edit the arrays */
|
|
2026
2301
|
this.dirty = false;
|
|
2027
|
-
/** @property {boolean|undefined} - Draw every use of this mesh in the opaque stage as one instanced call,
|
|
2302
|
+
/** @property {boolean|undefined} - Draw every use of this mesh in the opaque stage as one instanced call,
|
|
2303
|
+
* undefined follows render3D.instancing
|
|
2028
2304
|
* @type {boolean|undefined} */
|
|
2029
2305
|
this.instanced = undefined;
|
|
2030
2306
|
/** @property {boolean} - Draw both sides, each lit as the side that is seen; off skips the faces pointing away,
|
|
2031
2307
|
* which is faster and right for closed shapes, the open builders like buildGrid and buildRibbon turn it on */
|
|
2032
2308
|
this.doubleSided = false;
|
|
2309
|
+
/** @property {boolean} - The values change often but the shape never does, for a water surface or a cloth: set
|
|
2310
|
+
* once, the mesh keeps its GPU layout and a dirty upload only rewrites the vertices into the buffer it has;
|
|
2311
|
+
* the strip must keep the same points in the same order, a new point count asserts; the layout is decided by
|
|
2312
|
+
* the first upload, so strip entries equal then stay one vertex and triangles with no area then stay dropped,
|
|
2313
|
+
* set vertexKeys or give it distinct values at the start, not a flat grid of one color or points all in one place */
|
|
2314
|
+
this.dynamicDraw = false;
|
|
2315
|
+
/** @property {Array<number>|undefined} - The mesh as an indexed triangle list instead of a strip: the arrays
|
|
2316
|
+
* hold each vertex once and this says how they join, three vertex numbers per triangle, counter clockwise seen
|
|
2317
|
+
* from the front like a strip's first triangle; addTriangles and the loaders fill it, toIndexed turns a strip
|
|
2318
|
+
* mesh into this form
|
|
2319
|
+
* @type {Array<number>|undefined} */
|
|
2320
|
+
this.indices = undefined;
|
|
2321
|
+
/** @property {Int32Array|undefined} - Which strip entries are one vertex, set by a builder that knows, one
|
|
2322
|
+
* whole number per entry with equal numbers meaning the same vertex; upload skips its search for them, then
|
|
2323
|
+
* drops the keys, since an edit after that may tell the entries apart; adding geometry or recomputing normals
|
|
2324
|
+
* drops them too
|
|
2325
|
+
* @type {Int32Array|undefined} */
|
|
2326
|
+
this.vertexKeys = undefined;
|
|
2327
|
+
/** @type {{vertices: Array<number>, pointCount: number, data: ArrayBuffer}|undefined} */
|
|
2328
|
+
// the strip index of each GPU vertex, the point count and packed data of the last upload, for a dynamicDraw mesh
|
|
2329
|
+
this.vertexLayout = undefined;
|
|
2033
2330
|
this.instanceCount = 0; // draws waiting in this mesh's batch, with their values, texture and draw state
|
|
2331
|
+
/** @type {Float32Array|undefined} */
|
|
2034
2332
|
this.instanceData = undefined;
|
|
2035
2333
|
/** @property {number} - Bounding sphere radius around the origin, for culling and picking, computed by upload */
|
|
2036
2334
|
this.radius = 0;
|
|
2335
|
+
/** @property {{min: Vector3, max: Vector3}|undefined} - Bounding box, for picking, measured with the radius
|
|
2336
|
+
* @type {{min: Vector3, max: Vector3}|undefined} */
|
|
2337
|
+
this.bounds = undefined;
|
|
2037
2338
|
this.contextGeneration = 0; // the context the buffer belongs to, see render3D.contextGeneration
|
|
2038
2339
|
}
|
|
2039
2340
|
|
|
@@ -2052,6 +2353,12 @@ class Mesh
|
|
|
2052
2353
|
* @return {Mesh} */
|
|
2053
2354
|
addStrip(points, normals, uvs, colors)
|
|
2054
2355
|
{
|
|
2356
|
+
if (this.indices)
|
|
2357
|
+
{
|
|
2358
|
+
// an indexed mesh takes the strip as the triangles it makes
|
|
2359
|
+
const part = new Mesh().addStrip(points, normals, uvs, colors).toIndexed();
|
|
2360
|
+
return this.addTriangles(part.points, part.indices, part.normals, part.uvs, part.colors);
|
|
2361
|
+
}
|
|
2055
2362
|
render3DForEachStripVertex(points, normals, uvs, colors, (p, n, uv, c)=>
|
|
2056
2363
|
{
|
|
2057
2364
|
this.points.push(p);
|
|
@@ -2059,6 +2366,54 @@ class Mesh
|
|
|
2059
2366
|
this.uvs.push(uv);
|
|
2060
2367
|
this.colors.push(c);
|
|
2061
2368
|
});
|
|
2369
|
+
this.vertexKeys = undefined; // the new entries have no keys
|
|
2370
|
+
this.dirty = true;
|
|
2371
|
+
return this;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
/** Add triangles over their own vertices, the indexed form a model file comes in
|
|
2375
|
+
* - The mesh becomes indexed: a strip mesh is turned into triangles first, and strips added later join as triangles
|
|
2376
|
+
* - List each triangle counter clockwise as seen from the front, like a strip's first triangle
|
|
2377
|
+
* @param {Array<Vector3>} points - Each vertex once
|
|
2378
|
+
* @param {Array<number>} indices - Three vertex numbers per triangle, into points
|
|
2379
|
+
* @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
|
|
2380
|
+
* @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, default zero
|
|
2381
|
+
* @param {Color|Array<Color>} [colors] - One for all or one per point, default white
|
|
2382
|
+
* @return {Mesh} */
|
|
2383
|
+
addTriangles(points, indices, normals, uvs, colors)
|
|
2384
|
+
{
|
|
2385
|
+
ASSERT(isArray(points) && isArray(indices) && indices.length % 3 === 0,
|
|
2386
|
+
'addTriangles takes points and three indices per triangle');
|
|
2387
|
+
ASSERT(indices.every(i=> i >= 0 && i < points.length && i % 1 === 0), 'an index points past the vertices given');
|
|
2388
|
+
this.toIndexed();
|
|
2389
|
+
const offset = this.points.length, normalArray = isArray(normals), uvArray = isArray(uvs), colorArray = isArray(colors);
|
|
2390
|
+
for (let i = 0; i < points.length; ++i)
|
|
2391
|
+
{
|
|
2392
|
+
this.points.push(points[i]);
|
|
2393
|
+
this.normals.push(normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL);
|
|
2394
|
+
this.uvs.push(uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV);
|
|
2395
|
+
this.colors.push(colorArray ? colors[i] : colors || WHITE);
|
|
2396
|
+
}
|
|
2397
|
+
for (const i of indices)
|
|
2398
|
+
this.indices.push(i + offset);
|
|
2399
|
+
this.dirty = true;
|
|
2400
|
+
return this;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
/** Turn a strip mesh into the indexed form, each distinct vertex once and the real triangles over them, in place
|
|
2404
|
+
* - An indexed mesh is left as it is; the builders make strips and a loader makes this, and either draws the same
|
|
2405
|
+
* @return {Mesh} */
|
|
2406
|
+
toIndexed()
|
|
2407
|
+
{
|
|
2408
|
+
if (this.indices) return this;
|
|
2409
|
+
const {vertices, indices} = this.getTriangles();
|
|
2410
|
+
this.points = vertices.map(i=> this.points[i]);
|
|
2411
|
+
this.normals = vertices.map(i=> this.normals[i] || RENDER3D_DEFAULT_NORMAL);
|
|
2412
|
+
this.uvs = vertices.map(i=> this.uvs[i] || RENDER3D_DEFAULT_UV);
|
|
2413
|
+
this.colors = vertices.map(i=> this.colors[i] || WHITE);
|
|
2414
|
+
// the list upload sends reads clockwise, the pass draws it that way; the authoring form is counter clockwise
|
|
2415
|
+
this.indices = render3DFlipTriangles(indices);
|
|
2416
|
+
this.vertexKeys = undefined;
|
|
2062
2417
|
this.dirty = true;
|
|
2063
2418
|
return this;
|
|
2064
2419
|
}
|
|
@@ -2085,26 +2440,105 @@ class Mesh
|
|
|
2085
2440
|
combine(mesh, matrix=RENDER3D_IDENTITY, color=WHITE)
|
|
2086
2441
|
{
|
|
2087
2442
|
matrix = render3DMatrix(matrix); // most parts only need moving into place
|
|
2088
|
-
const normalMatrix = render3DNormalMatrix(matrix);
|
|
2089
|
-
|
|
2443
|
+
const normalMatrix = render3DNormalMatrix(matrix), mirrors = render3DMirrors(matrix);
|
|
2444
|
+
let part = mesh, order;
|
|
2445
|
+
if (this.indices || mesh.indices)
|
|
2446
|
+
{
|
|
2447
|
+
// one of them is indexed, so both are: the part as a copy if it is a strip
|
|
2448
|
+
this.toIndexed();
|
|
2449
|
+
part = mesh.indices ? mesh : new Mesh().combine(mesh).toIndexed();
|
|
2450
|
+
const offset = this.points.length, indices = part.indices;
|
|
2451
|
+
// the count read once, a mesh combined with itself grows as it is read; a mirror turns every triangle the
|
|
2452
|
+
// other way round
|
|
2453
|
+
for (let t = 0, n = indices.length; t < n; t += 3)
|
|
2454
|
+
this.indices.push(indices[t] + offset, indices[t + (mirrors ? 2 : 1)] + offset,
|
|
2455
|
+
indices[t + (mirrors ? 1 : 2)] + offset);
|
|
2456
|
+
}
|
|
2457
|
+
else if (mirrors && part.points.length)
|
|
2090
2458
|
{
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2459
|
+
// a strip reads the other way round with one more point at each end, as render3DFlipWinding does
|
|
2460
|
+
const last = part.points.length - 1;
|
|
2461
|
+
order = [0, ...part.points.keys(), last];
|
|
2462
|
+
}
|
|
2463
|
+
for (let j = 0, count = order ? order.length : part.points.length; j < count; ++j)
|
|
2464
|
+
{
|
|
2465
|
+
const i = order ? order[j] : j;
|
|
2466
|
+
this.points.push(matrix.transformPoint(part.points[i]));
|
|
2467
|
+
this.normals.push(normalMatrix.transformDirection(part.normals[i] || RENDER3D_DEFAULT_NORMAL).normalize());
|
|
2468
|
+
this.uvs.push((part.uvs[i] || RENDER3D_DEFAULT_UV).copy());
|
|
2469
|
+
this.colors.push((part.colors[i] || WHITE).multiply(color));
|
|
2095
2470
|
}
|
|
2096
2471
|
this.doubleSided ||= mesh.doubleSided; // an open part leaves the whole mesh open
|
|
2472
|
+
this.vertexKeys = undefined; // the new entries have no keys
|
|
2097
2473
|
this.dirty = true;
|
|
2098
2474
|
return this;
|
|
2099
2475
|
}
|
|
2100
2476
|
|
|
2477
|
+
/** Returns a new mesh: this one and its mirror image across the plane through the origin facing axis
|
|
2478
|
+
* - For modeling half a shape against that plane, a part that crosses it overlaps its image
|
|
2479
|
+
* @param {Vector3} [axis] - Faces the mirror plane, vec3(1,0,0) mirrors across x
|
|
2480
|
+
* @return {Mesh} */
|
|
2481
|
+
mirror(axis=vec3(1, 0, 0))
|
|
2482
|
+
{
|
|
2483
|
+
ASSERT(isVector3(axis) && axis.lengthSquared() > 0, 'mirror needs an axis');
|
|
2484
|
+
const n = axis.normalize(), a = -2 * n.x, b = -2 * n.y, c = -2 * n.z;
|
|
2485
|
+
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,
|
|
2486
|
+
a * n.z, b * n.z, 1 + c * n.z, 0, 0, 0, 0, 1]);
|
|
2487
|
+
return new Mesh().combine(this).combine(this, reflect); // combine turns the image's faces the right way out
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
/** Returns a new mesh of count copies of this one, each turned further around an axis through the origin
|
|
2491
|
+
* @param {number} count - Copies, spaced evenly around the whole turn
|
|
2492
|
+
* @param {Vector3} [axis] - Up by default
|
|
2493
|
+
* @return {Mesh} */
|
|
2494
|
+
spin(count, axis=vec3(0, 1, 0))
|
|
2495
|
+
{
|
|
2496
|
+
ASSERT(count >= 1 && count % 1 === 0, 'spin count must be a whole number, 1 or more');
|
|
2497
|
+
ASSERT(isVector3(axis) && axis.lengthSquared() > 0, 'spin needs an axis');
|
|
2498
|
+
const mesh = new Mesh, unit = axis.normalize();
|
|
2499
|
+
for (let i = 0; i < count; ++i)
|
|
2500
|
+
mesh.combine(this, render3DAxisRotation(unit, i / count * 2 * PI));
|
|
2501
|
+
return mesh;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
/** Returns a new mesh of everything in this mesh or the other, see subtract
|
|
2505
|
+
* @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
|
|
2506
|
+
* @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
|
|
2507
|
+
* @return {Mesh} */
|
|
2508
|
+
union(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 0); }
|
|
2509
|
+
|
|
2510
|
+
/** Returns a new mesh of this one with the other cut out of it, CSG with BSP trees
|
|
2511
|
+
* - Both must be closed, every edge shared by two triangles, as the builders make them apart from the open
|
|
2512
|
+
* ones like buildGrid and buildRibbon; the result is closed and indexed, and neither mesh changes
|
|
2513
|
+
* - The faces a cut makes come from the other mesh's surface, turned to face out, with its normals, uvs and
|
|
2514
|
+
* colors, so a smooth cylinder drills a round hole
|
|
2515
|
+
* - Parts that overlap must be joined with union to be one solid, not with combine, mirror or spin, which
|
|
2516
|
+
* leave them overlapping, and CSG then gives a wrong shape
|
|
2517
|
+
* - Cuts split the triangles near them, so the result has more: a few thousand triangles take tens to a few
|
|
2518
|
+
* hundred milliseconds, so build shapes this way at load time, not every frame; joining several cutters
|
|
2519
|
+
* with union and cutting once is quicker than cutting with each in turn
|
|
2520
|
+
* - Details closer than about 1e-4 are made one, so build a very small part larger and scale it after
|
|
2521
|
+
* @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
|
|
2522
|
+
* @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
|
|
2523
|
+
* @return {Mesh}
|
|
2524
|
+
* @example
|
|
2525
|
+
* const wall = buildBox(vec3(4, 3, .5)).subtract(buildBox(vec3(1, 2, 1)), vec3(0, -.5, 0)); // a doorway */
|
|
2526
|
+
subtract(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 1); }
|
|
2527
|
+
|
|
2528
|
+
/** Returns a new mesh of only what is in both this mesh and the other, see subtract
|
|
2529
|
+
* @param {Mesh} mesh - Closed, as the builders make them apart from the open ones like buildGrid
|
|
2530
|
+
* @param {Matrix4|Vector3} [matrix] - Places the other mesh, or just a position to move it to
|
|
2531
|
+
* @return {Mesh} */
|
|
2532
|
+
intersect(mesh, matrix) { return render3DMeshCSG(this, mesh, matrix, 2); }
|
|
2533
|
+
|
|
2101
2534
|
/** Scale every uv, so a whole texture repeats across the mesh when its TextureInfo wraps
|
|
2102
2535
|
* @param {Vector2|number} scale - Repeats across and up, a number for both
|
|
2103
2536
|
* @return {Mesh} */
|
|
2104
2537
|
scaleUVs(scale)
|
|
2105
2538
|
{
|
|
2106
|
-
const s =
|
|
2107
|
-
|
|
2539
|
+
const s = render3DSize2(scale);
|
|
2540
|
+
// new vectors, builders share uv objects between faces
|
|
2541
|
+
this.uvs = this.uvs.map(uv=> vec2(uv.x * s.x, uv.y * s.y));
|
|
2108
2542
|
this.dirty = true;
|
|
2109
2543
|
return this;
|
|
2110
2544
|
}
|
|
@@ -2122,6 +2556,7 @@ class Mesh
|
|
|
2122
2556
|
// a mesh built by hand may have no normals yet, and then there is nothing to turn
|
|
2123
2557
|
this.normals[i] &&= normalMatrix.transformDirection(this.normals[i]).normalize();
|
|
2124
2558
|
}
|
|
2559
|
+
render3DMirrors(matrix) && render3DFlipWinding(this); // a mirror would leave the faces pointing in
|
|
2125
2560
|
this.dirty = true;
|
|
2126
2561
|
return this;
|
|
2127
2562
|
}
|
|
@@ -2130,13 +2565,7 @@ class Mesh
|
|
|
2130
2565
|
* @return {Mesh} */
|
|
2131
2566
|
flipNormals()
|
|
2132
2567
|
{
|
|
2133
|
-
|
|
2134
|
-
for (const key of ['points', 'normals', 'uvs', 'colors'])
|
|
2135
|
-
{
|
|
2136
|
-
const a = this[key];
|
|
2137
|
-
if (a.length)
|
|
2138
|
-
a.unshift(a[0]), a.push(a[a.length - 1]);
|
|
2139
|
-
}
|
|
2568
|
+
render3DFlipWinding(this);
|
|
2140
2569
|
this.normals = this.normals.map(n=> n.scale(-1));
|
|
2141
2570
|
this.dirty = true;
|
|
2142
2571
|
return this;
|
|
@@ -2195,22 +2624,63 @@ class Mesh
|
|
|
2195
2624
|
let r = 0;
|
|
2196
2625
|
for (const p of this.points)
|
|
2197
2626
|
r = max(r, p.lengthSquared());
|
|
2627
|
+
this.bounds = this.getBounds(); // measured with it, a pick tests a mesh's box after its sphere
|
|
2198
2628
|
return this.radius = r ** .5;
|
|
2199
2629
|
}
|
|
2200
2630
|
|
|
2201
|
-
/** Derive normals from the strip
|
|
2202
|
-
* @param {boolean} [smooth] - Round the lighting across faces instead of giving each face a hard edge
|
|
2631
|
+
/** Derive normals from the triangles, of the strip or of the index list
|
|
2632
|
+
* @param {boolean} [smooth] - Round the lighting across faces instead of giving each face a hard edge; flat
|
|
2633
|
+
* normals on an indexed mesh give every corner its own vertex
|
|
2203
2634
|
* @return {Mesh} */
|
|
2204
2635
|
computeNormals(smooth=false)
|
|
2205
2636
|
{
|
|
2206
|
-
|
|
2207
|
-
const points = this.points, n = points.length;
|
|
2208
|
-
const faceNormals = [];
|
|
2209
|
-
for (let i = 0; i + 2 < n; ++i)
|
|
2637
|
+
if (this.indices)
|
|
2210
2638
|
{
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
//
|
|
2639
|
+
// the triangles are listed: smooth normals add up around each position, weighted by the corner angle
|
|
2640
|
+
// like the strip's, so vertices split apart at one place smooth back together and a mesh can go flat and
|
|
2641
|
+
// smooth again; flat ones need a vertex per corner, so the vertices are split up first
|
|
2642
|
+
if (!smooth)
|
|
2643
|
+
{
|
|
2644
|
+
const split = (a)=> this.indices.map(i=> a[i]);
|
|
2645
|
+
this.points = split(this.points), this.normals = split(this.normals);
|
|
2646
|
+
this.uvs = split(this.uvs), this.colors = split(this.colors);
|
|
2647
|
+
this.indices = this.indices.map((_, i)=> i);
|
|
2648
|
+
}
|
|
2649
|
+
const points = this.points, indices = this.indices, sums = new Map;
|
|
2650
|
+
const normals = points.map(()=> RENDER3D_DEFAULT_NORMAL);
|
|
2651
|
+
for (let t = 0; t < indices.length; t += 3)
|
|
2652
|
+
{
|
|
2653
|
+
const a = points[indices[t]], b = points[indices[t+1]], c = points[indices[t+2]];
|
|
2654
|
+
const cross = b.subtract(a).cross(c.subtract(a));
|
|
2655
|
+
if (!cross.lengthSquared()) continue;
|
|
2656
|
+
const normal = cross.normalize();
|
|
2657
|
+
if (!smooth)
|
|
2658
|
+
{
|
|
2659
|
+
// its own three corners
|
|
2660
|
+
normals[indices[t]] = normals[indices[t+1]] = normals[indices[t+2]] = normal;
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
for (let j = 0; j < 3; ++j)
|
|
2664
|
+
render3DAddCornerNormal(sums, points[indices[t+j]], points[indices[t+(j+1)%3]],
|
|
2665
|
+
points[indices[t+(j+2)%3]], normal);
|
|
2666
|
+
}
|
|
2667
|
+
this.normals = !smooth ? normals : points.map(p=>
|
|
2668
|
+
{
|
|
2669
|
+
const s = sums.get(render3DPlaceKey(p));
|
|
2670
|
+
return s && s.lengthSquared() ? s.normalize() : RENDER3D_DEFAULT_NORMAL;
|
|
2671
|
+
});
|
|
2672
|
+
this.dirty = true;
|
|
2673
|
+
return this;
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
// the outward normal of each triangle in the strip
|
|
2677
|
+
const points = this.points, n = points.length;
|
|
2678
|
+
const faceNormals = [];
|
|
2679
|
+
for (let i = 0; i + 2 < n; ++i)
|
|
2680
|
+
{
|
|
2681
|
+
const a = points[i], b = points[i+1], c = points[i+2];
|
|
2682
|
+
const normal = b.subtract(a).cross(c.subtract(a));
|
|
2683
|
+
// triangles in a strip alternate which way they wind, so every other one is flipped back
|
|
2214
2684
|
// a zero normal means a flat triangle joining two strips, so skip it
|
|
2215
2685
|
faceNormals.push(normal.lengthSquared() ? normal.normalize(i & 1 ? 1 : -1) : undefined);
|
|
2216
2686
|
}
|
|
@@ -2222,16 +2692,10 @@ class Mesh
|
|
|
2222
2692
|
// add up the face normals meeting at each position, each weighted by its corner angle so a cube
|
|
2223
2693
|
// corner averages its three faces evenly however the strips cut them, then normalize
|
|
2224
2694
|
const sums = new Map;
|
|
2225
|
-
const key = (p)=> `${round(p.x * 1e5)},${round(p.y * 1e5)},${round(p.z * 1e5)}`;
|
|
2226
2695
|
faceNormals.forEach((f, i)=> f && [0, 1, 2].forEach(j=>
|
|
2227
|
-
|
|
2228
|
-
const a = points[i + j], u = points[i + (j + 1) % 3].subtract(a), v = points[i + (j + 2) % 3].subtract(a);
|
|
2229
|
-
const angle = Math.acos(clamp(u.dot(v) / (u.length() * v.length() || 1), -1, 1));
|
|
2230
|
-
const k = key(a);
|
|
2231
|
-
sums.set(k, (sums.get(k) || vec3()).add(f.scale(angle)));
|
|
2232
|
-
}));
|
|
2696
|
+
render3DAddCornerNormal(sums, points[i+j], points[i+(j+1)%3], points[i+(j+2)%3], f)));
|
|
2233
2697
|
for (let i = 0; i < n; ++i)
|
|
2234
|
-
normals[i] = (sums.get(
|
|
2698
|
+
normals[i] = (sums.get(render3DPlaceKey(points[i])) || RENDER3D_DEFAULT_NORMAL).normalize();
|
|
2235
2699
|
}
|
|
2236
2700
|
else
|
|
2237
2701
|
// every triangle writes its own three corners, so the only vertices left with the default
|
|
@@ -2239,6 +2703,7 @@ class Mesh
|
|
|
2239
2703
|
faceNormals.forEach((f, i)=> f && (normals[i] = normals[i+1] = normals[i+2] = f));
|
|
2240
2704
|
|
|
2241
2705
|
this.normals = normals;
|
|
2706
|
+
this.vertexKeys = undefined; // flat normals tell entries at one place apart
|
|
2242
2707
|
this.dirty = true;
|
|
2243
2708
|
return this;
|
|
2244
2709
|
}
|
|
@@ -2248,29 +2713,123 @@ class Mesh
|
|
|
2248
2713
|
upload()
|
|
2249
2714
|
{
|
|
2250
2715
|
this.computeRadius();
|
|
2251
|
-
if (!render3D?.program) return this;
|
|
2252
|
-
this.
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
const floats = new Float32Array(data), ints = new Uint32Array(data);
|
|
2256
|
-
for (let i = 0; i < count; ++i)
|
|
2716
|
+
if (!render3D?.program || !glContext) return this;
|
|
2717
|
+
const gl = glContext, layout = this.vertexLayout;
|
|
2718
|
+
// no layout when dynamicDraw was turned on after an upload, the next upload makes one
|
|
2719
|
+
if (this.dynamicDraw && layout && this.buffer && this.contextGeneration === render3D.contextGeneration)
|
|
2257
2720
|
{
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2721
|
+
// the layout of the last upload stands, only the values are written again into the buffer it has,
|
|
2722
|
+
// packed into the same memory each time so a mesh uploaded every frame makes no garbage
|
|
2723
|
+
ASSERT(layout.pointCount === this.points.length,
|
|
2724
|
+
'a dynamicDraw mesh keeps its shape, the same points in the same order; ' +
|
|
2725
|
+
'for a new shape make a new mesh or turn dynamicDraw off', this.points.length);
|
|
2726
|
+
if (layout.pointCount === this.points.length)
|
|
2727
|
+
{
|
|
2728
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
2729
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, render3DMeshVertexData(this, layout.vertices, layout.data));
|
|
2730
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer);
|
|
2731
|
+
this.dirty = false;
|
|
2732
|
+
return this;
|
|
2733
|
+
}
|
|
2261
2734
|
}
|
|
2262
|
-
|
|
2735
|
+
this.dispose();
|
|
2736
|
+
const {vertices, indices} = this.getTriangles(), count = vertices.length, wide = count > 65535;
|
|
2737
|
+
const data = render3DMeshVertexData(this, vertices);
|
|
2738
|
+
this.vertexLayout = this.dynamicDraw ? {vertices, pointCount: this.points.length, data} : undefined;
|
|
2739
|
+
this.vertexKeys = undefined; // used once: an edit after this may tell entries apart
|
|
2263
2740
|
this.buffer = gl.createBuffer();
|
|
2264
|
-
this.
|
|
2741
|
+
this.indexBuffer = gl.createBuffer();
|
|
2742
|
+
this.bufferCount = indices.length;
|
|
2743
|
+
this.indexType = wide ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
|
|
2265
2744
|
this.dirty = false;
|
|
2266
2745
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
2267
|
-
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
|
|
2746
|
+
gl.bufferData(gl.ARRAY_BUFFER, data, this.dynamicDraw ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW);
|
|
2747
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
|
2748
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, wide ? new Uint32Array(indices) : new Uint16Array(indices), gl.STATIC_DRAW);
|
|
2268
2749
|
this.contextGeneration = render3D.contextGeneration;
|
|
2269
|
-
|
|
2750
|
+
const buffers = {buffer: this.buffer, indexBuffer: this.indexBuffer, generation: this.contextGeneration};
|
|
2751
|
+
render3DMeshBuffers?.register(this, buffers, this);
|
|
2270
2752
|
gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer); // the engine's 2D batch writes through this binding
|
|
2271
2753
|
return this;
|
|
2272
2754
|
}
|
|
2273
2755
|
|
|
2756
|
+
/** The mesh as an indexed triangle list, what upload sends to the GPU: the strip's real triangles over its
|
|
2757
|
+
* distinct vertices, the joins between its pieces dropped and every triangle facing the way it did in the strip
|
|
2758
|
+
* - Vertices are compared to a millionth, so two at one place with the same normal, uv and color are one
|
|
2759
|
+
* @return {{vertices: Array<number>, indices: Array<number>}} - vertices are strip indices, one per distinct
|
|
2760
|
+
* vertex; indices are the triangles, three per triangle, into vertices */
|
|
2761
|
+
getTriangles()
|
|
2762
|
+
{
|
|
2763
|
+
const count = this.points.length, vertices = [];
|
|
2764
|
+
if (this.indices)
|
|
2765
|
+
{
|
|
2766
|
+
// already a list: every vertex as it is, and the triangles read the way the pass draws, clockwise
|
|
2767
|
+
for (let i = 0; i < count; ++i)
|
|
2768
|
+
vertices.push(i);
|
|
2769
|
+
return {vertices, indices: render3DFlipTriangles(this.indices.slice())};
|
|
2770
|
+
}
|
|
2771
|
+
const remap = new Int32Array(count), place = new Int32Array(count), keys = this.vertexKeys;
|
|
2772
|
+
if (keys && keys.length === count)
|
|
2773
|
+
{
|
|
2774
|
+
// a builder said which entries are one vertex: the first entry with a key stands for every entry with it,
|
|
2775
|
+
// and is their place, so nothing is searched
|
|
2776
|
+
const first = new Int32Array(count).fill(-1);
|
|
2777
|
+
for (let i = 0; i < count; ++i)
|
|
2778
|
+
{
|
|
2779
|
+
const key = keys[i];
|
|
2780
|
+
ASSERT(key >= 0 && key < count, 'vertexKeys must be whole numbers below the entry count', key);
|
|
2781
|
+
const j = first[key];
|
|
2782
|
+
if (j < 0)
|
|
2783
|
+
first[key] = i, remap[i] = vertices.length, vertices.push(i);
|
|
2784
|
+
else
|
|
2785
|
+
remap[i] = remap[j];
|
|
2786
|
+
place[i] = first[key];
|
|
2787
|
+
}
|
|
2788
|
+
return {vertices, indices: render3DStripTriangles(count, remap, place)};
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// each vertex as nine whole numbers, its values in millionths and its color, so vertices hash and compare as
|
|
2792
|
+
// numbers; a hash table over all nine finds the distinct vertices and one over the first three the places,
|
|
2793
|
+
// each slot holding a vertex index plus one and a taken slot moving on to the next
|
|
2794
|
+
const values = new Float64Array(count * 9);
|
|
2795
|
+
let size = 1;
|
|
2796
|
+
while (size < count * 2) size *= 2;
|
|
2797
|
+
const mask = size - 1, seen = new Int32Array(size), places = new Int32Array(size);
|
|
2798
|
+
const mix = (h, v)=> Math.imul(h ^ v, 0x9e3779b1) >>> 0;
|
|
2799
|
+
for (let i = 0; i < count; ++i)
|
|
2800
|
+
{
|
|
2801
|
+
const p = this.points[i], n = this.normals[i] || RENDER3D_DEFAULT_NORMAL;
|
|
2802
|
+
const uv = this.uvs[i] || RENDER3D_DEFAULT_UV, k = i * 9;
|
|
2803
|
+
const x = values[k] = round(p.x * 1e6), y = values[k+1] = round(p.y * 1e6), z = values[k+2] = round(p.z * 1e6);
|
|
2804
|
+
values[k+3] = round(n.x * 1e6), values[k+4] = round(n.y * 1e6), values[k+5] = round(n.z * 1e6);
|
|
2805
|
+
values[k+6] = round(uv.x * 1e6), values[k+7] = round(uv.y * 1e6);
|
|
2806
|
+
values[k+8] = (this.colors[i] || WHITE).rgbaInt();
|
|
2807
|
+
let h = mix(mix(mix(0x811c9dc5, x), y), z);
|
|
2808
|
+
|
|
2809
|
+
// the place, shared with a vertex there that has another normal or uv, as at a lathe's poles
|
|
2810
|
+
for (let slot = h & mask;; slot = (slot + 1) & mask)
|
|
2811
|
+
{
|
|
2812
|
+
const o = places[slot] - 1;
|
|
2813
|
+
if (o < 0) { places[slot] = i + 1, place[i] = i; break; }
|
|
2814
|
+
if (values[o*9] === x && values[o*9+1] === y && values[o*9+2] === z) { place[i] = o; break; }
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// the whole vertex
|
|
2818
|
+
for (let m = 3; m < 9; ++m)
|
|
2819
|
+
h = mix(h, values[k+m]);
|
|
2820
|
+
for (let slot = h & mask;; slot = (slot + 1) & mask)
|
|
2821
|
+
{
|
|
2822
|
+
const o = seen[slot] - 1;
|
|
2823
|
+
if (o < 0) { seen[slot] = i + 1, remap[i] = vertices.length, vertices.push(i); break; }
|
|
2824
|
+
let same = true;
|
|
2825
|
+
for (let m = 0; same && m < 9; ++m)
|
|
2826
|
+
same = values[o*9+m] === values[k+m];
|
|
2827
|
+
if (same) { remap[i] = remap[o]; break; }
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
return {vertices, indices: render3DStripTriangles(count, remap, place)};
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2274
2833
|
/** Draw the mesh with the current draw state, batched with its other uses in the opaque stage
|
|
2275
2834
|
* @param {Matrix4|Vector3} [matrix] - Object transform, or just a position to draw it at
|
|
2276
2835
|
* @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile or the whole texture
|
|
@@ -2285,8 +2844,8 @@ class Mesh
|
|
|
2285
2844
|
render3DMeshBuffers?.unregister(this); // freed here, so not again when the mesh is collected
|
|
2286
2845
|
// a buffer from a context that was lost is gone with it, and the new context refuses to delete it
|
|
2287
2846
|
if (this.contextGeneration === render3D?.contextGeneration)
|
|
2288
|
-
glContext?.deleteBuffer(this.buffer);
|
|
2289
|
-
this.buffer = undefined;
|
|
2847
|
+
glContext?.deleteBuffer(this.buffer), glContext?.deleteBuffer(this.indexBuffer);
|
|
2848
|
+
this.buffer = this.indexBuffer = undefined;
|
|
2290
2849
|
this.bufferCount = 0;
|
|
2291
2850
|
}
|
|
2292
2851
|
}
|
|
@@ -2300,6 +2859,8 @@ class Mesh
|
|
|
2300
2859
|
* - profile is [[radius, y], ...] from bottom to top
|
|
2301
2860
|
* - A profile that ends where it starts makes a closed ring like a donut
|
|
2302
2861
|
* - An end left open, with a radius and no cap, makes the mesh doubleSided so its inside shows
|
|
2862
|
+
* - An end on the axis smooth shades as a round pole like a sphere's when its segment is within 45 degrees of
|
|
2863
|
+
* level, and as a point like a cone's tip when it is steeper
|
|
2303
2864
|
* @param {Array<Array<number>>} profile
|
|
2304
2865
|
* @param {number} [sides] - Around the axis
|
|
2305
2866
|
* @param {boolean} [smooth] - Defaults to render3D.smoothShading
|
|
@@ -2327,13 +2888,19 @@ function buildLathe(profile, sides=16, smooth=render3D?.smoothShading, capped=tr
|
|
|
2327
2888
|
};
|
|
2328
2889
|
// vertex normal: average of the adjacent segment normals, across the seam when the profile is closed,
|
|
2329
2890
|
// and a closed profile needs no caps
|
|
2330
|
-
const closed = rings > 2 && abs(profile[0][0] - profile[rings-1][0]) < 1e-9 &&
|
|
2891
|
+
const closed = rings > 2 && abs(profile[0][0] - profile[rings-1][0]) < 1e-9 &&
|
|
2892
|
+
abs(profile[0][1] - profile[rings-1][1]) < 1e-9;
|
|
2331
2893
|
const segmentLength = (i)=> hypot(profile[i+1][0] - profile[i][0], profile[i+1][1] - profile[i][1]);
|
|
2332
2894
|
const vertexNormal = (i)=>
|
|
2333
2895
|
{
|
|
2334
|
-
// an open end on the axis is a pole and points along it
|
|
2896
|
+
// an open end on the axis is a pole and points along it, like a sphere's, when the surface there is
|
|
2897
|
+
// within 45 degrees of level; a steeper one is a point like a cone's tip and takes its side's normal
|
|
2335
2898
|
if (!closed && (!i || i == rings - 1) && abs(profile[i][0]) < 1e-9)
|
|
2336
|
-
|
|
2899
|
+
{
|
|
2900
|
+
const up = i ? 1 : -1;
|
|
2901
|
+
if (segmentNormal(i ? i - 1 : 0).y * up > Math.SQRT1_2 - 1e-9)
|
|
2902
|
+
return vec2(0, up);
|
|
2903
|
+
}
|
|
2337
2904
|
// otherwise the neighbors weighted by their length, so a short band does not tilt a long wall
|
|
2338
2905
|
let n = vec2();
|
|
2339
2906
|
const add = (s)=> n = n.add(segmentNormal(s).scale(segmentLength(s)));
|
|
@@ -2359,11 +2926,15 @@ function buildLathe(profile, sides=16, smooth=render3D?.smoothShading, capped=tr
|
|
|
2359
2926
|
// one ribbon around the ring pair, top point then bottom point per column
|
|
2360
2927
|
const points = [], normals = [], uvs = [];
|
|
2361
2928
|
const n0 = vertexNormal(i), n1 = vertexNormal(i + 1);
|
|
2929
|
+
// a point on the axis is one per column, each drawn by the face beside it, so its normal turns half
|
|
2930
|
+
// a side toward the middle of that face; a pole's points along the axis and does not turn
|
|
2931
|
+
const half = PI / sides;
|
|
2932
|
+
const turn1 = abs(profile[i + 1][0]) < 1e-9 ? -half : 0, turn0 = abs(profile[i][0]) < 1e-9 ? half : 0;
|
|
2362
2933
|
for (let j = 0; j <= sides; ++j)
|
|
2363
2934
|
{
|
|
2364
2935
|
const a = j / sides * 2 * PI, u = j / sides;
|
|
2365
2936
|
points.push(point(i + 1, a), point(i, a));
|
|
2366
|
-
normals.push(normal3D(n1, a), normal3D(n0, a));
|
|
2937
|
+
normals.push(normal3D(n1, a + turn1), normal3D(n0, a + turn0));
|
|
2367
2938
|
uvs.push(vec2(u, v(i + 1)), vec2(u, v(i)));
|
|
2368
2939
|
}
|
|
2369
2940
|
mesh.addStrip(points, normals, uvs);
|
|
@@ -2386,7 +2957,7 @@ function buildLathe(profile, sides=16, smooth=render3D?.smoothShading, capped=tr
|
|
|
2386
2957
|
|
|
2387
2958
|
// flat discs close the ends that have a radius, a hard edge even when the sides are smooth
|
|
2388
2959
|
if (capped && !closed)
|
|
2389
|
-
for (const [i, up] of [[0, false], [rings - 1, true]])
|
|
2960
|
+
for (const [i, up] of /** @type {Array<[number, boolean]>} */ ([[0, false], [rings - 1, true]]))
|
|
2390
2961
|
{
|
|
2391
2962
|
if (abs(profile[i][0]) < 1e-9) continue; // a pole has no cap
|
|
2392
2963
|
const points = [], uvs = [];
|
|
@@ -2404,36 +2975,6 @@ function buildLathe(profile, sides=16, smooth=render3D?.smoothShading, capped=tr
|
|
|
2404
2975
|
return mesh;
|
|
2405
2976
|
}
|
|
2406
2977
|
|
|
2407
|
-
/**
|
|
2408
|
-
* Build a cylinder standing on the Y axis, centered on the origin
|
|
2409
|
-
* @param {number} [size] - Diameter
|
|
2410
|
-
* @param {number} [height]
|
|
2411
|
-
* @param {number} [sides] - Around
|
|
2412
|
-
* @param {boolean} [smooth] - Defaults to render3D.smoothShading
|
|
2413
|
-
* @param {boolean} [capped] - Close the ends
|
|
2414
|
-
* @return {Mesh}
|
|
2415
|
-
* @memberof Render3D
|
|
2416
|
-
*/
|
|
2417
|
-
function buildCylinder(size=1, height=1, sides=16, smooth=render3D?.smoothShading, capped=true)
|
|
2418
|
-
{
|
|
2419
|
-
return buildLathe([[size / 2, -height / 2], [size / 2, height / 2]], sides, smooth, capped);
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
|
-
/**
|
|
2423
|
-
* Build a cone standing on the Y axis, centered on the origin, the point up
|
|
2424
|
-
* @param {number} [size] - Diameter of the base
|
|
2425
|
-
* @param {number} [height]
|
|
2426
|
-
* @param {number} [sides] - Around
|
|
2427
|
-
* @param {boolean} [smooth] - Defaults to render3D.smoothShading
|
|
2428
|
-
* @param {boolean} [capped] - Close the base
|
|
2429
|
-
* @return {Mesh}
|
|
2430
|
-
* @memberof Render3D
|
|
2431
|
-
*/
|
|
2432
|
-
function buildCone(size=1, height=1, sides=16, smooth=render3D?.smoothShading, capped=true)
|
|
2433
|
-
{
|
|
2434
|
-
return buildLathe([[size / 2, -height / 2], [0, height / 2]], sides, smooth, capped);
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
2978
|
/**
|
|
2438
2979
|
* Build a sphere centered on the origin
|
|
2439
2980
|
* @param {number} [size] - Diameter
|
|
@@ -2455,65 +2996,94 @@ function buildSphere(size=1, sides=16, rings=8, smooth=render3D?.smoothShading)
|
|
|
2455
2996
|
return buildLathe(profile, sides, smooth);
|
|
2456
2997
|
}
|
|
2457
2998
|
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2999
|
+
// 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
|
|
3000
|
+
// circle, and an eighth of a sphere at each corner; one segment makes them flat, a chamfer
|
|
3001
|
+
function render3DBevelBox(half, t, segments, faces)
|
|
3002
|
+
{
|
|
3003
|
+
const mesh = new Mesh, inner = half.subtract(vec3(t)), n = max(1, segments | 0);
|
|
3004
|
+
|
|
3005
|
+
// a triangle facing out, since the box is convex around the origin, flat shaded or with the normals of the
|
|
3006
|
+
// round, and uvs from the face it faces most, as a plain box maps that face
|
|
3007
|
+
const triangle = (a, b, c, na, nb, nc)=>
|
|
3008
|
+
{
|
|
3009
|
+
let normal = b.subtract(a).cross(c.subtract(a));
|
|
3010
|
+
if (normal.lengthSquared() < 1e-20) return; // an edge or face with no size
|
|
3011
|
+
if (normal.dot(a.add(b).add(c)) < 0)
|
|
3012
|
+
[b, c, nb, nc, normal] = [c, b, nc, nb, normal.scale(-1)];
|
|
3013
|
+
normal = normal.normalize();
|
|
3014
|
+
let face = faces[0];
|
|
3015
|
+
for (const f of faces)
|
|
3016
|
+
if (f[0].dot(normal) > face[0].dot(normal))
|
|
3017
|
+
face = f;
|
|
3018
|
+
const [, r, u] = face, hr = abs(half.dot(r)), hu = abs(half.dot(u));
|
|
3019
|
+
const uv = (p)=> vec2(.5 + p.dot(r) / (2 * hr), .5 - p.dot(u) / (2 * hu));
|
|
3020
|
+
mesh.addStrip([a, b, c], n > 1 && na ? [na, nb, nc] : normal, [uv(a), uv(b), uv(c)]);
|
|
3021
|
+
};
|
|
3022
|
+
|
|
3023
|
+
// the faces, shrunk by the bevel
|
|
3024
|
+
for (const [normal, r, u] of faces)
|
|
2474
3025
|
{
|
|
2475
|
-
const a
|
|
2476
|
-
|
|
3026
|
+
const [a, b, c, d] = render3DQuadAxes(normal.multiply(half), r.multiply(inner), u.multiply(inner));
|
|
3027
|
+
triangle(a, b, c);
|
|
3028
|
+
triangle(c, b, d);
|
|
2477
3029
|
}
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
3030
|
+
|
|
3031
|
+
// the edges, each a strip around a quarter circle from one face to the next
|
|
3032
|
+
const axes = [vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1)];
|
|
3033
|
+
for (let k = 0; k < 3; ++k)
|
|
3034
|
+
for (const si of [-1, 1])
|
|
3035
|
+
for (const sj of [-1, 1])
|
|
3036
|
+
{
|
|
3037
|
+
const n1 = axes[(k + 1) % 3].scale(si), n2 = axes[(k + 2) % 3].scale(sj);
|
|
3038
|
+
const middle = n1.multiply(inner).add(n2.multiply(inner)), along = axes[k].multiply(inner);
|
|
3039
|
+
const start = middle.subtract(along), end = middle.add(along);
|
|
3040
|
+
const dir = (m)=> n1.scale(cos(m / n * PI / 2)).add(n2.scale(sin(m / n * PI / 2)));
|
|
3041
|
+
for (let m = 0; m < n; ++m)
|
|
3042
|
+
{
|
|
3043
|
+
const d0 = dir(m), d1 = dir(m + 1);
|
|
3044
|
+
const a = start.add(d0.scale(t)), b = end.add(d0.scale(t));
|
|
3045
|
+
const c = start.add(d1.scale(t)), d = end.add(d1.scale(t));
|
|
3046
|
+
triangle(a, b, c, d0, d0, d1);
|
|
3047
|
+
triangle(c, b, d, d1, d0, d1);
|
|
3048
|
+
}
|
|
2482
3049
|
}
|
|
2483
|
-
return buildLathe(profile, sides, smooth);
|
|
2484
|
-
}
|
|
2485
3050
|
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
* @param {number} [sides] - Around the ring
|
|
2491
|
-
* @param {number} [tubeSides] - Around the tube
|
|
2492
|
-
* @param {boolean} [smooth] - Defaults to render3D.smoothShading
|
|
2493
|
-
* @return {Mesh}
|
|
2494
|
-
* @memberof Render3D
|
|
2495
|
-
*/
|
|
2496
|
-
function buildTorus(size=1, tubeSize=.3, sides=16, tubeSides=8, smooth=render3D?.smoothShading)
|
|
2497
|
-
{
|
|
2498
|
-
ASSERT(tubeSize <= size, 'the tube must fit inside the torus');
|
|
2499
|
-
const profile = [], radius = (size - tubeSize) / 2, tubeRadius = tubeSize / 2;
|
|
2500
|
-
for (let i = 0; i <= tubeSides; ++i)
|
|
3051
|
+
// the corners, an eighth of a sphere in rows from one face's axis down to the edge between the other two
|
|
3052
|
+
for (const sx of [-1, 1])
|
|
3053
|
+
for (const sy of [-1, 1])
|
|
3054
|
+
for (const sz of [-1, 1])
|
|
2501
3055
|
{
|
|
2502
|
-
const
|
|
2503
|
-
|
|
3056
|
+
const nx = vec3(sx, 0, 0), ny = vec3(0, sy, 0), nz = vec3(0, 0, sz), corner = inner.multiply(vec3(sx, sy, sz));
|
|
3057
|
+
const dir = (i, j)=>
|
|
3058
|
+
{
|
|
3059
|
+
const polar = i / n * PI / 2, around = i ? j / i * PI / 2 : 0;
|
|
3060
|
+
return nz.scale(cos(polar)).add(nx.scale(cos(around) * sin(polar))).add(ny.scale(sin(around) * sin(polar)));
|
|
3061
|
+
};
|
|
3062
|
+
const point = (i, j)=> corner.add(dir(i, j).scale(t));
|
|
3063
|
+
for (let i = 0; i < n; ++i)
|
|
3064
|
+
{
|
|
3065
|
+
for (let j = 0; j <= i; ++j)
|
|
3066
|
+
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));
|
|
3067
|
+
for (let j = 0; j < i; ++j)
|
|
3068
|
+
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));
|
|
3069
|
+
}
|
|
2504
3070
|
}
|
|
2505
|
-
return
|
|
3071
|
+
return mesh;
|
|
2506
3072
|
}
|
|
2507
3073
|
|
|
2508
3074
|
/**
|
|
2509
3075
|
* Build a box centered on the origin, six flat faces with uvs covering each face
|
|
3076
|
+
* - bevel cuts its edges and corners: 1 segment is a flat chamfer, more round them, and the biggest bevel, half
|
|
3077
|
+
* the smallest side, rounds a cube into a ball
|
|
2510
3078
|
* @param {Vector3|number} [size] - Full size, a number for a cube
|
|
3079
|
+
* @param {number} [bevel] - Size of the cut on each edge, clamped to half the smallest side
|
|
3080
|
+
* @param {number} [bevelSegments] - Steps around each edge, 1 for a flat chamfer
|
|
2511
3081
|
* @return {Mesh}
|
|
2512
3082
|
* @memberof Render3D
|
|
2513
3083
|
*/
|
|
2514
|
-
function buildBox(size=1)
|
|
3084
|
+
function buildBox(size=1, bevel=0, bevelSegments=1)
|
|
2515
3085
|
{
|
|
2516
|
-
|
|
3086
|
+
ASSERT(isNumber(bevel) && bevel >= 0, 'bevel must be a number, 0 or more');
|
|
2517
3087
|
const half = render3DSize3(size).scale(.5);
|
|
2518
3088
|
// each face: normal, right axis, up axis (right cross up = normal)
|
|
2519
3089
|
const faces = [
|
|
@@ -2524,6 +3094,10 @@ function buildBox(size=1)
|
|
|
2524
3094
|
[vec3(0, 1, 0), vec3(1, 0, 0), vec3(0, 0, -1)],
|
|
2525
3095
|
[vec3(0, -1, 0), vec3(1, 0, 0), vec3(0, 0, 1)],
|
|
2526
3096
|
];
|
|
3097
|
+
const t = min(bevel, half.x, half.y, half.z);
|
|
3098
|
+
if (t > 0)
|
|
3099
|
+
return render3DBevelBox(half, t, bevelSegments, faces);
|
|
3100
|
+
const mesh = new Mesh;
|
|
2527
3101
|
for (const [n, r, u] of faces)
|
|
2528
3102
|
{
|
|
2529
3103
|
const center = n.multiply(half);
|
|
@@ -2533,87 +3107,66 @@ function buildBox(size=1)
|
|
|
2533
3107
|
return mesh;
|
|
2534
3108
|
}
|
|
2535
3109
|
|
|
2536
|
-
/**
|
|
2537
|
-
* Build a lit ribbon along a path, for roads, tracks and walls
|
|
2538
|
-
* - Each segment is a flat quad, the sides are across the path in the plane of the up vector
|
|
2539
|
-
* - doubleSided, so it is seen and lit from below as well
|
|
2540
|
-
* @param {Array<Vector3>} points - Center line in order
|
|
2541
|
-
* @param {number|Array<number>} [width] - Full width, one for all or one per point
|
|
2542
|
-
* @param {Color|Array<Color>} [color] - One for all or one per point
|
|
2543
|
-
* @param {boolean} [closed] - Join the last point back to the first
|
|
2544
|
-
* @param {Vector3} [up] - Which way the ribbon faces
|
|
2545
|
-
* @return {Mesh}
|
|
2546
|
-
* @memberof Render3D
|
|
2547
|
-
* @example
|
|
2548
|
-
* const road = buildRibbon(trackPoints, 8, GRAY, true); // a loop of road
|
|
2549
|
-
*/
|
|
2550
|
-
function buildRibbon(points, width=1, color=WHITE, closed=false, up=vec3(0, 1, 0))
|
|
2551
|
-
{
|
|
2552
|
-
ASSERT(isArray(points) && points.length > 1, 'ribbon needs at least 2 points');
|
|
2553
|
-
const mesh = new Mesh, count = points.length, edges = [];
|
|
2554
|
-
let across = (abs(up.y) < .9 ? vec3(0, 1, 0) : vec3(1, 0, 0)).cross(up).normalize(); // anything across up
|
|
2555
|
-
for (let i = 0; i < count; ++i)
|
|
2556
|
-
{
|
|
2557
|
-
// across the path, from the tangent through this point; a step along up keeps the last across
|
|
2558
|
-
const next = points[closed ? (i + 1) % count : min(i + 1, count - 1)];
|
|
2559
|
-
const last = points[closed ? (i + count - 1) % count : max(i - 1, 0)];
|
|
2560
|
-
const dir = next.subtract(last).cross(up);
|
|
2561
|
-
if (dir.lengthSquared() > 1e-12)
|
|
2562
|
-
across = dir.normalize();
|
|
2563
|
-
const half = across.scale((isArray(width) ? width[i] : width) / 2);
|
|
2564
|
-
edges.push([points[i].subtract(half), points[i].add(half)]);
|
|
2565
|
-
}
|
|
2566
|
-
for (let i = 0; i + 1 < count + (closed ? 1 : 0); ++i)
|
|
2567
|
-
{
|
|
2568
|
-
const j = (i + 1) % count, a = edges[i], b = edges[j];
|
|
2569
|
-
const c = isArray(color) ? [color[i], color[i], color[j], color[j]] : color;
|
|
2570
|
-
mesh.addQuad(a[0], a[1], b[1], b[0], c); // counter clockwise seen from above
|
|
2571
|
-
}
|
|
2572
|
-
mesh.doubleSided = true; // a flat strip, seen from both sides
|
|
2573
|
-
return mesh;
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
3110
|
/**
|
|
2577
3111
|
* Build a heightfield grid in the XZ plane centered on the origin
|
|
2578
3112
|
* - smooth rounds the lighting across cells and colors each corner
|
|
2579
3113
|
* - flat lights and colors each cell on its own, so a checkerboard stays crisp
|
|
2580
3114
|
* - doubleSided, a sheet seen from both sides; turn it off for ground only ever seen from above
|
|
2581
3115
|
* - One cell is a plain square, render3D.planeMesh and planeMeshDoubleSided are shared ones
|
|
2582
|
-
* @param {Vector2} [size] - World size along X and Z
|
|
3116
|
+
* @param {Vector2|number} [size] - World size along X and Z, a number for a square
|
|
2583
3117
|
* @param {Vector2|number} [segments] - Cells along X and Z, a number for both
|
|
2584
|
-
* @param {Color|
|
|
2585
|
-
* @param {
|
|
2586
|
-
* @param {boolean} [smooth] - Defaults to render3D.smoothShading
|
|
3118
|
+
* @param {Color|function(number, number): Color} [color] - One Color for the whole grid, or (x, z) => Color
|
|
3119
|
+
* @param {function(number, number): number} [heightFunction] - (x, z) => y, default flat
|
|
3120
|
+
* @param {boolean} [smooth] - Defaults to render3D.smoothShading with a heightFunction; a flat grid is flat shaded,
|
|
3121
|
+
* since its light is even anyway and smoothing would only blend its cell colors; pass smooth to blend a gradient
|
|
2587
3122
|
* @return {Mesh}
|
|
2588
3123
|
* @memberof Render3D
|
|
2589
3124
|
* @example
|
|
2590
3125
|
* const ground = buildGrid(vec2(20), 10, (x, z)=> (floor(x / 2) + floor(z / 2)) & 1 ? GRAY : WHITE); // 2 unit checks
|
|
2591
3126
|
*/
|
|
2592
|
-
function buildGrid(size=vec2(1), segments=1, color, heightFunction
|
|
3127
|
+
function buildGrid(size=vec2(1), segments=1, color, heightFunction, smooth=heightFunction && render3D?.smoothShading)
|
|
2593
3128
|
{
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
3129
|
+
heightFunction ||= ()=> 0;
|
|
3130
|
+
size = render3DSize2(size);
|
|
3131
|
+
segments = render3DSize2(segments);
|
|
3132
|
+
ASSERT(segments.x > 0 && segments.y > 0 && segments.x % 1 === 0 && segments.y % 1 === 0,
|
|
3133
|
+
'grid segments must be whole numbers above zero');
|
|
2597
3134
|
const mesh = new Mesh;
|
|
2598
3135
|
const segmentsX = segments.x, segmentsZ = segments.y;
|
|
2599
3136
|
const cellX = size.x / segmentsX, cellZ = size.y / segmentsZ;
|
|
2600
|
-
const
|
|
2601
|
-
const
|
|
2602
|
-
const
|
|
2603
|
-
const
|
|
2604
|
-
|
|
3137
|
+
const halfX = size.x / 2, halfZ = size.y / 2, ex = cellX / 2, ez = cellZ / 2;
|
|
3138
|
+
const px = (i)=> i * cellX - halfX, pz = (j)=> j * cellZ - halfZ;
|
|
3139
|
+
const colorAt = /** @type {function(number, number): Color} */ (color);
|
|
3140
|
+
const cellColor = (i, j)=> !color ? WHITE : isColor(color) ? /** @type {Color} */ (color) : colorAt(px(i), pz(j));
|
|
3141
|
+
// a big terrain has millions of vertices, so each one is made once, shared by the rows above and below it
|
|
3142
|
+
const row = (j)=>
|
|
3143
|
+
{
|
|
3144
|
+
const points = [], normals = [], uvs = [], colors = [], z = pz(j);
|
|
3145
|
+
for (let i = 0; i <= segmentsX; ++i)
|
|
3146
|
+
{
|
|
3147
|
+
const x = px(i);
|
|
3148
|
+
points.push(vec3(x, heightFunction(x, z), z));
|
|
3149
|
+
uvs.push(vec2(i / segmentsX, j / segmentsZ));
|
|
3150
|
+
if (!smooth) continue;
|
|
3151
|
+
normals.push(render3DSlopeNormal(heightFunction, x, z, ex, ez, halfX, halfZ));
|
|
3152
|
+
colors.push(cellColor(i, j));
|
|
3153
|
+
}
|
|
3154
|
+
return {points, normals, uvs, colors};
|
|
3155
|
+
};
|
|
3156
|
+
let above = row(0);
|
|
2605
3157
|
for (let j = 0; j < segmentsZ; ++j)
|
|
2606
3158
|
{
|
|
3159
|
+
const below = row(j + 1);
|
|
2607
3160
|
if (smooth)
|
|
2608
3161
|
{
|
|
2609
3162
|
// one ribbon per row with vertex normals from the slope
|
|
2610
3163
|
const points = [], normals = [], uvs = [], colors = [];
|
|
2611
3164
|
for (let i = 0; i <= segmentsX; ++i)
|
|
2612
3165
|
{
|
|
2613
|
-
points.push(
|
|
2614
|
-
normals.push(
|
|
2615
|
-
uvs.push(
|
|
2616
|
-
colors.push(
|
|
3166
|
+
points.push(above.points[i], below.points[i]);
|
|
3167
|
+
normals.push(above.normals[i], below.normals[i]);
|
|
3168
|
+
uvs.push(above.uvs[i], below.uvs[i]);
|
|
3169
|
+
colors.push(above.colors[i], below.colors[i]);
|
|
2617
3170
|
}
|
|
2618
3171
|
mesh.addStrip(points, normals, uvs, colors);
|
|
2619
3172
|
}
|
|
@@ -2621,52 +3174,37 @@ function buildGrid(size=vec2(1), segments=1, color, heightFunction=()=>0, smooth
|
|
|
2621
3174
|
{
|
|
2622
3175
|
// one quad per cell with its face normal and one color sampled at its center
|
|
2623
3176
|
for (let i = 0; i < segmentsX; ++i)
|
|
2624
|
-
mesh.addQuad(
|
|
2625
|
-
[
|
|
3177
|
+
mesh.addQuad(above.points[i], below.points[i], below.points[i+1], above.points[i+1], cellColor(i + .5, j + .5),
|
|
3178
|
+
[above.uvs[i], below.uvs[i], below.uvs[i+1], above.uvs[i+1]]);
|
|
3179
|
+
}
|
|
3180
|
+
above = below;
|
|
3181
|
+
}
|
|
3182
|
+
if (smooth)
|
|
3183
|
+
{
|
|
3184
|
+
// the grid knows which strip entries are one vertex, each shared by the rows above and below it and by the
|
|
3185
|
+
// repeats at the ends of its ribbons, so the upload of a big terrain skips searching millions of entries
|
|
3186
|
+
const n = 2 * (segmentsX + 1) + 2, keys = mesh.vertexKeys = new Int32Array(mesh.points.length);
|
|
3187
|
+
const id = (i, j)=> j * (segmentsX + 1) + i;
|
|
3188
|
+
for (let j = 0; j < segmentsZ; ++j)
|
|
3189
|
+
{
|
|
3190
|
+
const start = j * n;
|
|
3191
|
+
keys[start] = id(0, j); // the leading repeat
|
|
3192
|
+
for (let i = 0; i <= segmentsX; ++i)
|
|
3193
|
+
keys[start + 1 + 2*i] = id(i, j), keys[start + 2 + 2*i] = id(i, j + 1);
|
|
3194
|
+
keys[start + n - 1] = id(segmentsX, j + 1); // the trailing repeat
|
|
2626
3195
|
}
|
|
2627
3196
|
}
|
|
2628
3197
|
mesh.doubleSided = true; // a sheet, seen from both sides; terrain seen only from above can turn it off
|
|
2629
3198
|
return mesh;
|
|
2630
3199
|
}
|
|
2631
3200
|
|
|
2632
|
-
/**
|
|
2633
|
-
* Build a hull from a row of diamond shaped slices along Z, for ships, planes and cars
|
|
2634
|
-
* - Each slice is [z, width, top, bottom, sideHeight]
|
|
2635
|
-
* - sideHeight is 0 to 1 and puts the side corners between the bottom and the top
|
|
2636
|
-
* - List the slices nose first, with the nose at the largest z
|
|
2637
|
-
* @param {Array<Array<number>>} stations
|
|
2638
|
-
* @return {Mesh}
|
|
2639
|
-
* @memberof Render3D
|
|
2640
|
-
* @example
|
|
2641
|
-
* const hull = buildLoft([[1.2, .4, .2, -.1], [0, 1.4, .5, -.4], [-1, 1, .3, -.3]]);
|
|
2642
|
-
*/
|
|
2643
|
-
function buildLoft(stations)
|
|
2644
|
-
{
|
|
2645
|
-
ASSERT(isArray(stations) && stations.length > 1, 'loft needs at least 2 stations');
|
|
2646
|
-
// the caps and the winding both assume the nose leads, so the other order turns the hull inside out
|
|
2647
|
-
ASSERT(stations[0][0] > stations[stations.length-1][0], 'loft stations go nose first, from the largest z to the smallest');
|
|
2648
|
-
const mesh = new Mesh;
|
|
2649
|
-
// section points: left, top, right, bottom, wound clockwise seen from +z
|
|
2650
|
-
const section = ([z, w, t, b, m=.5])=>
|
|
2651
|
-
[vec3(-w / 2, lerp(b, t, m), z), vec3(0, t, z), vec3(w / 2, lerp(b, t, m), z), vec3(0, b, z)];
|
|
2652
|
-
for (let i = 0; i + 1 < stations.length; ++i)
|
|
2653
|
-
{
|
|
2654
|
-
const s1 = section(stations[i]), s2 = section(stations[i + 1]);
|
|
2655
|
-
for (let k = 0; k < 4; ++k)
|
|
2656
|
-
mesh.addQuad(s1[k], s1[(k + 1) % 4], s2[(k + 1) % 4], s2[k]);
|
|
2657
|
-
}
|
|
2658
|
-
const tail = section(stations[stations.length - 1]), nose = section(stations[0]);
|
|
2659
|
-
mesh.addQuad(tail[0], tail[1], tail[2], tail[3]);
|
|
2660
|
-
mesh.addQuad(nose[3], nose[2], nose[1], nose[0]);
|
|
2661
|
-
return mesh;
|
|
2662
|
-
}
|
|
2663
|
-
|
|
2664
3201
|
/**
|
|
2665
3202
|
* Build a sky dome: a sphere colored by direction, wound to be seen from inside
|
|
2666
3203
|
* - set it as render3D.sky and the pass draws it around the camera behind everything
|
|
2667
3204
|
* @param {Color} [topColor] - Straight up
|
|
2668
3205
|
* @param {Color} [horizonColor] - Level with the camera
|
|
2669
|
-
* @param {Color} [bottomColor] - Straight down, what a camera looking at the ground sees past its edge; defaults to the
|
|
3206
|
+
* @param {Color} [bottomColor] - Straight down, what a camera looking at the ground sees past its edge; defaults to the
|
|
3207
|
+
* horizon color
|
|
2670
3208
|
* @param {number} [sides] - Around
|
|
2671
3209
|
* @param {number} [rings] - Top to bottom
|
|
2672
3210
|
* @return {Mesh}
|
|
@@ -2674,370 +3212,12 @@ function buildLoft(stations)
|
|
|
2674
3212
|
*/
|
|
2675
3213
|
function buildSky(topColor=hsl(.6, .8, .55), horizonColor=hsl(.6, 1, .9), bottomColor=horizonColor, sides=16, rings=8)
|
|
2676
3214
|
{
|
|
2677
|
-
|
|
2678
|
-
const
|
|
2679
|
-
|
|
2680
|
-
const e = i / rings * PI - PI/2;
|
|
2681
|
-
return vec3(sin(a) * cos(e), sin(e), cos(a) * cos(e));
|
|
2682
|
-
};
|
|
2683
|
-
const color = (i)=>
|
|
2684
|
-
{
|
|
2685
|
-
const y = point(i, 0).y;
|
|
2686
|
-
return y < 0 ? horizonColor.lerp(bottomColor, -y) : horizonColor.lerp(topColor, y);
|
|
2687
|
-
};
|
|
2688
|
-
for (let i = 0; i < rings; ++i)
|
|
2689
|
-
{
|
|
2690
|
-
// bottom point then top point per column, the reverse of the lathe, so the front faces inward
|
|
2691
|
-
const points = [], colors = [];
|
|
2692
|
-
for (let j = 0; j <= sides; ++j)
|
|
2693
|
-
{
|
|
2694
|
-
const a = j / sides * 2 * PI;
|
|
2695
|
-
points.push(point(i, a), point(i + 1, a));
|
|
2696
|
-
colors.push(color(i), color(i + 1));
|
|
2697
|
-
}
|
|
2698
|
-
mesh.addStrip(points, undefined, undefined, colors);
|
|
2699
|
-
}
|
|
3215
|
+
// a sphere turned inside out so it is seen from within, each point colored by how high it is
|
|
3216
|
+
const mesh = buildSphere(2, sides, rings, true).flipNormals();
|
|
3217
|
+
mesh.colors = mesh.points.map(p=> p.y < 0 ? horizonColor.lerp(bottomColor, -p.y) : horizonColor.lerp(topColor, p.y));
|
|
2700
3218
|
return mesh;
|
|
2701
3219
|
}
|
|
2702
3220
|
|
|
2703
|
-
/**
|
|
2704
|
-
* Turn a sprite into a 3D block model by giving its pixels thickness
|
|
2705
|
-
* - A pixel counts as solid when it is more than half opaque
|
|
2706
|
-
* - Each pixel keeps its own color, so white art takes the object's tint
|
|
2707
|
-
* - Runs of matching pixels merge into one face, and side walls appear only at the sprite's edges
|
|
2708
|
-
* - A texture's pixels are read once and kept, so redrawing a canvas texture will not change what this builds
|
|
2709
|
-
* - Pixels can also be an array of rows, each a Color, a truthy value for white, or a falsy value for empty
|
|
2710
|
-
* @param {TileInfo|Array<Array<Color|number|boolean>>} pixels - A tile from a loaded texture, or rows of pixels,
|
|
2711
|
-
* each a Color (empty when see through), a truthy value for white or a falsy value for empty
|
|
2712
|
-
* @param {Vector2} [size] - World width and height of the whole tile, centered like buildBox
|
|
2713
|
-
* @param {number} [depth] - Thickness along Z
|
|
2714
|
-
* @return {Mesh}
|
|
2715
|
-
* @memberof Render3D
|
|
2716
|
-
* @example
|
|
2717
|
-
* new EngineObject3D(vec3(), buildExtrude(tile(3, 16), vec2(2), .5)); // a chunky version of tile 3
|
|
2718
|
-
*/
|
|
2719
|
-
function buildExtrude(pixels, size=vec2(1), depth=1)
|
|
2720
|
-
{
|
|
2721
|
-
let rows = pixels, width, height;
|
|
2722
|
-
if (pixels instanceof TileInfo)
|
|
2723
|
-
{
|
|
2724
|
-
// colors for the tile's pixels only, undefined where alpha is half or less
|
|
2725
|
-
const image = render3DReadPixels(pixels.textureInfo), data = image.data;
|
|
2726
|
-
const x0 = pixels.pos.x | 0, y0 = pixels.pos.y | 0;
|
|
2727
|
-
width = pixels.size.x | 0, height = pixels.size.y | 0;
|
|
2728
|
-
rows = [];
|
|
2729
|
-
for (let y = 0; y < height; ++y)
|
|
2730
|
-
{
|
|
2731
|
-
const row = rows[y] = [];
|
|
2732
|
-
for (let x = 0; x < width; ++x)
|
|
2733
|
-
{
|
|
2734
|
-
const k = ((y0 + y) * image.width + x0 + x) * 4;
|
|
2735
|
-
row.push(data[k + 3] > 127 ? rgb(data[k] / 255, data[k + 1] / 255, data[k + 2] / 255) : undefined);
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
}
|
|
2739
|
-
else
|
|
2740
|
-
{
|
|
2741
|
-
ASSERT(isArray(pixels) && pixels.length, 'pixels must be a TileInfo or rows of pixels');
|
|
2742
|
-
height = rows.length, width = rows[0].length;
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
// the color of a solid pixel, undefined outside or where it is empty
|
|
2746
|
-
const solid = (x, y)=>
|
|
2747
|
-
{
|
|
2748
|
-
if (x < 0 || y < 0 || x >= width || y >= height) return;
|
|
2749
|
-
const c = rows[y] && rows[y][x];
|
|
2750
|
-
if (!c) return;
|
|
2751
|
-
return isColor(c) ? (c.a > .5 ? c : undefined) : WHITE; // a see through Color is empty too
|
|
2752
|
-
};
|
|
2753
|
-
const same = (a, b)=> a === b || !!a && !!b && a.rgbaInt() === b.rgbaInt();
|
|
2754
|
-
|
|
2755
|
-
// call emit(start, end, color) for each run of same colored pixels, colorAt(i) undefined breaks the run
|
|
2756
|
-
const runs = (count, colorAt, emit)=>
|
|
2757
|
-
{
|
|
2758
|
-
let start = 0, color;
|
|
2759
|
-
for (let i = 0; i <= count; ++i)
|
|
2760
|
-
{
|
|
2761
|
-
const c = i < count ? colorAt(i) : undefined;
|
|
2762
|
-
if (same(c, color)) continue;
|
|
2763
|
-
if (color) emit(start, i, color);
|
|
2764
|
-
start = i, color = c;
|
|
2765
|
-
}
|
|
2766
|
-
};
|
|
2767
|
-
|
|
2768
|
-
// pixel edges in world space, y runs down the image
|
|
2769
|
-
const mesh = new Mesh, sx = size.x / width, sy = size.y / height, hz = depth / 2;
|
|
2770
|
-
const px = x=> x * sx - size.x / 2, py = y=> size.y / 2 - y * sy;
|
|
2771
|
-
const quad = (origin, right, up, normal, color)=>
|
|
2772
|
-
mesh.addStrip(render3DQuadAxes(origin.add(right.scale(.5)).add(up.scale(.5)), right.scale(.5), up.scale(.5)), normal, RENDER3D_QUAD_UVS, color);
|
|
2773
|
-
const X = vec3(1, 0, 0), Y = vec3(0, 1, 0), Z = vec3(0, 0, 1);
|
|
2774
|
-
for (let y = 0; y < height; ++y)
|
|
2775
|
-
{
|
|
2776
|
-
// front and back faces along each row
|
|
2777
|
-
runs(width, x=> solid(x, y), (a, b, c)=>
|
|
2778
|
-
{
|
|
2779
|
-
const w = X.scale((b - a) * sx), h = Y.scale(sy);
|
|
2780
|
-
quad(vec3(px(a), py(y + 1), hz), w, h, Z, c);
|
|
2781
|
-
quad(vec3(px(b), py(y + 1), -hz), w.scale(-1), h, Z.scale(-1), c);
|
|
2782
|
-
});
|
|
2783
|
-
// walls facing up and down where the pixel above or below is empty
|
|
2784
|
-
runs(width, x=> solid(x, y - 1) ? undefined : solid(x, y), (a, b, c)=>
|
|
2785
|
-
quad(vec3(px(a), py(y), hz), X.scale((b - a) * sx), Z.scale(-depth), Y, c));
|
|
2786
|
-
runs(width, x=> solid(x, y + 1) ? undefined : solid(x, y), (a, b, c)=>
|
|
2787
|
-
quad(vec3(px(a), py(y + 1), -hz), X.scale((b - a) * sx), Z.scale(depth), Y.scale(-1), c));
|
|
2788
|
-
}
|
|
2789
|
-
for (let x = 0; x < width; ++x)
|
|
2790
|
-
{
|
|
2791
|
-
// walls facing left and right where the pixel beside is empty
|
|
2792
|
-
runs(height, y=> solid(x - 1, y) ? undefined : solid(x, y), (a, b, c)=>
|
|
2793
|
-
quad(vec3(px(x), py(b), -hz), Z.scale(depth), Y.scale((b - a) * sy), X.scale(-1), c));
|
|
2794
|
-
runs(height, y=> solid(x + 1, y) ? undefined : solid(x, y), (a, b, c)=>
|
|
2795
|
-
quad(vec3(px(x + 1), py(b), hz), Z.scale(-depth), Y.scale((b - a) * sy), X, c));
|
|
2796
|
-
}
|
|
2797
|
-
return mesh;
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
|
-
/**
|
|
2801
|
-
* Build a mesh of extruded text from an image font, the engine font by default so it needs no assets
|
|
2802
|
-
* - Each glyph is extruded once per font and reused, the block is centered and faces +Z
|
|
2803
|
-
* - Newlines stack downward, spaced a little wider than the character height so the sides do not collide
|
|
2804
|
-
* - Every call builds a new mesh, dispose the old one when text changes often
|
|
2805
|
-
* - Glyphs are white in the engine font, so the object's color tints the text
|
|
2806
|
-
* @param {string|number} text
|
|
2807
|
-
* @param {number} [size] - Character height in world units
|
|
2808
|
-
* @param {number} [depth] - Thickness along Z
|
|
2809
|
-
* @param {ImageFont} [font] - Defaults to engineImageFont
|
|
2810
|
-
* @return {Mesh}
|
|
2811
|
-
* @memberof Render3D
|
|
2812
|
-
* @example
|
|
2813
|
-
* new EngineObject3D(vec3(0, 2, 0), buildText3D('HELLO'), undefined, YELLOW);
|
|
2814
|
-
*/
|
|
2815
|
-
function buildText3D(text, size=1, depth=.2, font=engineImageFont)
|
|
2816
|
-
{
|
|
2817
|
-
ASSERT(font instanceof ImageFont, 'font must be an ImageFont, the engine font loads before gameInit');
|
|
2818
|
-
const tileInfo = font.tileInfo, padding = tileInfo.padding;
|
|
2819
|
-
const paddedX = tileInfo.size.x + padding * 2, paddedY = tileInfo.size.y + padding * 2;
|
|
2820
|
-
const columns = tileInfo.textureInfo.size.x / paddedX | 0;
|
|
2821
|
-
let glyphs = render3DGlyphCache.get(font); // unit sized, scaled when combined
|
|
2822
|
-
glyphs || render3DGlyphCache.set(font, glyphs = new Map);
|
|
2823
|
-
const charSize = vec2(size * tileInfo.size.x / tileInfo.size.y, size);
|
|
2824
|
-
const mesh = new Mesh, lines = (text + '').split('\n');
|
|
2825
|
-
lines.forEach((line, j)=>
|
|
2826
|
-
{
|
|
2827
|
-
const y = ((lines.length - 1) / 2 - j) * charSize.y * RENDER3D_TEXT_LEADING;
|
|
2828
|
-
for (let i = 0; i < line.length; ++i)
|
|
2829
|
-
{
|
|
2830
|
-
const charCode = line.charCodeAt(i);
|
|
2831
|
-
const index = charCode < 32 || charCode > 127 ? 95 : charCode - 32; // like ImageFont
|
|
2832
|
-
if (!index) continue; // space
|
|
2833
|
-
let glyph = glyphs.get(index);
|
|
2834
|
-
if (!glyph)
|
|
2835
|
-
{
|
|
2836
|
-
const pos = vec2(index % columns * paddedX + padding, (index / columns | 0) * paddedY + padding);
|
|
2837
|
-
glyphs.set(index, glyph = buildExtrude(new TileInfo(pos, tileInfo.size, tileInfo.textureInfo)));
|
|
2838
|
-
}
|
|
2839
|
-
const x = (i - (line.length - 1) / 2) * charSize.x;
|
|
2840
|
-
mesh.combine(glyph, buildMatrix(vec3(x, y, 0), undefined, vec3(charSize.x, charSize.y, depth)));
|
|
2841
|
-
}
|
|
2842
|
-
});
|
|
2843
|
-
return mesh;
|
|
2844
|
-
}
|
|
2845
|
-
|
|
2846
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2847
|
-
/**
|
|
2848
|
-
* HeightMap - Terrain built from a grid of heights, with a mesh, a height lookup and a raycast
|
|
2849
|
-
* - heights is a 2D array [row][column] of 0 to 1 values
|
|
2850
|
-
* - Row 0 is the far edge at -Z and column 0 is the left edge at -X
|
|
2851
|
-
* - It can be an image instead, where the red channel is the height
|
|
2852
|
-
* - colors is an optional 2D array of Colors or an image, sampled per vertex
|
|
2853
|
-
* - images are read through a canvas, so they must be same origin or loaded with crossOrigin set
|
|
2854
|
-
* @memberof Render3D
|
|
2855
|
-
* @example
|
|
2856
|
-
* const terrain = new HeightMap(heightImage, vec2(100, 100), 10, colorImage);
|
|
2857
|
-
* new EngineObject3D(vec3(), terrain.buildMesh());
|
|
2858
|
-
* const y = terrain.getHeight(x, z); // stand things on it
|
|
2859
|
-
*/
|
|
2860
|
-
class HeightMap
|
|
2861
|
-
{
|
|
2862
|
-
/** Create a height map from an array or an image
|
|
2863
|
-
* @param {Array<Array<number>>|HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|TextureInfo} heights
|
|
2864
|
-
* @param {Vector2} [size] - World size along X and Z
|
|
2865
|
-
* @param {number} [height] - World height of a full value
|
|
2866
|
-
* @param {Array<Array<Color>>|HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|TextureInfo} [colors] */
|
|
2867
|
-
constructor(heights, size=vec2(1), height=1, colors)
|
|
2868
|
-
{
|
|
2869
|
-
if (!isArray(heights))
|
|
2870
|
-
heights = render3DImageToArray(heights, (r)=> r / 255);
|
|
2871
|
-
if (colors && !isArray(colors))
|
|
2872
|
-
colors = render3DImageToArray(colors, (r, g, b, a)=> rgb(r / 255, g / 255, b / 255, a / 255));
|
|
2873
|
-
ASSERT(isArray(heights) && heights.length > 1 && isArray(heights[0]) && heights[0].length > 1, 'height map needs at least 2 rows and 2 columns');
|
|
2874
|
-
ASSERT(size.x > 0 && size.y > 0, 'height map size must be positive, a zero size has nowhere to look things up');
|
|
2875
|
-
|
|
2876
|
-
/** @property {Array<Array<number>>} - Heights 0-1 as [row][column], rows along Z */
|
|
2877
|
-
this.heights = heights;
|
|
2878
|
-
/** @property {Array<Array<Color>>|undefined} - Vertex colors as [row][column], undefined for white
|
|
2879
|
-
* @type {Array<Array<Color>>|undefined} */
|
|
2880
|
-
this.colors = colors;
|
|
2881
|
-
/** @property {Vector2} - World size along X and Z */
|
|
2882
|
-
this.size = size.copy();
|
|
2883
|
-
/** @property {number} - World height of a full value */
|
|
2884
|
-
this.height = height;
|
|
2885
|
-
}
|
|
2886
|
-
|
|
2887
|
-
/** Number of rows, along Z
|
|
2888
|
-
* @return {number} */
|
|
2889
|
-
get rows() { return this.heights.length; }
|
|
2890
|
-
|
|
2891
|
-
/** Number of columns, along X
|
|
2892
|
-
* @return {number} */
|
|
2893
|
-
get columns() { return this.heights[0].length; }
|
|
2894
|
-
|
|
2895
|
-
/** World height at a position, exactly the height of the mesh buildMesh draws there, clamped at the edges
|
|
2896
|
-
* @param {number|Vector3} x - X, or a position to take X and Z from
|
|
2897
|
-
* @param {number} [z]
|
|
2898
|
-
* @return {number} */
|
|
2899
|
-
getHeight(x, z)
|
|
2900
|
-
{
|
|
2901
|
-
if (x instanceof Vector3)
|
|
2902
|
-
z = x.z, x = x.x; // a position works as well as its two numbers, its own y is ignored
|
|
2903
|
-
const columns = this.columns, rows = this.rows, h = this.heights;
|
|
2904
|
-
const u = clamp((x / this.size.x + .5) * (columns - 1), 0, columns - 1);
|
|
2905
|
-
const v = clamp((z / this.size.y + .5) * (rows - 1), 0, rows - 1);
|
|
2906
|
-
const i = min(floor(u), columns - 2), j = min(floor(v), rows - 2);
|
|
2907
|
-
const fu = u - i, fv = v - j;
|
|
2908
|
-
// each cell is two triangles split from (i, j+1) to (i+1, j), the same split buildGrid's quads use
|
|
2909
|
-
const a = h[j][i], b = h[j+1][i], c = h[j+1][i+1], d = h[j][i+1];
|
|
2910
|
-
const height = fu + fv <= 1 ? a + fu * (d - a) + fv * (b - a) : c + (1 - fu) * (b - c) + (1 - fv) * (d - c);
|
|
2911
|
-
return height * this.height;
|
|
2912
|
-
}
|
|
2913
|
-
|
|
2914
|
-
/** Surface normal at a position, from the slope across a sample
|
|
2915
|
-
* @param {number|Vector3} x - X, or a position to take X and Z from
|
|
2916
|
-
* @param {number} [z]
|
|
2917
|
-
* @return {Vector3} */
|
|
2918
|
-
getNormal(x, z)
|
|
2919
|
-
{
|
|
2920
|
-
if (x instanceof Vector3)
|
|
2921
|
-
z = x.z, x = x.x;
|
|
2922
|
-
const ex = this.size.x / (this.columns - 1) / 2, ez = this.size.y / (this.rows - 1) / 2;
|
|
2923
|
-
return render3DSlopeNormal((x, z)=> this.getHeight(x, z), x, z, ex, ez, this.size.x / 2, this.size.y / 2);
|
|
2924
|
-
}
|
|
2925
|
-
|
|
2926
|
-
/** Color of the nearest sample to a position, white when there are no colors
|
|
2927
|
-
* @param {number|Vector3} x - X, or a position to take X and Z from
|
|
2928
|
-
* @param {number} [z]
|
|
2929
|
-
* @return {Color} */
|
|
2930
|
-
getColor(x, z)
|
|
2931
|
-
{
|
|
2932
|
-
if (x instanceof Vector3)
|
|
2933
|
-
z = x.z, x = x.x;
|
|
2934
|
-
const c = this.colors;
|
|
2935
|
-
if (!c) return WHITE;
|
|
2936
|
-
const columns = c[0].length, rows = c.length;
|
|
2937
|
-
const i = clamp(round((x / this.size.x + .5) * (columns - 1)), 0, columns - 1);
|
|
2938
|
-
const j = clamp(round((z / this.size.y + .5) * (rows - 1)), 0, rows - 1);
|
|
2939
|
-
return c[j][i];
|
|
2940
|
-
}
|
|
2941
|
-
|
|
2942
|
-
/** Distance along a ray to where it crosses the terrain surface, or undefined for a miss
|
|
2943
|
-
* - Steps along the ray half a cell at a time, then narrows in on the exact spot
|
|
2944
|
-
* - A ray that starts under the ground crosses on its way out, so the hit is still on the surface
|
|
2945
|
-
* @param {Ray3D} ray - From screenToRay, or any ray
|
|
2946
|
-
* @return {number|undefined} */
|
|
2947
|
-
raycast(ray)
|
|
2948
|
-
{
|
|
2949
|
-
const {origin, direction} = ray;
|
|
2950
|
-
const size = this.size, height = this.height, length = direction.length();
|
|
2951
|
-
if (!length) return;
|
|
2952
|
-
|
|
2953
|
-
// clip to the box around the terrain, and walk it in half cell steps from there
|
|
2954
|
-
let t = raycastBox(ray, vec3(0, height / 2, 0), vec3(size.x, abs(height) + 1e-3, size.y));
|
|
2955
|
-
if (t === undefined) return;
|
|
2956
|
-
const cell = min(size.x / (this.columns - 1), size.y / (this.rows - 1));
|
|
2957
|
-
const step = cell / 2 / length, end = t + hypot(size.x, size.y, height) / length;
|
|
2958
|
-
if (!(step > 0)) return; // a zero size
|
|
2959
|
-
|
|
2960
|
-
// is the ray below the ground this far along, or undefined where it is off the map
|
|
2961
|
-
const under = (at)=>
|
|
2962
|
-
{
|
|
2963
|
-
const p = origin.add(direction.scale(at));
|
|
2964
|
-
if (abs(p.x) > size.x / 2 || abs(p.z) > size.y / 2) return;
|
|
2965
|
-
return p.y <= this.getHeight(p.x, p.z);
|
|
2966
|
-
};
|
|
2967
|
-
|
|
2968
|
-
// look for where the ray changes sides, so one coming up from under the ground
|
|
2969
|
-
// lands on the surface it breaks through instead of wherever it entered the box
|
|
2970
|
-
const startUnder = under(t);
|
|
2971
|
-
if (startUnder === undefined) return; // it meets the box outside the map itself
|
|
2972
|
-
for (; t < end; t += step)
|
|
2973
|
-
{
|
|
2974
|
-
const u = under(t + step);
|
|
2975
|
-
if (u === undefined) return; // it left the map before crossing
|
|
2976
|
-
if (u === startUnder) continue;
|
|
2977
|
-
|
|
2978
|
-
// it crossed between the last two samples, halve the gap until it is exact
|
|
2979
|
-
let a = t, b = t + step;
|
|
2980
|
-
for (let i = 0; i < 16; ++i)
|
|
2981
|
-
{
|
|
2982
|
-
const mid = (a + b) / 2;
|
|
2983
|
-
under(mid) === startUnder ? a = mid : b = mid;
|
|
2984
|
-
}
|
|
2985
|
-
return b;
|
|
2986
|
-
}
|
|
2987
|
-
}
|
|
2988
|
-
|
|
2989
|
-
/** Build the terrain mesh, one vertex per sample, centered on the origin
|
|
2990
|
-
* @param {boolean} [smooth] - Defaults to render3D.smoothShading
|
|
2991
|
-
* @return {Mesh} */
|
|
2992
|
-
buildMesh(smooth=render3D?.smoothShading)
|
|
2993
|
-
{
|
|
2994
|
-
return buildGrid(this.size, vec2(this.columns - 1, this.rows - 1),
|
|
2995
|
-
this.colors && ((x, z)=> this.getColor(x, z)), (x, z)=> this.getHeight(x, z), smooth);
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
|
|
2999
|
-
// read an image's pixel bytes through the engine's work canvas, as {data, width, height}
|
|
3000
|
-
function render3DImageData(image)
|
|
3001
|
-
{
|
|
3002
|
-
if (image instanceof TextureInfo)
|
|
3003
|
-
image = image.image;
|
|
3004
|
-
ASSERT(image && image.width && image.height, 'image is not loaded');
|
|
3005
|
-
ASSERT(workReadCanvas, 'reading an image needs a canvas, pass arrays in headless mode');
|
|
3006
|
-
const width = image.width, height = image.height;
|
|
3007
|
-
workReadCanvas.width = width;
|
|
3008
|
-
workReadCanvas.height = height;
|
|
3009
|
-
workReadContext.drawImage(image, 0, 0);
|
|
3010
|
-
return workReadContext.getImageData(0, 0, width, height);
|
|
3011
|
-
}
|
|
3012
|
-
|
|
3013
|
-
// read an image into a 2D array [row][column], sample is called with (r, g, b, a) bytes for each pixel
|
|
3014
|
-
function render3DImageToArray(image, sample)
|
|
3015
|
-
{
|
|
3016
|
-
const {data, width, height} = render3DImageData(image);
|
|
3017
|
-
const rows = [];
|
|
3018
|
-
for (let y = 0; y < height; ++y)
|
|
3019
|
-
{
|
|
3020
|
-
const row = rows[y] = [];
|
|
3021
|
-
for (let x = 0; x < width; ++x)
|
|
3022
|
-
{
|
|
3023
|
-
const k = (y * width + x) * 4;
|
|
3024
|
-
row.push(sample(data[k], data[k+1], data[k+2], data[k+3]));
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
return rows;
|
|
3028
|
-
}
|
|
3029
|
-
|
|
3030
|
-
// extruded glyph meshes by font, and the pixel bytes of a texture, read once per image
|
|
3031
|
-
const render3DGlyphCache = new WeakMap, render3DPixelCache = new WeakMap;
|
|
3032
|
-
function render3DReadPixels(textureInfo)
|
|
3033
|
-
{
|
|
3034
|
-
const image = textureInfo.image;
|
|
3035
|
-
let pixels = render3DPixelCache.get(image);
|
|
3036
|
-
if (!pixels)
|
|
3037
|
-
render3DPixelCache.set(image, pixels = render3DImageData(image));
|
|
3038
|
-
return pixels;
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
3221
|
///////////////////////////////////////////////////////////////////////////////
|
|
3042
3222
|
/**
|
|
3043
3223
|
* EngineObject3D - An EngineObject with a 3D transform and a mesh
|
|
@@ -3052,10 +3232,13 @@ function render3DReadPixels(textureInfo)
|
|
|
3052
3232
|
* - Set sync2D for a 2D game with 3D looks, pos and angle then drive pos3D and rotation3D,
|
|
3053
3233
|
* which is the one way those 2D fields reach a 3D object
|
|
3054
3234
|
* - setCollision takes the same flags as in 2D, but the solid collision happens in 3D against size3D
|
|
3235
|
+
* - The solid box is axis aligned in the world, rotation3D is ignored like angle is in 2D, so give a turned wall a
|
|
3236
|
+
* size3D along the world axes
|
|
3055
3237
|
* - Its tile and raycast halves are 2D only so they default off here, and a child sits solid collision out
|
|
3056
3238
|
* - A sync2D object collides in 2D instead, which needs the 2D size set as well as size3D
|
|
3057
3239
|
* - setMesh swaps the mesh and frees the old one, for text and terrain that get built again
|
|
3058
|
-
* - addChild attaches the 3D transform, and pos3D becomes an offset from the parent
|
|
3240
|
+
* - addChild attaches the 3D transform, and pos3D becomes an offset from the parent; attach keeps the child where
|
|
3241
|
+
* it is and works the offset out, and removeChild leaves it where it was in the world
|
|
3059
3242
|
* - The 2D offset arguments of addChild do nothing here, set the child's pos3D
|
|
3060
3243
|
* @extends EngineObject
|
|
3061
3244
|
* @memberof Render3D
|
|
@@ -3071,65 +3254,90 @@ class EngineObject3D extends EngineObject
|
|
|
3071
3254
|
/** Create a 3D object and add it to the object list
|
|
3072
3255
|
* @param {Vector3} [pos3D] - World space position
|
|
3073
3256
|
* @param {Mesh} [mesh] - Mesh to draw, undefined draws nothing
|
|
3074
|
-
* @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile; a whole TextureInfo becomes
|
|
3257
|
+
* @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile; a whole TextureInfo becomes
|
|
3258
|
+
* the tile that covers it
|
|
3075
3259
|
* @param {Color} [color] - Tint */
|
|
3076
3260
|
constructor(pos3D=vec3(), mesh, tileInfo, color=WHITE)
|
|
3077
3261
|
{
|
|
3078
|
-
ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
|
|
3262
|
+
ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
|
|
3263
|
+
'tileInfo must be a TileInfo or TextureInfo, it comes before color');
|
|
3079
3264
|
// a whole texture is stored as the tile that covers it, with no padding or bleed to trim
|
|
3080
3265
|
// the edges, so this is always a TileInfo like the 2D one and the object stays an EngineObject
|
|
3081
3266
|
if (tileInfo instanceof TextureInfo)
|
|
3082
|
-
tileInfo = new TileInfo(vec2(), tileInfo.size, tileInfo, 0, 0);
|
|
3083
|
-
|
|
3267
|
+
render3DWholeTiles.add(tileInfo = new TileInfo(vec2(), tileInfo.size, tileInfo, 0, 0));
|
|
3268
|
+
// the 2D pos starts where the object is, so turning on sync2D keeps it there
|
|
3269
|
+
super(vec2(pos3D.x, pos3D.y), vec2(), tileInfo, 0, color);
|
|
3084
3270
|
ASSERT(isVector3(pos3D), 'pos3D must be a vec3');
|
|
3085
3271
|
ASSERT(!mesh || mesh instanceof Mesh, 'mesh must be a Mesh or undefined');
|
|
3086
3272
|
this.mass = 0; // static: no 2D physics, and no 3D gravity until a mass is set
|
|
3087
3273
|
|
|
3088
3274
|
/** @property {Vector3} - World space position, local to the parent when attached to an EngineObject3D */
|
|
3089
3275
|
this.pos3D = pos3D.copy();
|
|
3090
|
-
/** @property {Vector3} - Rotation vec3(pitch, yaw, roll) in radians, local to the parent when attached to an
|
|
3276
|
+
/** @property {Vector3} - Rotation vec3(pitch, yaw, roll) in radians, local to the parent when attached to an
|
|
3277
|
+
* EngineObject3D */
|
|
3091
3278
|
this.rotation3D = vec3();
|
|
3092
3279
|
/** @property {Vector3} - Scale, local to the parent when attached to an EngineObject3D */
|
|
3093
3280
|
this.scale3D = vec3(1);
|
|
3094
|
-
/** @property {Vector3} - Added to pos3D each frame by the engine before update, like the 2D velocity, no super
|
|
3095
|
-
* damping and render3D.gravity act on it once the object has a mass */
|
|
3281
|
+
/** @property {Vector3} - Added to pos3D each frame by the engine before update, like the 2D velocity, no super
|
|
3282
|
+
* call needed; damping and render3D.gravity act on it once the object has a mass */
|
|
3096
3283
|
this.velocity3D = vec3();
|
|
3097
3284
|
/** @property {Vector3} - Added to rotation3D each frame by the engine before update, angleDamping is 2D only */
|
|
3098
3285
|
this.angleVelocity3D = vec3();
|
|
3099
3286
|
/** @property {Mesh|undefined} - Mesh to draw
|
|
3100
3287
|
* @type {Mesh|undefined} */
|
|
3101
3288
|
this.mesh = mesh;
|
|
3102
|
-
/** @property {Vector3} - Size for the collect and callback helpers, and of the sprite when
|
|
3103
|
-
* and no mesh;
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3289
|
+
/** @property {Vector3} - Size for solid collision and the collect and callback helpers, and of the sprite when
|
|
3290
|
+
* there is a tileInfo and no mesh; starts at the size of the mesh's box, or 1 with no mesh, and setMesh leaves
|
|
3291
|
+
* it as it is; the box is centered on pos3D, so center() a mesh whose origin is not its middle, like a model
|
|
3292
|
+
* standing on its feet, or set size3D; scale3D and any parent's scale grow it, so drawing and picking agree */
|
|
3293
|
+
// a shared mesh measured since it last changed is not walked again for each object made from it
|
|
3294
|
+
const bounds = mesh && mesh.points.length ? !mesh.dirty && mesh.bounds || mesh.getBounds() : undefined;
|
|
3295
|
+
this.size3D = bounds ? bounds.max.subtract(bounds.min) : vec3(1);
|
|
3296
|
+
/** @property {number} - Diameter of a soft shadow drawn under the object on render3D.softShadowHeight, 0 for
|
|
3297
|
+
* none; scale3D and a parent's scale grow it, so set it once for the unscaled object */
|
|
3107
3298
|
this.softShadow = 0;
|
|
3108
3299
|
/** @property {boolean} - A sprite stands on world up instead of tilting toward the camera */
|
|
3109
3300
|
this.upright = false;
|
|
3110
3301
|
/** @property {boolean} - Keep this object's texture pixels hard edged, for pixel art that should not blur or bleed */
|
|
3111
3302
|
this.pixelated = false;
|
|
3112
|
-
/** @property {boolean} - Copy the 2D pos and angle into pos3D and rotation3D each frame, for 2D games with 3D
|
|
3113
|
-
* set mass to use 2D physics, and pos3D.z stays yours to set or move with velocity3D.z */
|
|
3303
|
+
/** @property {boolean} - Copy the 2D pos and angle into pos3D and rotation3D each frame, for 2D games with 3D
|
|
3304
|
+
* looks; set mass to use 2D physics, and pos3D.z stays yours to set or move with velocity3D.z */
|
|
3114
3305
|
this.sync2D = false;
|
|
3115
|
-
/** @property {boolean} - Draw in the transparent stage, blended and sorted far to near with depth writes off;
|
|
3306
|
+
/** @property {boolean} - Draw in the transparent stage, blended and sorted far to near with depth writes off;
|
|
3307
|
+
* on for a sprite */
|
|
3116
3308
|
this.transparent = !mesh && !!tileInfo;
|
|
3117
3309
|
/** @property {boolean} - Additive blending, in the transparent stage */
|
|
3118
3310
|
this.additive = false;
|
|
3119
3311
|
/** @property {number} - How much it lights itself: 0 is lit as normal, 1 is its own color with no shading, for
|
|
3120
3312
|
* lamps and glowing things, between is partly self lit, and above 1 is brighter than its color, for bloom */
|
|
3121
3313
|
this.emissive = 0;
|
|
3122
|
-
/** @property {number} - Strength of the highlight where the
|
|
3314
|
+
/** @property {number} - Strength of the highlight where the sun and the Light3D objects reflect, 0 is none and
|
|
3315
|
+
* 1 adds a light's full color at its brightest; its size is fixed */
|
|
3123
3316
|
this.specular = 0;
|
|
3124
|
-
/** @property {boolean} - Draw into the shadow map when render3D.shadows is on; sprites and cut out textures
|
|
3317
|
+
/** @property {boolean} - Draw into the shadow map when render3D.shadows is on; sprites and cut out textures
|
|
3318
|
+
* cast their outline, an object faded below half its alpha casts nothing, a see through one casts only when
|
|
3319
|
+
* textured, and additive objects never cast */
|
|
3125
3320
|
this.castShadow = true;
|
|
3126
|
-
/** @property {boolean} - Collide as the sphere that fits size3D instead of as the size3D box, so it rolls
|
|
3321
|
+
/** @property {boolean} - Collide as the sphere that fits size3D instead of as the size3D box, so it rolls
|
|
3322
|
+
* around corners */
|
|
3127
3323
|
this.collideAsSphere3D = false;
|
|
3128
3324
|
/** @property {boolean} - Darkened by the shadow map when render3D.shadows is on */
|
|
3129
3325
|
this.receiveShadow = true;
|
|
3130
3326
|
/** @property {boolean|undefined} - Draw this object over the 2D scene, undefined uses render3D.renderAfter2D
|
|
3131
3327
|
* @type {boolean|undefined} */
|
|
3132
3328
|
this.renderAfter2D = undefined;
|
|
3329
|
+
/** @property {Matrix4|undefined} - The transform from its parent, used in place of pos3D, rotation3D and
|
|
3330
|
+
* scale3D when set, for one they cannot hold like a glTF pose with shear; read every frame it is set
|
|
3331
|
+
* @type {Matrix4|undefined} */
|
|
3332
|
+
this.localMatrix = undefined;
|
|
3333
|
+
// the world transform, kept up to date by render3DObjectMatrix; getMatrix returns a copy
|
|
3334
|
+
this.worldMatrix = new Matrix4;
|
|
3335
|
+
this.matrixBuilt = new Float64Array(9).fill(NaN); // the position, rotation and scale it was built from
|
|
3336
|
+
this.matrixVersion = 0; // counts the rebuilds, so a child knows when its parent's changed
|
|
3337
|
+
/** @type {EngineObject3D|undefined} */
|
|
3338
|
+
this.matrixParent = undefined; // the parent it was built under, and that parent's version then
|
|
3339
|
+
this.matrixParentVersion = 0;
|
|
3340
|
+
this.movePass = engineObjectsUpdateCount; // the engine pass a child last moved in, so a refresh is not a step
|
|
3133
3341
|
}
|
|
3134
3342
|
|
|
3135
3343
|
/** Move by the 3D velocities and push out of solids, called automatically each frame before update, like the 2D physics
|
|
@@ -3143,60 +3351,80 @@ class EngineObject3D extends EngineObject
|
|
|
3143
3351
|
'a sync2D object collides in 2D, so give it a 2D size as well as a size3D', this.size);
|
|
3144
3352
|
if (this.sync2D)
|
|
3145
3353
|
super.updatePhysics();
|
|
3354
|
+
// a moving object keeps out of the level, the height maps and voxel maps, from where it was before it moved
|
|
3355
|
+
const oldPos = this.collideLevel && this.mass && !this.sync2D ? this.pos3D.copy() : undefined;
|
|
3146
3356
|
render3DMove(this);
|
|
3357
|
+
oldPos && render3DCollideLevel(this, oldPos);
|
|
3147
3358
|
// the engine only runs this for objects that own where they are, a child rides along with its parent
|
|
3148
3359
|
if (this.collideSolidObjects && !this.sync2D)
|
|
3149
3360
|
render3DCollideSolid(this);
|
|
3150
3361
|
}
|
|
3151
3362
|
|
|
3152
3363
|
/** Move a child by its own velocities, bring a sync2D object's pos3D up to its 2D pos, then update the children,
|
|
3153
|
-
* called automatically each frame
|
|
3154
|
-
|
|
3364
|
+
* called automatically each frame
|
|
3365
|
+
* @param {boolean} [updateChildren] - Also update the children's transforms */
|
|
3366
|
+
updateTransforms(updateChildren=true)
|
|
3155
3367
|
{
|
|
3156
3368
|
if (!paused)
|
|
3157
3369
|
{
|
|
3158
|
-
// a child is never given updatePhysics, so it moves here, as an offset from its parent
|
|
3159
|
-
|
|
3160
|
-
if (this.
|
|
3161
|
-
|
|
3370
|
+
// a child is never given updatePhysics, so it moves here, as an offset from its parent: once per
|
|
3371
|
+
// engine pass, since addChild, attach and a game bring the transforms up to date too
|
|
3372
|
+
if (this.parent && this.movePass !== engineObjectsUpdateCount)
|
|
3373
|
+
{
|
|
3374
|
+
this.movePass = engineObjectsUpdateCount;
|
|
3375
|
+
render3DMove(this);
|
|
3376
|
+
}
|
|
3162
3377
|
}
|
|
3163
|
-
|
|
3378
|
+
|
|
3379
|
+
// placed from its parent first, so a sync2D object copies where it is now, then its children from it
|
|
3380
|
+
super.updateTransforms(false);
|
|
3381
|
+
if (!paused && this.sync2D)
|
|
3382
|
+
this.pos3D.x = this.pos.x, this.pos3D.y = this.pos.y, this.rotation3D.z = -this.angle;
|
|
3383
|
+
if (updateChildren)
|
|
3384
|
+
for (const child of this.children)
|
|
3385
|
+
child.updateTransforms();
|
|
3164
3386
|
}
|
|
3165
3387
|
|
|
3166
3388
|
/** Set how this object collides, the same flags as in 2D
|
|
3167
3389
|
* - Solid collision happens in 3D here, against size3D boxes or spheres; a child sits it out
|
|
3390
|
+
* - The boxes are axis aligned in the world, rotation3D is ignored
|
|
3168
3391
|
* - A sync2D object collides in 2D instead, against the 2D size, so set that as well as size3D
|
|
3169
3392
|
* @param {boolean} [collideSolidObjects] - Take part in solid collision
|
|
3170
3393
|
* @param {boolean} [isSolid] - Block other objects, a pair where neither one blocks passes through;
|
|
3171
3394
|
* blocking needs collideSolidObjects, so isSolid on its own is not allowed
|
|
3172
|
-
* @param {boolean} [
|
|
3395
|
+
* @param {boolean} [collideLevel] - Collide with the level, the height maps and voxel maps, or the 2D tile layers
|
|
3396
|
+
* for a sync2D object
|
|
3173
3397
|
* @param {boolean} [collideRaycast] - Raycasts, 2D only; 3D has render3D.pick and engineObjectsRaycast3D */
|
|
3174
|
-
setCollision(collideSolidObjects=true, isSolid=true,
|
|
3175
|
-
{ super.setCollision(collideSolidObjects, isSolid,
|
|
3398
|
+
setCollision(collideSolidObjects=true, isSolid=true, collideLevel=true, collideRaycast=false)
|
|
3399
|
+
{ super.setCollision(collideSolidObjects, isSolid, collideLevel, collideRaycast); }
|
|
3400
|
+
|
|
3401
|
+
/** Called by a VoxelMap to ask whether a block stops this object, a hook to let one through or react to it
|
|
3402
|
+
* @param {number} type - The block's type, 1 to 255
|
|
3403
|
+
* @param {Vector3} cell - The block's cell in the map
|
|
3404
|
+
* @return {boolean} - true to be stopped by it, every block stops it by default */
|
|
3405
|
+
collideWithVoxel(type, cell) { return true; }
|
|
3176
3406
|
|
|
3177
3407
|
/** Returns the world position
|
|
3178
3408
|
* @return {Vector3} */
|
|
3179
|
-
getWorldPos3D() { return this
|
|
3409
|
+
getWorldPos3D() { return render3DObjectMatrix(this).getTranslation(); }
|
|
3180
3410
|
|
|
3181
3411
|
/** Returns the direction the object faces, its -Z axis in the world
|
|
3182
3412
|
* @return {Vector3} */
|
|
3183
|
-
getForward3D() { return render3DAxis(this
|
|
3413
|
+
getForward3D() { return render3DAxis(render3DObjectMatrix(this).m, 8).normalize(-1); }
|
|
3184
3414
|
|
|
3185
3415
|
/** Returns the object's right axis in the world
|
|
3186
3416
|
* @return {Vector3} */
|
|
3187
|
-
getRight3D() { return render3DAxis(this
|
|
3417
|
+
getRight3D() { return render3DAxis(render3DObjectMatrix(this).m, 0).normalize(); }
|
|
3188
3418
|
|
|
3189
3419
|
/** Returns the object's up axis in the world
|
|
3190
3420
|
* @return {Vector3} */
|
|
3191
|
-
getUp3D() { return render3DAxis(this
|
|
3421
|
+
getUp3D() { return render3DAxis(render3DObjectMatrix(this).m, 4).normalize(); }
|
|
3192
3422
|
|
|
3193
|
-
/** Returns the object's world transform,
|
|
3423
|
+
/** Returns a copy of the object's world transform, the parent's included when attached to an EngineObject3D
|
|
3424
|
+
* - The object keeps its matrix and rebuilds it only when its position, rotation or scale changed, so this is
|
|
3425
|
+
* cheap to call
|
|
3194
3426
|
* @return {Matrix4} */
|
|
3195
|
-
getMatrix()
|
|
3196
|
-
{
|
|
3197
|
-
const matrix = buildMatrix(this.pos3D, this.rotation3D, this.scale3D);
|
|
3198
|
-
return this.parent instanceof EngineObject3D ? this.parent.getMatrix().multiply(matrix) : matrix;
|
|
3199
|
-
}
|
|
3427
|
+
getMatrix() { return render3DObjectMatrix(this).copy(); }
|
|
3200
3428
|
|
|
3201
3429
|
/** Turn the object so its -Z axis points at a world space target, sets pitch and yaw and clears roll
|
|
3202
3430
|
* @param {Vector3} target */
|
|
@@ -3208,6 +3436,59 @@ class EngineObject3D extends EngineObject
|
|
|
3208
3436
|
this.rotation3D = render3DLookRotation(local.subtract(this.pos3D), this.rotation3D);
|
|
3209
3437
|
}
|
|
3210
3438
|
|
|
3439
|
+
/** Attaches a child, its pos3D, rotation3D and scale3D taken as the offset from this one; returns child for chaining
|
|
3440
|
+
* - The 2D offset arguments do nothing for an EngineObject3D child, set its pos3D
|
|
3441
|
+
* @param {EngineObject} child
|
|
3442
|
+
* @param {Vector2} [localPos]
|
|
3443
|
+
* @param {number} [localAngle]
|
|
3444
|
+
* @return {EngineObject} The child object attached */
|
|
3445
|
+
addChild(child, localPos, localAngle)
|
|
3446
|
+
{
|
|
3447
|
+
// addChild brings the child's transform up to date, which is not a step: it already moved this pass as
|
|
3448
|
+
// a root, or moves in the next one, so it must not move by its velocity again here
|
|
3449
|
+
if (child instanceof EngineObject3D)
|
|
3450
|
+
child.movePass = engineObjectsUpdateCount;
|
|
3451
|
+
return super.addChild(child, localPos, localAngle);
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
/** Attaches a child without moving it: its pos3D, rotation3D and scale3D become what they have to be under this
|
|
3455
|
+
* parent to keep its world transform, where addChild takes them as the offset; returns child for chaining
|
|
3456
|
+
* - A parent scaled unevenly and a child turned under it make a shear, which those three values cannot hold,
|
|
3457
|
+
* so the child comes out as close as they can get; a uniform scale is exact
|
|
3458
|
+
* @param {EngineObject} child
|
|
3459
|
+
* @return {EngineObject} The child object attached */
|
|
3460
|
+
attach(child)
|
|
3461
|
+
{
|
|
3462
|
+
if (!(child instanceof EngineObject3D))
|
|
3463
|
+
return super.attach(child); // a 2D child only has the 2D transform to keep
|
|
3464
|
+
child.parent?.removeChild(child); // keeps its world values, so its own matrix is its world matrix
|
|
3465
|
+
const local = render3DObjectMatrix(this).copy().invert().multiply(render3DObjectMatrix(child));
|
|
3466
|
+
super.attach(child);
|
|
3467
|
+
child.pos3D = local.getTranslation();
|
|
3468
|
+
child.rotation3D = local.getRotation();
|
|
3469
|
+
child.scale3D = local.getScale();
|
|
3470
|
+
if (child.localMatrix)
|
|
3471
|
+
child.localMatrix = local; // a matrix given whole stays whole, shear and all
|
|
3472
|
+
return child;
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
/** Removes a child from this one, it stays where it is in the world: its pos3D, rotation3D and scale3D become
|
|
3476
|
+
* its world values, with the same shear caveat as attach; a child being destroyed is let go as it is
|
|
3477
|
+
* @param {EngineObject} child */
|
|
3478
|
+
removeChild(child)
|
|
3479
|
+
{
|
|
3480
|
+
if (child instanceof EngineObject3D && !child.destroyed)
|
|
3481
|
+
{
|
|
3482
|
+
const world = render3DObjectMatrix(child);
|
|
3483
|
+
child.pos3D = world.getTranslation();
|
|
3484
|
+
child.rotation3D = world.getRotation();
|
|
3485
|
+
child.scale3D = world.getScale();
|
|
3486
|
+
if (child.localMatrix)
|
|
3487
|
+
child.localMatrix = world.copy(); // a matrix given whole stays whole, shear and all
|
|
3488
|
+
}
|
|
3489
|
+
super.removeChild(child);
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3211
3492
|
/** Draw a different mesh and free the GPU buffer of the one it replaces
|
|
3212
3493
|
* - For a mesh built again when something changes, like a score, a rebuilt terrain or a loaded model
|
|
3213
3494
|
* - A mesh another object is still drawing is left alone, since builders are often shared
|
|
@@ -3220,7 +3501,7 @@ class EngineObject3D extends EngineObject
|
|
|
3220
3501
|
const old = this.mesh;
|
|
3221
3502
|
this.mesh = mesh;
|
|
3222
3503
|
// nothing to free and nothing to look for when it was never uploaded
|
|
3223
|
-
if (old && old !== mesh && old.buffer && !engineObjects.some(o=> o.mesh === old))
|
|
3504
|
+
if (old && old !== mesh && old.buffer && !engineObjects.some(o=> /** @type {EngineObject3D} */ (o).mesh === old))
|
|
3224
3505
|
old.dispose();
|
|
3225
3506
|
return mesh;
|
|
3226
3507
|
}
|
|
@@ -3228,37 +3509,95 @@ class EngineObject3D extends EngineObject
|
|
|
3228
3509
|
/** 2D rendering is skipped, the mesh is drawn by render3D during the 3D pass */
|
|
3229
3510
|
render() {}
|
|
3230
3511
|
|
|
3231
|
-
/** Draw the object in 3D, called by the 3D pass with the draw state set from this object's flags, draws the mesh by default
|
|
3512
|
+
/** Draw the object in 3D, called by the 3D pass with the draw state set from this object's flags, draws the mesh by default
|
|
3513
|
+
* @return {void} */
|
|
3232
3514
|
render3D()
|
|
3233
3515
|
{
|
|
3234
3516
|
// an opaque draw comes out solid however low its alpha is, so a fade with no flag looks like nothing happened
|
|
3235
|
-
ASSERT(this.transparent || this.additive || this.color.a >= 1,
|
|
3517
|
+
ASSERT(this.transparent || this.additive || this.color.a >= 1,
|
|
3518
|
+
'an object that fades needs its transparent flag, an opaque draw ignores the color alpha', this.color);
|
|
3519
|
+
// the matrix the object keeps, rebuilt only when it moved, the same one for the shadow pass and the main pass
|
|
3520
|
+
const matrix = render3DObjectMatrix(this);
|
|
3236
3521
|
if (this.mesh)
|
|
3237
|
-
render3D.drawMesh(this.mesh,
|
|
3522
|
+
render3D.drawMesh(this.mesh, matrix, this.tileInfo, this.color);
|
|
3238
3523
|
else if (this.tileInfo)
|
|
3239
3524
|
{
|
|
3240
|
-
// a sprite
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
vec2(this.size3D.x * hypot(m[0], m[1], m[2]), this.size3D.y * hypot(m[4], m[5], m[6])),
|
|
3245
|
-
this.tileInfo, this.color, this.rotation3D.z, this.upright);
|
|
3525
|
+
// a sprite, at the world size the collect, pick and solid collision helpers measure it at
|
|
3526
|
+
const m = matrix.m, size = render3DWorldSize(this, m);
|
|
3527
|
+
render3D.drawBillboard(vec3(m[12], m[13], m[14]), vec2(size.x, size.y), this.tileInfo, this.color,
|
|
3528
|
+
this.rotation3D.z, this.upright);
|
|
3246
3529
|
}
|
|
3247
3530
|
}
|
|
3248
3531
|
}
|
|
3249
3532
|
|
|
3533
|
+
// an object's world matrix, the one it keeps: rebuilt only when its position, rotation or scale changed since the
|
|
3534
|
+
// last build, or its parent's matrix did, so an object that stands still costs nine compares a frame instead of
|
|
3535
|
+
// the trig and a new matrix; the matrix returned is the object's own, read it and never change it
|
|
3536
|
+
const render3DLocalMatrix = new Matrix4;
|
|
3537
|
+
function render3DObjectMatrix(o)
|
|
3538
|
+
{
|
|
3539
|
+
const parent = o.parent instanceof EngineObject3D ? o.parent : undefined;
|
|
3540
|
+
const parentMatrix = parent && render3DObjectMatrix(parent); // the parent first, so its version is current
|
|
3541
|
+
const p = o.pos3D, r = o.rotation3D, s = o.scale3D, k = o.matrixBuilt, local = o.localMatrix;
|
|
3542
|
+
if (local)
|
|
3543
|
+
{
|
|
3544
|
+
// a matrix given whole is taken as it is each time, it can change in place
|
|
3545
|
+
if (parent)
|
|
3546
|
+
{
|
|
3547
|
+
o.worldMatrix.m.set(parentMatrix.m);
|
|
3548
|
+
o.worldMatrix.multiply(local);
|
|
3549
|
+
o.matrixParentVersion = parent.matrixVersion;
|
|
3550
|
+
}
|
|
3551
|
+
else
|
|
3552
|
+
o.worldMatrix.m.set(local.m);
|
|
3553
|
+
k[0] = NaN; // built from the matrix, so going back to pos3D builds again
|
|
3554
|
+
o.matrixParent = parent;
|
|
3555
|
+
++o.matrixVersion;
|
|
3556
|
+
}
|
|
3557
|
+
else if (k[0] !== p.x || k[1] !== p.y || k[2] !== p.z || k[3] !== r.x || k[4] !== r.y || k[5] !== r.z
|
|
3558
|
+
|| k[6] !== s.x || k[7] !== s.y || k[8] !== s.z || o.matrixParent !== parent
|
|
3559
|
+
|| parent && o.matrixParentVersion !== parent.matrixVersion)
|
|
3560
|
+
{
|
|
3561
|
+
k[0] = p.x, k[1] = p.y, k[2] = p.z, k[3] = r.x, k[4] = r.y, k[5] = r.z, k[6] = s.x, k[7] = s.y, k[8] = s.z;
|
|
3562
|
+
if (parent)
|
|
3563
|
+
{
|
|
3564
|
+
// the parent's world matrix times the local one
|
|
3565
|
+
buildMatrix(p, r, s, render3DLocalMatrix);
|
|
3566
|
+
o.worldMatrix.m.set(parentMatrix.m);
|
|
3567
|
+
o.worldMatrix.multiply(render3DLocalMatrix);
|
|
3568
|
+
o.matrixParentVersion = parent.matrixVersion;
|
|
3569
|
+
}
|
|
3570
|
+
else
|
|
3571
|
+
buildMatrix(p, r, s, o.worldMatrix);
|
|
3572
|
+
o.matrixParent = parent;
|
|
3573
|
+
++o.matrixVersion;
|
|
3574
|
+
}
|
|
3575
|
+
return o.worldMatrix;
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3250
3578
|
// move an object by its 3D velocities, an object with mass falling with render3D.gravity and slowing by its damping
|
|
3251
3579
|
function render3DMove(o)
|
|
3252
3580
|
{
|
|
3581
|
+
// the vectors change in place, as the 2D object's do: this runs for every object every frame
|
|
3582
|
+
const p = o.pos3D, v = o.velocity3D, r = o.rotation3D, a = o.angleVelocity3D;
|
|
3253
3583
|
if (o.mass && !o.sync2D) // a 2D driven object gets the 2D gravity instead
|
|
3254
3584
|
{
|
|
3255
3585
|
// damped first and gravity added after, the order EngineObject.updatePhysics uses,
|
|
3256
3586
|
// so the same mass, damping and gravity fall the same way in both
|
|
3257
|
-
const
|
|
3258
|
-
|
|
3587
|
+
const g = render3D.gravity, s = o.gravityScale, d = o.damping;
|
|
3588
|
+
v.x = v.x * d + g.x * s, v.y = v.y * d + g.y * s, v.z = v.z * d + g.z * s;
|
|
3259
3589
|
}
|
|
3260
|
-
|
|
3261
|
-
|
|
3590
|
+
p.x += v.x, p.y += v.y, p.z += v.z;
|
|
3591
|
+
r.x += a.x, r.y += a.y, r.z += a.z;
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
// keep an object that moved out of the level's solid geometry, clearing what it stood on for the level to set again
|
|
3595
|
+
function render3DCollideLevel(o, oldPos)
|
|
3596
|
+
{
|
|
3597
|
+
const ground = o.groundObject;
|
|
3598
|
+
o.groundObject = undefined;
|
|
3599
|
+
for (const level of render3DLevel)
|
|
3600
|
+
level.destroyed || level.levelCollide3D(o, oldPos, level === ground);
|
|
3262
3601
|
}
|
|
3263
3602
|
|
|
3264
3603
|
// where a solid object is in the world and what it collides as: the sphere that fits size3D, or the size3D box,
|
|
@@ -3314,7 +3653,8 @@ function render3DCollideSolid(a)
|
|
|
3314
3653
|
for (const b of engineObjectsCollide)
|
|
3315
3654
|
{
|
|
3316
3655
|
if (b === a) break;
|
|
3317
|
-
|
|
3656
|
+
// a child is part of its parent
|
|
3657
|
+
if (b.destroyed || !(b instanceof EngineObject3D) || b.parent || b.sync2D) continue;
|
|
3318
3658
|
if (!a.isSolid && !b.isSolid) continue; // neither one blocks, so they pass through each other
|
|
3319
3659
|
|
|
3320
3660
|
// the pairs nowhere near each other are almost all of them in a scene of any size, so
|
|
@@ -3340,48 +3680,118 @@ function render3DCollideSolid(a)
|
|
|
3340
3680
|
b.pos3D = b.pos3D.subtract(push.scale(weightB));
|
|
3341
3681
|
if (weightA)
|
|
3342
3682
|
shapeA = render3DSolidShape(a); // it moved, so the next solid must be tested against where it is now
|
|
3683
|
+
// mass 0 keeps its velocity too, so a moving platform keeps moving, and what hits it bounces by its own
|
|
3684
|
+
// restitution as it would off a static wall
|
|
3343
3685
|
const normal = push.normalize();
|
|
3344
|
-
if (a.velocity3D.dot(normal) < 0)
|
|
3686
|
+
if (weightA && a.velocity3D.dot(normal) < 0)
|
|
3345
3687
|
a.velocity3D = a.velocity3D.reflect(normal, a.restitution);
|
|
3346
|
-
if (b.velocity3D.dot(normal) > 0)
|
|
3688
|
+
if (weightB && b.velocity3D.dot(normal) > 0)
|
|
3347
3689
|
b.velocity3D = b.velocity3D.reflect(normal, b.restitution);
|
|
3348
3690
|
}
|
|
3349
3691
|
}
|
|
3350
3692
|
|
|
3351
3693
|
/**
|
|
3352
|
-
* Collect the EngineObject3D objects whose boxes overlap a box,
|
|
3694
|
+
* Collect the EngineObject3D objects whose boxes overlap a sphere or a box, the 3D twin of engineObjectsCollect
|
|
3353
3695
|
* - Boxes are axis aligned around the world position, rotation3D is ignored; lights, emitters and trails have no size
|
|
3354
|
-
*
|
|
3355
|
-
* @param {Vector3
|
|
3696
|
+
* and are never collected
|
|
3697
|
+
* @param {Vector3} pos - Center of the area
|
|
3698
|
+
* @param {Vector3|number} size - Diameter of a sphere if a number, 0 for a point, full size of a box if a Vector3
|
|
3356
3699
|
* @param {Array<EngineObject>} [objects] - Defaults to every object
|
|
3700
|
+
* @param {boolean} [testCenters] - Test only each object's center, a little faster, and ignores object sizes
|
|
3357
3701
|
* @return {Array<EngineObject3D>}
|
|
3358
3702
|
* @memberof Render3D
|
|
3359
3703
|
*/
|
|
3360
|
-
function engineObjectsCollect3D(pos, size, objects=engineObjects)
|
|
3704
|
+
function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=false)
|
|
3361
3705
|
{
|
|
3362
|
-
size
|
|
3706
|
+
// a size of 0 is a point, tested against each box, since a sphere of no size could never hit
|
|
3707
|
+
const radiusSquared = typeof size === 'number' && size > 0 ? (size/2)**2 : undefined;
|
|
3708
|
+
const box = radiusSquared ? undefined : typeof size === 'number' ? vec3() : render3DSize3(size);
|
|
3363
3709
|
const collected = [];
|
|
3364
3710
|
for (const o of objects)
|
|
3365
3711
|
{
|
|
3366
3712
|
if (!(o instanceof EngineObject3D) || o.destroyed) continue;
|
|
3367
|
-
|
|
3713
|
+
// the box in world space, scaled by the object and its parents
|
|
3714
|
+
const m = render3DObjectMatrix(o).m, s = o.size3D;
|
|
3368
3715
|
if (!(s.x || s.y || s.z)) continue;
|
|
3369
|
-
const
|
|
3370
|
-
|
|
3371
|
-
|
|
3716
|
+
const center = vec3(m[12], m[13], m[14]);
|
|
3717
|
+
const worldSize = testCenters ? vec3() : render3DWorldSize(o, m);
|
|
3718
|
+
let hit;
|
|
3719
|
+
if (box)
|
|
3720
|
+
hit = isOverlapping3D(pos, box, center, worldSize);
|
|
3721
|
+
else
|
|
3722
|
+
{
|
|
3723
|
+
// a sphere against the nearest point of the box
|
|
3724
|
+
const dx = max(abs(pos.x - center.x) - worldSize.x/2, 0);
|
|
3725
|
+
const dy = max(abs(pos.y - center.y) - worldSize.y/2, 0);
|
|
3726
|
+
const dz = max(abs(pos.z - center.z) - worldSize.z/2, 0);
|
|
3727
|
+
hit = dx*dx + dy*dy + dz*dz < radiusSquared;
|
|
3728
|
+
}
|
|
3729
|
+
hit && collected.push(o);
|
|
3372
3730
|
}
|
|
3373
3731
|
return collected;
|
|
3374
3732
|
}
|
|
3375
3733
|
|
|
3376
|
-
// how far along a ray an object is hit, or undefined for a miss; each one is tested as
|
|
3377
|
-
//
|
|
3734
|
+
// how far along a ray an object is hit, or undefined for a miss; each one is tested as the box around its mesh in
|
|
3735
|
+
// its own space, or a sprite as the quad it draws, not triangle by triangle
|
|
3378
3736
|
function render3DRaycastObject(ray, o)
|
|
3379
3737
|
{
|
|
3380
|
-
if (o.destroyed || !(o instanceof EngineObject3D)
|
|
3381
|
-
|
|
3382
|
-
|
|
3738
|
+
if (o.destroyed || !(o instanceof EngineObject3D)) return;
|
|
3739
|
+
// a height map or voxel map is hit on its surface, not its box
|
|
3740
|
+
if (o instanceof HeightMap || o instanceof VoxelMap) return o.levelRaycast3D(ray);
|
|
3741
|
+
if (!(o.mesh || o.tileInfo)) return;
|
|
3742
|
+
if (o instanceof InstancedMesh3D) return; // its instances are not objects, and its one sphere is not a thing to hit
|
|
3743
|
+
const matrix = render3DObjectMatrix(o), mesh = o.mesh;
|
|
3744
|
+
if (!mesh) return render3DRaycastSprite(ray, o, matrix);
|
|
3745
|
+
// a mesh that changed since it was measured is measured again, an upload may not have come yet
|
|
3746
|
+
const radius = (mesh.dirty || !mesh.radius ? mesh.computeRadius() : mesh.radius) * render3DMaxStretch(matrix.m);
|
|
3383
3747
|
if (!(radius > 0)) return; // nothing to hit
|
|
3384
|
-
|
|
3748
|
+
const center = matrix.getTranslation();
|
|
3749
|
+
const distance = raycastSphere(ray, center, radius);
|
|
3750
|
+
if (distance === undefined) return;
|
|
3751
|
+
|
|
3752
|
+
// the sphere is a quick reject, a mesh is hit where the ray meets its box in its own space, since a wide floor's
|
|
3753
|
+
// sphere reaches far above it; the direction is not made unit length, so the distance holds in the world;
|
|
3754
|
+
// a mesh flattened to nothing on an axis has no inverse, it is hit as a disc like a sprite
|
|
3755
|
+
if (!matrix.determinant()) return render3DRaycastDisc(ray, center, radius);
|
|
3756
|
+
const inverse = matrix.copy().invert(), bounds = mesh.bounds || mesh.getBounds();
|
|
3757
|
+
const local = new Ray3D(inverse.transformPoint(ray.origin), inverse.transformDirection(ray.direction));
|
|
3758
|
+
const hit = raycastBox(local, bounds.min.add(bounds.max).scale(.5), bounds.max.subtract(bounds.min));
|
|
3759
|
+
if (hit !== 0) return hit;
|
|
3760
|
+
|
|
3761
|
+
// it starts inside the box, like a camera on terrain or in a room, so it is hit where it leaves, and what stands
|
|
3762
|
+
// inside comes first
|
|
3763
|
+
let exit = Infinity;
|
|
3764
|
+
for (const k of ['x', 'y', 'z'])
|
|
3765
|
+
{
|
|
3766
|
+
const d = local.direction[k];
|
|
3767
|
+
if (d)
|
|
3768
|
+
exit = min(exit, ((d > 0 ? bounds.max[k] : bounds.min[k]) - local.origin[k]) / d);
|
|
3769
|
+
}
|
|
3770
|
+
return exit === Infinity ? 0 : exit; // a ray of no length is where it starts
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
// a sprite is the quad drawBillboard draws, its size3D grown by the x and y scale as the draw does, facing the camera
|
|
3774
|
+
// as it was last drawn; where the ray meets its plane, inside its half axes
|
|
3775
|
+
function render3DRaycastSprite(ray, o, matrix)
|
|
3776
|
+
{
|
|
3777
|
+
const center = matrix.getTranslation(), worldSize = render3DWorldSize(o, matrix.m), size = vec2(worldSize.x, worldSize.y);
|
|
3778
|
+
if (!(size.x > 0 && size.y > 0)) return; // nothing to hit
|
|
3779
|
+
if (!ray.direction.lengthSquared()) // a ray of no length is where it starts
|
|
3780
|
+
return render3DRaycastDisc(ray, center, hypot(size.x, size.y) / 2);
|
|
3781
|
+
const a = render3DBillboardAxes(size, o.rotation3D.z, o.upright);
|
|
3782
|
+
const right = vec3(a[0], a[1], a[2]), up = vec3(a[3], a[4], a[5]);
|
|
3783
|
+
const t = raycastPlane(ray, center, right.cross(up));
|
|
3784
|
+
if (t === undefined) return; // behind, or seen edge on
|
|
3785
|
+
const p = ray.getPosition(t).subtract(center);
|
|
3786
|
+
return abs(p.dot(right)) <= right.lengthSquared() && abs(p.dot(up)) <= up.lengthSquared() ? t : undefined;
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
// a mesh flattened to nothing, or a sprite for a ray of no length: a disc facing the ray at its center's depth
|
|
3790
|
+
function render3DRaycastDisc(ray, center, radius)
|
|
3791
|
+
{
|
|
3792
|
+
const d = ray.direction, oc = center.subtract(ray.origin), dd = d.dot(d);
|
|
3793
|
+
const t = dd ? oc.dot(d) / dd : 0; // its depth along the ray, in the ray's own units
|
|
3794
|
+
return t >= 0 && oc.subtract(d.scale(t)).lengthSquared() <= radius*radius ? t : undefined;
|
|
3385
3795
|
}
|
|
3386
3796
|
|
|
3387
3797
|
/**
|
|
@@ -3399,28 +3809,206 @@ function engineObjectsRaycast3D(ray, objects=engineObjects)
|
|
|
3399
3809
|
for (const o of objects)
|
|
3400
3810
|
{
|
|
3401
3811
|
const distance = render3DRaycastObject(ray, o);
|
|
3402
|
-
if (distance !== undefined)
|
|
3403
|
-
hits.push({o, distance});
|
|
3812
|
+
if (distance !== undefined) // only a 3D object has a distance
|
|
3813
|
+
hits.push({o: /** @type {EngineObject3D} */ (o), distance});
|
|
3404
3814
|
}
|
|
3405
3815
|
return hits.sort((a, b)=> a.distance - b.distance).map(hit=> hit.o);
|
|
3406
3816
|
}
|
|
3407
3817
|
|
|
3408
3818
|
/**
|
|
3409
|
-
* Call a function for each EngineObject3D whose box overlaps a box
|
|
3410
|
-
*
|
|
3411
|
-
* @param {Vector3
|
|
3412
|
-
* @param {
|
|
3819
|
+
* Call a function for each EngineObject3D whose box overlaps a sphere or a box
|
|
3820
|
+
* - An object destroyed by an earlier callback is skipped
|
|
3821
|
+
* @param {Vector3} pos - Center of the area
|
|
3822
|
+
* @param {Vector3|number} size - Diameter of a sphere if a number, 0 for a point, full size of a box if a Vector3
|
|
3823
|
+
* @param {function(EngineObject3D): void} callback
|
|
3413
3824
|
* @param {Array<EngineObject>} [objects] - Defaults to every object
|
|
3825
|
+
* @param {boolean} [testCenters] - Test only each object's center, see engineObjectsCollect3D
|
|
3414
3826
|
* @memberof Render3D
|
|
3415
3827
|
*/
|
|
3416
|
-
function engineObjectsCallback3D(pos, size, callback, objects=engineObjects)
|
|
3417
|
-
{
|
|
3828
|
+
function engineObjectsCallback3D(pos, size, callback, objects=engineObjects, testCenters=false)
|
|
3829
|
+
{
|
|
3830
|
+
for (const o of engineObjectsCollect3D(pos, size, objects, testCenters))
|
|
3831
|
+
o.destroyed || callback(o);
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3835
|
+
/**
|
|
3836
|
+
* InstancedMesh3D - Many copies of one mesh drawn as one call, with their transforms kept on the GPU
|
|
3837
|
+
* - For big sets that mostly stay put: an instance costs nothing per frame until it changes, so a hundred thousand
|
|
3838
|
+
* trees cost what one tree does; objects and drawMesh batch by themselves too, but rebuild their batch every frame
|
|
3839
|
+
* - setMatrixAt and setColorAt change one instance, and only the changed range uploads before the next draw
|
|
3840
|
+
* - The instances are in world space; the object's own pos3D, rotation3D and scale3D do not move them
|
|
3841
|
+
* - The whole set is culled by one bounding sphere around the origin, and casts and receives shadows like any object
|
|
3842
|
+
* - The object's flags cover the whole set, one emissive, one tileInfo, one shader; only the colors are per instance
|
|
3843
|
+
* - A mirrored instance, one with a negative scale, shows its inside unless the mesh is doubleSided
|
|
3844
|
+
* - A transparent set draws in one go in the transparent stage, its instances are not sorted against each other;
|
|
3845
|
+
* the set sorts against other transparent draws by the object's position, so put pos3D at its middle
|
|
3846
|
+
* - pick, the raycast and the collect helpers do not see the instances, test them yourself from instanceData
|
|
3847
|
+
* @extends EngineObject3D
|
|
3848
|
+
* @memberof Render3D
|
|
3849
|
+
* @example
|
|
3850
|
+
* const forest = new InstancedMesh3D(treeMesh, 1000);
|
|
3851
|
+
* for (let i = 0; i < 1000; ++i)
|
|
3852
|
+
* forest.setMatrixAt(i, buildMatrix(randomGroundPos(), vec3(0, rand(2*PI), 0)));
|
|
3853
|
+
*/
|
|
3854
|
+
class InstancedMesh3D extends EngineObject3D
|
|
3855
|
+
{
|
|
3856
|
+
/** Create a set of instances of a mesh, each at the origin in the object's color until it is set
|
|
3857
|
+
* @param {Mesh} mesh
|
|
3858
|
+
* @param {number} count - How many instances there is room for, all of them draw until count is lowered
|
|
3859
|
+
* @param {TileInfo|TextureInfo} [tileInfo] - Texture for all of them
|
|
3860
|
+
* @param {Color} [color] - The color they start with */
|
|
3861
|
+
constructor(mesh, count, tileInfo, color=WHITE)
|
|
3862
|
+
{
|
|
3863
|
+
super(vec3(), mesh, tileInfo, color);
|
|
3864
|
+
ASSERT(mesh instanceof Mesh, 'an InstancedMesh3D needs a Mesh');
|
|
3865
|
+
ASSERT(count >= 1, 'an InstancedMesh3D needs room for at least one instance');
|
|
3866
|
+
this.size3D = vec3(); // not a solid thing to pick or collect
|
|
3867
|
+
/** @property {number} - How many instances draw, the first ones, up to the count it was made with */
|
|
3868
|
+
this.count = count;
|
|
3869
|
+
/** @property {number} - How many instances it was made with */
|
|
3870
|
+
this.maxCount = count;
|
|
3871
|
+
/** @property {Float32Array} - The per instance values the shader reads, 24 floats each: the matrix, the color
|
|
3872
|
+
* and the uv rect; edit it directly and call markDirty for the instances changed */
|
|
3873
|
+
this.instanceData = new Float32Array(count * RENDER3D_INSTANCE_FLOATS);
|
|
3874
|
+
/** @property {number} - Radius of the sphere around the origin that holds every instance set so far, for
|
|
3875
|
+
* culling; from the farthest instance and the largest scale, and the mesh's size when it draws */
|
|
3876
|
+
this.radius = 0;
|
|
3877
|
+
this.reach = 0; // the farthest any instance's position has been from the origin
|
|
3878
|
+
this.maxScale = 0; // and the largest scale any instance has had
|
|
3879
|
+
/** @property {number} - First instance to upload before the next draw */
|
|
3880
|
+
this.dirtyStart = 0;
|
|
3881
|
+
/** @property {number} - One past the last instance to upload, so nothing uploads when it is not past dirtyStart */
|
|
3882
|
+
this.dirtyEnd = count;
|
|
3883
|
+
this.buffer = undefined; // the GPU copy of instanceData
|
|
3884
|
+
this.bufferGeneration = -1; // the context it was made under
|
|
3885
|
+
this.uvTileInfo = tileInfo; // the tile the uv rects were written for
|
|
3886
|
+
const uv = render3DGetTileUVs(tileInfo), data = this.instanceData;
|
|
3887
|
+
for (let i = 0; i < count; ++i)
|
|
3888
|
+
{
|
|
3889
|
+
this.setMatrixAt(i, RENDER3D_IDENTITY);
|
|
3890
|
+
const k = i * RENDER3D_INSTANCE_FLOATS;
|
|
3891
|
+
data[k+16] = color.r; data[k+17] = color.g; data[k+18] = color.b; data[k+19] = color.a;
|
|
3892
|
+
data[k+20] = uv.x; data[k+21] = uv.y; data[k+22] = uv.w; data[k+23] = uv.h;
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
/** Place an instance, in world space
|
|
3897
|
+
* @param {number} i
|
|
3898
|
+
* @param {Matrix4} matrix */
|
|
3899
|
+
setMatrixAt(i, matrix)
|
|
3900
|
+
{
|
|
3901
|
+
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
3902
|
+
const data = this.instanceData, k = i * RENDER3D_INSTANCE_FLOATS, m = matrix.m;
|
|
3903
|
+
data.set(m, k);
|
|
3904
|
+
this.markDirty(i);
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
/** The matrix of an instance
|
|
3908
|
+
* @param {number} i
|
|
3909
|
+
* @return {Matrix4} */
|
|
3910
|
+
getMatrixAt(i)
|
|
3911
|
+
{
|
|
3912
|
+
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
3913
|
+
const k = i * RENDER3D_INSTANCE_FLOATS, matrix = new Matrix4;
|
|
3914
|
+
matrix.m.set(this.instanceData.subarray(k, k + 16));
|
|
3915
|
+
return matrix;
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
/** Color an instance
|
|
3919
|
+
* @param {number} i
|
|
3920
|
+
* @param {Color} color */
|
|
3921
|
+
setColorAt(i, color)
|
|
3922
|
+
{
|
|
3923
|
+
ASSERT(i >= 0 && i < this.maxCount, 'instance index out of range');
|
|
3924
|
+
ASSERT(isColor(color), 'color must be a Color');
|
|
3925
|
+
const data = this.instanceData, k = i * RENDER3D_INSTANCE_FLOATS;
|
|
3926
|
+
data[k+16] = color.r; data[k+17] = color.g; data[k+18] = color.b; data[k+19] = color.a;
|
|
3927
|
+
this.markDirty(i);
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
/** Note that an instance changed, so it uploads before the next draw and the bounds hold it; setMatrixAt and
|
|
3931
|
+
* setColorAt call this, and so must an edit made straight to instanceData
|
|
3932
|
+
* @param {number} i */
|
|
3933
|
+
markDirty(i)
|
|
3934
|
+
{
|
|
3935
|
+
this.dirtyStart = min(this.dirtyStart, i);
|
|
3936
|
+
this.dirtyEnd = max(this.dirtyEnd, i + 1);
|
|
3937
|
+
|
|
3938
|
+
// the bounds grow to hold where it is now and how big, read back from the matrix it has
|
|
3939
|
+
const d = this.instanceData, k = i * RENDER3D_INSTANCE_FLOATS;
|
|
3940
|
+
this.reach = max(this.reach, hypot(d[k+12], d[k+13], d[k+14]));
|
|
3941
|
+
this.maxScale = max(this.maxScale, render3DMaxStretch(d, k));
|
|
3942
|
+
const meshRadius = this.mesh.radius || this.mesh.computeRadius(); // measured once, the draw takes a new size up
|
|
3943
|
+
this.radius = this.reach + meshRadius * this.maxScale;
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
/** Draws every instance as one call, uploading the ones that changed first
|
|
3947
|
+
* @return {void} */
|
|
3948
|
+
render3D()
|
|
3949
|
+
{
|
|
3950
|
+
const r = render3D, gl = glContext, mesh = this.mesh;
|
|
3951
|
+
ASSERT(this.count >= 0 && this.count <= this.maxCount, 'count must be within the count it was made with');
|
|
3952
|
+
if (r.transparentQueue) // sorted as one thing with the other transparent draws
|
|
3953
|
+
return r.queueTransparent(this.getWorldPos3D(), ()=> this.render3D());
|
|
3954
|
+
if (!mesh || !this.count || !render3DCanDraw()) return;
|
|
3955
|
+
if (r.shadowPass && !r.lighting) return; // unlit things cast no shadow
|
|
3956
|
+
render3DMeshUpload(mesh);
|
|
3957
|
+
if (!mesh.bufferCount) return;
|
|
3958
|
+
this.radius = this.reach + mesh.radius * this.maxScale; // the mesh may have grown since
|
|
3959
|
+
if (r.frustumCulling && !render3DSphereVisible(0, 0, 0, this.radius)) return;
|
|
3960
|
+
|
|
3961
|
+
// the uv rect is the object's tile for every instance, rewritten when the tile changes
|
|
3962
|
+
if (this.uvTileInfo !== this.tileInfo)
|
|
3963
|
+
{
|
|
3964
|
+
const uv = render3DGetTileUVs(this.tileInfo), data = this.instanceData;
|
|
3965
|
+
for (let k = 20; k < data.length; k += RENDER3D_INSTANCE_FLOATS)
|
|
3966
|
+
data[k] = uv.x, data[k+1] = uv.y, data[k+2] = uv.w, data[k+3] = uv.h;
|
|
3967
|
+
this.uvTileInfo = this.tileInfo;
|
|
3968
|
+
this.dirtyStart = 0, this.dirtyEnd = this.maxCount;
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
// the GPU copy: all of it under a fresh context, otherwise just the changed range
|
|
3972
|
+
if (!this.buffer || this.bufferGeneration !== r.contextGeneration)
|
|
3973
|
+
{
|
|
3974
|
+
this.buffer = gl.createBuffer();
|
|
3975
|
+
this.bufferGeneration = r.contextGeneration;
|
|
3976
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
3977
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.instanceData, gl.DYNAMIC_DRAW);
|
|
3978
|
+
}
|
|
3979
|
+
else if (this.dirtyEnd > this.dirtyStart)
|
|
3980
|
+
{
|
|
3981
|
+
const start = this.dirtyStart * RENDER3D_INSTANCE_FLOATS, end = this.dirtyEnd * RENDER3D_INSTANCE_FLOATS;
|
|
3982
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
3983
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, start * 4, this.instanceData, start, end - start);
|
|
3984
|
+
}
|
|
3985
|
+
this.dirtyStart = Infinity, this.dirtyEnd = 0;
|
|
3986
|
+
|
|
3987
|
+
// one draw under the object's state, the mesh setting the culling as drawMesh does
|
|
3988
|
+
r.flush();
|
|
3989
|
+
render3DFlushBeforeOverlay(); // as in drawMesh, what was drawn before goes under it
|
|
3990
|
+
const cullBackFaces = r.cullBackFaces, tileInfo = this.tileInfo;
|
|
3991
|
+
r.cullBackFaces = !mesh.doubleSided;
|
|
3992
|
+
render3DDrawInstanced(mesh, this.buffer, this.count, render3DTextureOf(tileInfo), r);
|
|
3993
|
+
r.cullBackFaces = cullBackFaces;
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
/** Destroy the set and free its GPU buffer
|
|
3997
|
+
* @param {boolean} [immediate] */
|
|
3998
|
+
destroy(immediate)
|
|
3999
|
+
{
|
|
4000
|
+
if (this.buffer && this.bufferGeneration === render3D?.contextGeneration)
|
|
4001
|
+
glContext?.deleteBuffer(this.buffer);
|
|
4002
|
+
this.buffer = undefined;
|
|
4003
|
+
super.destroy(immediate);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
3418
4006
|
|
|
3419
4007
|
///////////////////////////////////////////////////////////////////////////////
|
|
3420
4008
|
/**
|
|
3421
4009
|
* Light3D - A light that is an EngineObject3D, so it can move, follow a parent or be destroyed like anything else
|
|
3422
4010
|
* - A point light: it lights what is near it and fades out by its radius, DirectionalLight3D shines from far away
|
|
3423
|
-
* - Only the sun, render3D.sunDirection, casts shadows and
|
|
4011
|
+
* - Only the sun, render3D.sunDirection, casts shadows; these light and make highlights without one
|
|
3424
4012
|
* - Only the 8 lights nearest the camera are used each frame
|
|
3425
4013
|
* - radius is where the light fades out, and it fades fast, so a small radius wants a higher intensity
|
|
3426
4014
|
* - intensity multiplies the color, above 1 for a light brighter than white
|
|
@@ -3464,7 +4052,7 @@ class Light3D extends EngineObject3D
|
|
|
3464
4052
|
* - It shines from its position toward the origin, like a three.js DirectionalLight: only the direction to it
|
|
3465
4053
|
* counts, so moving it or its parent swings the light around; parent it to a sun in the sky and it follows
|
|
3466
4054
|
* - It cannot sit on the origin, since that leaves no direction
|
|
3467
|
-
* - Like every Light3D it casts no shadow
|
|
4055
|
+
* - Like every Light3D it casts no shadow, only the sun, render3D.sunDirection, does
|
|
3468
4056
|
* @extends Light3D
|
|
3469
4057
|
* @memberof Render3D
|
|
3470
4058
|
* @example
|
|
@@ -3482,525 +4070,3 @@ class DirectionalLight3D extends Light3D
|
|
|
3482
4070
|
this.directional = true;
|
|
3483
4071
|
}
|
|
3484
4072
|
}
|
|
3485
|
-
|
|
3486
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
3487
|
-
/**
|
|
3488
|
-
* CameraControl3D - Drag to turn the camera around a point, roll the wheel to zoom
|
|
3489
|
-
* - An EngineObject3D, so move its pos3D to follow something, or parent it to an object
|
|
3490
|
-
* - Destroy it to hand the camera back, and it stops driving the camera
|
|
3491
|
-
* - Set persistent to keep it when engineObjectsDestroy clears out a level
|
|
3492
|
-
* - Every part of it is a field, so a game can change the buttons, speeds and limits
|
|
3493
|
-
* @extends EngineObject3D
|
|
3494
|
-
* @memberof Render3D
|
|
3495
|
-
* @example
|
|
3496
|
-
* new CameraControl3D(vec3(0, 1, 0), 15); // look at a point from 15 units away
|
|
3497
|
-
*/
|
|
3498
|
-
class CameraControl3D extends EngineObject3D
|
|
3499
|
-
{
|
|
3500
|
-
/** Create a camera control, it drives render3D.camera every frame
|
|
3501
|
-
* @param {Vector3} [target] - The point to look at, its pos3D
|
|
3502
|
-
* @param {number} [distance] - How far the camera sits from the target
|
|
3503
|
-
* @param {number} [pitch] - Angle above the horizon, PI/2 looks straight down
|
|
3504
|
-
* @param {number} [idleSpin] - Turned each frame while not dragging, 0 holds still */
|
|
3505
|
-
constructor(target=vec3(), distance=10, pitch=.4, idleSpin=0)
|
|
3506
|
-
{
|
|
3507
|
-
super(target);
|
|
3508
|
-
this.size3D = vec3(); // not a solid thing to pick or collect
|
|
3509
|
-
/** @property {number} - How far the camera sits from the target */
|
|
3510
|
-
this.distance = distance;
|
|
3511
|
-
/** @property {number} - Angle above the horizon */
|
|
3512
|
-
this.pitch = pitch;
|
|
3513
|
-
/** @property {number} - Turned each frame while not dragging */
|
|
3514
|
-
this.idleSpin = idleSpin;
|
|
3515
|
-
/** @property {number} - Angle around the target, dragging changes it */
|
|
3516
|
-
this.yaw = 0;
|
|
3517
|
-
/** @property {number} - Mouse button that turns the camera, 0 is left and 2 is right */
|
|
3518
|
-
this.dragButton = 0;
|
|
3519
|
-
/** @property {number} - How far dragging a pixel turns the camera */
|
|
3520
|
-
this.dragSpeed = .01;
|
|
3521
|
-
/** @property {number} - How much one wheel notch zooms, 0 turns zooming off */
|
|
3522
|
-
this.zoomSpeed = .1;
|
|
3523
|
-
/** @property {Vector2} - Closest and furthest the wheel can zoom to */
|
|
3524
|
-
this.zoomRange = vec2(distance/4, distance*3);
|
|
3525
|
-
/** @property {Vector2} - Lowest and highest pitch, so it cannot tip over the top */
|
|
3526
|
-
this.pitchRange = vec2(-.2, 1.4);
|
|
3527
|
-
}
|
|
3528
|
-
|
|
3529
|
-
/** Read the mouse and put the camera on its orbit, called automatically each frame */
|
|
3530
|
-
update()
|
|
3531
|
-
{
|
|
3532
|
-
if (mouseIsDown(this.dragButton))
|
|
3533
|
-
{
|
|
3534
|
-
// the scene follows the drag
|
|
3535
|
-
this.yaw -= mouseDeltaScreen.x * this.dragSpeed;
|
|
3536
|
-
this.pitch += mouseDeltaScreen.y * this.dragSpeed;
|
|
3537
|
-
}
|
|
3538
|
-
else
|
|
3539
|
-
this.yaw += this.idleSpin;
|
|
3540
|
-
this.pitch = clamp(this.pitch, this.pitchRange.x, this.pitchRange.y);
|
|
3541
|
-
if (this.zoomSpeed && mouseWheel)
|
|
3542
|
-
this.distance = clamp(this.distance * (1 + sign(mouseWheel) * this.zoomSpeed), this.zoomRange.x, this.zoomRange.y);
|
|
3543
|
-
render3D.camera.orbit(this.getWorldPos3D(), this.distance, this.yaw, this.pitch);
|
|
3544
|
-
}
|
|
3545
|
-
|
|
3546
|
-
/** Camera controls draw nothing */
|
|
3547
|
-
render3D() {}
|
|
3548
|
-
}
|
|
3549
|
-
|
|
3550
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
3551
|
-
/**
|
|
3552
|
-
* FirstPersonCamera3D - Look around with the mouse and move with the keys, with the camera at its position
|
|
3553
|
-
* - Click to capture the mouse so looking needs no button held, Esc lets it go; holding the button looks too, for touch
|
|
3554
|
-
* - WASD or the arrow keys walk level, or move the way it looks when fly is set
|
|
3555
|
-
* - An EngineObject3D that moves by velocity3D, so give it a size3D and call setCollision to walk into solid
|
|
3556
|
-
* objects instead of through them; walking keeps velocity3D.y, so render3D.gravity can pull it down
|
|
3557
|
-
* - Starts from wherever render3D.camera is, so it can take over from another camera without a jump
|
|
3558
|
-
* - Destroy it to hand the camera back
|
|
3559
|
-
* @extends EngineObject3D
|
|
3560
|
-
* @memberof Render3D
|
|
3561
|
-
* @example
|
|
3562
|
-
* const player = new FirstPersonCamera3D(vec3(0, 1.5, 5));
|
|
3563
|
-
* player.size3D = vec3(1); // bump into solid objects
|
|
3564
|
-
* player.collideAsSphere3D = true;
|
|
3565
|
-
* player.setCollision();
|
|
3566
|
-
*/
|
|
3567
|
-
class FirstPersonCamera3D extends EngineObject3D
|
|
3568
|
-
{
|
|
3569
|
-
/** Create a first person camera, it drives render3D.camera every frame
|
|
3570
|
-
* @param {Vector3} [pos3D] - Where the eye is, defaults to where the camera is now
|
|
3571
|
-
* @param {number} [yaw] - Radians around Y, defaults to the camera's
|
|
3572
|
-
* @param {number} [pitch] - Radians up from level, defaults to the camera's */
|
|
3573
|
-
constructor(pos3D=render3D.camera.pos, yaw=render3D.camera.rotation.y, pitch=render3D.camera.rotation.x)
|
|
3574
|
-
{
|
|
3575
|
-
super(pos3D);
|
|
3576
|
-
this.size3D = vec3(); // not a solid thing to pick or collect until it is given a size
|
|
3577
|
-
this.mass = 1; // so solids push it out, and render3D.gravity pulls on it
|
|
3578
|
-
/** @property {number} - Angle around Y, the mouse turns it */
|
|
3579
|
-
this.yaw = yaw;
|
|
3580
|
-
/** @property {number} - Angle up from level, the mouse tilts it */
|
|
3581
|
-
this.pitch = pitch;
|
|
3582
|
-
/** @property {number} - World units per frame at full speed */
|
|
3583
|
-
this.moveSpeed = .1;
|
|
3584
|
-
/** @property {number} - How far a pixel of mouse movement turns the view */
|
|
3585
|
-
this.lookSpeed = .003;
|
|
3586
|
-
/** @property {Vector2} - Lowest and highest pitch */
|
|
3587
|
-
this.pitchRange = vec2(-1.5, 1.5);
|
|
3588
|
-
/** @property {boolean} - Move the way it looks, up and down included, instead of walking level */
|
|
3589
|
-
this.fly = false;
|
|
3590
|
-
/** @property {boolean} - Capture the mouse on a click, so looking needs no button held */
|
|
3591
|
-
this.lockPointer = true;
|
|
3592
|
-
}
|
|
3593
|
-
|
|
3594
|
-
/** Read the mouse and keys and put the camera at the eye, called automatically each frame */
|
|
3595
|
-
update()
|
|
3596
|
-
{
|
|
3597
|
-
// a click captures the mouse, then it looks around while captured or while a button is held
|
|
3598
|
-
if (this.lockPointer && mouseWasPressed(0))
|
|
3599
|
-
pointerLockRequest();
|
|
3600
|
-
if (pointerLockIsActive() || mouseIsDown(0))
|
|
3601
|
-
{
|
|
3602
|
-
this.yaw -= mouseDeltaScreen.x * this.lookSpeed;
|
|
3603
|
-
this.pitch -= mouseDeltaScreen.y * this.lookSpeed;
|
|
3604
|
-
}
|
|
3605
|
-
this.pitch = clamp(this.pitch, this.pitchRange.x, this.pitchRange.y);
|
|
3606
|
-
|
|
3607
|
-
// the keys move it level, or the way it looks when flying, and walking keeps its fall
|
|
3608
|
-
const input = keyDirection();
|
|
3609
|
-
const move = vec3(input.x, 0, -input.y).clampLength(1).scale(this.moveSpeed)
|
|
3610
|
-
.rotateX(this.fly ? this.pitch : 0).rotateY(this.yaw);
|
|
3611
|
-
this.velocity3D = this.fly ? move : vec3(move.x, this.velocity3D.y, move.z);
|
|
3612
|
-
|
|
3613
|
-
// the camera sits at the eye, where this frame's physics left it
|
|
3614
|
-
render3D.camera.pos = this.getWorldPos3D();
|
|
3615
|
-
render3D.camera.rotation = vec3(this.pitch, this.yaw, 0);
|
|
3616
|
-
}
|
|
3617
|
-
|
|
3618
|
-
/** Let go of the mouse and stop driving the camera
|
|
3619
|
-
* @param {boolean} [immediate] */
|
|
3620
|
-
destroy(immediate)
|
|
3621
|
-
{
|
|
3622
|
-
this.lockPointer && pointerLockIsActive() && pointerLockExit();
|
|
3623
|
-
super.destroy(immediate);
|
|
3624
|
-
}
|
|
3625
|
-
|
|
3626
|
-
/** Camera controls draw nothing */
|
|
3627
|
-
render3D() {}
|
|
3628
|
-
}
|
|
3629
|
-
|
|
3630
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
3631
|
-
/**
|
|
3632
|
-
* ParticleEmitter3D - Spawns camera facing particles, the 3D twin of ParticleEmitter
|
|
3633
|
-
* - Each particle is a flat square facing the camera, with a soft round dot when no tile is given
|
|
3634
|
-
* - Set trailTime to draw each particle as a streak along where it has been, for sparks
|
|
3635
|
-
* - Set angleSpeed to tumble them in the camera plane, which the 2D emitter takes as an argument
|
|
3636
|
-
* - Particles shoot out along the emitter's own up axis, turned by rotation3D
|
|
3637
|
-
* - emitConeAngle spreads them, PI sprays in every direction
|
|
3638
|
-
* - Speeds are per frame and sizes are world units, the same as the 2D emitter
|
|
3639
|
-
* - scale3D, its own or a parent's, grows the whole effect: the spawn area, the sizes, the speed and the fall
|
|
3640
|
-
* - gravity here is its own number added to velocity y each frame: it is neither the engine's 2D
|
|
3641
|
-
* gravity nor render3D.gravity, so an effect keeps its own fall wherever it is used
|
|
3642
|
-
* - An emitter with an emitTime destroys itself once its last particle is gone, like the 2D emitter
|
|
3643
|
-
* @extends EngineObject3D
|
|
3644
|
-
* @memberof Render3D
|
|
3645
|
-
* @example
|
|
3646
|
-
* // fire: a stream upward, yellow fading to transparent red, additive
|
|
3647
|
-
* new ParticleEmitter3D(vec3(), .5, 0, 100, .3, undefined, hsl(.12, 1, .6), hsl(.08, 1, .5), hsl(0, 1, .5, 0), hsl(0, 1, .25, 0), 1, .5, 1.5, .05, .95, 0, .3, .2, true);
|
|
3648
|
-
*/
|
|
3649
|
-
class ParticleEmitter3D extends EngineObject3D
|
|
3650
|
-
{
|
|
3651
|
-
/** Create a particle emitter
|
|
3652
|
-
* @param {Vector3} [pos3D] - World space position of the emitter
|
|
3653
|
-
* @param {number|Vector3} [emitSize] - Spawn area, a number for a sphere diameter or a vec3 for a box
|
|
3654
|
-
* @param {number} [emitTime] - How long to keep emitting, 0 is forever
|
|
3655
|
-
* @param {number} [emitRate] - Particles per second, 0 does not emit
|
|
3656
|
-
* @param {number} [emitConeAngle] - Half angle around the emit direction, PI is every direction
|
|
3657
|
-
* @param {TileInfo|TextureInfo} [tileInfo] - Tile to render particles with, or a whole texture, undefined is untextured
|
|
3658
|
-
* @param {Color} [colorStartA] - Color at start of life, randomized between the start colors
|
|
3659
|
-
* @param {Color} [colorStartB]
|
|
3660
|
-
* @param {Color} [colorEndA] - Color at end of life, randomized between the end colors
|
|
3661
|
-
* @param {Color} [colorEndB]
|
|
3662
|
-
* @param {number} [particleTime] - How long particles live in seconds
|
|
3663
|
-
* @param {number} [sizeStart] - Particle size at start of life
|
|
3664
|
-
* @param {number} [sizeEnd] - Particle size at end of life
|
|
3665
|
-
* @param {number} [speed] - Spawn speed in world units per frame
|
|
3666
|
-
* @param {number} [damping] - Per frame velocity multiplier, 1 is none
|
|
3667
|
-
* @param {number} [gravity] - Per frame change to velocity y, negative pulls down; its own number,
|
|
3668
|
-
* not render3D.gravity, so the 2D emitter's gravityScale has no equivalent here
|
|
3669
|
-
* @param {number} [fadeRate] - Fraction of life spent fading, half in and half out
|
|
3670
|
-
* @param {number} [randomness] - Extra randomness applied to speed, size and life
|
|
3671
|
-
* @param {boolean} [additive] - Additive blending */
|
|
3672
|
-
constructor(pos3D=vec3(), emitSize=0, emitTime=0, emitRate=100, emitConeAngle=PI, tileInfo,
|
|
3673
|
-
colorStartA=WHITE, colorStartB=WHITE, colorEndA=CLEAR_WHITE, colorEndB=CLEAR_WHITE,
|
|
3674
|
-
particleTime=.5, sizeStart=.1, sizeEnd=1, speed=.1, damping=1, gravity=0, fadeRate=.1, randomness=.2, additive=false)
|
|
3675
|
-
{
|
|
3676
|
-
super(pos3D, undefined, tileInfo);
|
|
3677
|
-
this.transparent = true;
|
|
3678
|
-
this.castShadow = false;
|
|
3679
|
-
this.size3D = vec3(); // not a solid thing to pick or collect
|
|
3680
|
-
|
|
3681
|
-
/** @property {number|Vector3} - Spawn area, a number for a sphere diameter or a vec3 for a box */
|
|
3682
|
-
this.emitSize = emitSize;
|
|
3683
|
-
/** @property {number} - How long to keep emitting, 0 is forever */
|
|
3684
|
-
this.emitTime = emitTime;
|
|
3685
|
-
/** @property {number} - Particles per second, 0 does not emit */
|
|
3686
|
-
this.emitRate = emitRate;
|
|
3687
|
-
/** @property {number} - Half angle around the emit direction, PI is every direction */
|
|
3688
|
-
this.emitConeAngle = emitConeAngle;
|
|
3689
|
-
/** @property {Color} - Color at start of life, randomized between the start colors */
|
|
3690
|
-
this.colorStartA = colorStartA.copy();
|
|
3691
|
-
/** @property {Color} - Color at start of life, randomized between the start colors */
|
|
3692
|
-
this.colorStartB = colorStartB.copy();
|
|
3693
|
-
/** @property {Color} - Color at end of life, randomized between the end colors */
|
|
3694
|
-
this.colorEndA = colorEndA.copy();
|
|
3695
|
-
/** @property {Color} - Color at end of life, randomized between the end colors */
|
|
3696
|
-
this.colorEndB = colorEndB.copy();
|
|
3697
|
-
/** @property {number} - How long particles live in seconds */
|
|
3698
|
-
this.particleTime = particleTime;
|
|
3699
|
-
/** @property {number} - Particle size at start of life */
|
|
3700
|
-
this.sizeStart = sizeStart;
|
|
3701
|
-
/** @property {number} - Particle size at end of life */
|
|
3702
|
-
this.sizeEnd = sizeEnd;
|
|
3703
|
-
/** @property {number} - Spawn speed in world units per frame */
|
|
3704
|
-
this.speed = speed;
|
|
3705
|
-
/** @property {number} - Per frame velocity multiplier */
|
|
3706
|
-
this.damping = damping;
|
|
3707
|
-
/** @property {number} - Per frame change to velocity y, its own number and not render3D.gravity */
|
|
3708
|
-
this.gravity = gravity;
|
|
3709
|
-
/** @property {number} - Fraction of life spent fading, half in and half out */
|
|
3710
|
-
this.fadeRate = fadeRate;
|
|
3711
|
-
/** @property {number} - Extra randomness applied to speed, size and life */
|
|
3712
|
-
this.randomness = randomness;
|
|
3713
|
-
/** @property {boolean} - Additive blending */
|
|
3714
|
-
this.additive = additive;
|
|
3715
|
-
/** @property {number} - Seconds of each particle's path to draw as a ribbon behind it, 0 draws billboards */
|
|
3716
|
-
this.trailTime = 0;
|
|
3717
|
-
/** @property {number} - Radians per frame each particle turns in the camera plane, either way; 0 is no spin */
|
|
3718
|
-
this.angleSpeed = 0;
|
|
3719
|
-
/** @property {number} - Per frame multiplier on that spin, 1 keeps it */
|
|
3720
|
-
this.angleDamping = 1;
|
|
3721
|
-
/** @property {Array<Object>} - Live particles
|
|
3722
|
-
* @type {Array<Object>} */
|
|
3723
|
-
this.particles = [];
|
|
3724
|
-
this.emitTimeBuffer = 0;
|
|
3725
|
-
}
|
|
3726
|
-
|
|
3727
|
-
/** Spawn new particles, move the live ones, and go away when done */
|
|
3728
|
-
update()
|
|
3729
|
-
{
|
|
3730
|
-
// one transform for the frame: where the emitter is, and how big the effect it makes is
|
|
3731
|
-
const matrix = this.getMatrix();
|
|
3732
|
-
this.worldPos3D = matrix.getTranslation(); // remembered for when the parent is destroyed
|
|
3733
|
-
const scale = render3DMaxScale(matrix.m);
|
|
3734
|
-
|
|
3735
|
-
// emit until the emit time is up, then wait for the last particle and go away
|
|
3736
|
-
if (!this.emitTime || this.getAliveTime() <= this.emitTime)
|
|
3737
|
-
{
|
|
3738
|
-
// a rate of zero is an emitter fed by hand, and the global scale only quiets it,
|
|
3739
|
-
// neither is a reason to stop counting down the emit time
|
|
3740
|
-
if (this.emitRate && particleEmitRateScale)
|
|
3741
|
-
{
|
|
3742
|
-
this.emitTimeBuffer += this.emitRate * particleEmitRateScale * timeDelta;
|
|
3743
|
-
for (; this.emitTimeBuffer >= 1; --this.emitTimeBuffer)
|
|
3744
|
-
this.emitParticle();
|
|
3745
|
-
}
|
|
3746
|
-
}
|
|
3747
|
-
else if (!this.particles.length)
|
|
3748
|
-
this.destroy();
|
|
3749
|
-
|
|
3750
|
-
// move the particles and drop the dead ones
|
|
3751
|
-
const particles = this.particles;
|
|
3752
|
-
for (let i = particles.length; i--;)
|
|
3753
|
-
{
|
|
3754
|
-
// damped first and gravity added after, the order the 2D particle uses, so the same
|
|
3755
|
-
// damping and gravity give the same arc in both
|
|
3756
|
-
const p = particles[i], v = p.velocity;
|
|
3757
|
-
v.x *= this.damping, v.y *= this.damping, v.z *= this.damping; // in place, this runs per particle
|
|
3758
|
-
v.y += this.gravity * scale; // a bigger effect has to fall faster to keep the same arc
|
|
3759
|
-
p.pos = p.pos.add(v);
|
|
3760
|
-
p.angle += p.angleVelocity *= this.angleDamping;
|
|
3761
|
-
if (this.trailTime)
|
|
3762
|
-
{
|
|
3763
|
-
// remember where it has been, oldest first
|
|
3764
|
-
const trail = p.trail || (p.trail = []);
|
|
3765
|
-
trail.push(p.pos);
|
|
3766
|
-
const extra = trail.length - this.trailTime / timeDelta;
|
|
3767
|
-
extra > 0 && trail.splice(0, extra);
|
|
3768
|
-
}
|
|
3769
|
-
if ((p.age += timeDelta) >= p.life)
|
|
3770
|
-
particles[i] = particles[particles.length - 1], particles.pop(); // swap with the last, order does not matter
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
|
-
|
|
3774
|
-
/** Stop emitting, and go away once the particles already out have finished like the 2D emitter's do
|
|
3775
|
-
* @param {boolean} [immediate] */
|
|
3776
|
-
destroy(immediate)
|
|
3777
|
-
{
|
|
3778
|
-
if (immediate || !this.particles.length || this.destroyed)
|
|
3779
|
-
return super.destroy(immediate);
|
|
3780
|
-
this.emitTime = -1; // stops emitting, and update destroys it once the particles are gone
|
|
3781
|
-
render3DDetach(this); // the particles are in world space, they no longer need the parent
|
|
3782
|
-
}
|
|
3783
|
-
|
|
3784
|
-
/** Spawn one particle now */
|
|
3785
|
-
emitParticle()
|
|
3786
|
-
{
|
|
3787
|
-
const random = ()=> rand(1 - this.randomness, 1 + this.randomness);
|
|
3788
|
-
const matrix = this.getMatrix();
|
|
3789
|
-
// the whole effect grows with the emitter, not just the area the particles start in
|
|
3790
|
-
const scale = render3DMaxScale(matrix.m);
|
|
3791
|
-
|
|
3792
|
-
// spawn offset: inside a box or a sphere
|
|
3793
|
-
const size = this.emitSize;
|
|
3794
|
-
const offset = isVector3(size) ? vec3(rand(-.5, .5) * size.x, rand(-.5, .5) * size.y, rand(-.5, .5) * size.z)
|
|
3795
|
-
: randInSphere(size / 2);
|
|
3796
|
-
|
|
3797
|
-
// direction inside the cone around local +Y
|
|
3798
|
-
const direction = matrix.transformDirection(randVector3(1, this.emitConeAngle)).normalize();
|
|
3799
|
-
|
|
3800
|
-
this.particles.push({
|
|
3801
|
-
pos: matrix.transformPoint(offset),
|
|
3802
|
-
velocity: direction.scale(this.speed * random() * scale),
|
|
3803
|
-
colorStart: randColor(this.colorStartA, this.colorStartB, true),
|
|
3804
|
-
colorEnd: randColor(this.colorEndA, this.colorEndB, true),
|
|
3805
|
-
sizeStart: this.sizeStart * random() * scale,
|
|
3806
|
-
sizeEnd: this.sizeEnd * random() * scale,
|
|
3807
|
-
life: this.particleTime * random(),
|
|
3808
|
-
// a spinning particle starts anywhere and turns either way, one that is not stays at zero
|
|
3809
|
-
angle: this.angleSpeed ? rand(2*PI) : 0,
|
|
3810
|
-
angleVelocity: this.angleSpeed ? this.angleSpeed * random() * randSign() : 0,
|
|
3811
|
-
age: 0 });
|
|
3812
|
-
}
|
|
3813
|
-
|
|
3814
|
-
/** Draw the particles, as flat squares or as streaks when trailTime is set
|
|
3815
|
-
* - The whole emitter sorts as one thing, its particles are not sorted against each other */
|
|
3816
|
-
render3D()
|
|
3817
|
-
{
|
|
3818
|
-
if (render3D.transparentQueue)
|
|
3819
|
-
return render3D.queueTransparent(this.getWorldPos3D(), ()=> this.render3D());
|
|
3820
|
-
const fade = this.fadeRate / 2, texture = this.tileInfo || render3DSoftDot(); // no dot headless
|
|
3821
|
-
for (const p of this.particles)
|
|
3822
|
-
{
|
|
3823
|
-
const t = p.age / p.life;
|
|
3824
|
-
const alpha = t < fade ? t / fade : t > 1 - fade ? (1 - t) / fade : 1;
|
|
3825
|
-
const color = p.colorStart.lerp(p.colorEnd, t), size = lerp(p.sizeStart, p.sizeEnd, t);
|
|
3826
|
-
color.a *= alpha;
|
|
3827
|
-
const trail = p.trail;
|
|
3828
|
-
if (trail && trail.length > 1)
|
|
3829
|
-
{
|
|
3830
|
-
// a ribbon from the tail to the head, the tail thins and fades out
|
|
3831
|
-
const widths = [], colors = [];
|
|
3832
|
-
for (let i = 0; i < trail.length; ++i)
|
|
3833
|
-
{
|
|
3834
|
-
const s = (i + 1) / trail.length;
|
|
3835
|
-
widths.push(size * s);
|
|
3836
|
-
colors.push(color.scale(1, s));
|
|
3837
|
-
}
|
|
3838
|
-
render3D.drawRibbon(trail, widths, this.tileInfo, colors);
|
|
3839
|
-
}
|
|
3840
|
-
else if (texture)
|
|
3841
|
-
render3D.drawBillboard(p.pos, vec2(size), texture, color, p.angle);
|
|
3842
|
-
else
|
|
3843
|
-
render3D.drawSoftDisc(p.pos, size, color, undefined, 8); // no canvas for the dot, headless
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
3846
|
-
}
|
|
3847
|
-
|
|
3848
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
3849
|
-
/**
|
|
3850
|
-
* Trail3D - A ribbon through where the object has been, thinning and fading with age
|
|
3851
|
-
* - Records its world position each frame it moves, so parent it to something that moves or set pos3D yourself
|
|
3852
|
-
* - The samples are world space, so width is a world width and scale3D does nothing to the ribbon
|
|
3853
|
-
* - Drawn unlit in the transparent stage, dies down on its own once the object stops
|
|
3854
|
-
* @extends EngineObject3D
|
|
3855
|
-
* @memberof Render3D
|
|
3856
|
-
* @example
|
|
3857
|
-
* const trail = new Trail3D(vec3(), 1, .3, undefined, hsl(.08, 1, .5), hsl(0, 1, .5, 0), true);
|
|
3858
|
-
* ball.addChild(trail); // follows the ball
|
|
3859
|
-
*/
|
|
3860
|
-
class Trail3D extends EngineObject3D
|
|
3861
|
-
{
|
|
3862
|
-
/** Create a trail
|
|
3863
|
-
* @param {Vector3} [pos3D]
|
|
3864
|
-
* @param {number} [lifeTime] - Seconds the ribbon takes to thin and fade from head to tail,
|
|
3865
|
-
* Infinity keeps every sample at full width and never drops one, so it grows as long as the object moves
|
|
3866
|
-
* @param {number} [width] - Width at the head, it thins to nothing at the tail
|
|
3867
|
-
* @param {TileInfo|TextureInfo} [tileInfo] - Tile or whole texture stretched along the trail, undefined is untextured
|
|
3868
|
-
* @param {Color} [color] - Color at the head
|
|
3869
|
-
* @param {Color} [colorEnd] - Color at the tail
|
|
3870
|
-
* @param {boolean} [additive] - Additive blending */
|
|
3871
|
-
constructor(pos3D=vec3(), lifeTime=1, width=.2, tileInfo, color=WHITE, colorEnd=CLEAR_WHITE, additive=false)
|
|
3872
|
-
{
|
|
3873
|
-
super(pos3D, undefined, tileInfo, color);
|
|
3874
|
-
this.transparent = true;
|
|
3875
|
-
this.additive = additive;
|
|
3876
|
-
this.castShadow = false;
|
|
3877
|
-
this.size3D = vec3(); // not a solid thing to pick or collect
|
|
3878
|
-
this.finishing = false; // set by destroy, the ribbon fades out then goes away
|
|
3879
|
-
|
|
3880
|
-
/** @property {number} - Seconds the ribbon takes to thin and fade from head to tail, Infinity never drops a sample */
|
|
3881
|
-
this.lifeTime = lifeTime;
|
|
3882
|
-
/** @property {number} - Width at the head */
|
|
3883
|
-
this.width = width;
|
|
3884
|
-
/** @property {Color} - Color at the tail */
|
|
3885
|
-
this.colorEnd = colorEnd.copy();
|
|
3886
|
-
/** @property {Vector3|undefined} - Direction across the ribbon, recorded with each sample, undefined faces the camera
|
|
3887
|
-
* @type {Vector3|undefined} */
|
|
3888
|
-
this.side = undefined;
|
|
3889
|
-
/** @property {Array<Object>} - Recorded samples, oldest first
|
|
3890
|
-
* @type {Array<Object>} */
|
|
3891
|
-
this.samples = [];
|
|
3892
|
-
}
|
|
3893
|
-
|
|
3894
|
-
/** Forget the trail so far, for when the object teleports */
|
|
3895
|
-
clear() { this.samples.length = 0; }
|
|
3896
|
-
|
|
3897
|
-
/** Stop recording, and go away once the ribbon has faded
|
|
3898
|
-
* @param {boolean} [immediate] */
|
|
3899
|
-
destroy(immediate)
|
|
3900
|
-
{
|
|
3901
|
-
if (immediate || !this.samples.length || this.destroyed || this.lifeTime == Infinity)
|
|
3902
|
-
return super.destroy(immediate);
|
|
3903
|
-
this.finishing = true;
|
|
3904
|
-
render3DDetach(this); // the samples are in world space, they no longer need the parent
|
|
3905
|
-
}
|
|
3906
|
-
|
|
3907
|
-
/** Record the position when it moved and drop old samples, called automatically each frame */
|
|
3908
|
-
update()
|
|
3909
|
-
{
|
|
3910
|
-
const samples = this.samples;
|
|
3911
|
-
if (!this.finishing)
|
|
3912
|
-
{
|
|
3913
|
-
const pos = this.worldPos3D = this.getWorldPos3D(), last = samples[samples.length - 1];
|
|
3914
|
-
if (!last || pos.distanceSquared(last.pos) > 1e-8)
|
|
3915
|
-
samples.push({pos, side: this.side?.copy(), time});
|
|
3916
|
-
}
|
|
3917
|
-
while (samples.length && time - samples[0].time > this.lifeTime)
|
|
3918
|
-
samples.shift();
|
|
3919
|
-
this.finishing && !samples.length && this.destroy();
|
|
3920
|
-
}
|
|
3921
|
-
|
|
3922
|
-
/** Draw the ribbon */
|
|
3923
|
-
render3D()
|
|
3924
|
-
{
|
|
3925
|
-
const samples = this.samples;
|
|
3926
|
-
if (samples.length < 2) return;
|
|
3927
|
-
const points = [], widths = [], colors = [], sides = this.side ? [] : undefined;
|
|
3928
|
-
for (const s of samples)
|
|
3929
|
-
{
|
|
3930
|
-
const age = clamp((time - s.time) / this.lifeTime);
|
|
3931
|
-
points.push(s.pos);
|
|
3932
|
-
widths.push(this.width * (1 - age));
|
|
3933
|
-
colors.push(this.color.lerp(this.colorEnd, age));
|
|
3934
|
-
sides?.push(s.side);
|
|
3935
|
-
}
|
|
3936
|
-
render3D.drawRibbon(points, widths, this.tileInfo, colors, sides);
|
|
3937
|
-
}
|
|
3938
|
-
}
|
|
3939
|
-
|
|
3940
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
3941
|
-
// OBJ meshes
|
|
3942
|
-
|
|
3943
|
-
/**
|
|
3944
|
-
* Parse Wavefront OBJ text into a Mesh
|
|
3945
|
-
* - Reads v, vt, vn and f lines with convex polygons of any size, materials and groups are ignored
|
|
3946
|
-
* - Normals come from the file when every corner of a face has one, otherwise from the face
|
|
3947
|
-
* - Use mesh.center() and mesh.fit(size) to bring a model of unknown units to the origin
|
|
3948
|
-
* - Back faces are skipped like any mesh, set doubleSided for a model with open walls or single sided parts
|
|
3949
|
-
* @param {string} text
|
|
3950
|
-
* @param {boolean} [smooth] - Compute smooth normals when the file has none, defaults to render3D.smoothShading
|
|
3951
|
-
* @return {Mesh}
|
|
3952
|
-
* @memberof Render3D
|
|
3953
|
-
* @example
|
|
3954
|
-
* new EngineObject3D(vec3(), parseOBJ(objText).center().fit(4));
|
|
3955
|
-
*/
|
|
3956
|
-
function parseOBJ(text, smooth=render3D?.smoothShading)
|
|
3957
|
-
{
|
|
3958
|
-
const positions = [], normals = [], uvs = [], mesh = new Mesh;
|
|
3959
|
-
let fileNormals = false;
|
|
3960
|
-
|
|
3961
|
-
// OBJ indices count from 1, and a negative one counts back from the end of the list so far
|
|
3962
|
-
const lookup = (s, list)=> { const i = parseInt(s); return list[i < 0 ? list.length + i : i - 1]; };
|
|
3963
|
-
for (const line of text.split('\n'))
|
|
3964
|
-
{
|
|
3965
|
-
const parts = line.trim().split(/\s+/);
|
|
3966
|
-
switch (parts[0])
|
|
3967
|
-
{
|
|
3968
|
-
case 'v': positions.push(vec3(+parts[1], +parts[2], +parts[3])); break;
|
|
3969
|
-
case 'vn': normals.push(vec3(+parts[1], +parts[2], +parts[3])); break;
|
|
3970
|
-
case 'vt': uvs.push(vec2(+parts[1], 1 - +parts[2])); break; // OBJ v runs up, tiles run down
|
|
3971
|
-
case 'f':
|
|
3972
|
-
{
|
|
3973
|
-
const corners = parts.slice(1).map(c=> c.split('/'));
|
|
3974
|
-
if (corners.length < 3) break;
|
|
3975
|
-
const points = corners.map(c=> lookup(c[0], positions));
|
|
3976
|
-
ASSERT(points.every(isVector3), 'OBJ face uses a vertex index the file does not have', line);
|
|
3977
|
-
const uv = corners.map(c=> c[1] ? lookup(c[1], uvs) : RENDER3D_DEFAULT_UV);
|
|
3978
|
-
const hasNormals = corners.every(c=> c[2]);
|
|
3979
|
-
fileNormals ||= hasNormals;
|
|
3980
|
-
const n = hasNormals ? render3DPolygonStrip(corners.map(c=> lookup(c[2], normals)))
|
|
3981
|
-
: render3DFaceNormal(points[0], points[1], points[2], points[3]);
|
|
3982
|
-
mesh.addStrip(render3DPolygonStrip(points), n, render3DPolygonStrip(uv));
|
|
3983
|
-
}
|
|
3984
|
-
}
|
|
3985
|
-
}
|
|
3986
|
-
if (!fileNormals && smooth)
|
|
3987
|
-
mesh.computeNormals(true);
|
|
3988
|
-
return mesh;
|
|
3989
|
-
}
|
|
3990
|
-
|
|
3991
|
-
/**
|
|
3992
|
-
* Fetch and parse an OBJ file
|
|
3993
|
-
* @param {string} url
|
|
3994
|
-
* @param {boolean} [smooth] - Compute smooth normals when the file has none, defaults to render3D.smoothShading
|
|
3995
|
-
* @return {Promise<Mesh>}
|
|
3996
|
-
* @memberof Render3D
|
|
3997
|
-
* @example
|
|
3998
|
-
* const mesh = await loadOBJ('ship.obj'); // in an async gameInit
|
|
3999
|
-
*/
|
|
4000
|
-
async function loadOBJ(url, smooth=render3D?.smoothShading)
|
|
4001
|
-
{
|
|
4002
|
-
const response = await fetch(url);
|
|
4003
|
-
if (!response.ok)
|
|
4004
|
-
throw new Error('loadOBJ failed: ' + url);
|
|
4005
|
-
return parseOBJ(await response.text(), smooth);
|
|
4006
|
-
}
|