littlejsengine 1.18.29 → 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 -815
- package/README.md +211 -199
- package/dist/littlejs.d.ts +4383 -812
- package/dist/littlejs.esm.js +31848 -16680
- package/dist/littlejs.esm.min.js +14 -1
- package/dist/littlejs.js +31230 -16166
- package/dist/littlejs.min.js +13 -1
- package/dist/littlejs.release.js +27419 -15457
- package/package.json +10 -5
- package/plugins/audioEffects.js +375 -0
- 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 -330
- package/plugins/math3d.js +911 -0
- package/plugins/medalSystem.js +118 -72
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +196 -115
- package/plugins/postProcess.js +284 -166
- package/plugins/render3d.js +4072 -0
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +86 -48
- package/plugins/threejs.js +183 -158
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +646 -526
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +8 -15
- package/src/engine.js +345 -194
- package/src/engineAudio.js +1022 -777
- package/src/engineBuild.mjs +289 -253
- package/src/engineDebug.js +912 -735
- package/src/engineDraw.js +1774 -1510
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +419 -396
- package/src/engineInput.js +1462 -1293
- package/src/engineLogo.js +4 -3
- package/src/engineMath.js +1274 -1183
- package/src/engineObject.js +674 -551
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +808 -726
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +32 -13
- package/src/engineWebGL.js +1162 -941
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
|
@@ -0,0 +1,4072 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS 3D Rendering Plugin
|
|
3
|
+
* - Adds a 3D scene that draws into the same WebGL canvas as the 2D game
|
|
4
|
+
* - Call new Render3DPlugin() in gameInit, then move render3D.camera and make EngineObject3D objects
|
|
5
|
+
* - EngineObject3D is an EngineObject with a 3D position, rotation and mesh
|
|
6
|
+
* - The 3D scene draws under the 2D sprites, so HUD and text land on top
|
|
7
|
+
* - Lighting is the sun plus ambient, with optional extra lights, fog and shadows
|
|
8
|
+
* - Any object or draw can bring its own Shader, a mainImage snippet the lighting then applies to
|
|
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
|
|
11
|
+
* - Requires the Math3D plugin
|
|
12
|
+
* @namespace Render3D
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
18
|
+
|
|
19
|
+
/** Global Render3D plugin object
|
|
20
|
+
* @type {Render3DPlugin}
|
|
21
|
+
* @memberof Render3D */
|
|
22
|
+
let render3D;
|
|
23
|
+
|
|
24
|
+
// vertex format: position xyz, normal xyz, uv, rgba bytes
|
|
25
|
+
const RENDER3D_VERTEX_FLOATS = 9;
|
|
26
|
+
const RENDER3D_VERTEX_BYTES = RENDER3D_VERTEX_FLOATS * 4;
|
|
27
|
+
|
|
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;
|
|
31
|
+
const RENDER3D_INSTANCE_BYTES = RENDER3D_INSTANCE_FLOATS * 4;
|
|
32
|
+
const RENDER3D_INSTANCE_ATTRIBS = [[4, 4, 0], [5, 4, 16], [6, 4, 32], [7, 4, 48], [11, 4, 64], [12, 4, 80]];
|
|
33
|
+
const RENDER3D_VERTEX_INPUTS =
|
|
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;' +
|
|
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;' +
|
|
36
|
+
'layout(location=11) in vec4 tint;layout(location=12) in vec4 uvRect;';
|
|
37
|
+
const RENDER3D_MAX_STREAM_VERTS = 32768;
|
|
38
|
+
const RENDER3D_MAX_LIGHTS = 8; // Light3D objects per frame, the shader loops over this many
|
|
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))));
|
|
42
|
+
const RENDER3D_FULL_UV_RECT = Object.freeze({x:0, y:0, w:1, h:1});
|
|
43
|
+
const RENDER3D_DEFAULT_NORMAL = Object.freeze(vec3(0, 1, 0));
|
|
44
|
+
const RENDER3D_DEFAULT_UV = Object.freeze(vec2());
|
|
45
|
+
const RENDER3D_SHADOW_COLOR = Object.freeze(hsl(0, 0, 0, .5));
|
|
46
|
+
const RENDER3D_IDENTITY = new Matrix4; // never modified
|
|
47
|
+
let render3DShadowCut; // the cut last sent to the shadow shader, see render3DSetDrawUniforms
|
|
48
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
49
|
+
// Private helpers
|
|
50
|
+
|
|
51
|
+
// outward normal of a triangle or a quad given its corners in loop order,
|
|
52
|
+
// from the diagonals so a collapsed corner still works
|
|
53
|
+
function render3DFaceNormal(a, b, c, d=a)
|
|
54
|
+
{
|
|
55
|
+
const n = c.subtract(a).cross(d.subtract(b));
|
|
56
|
+
return n.lengthSquared() ? n.normalize() : RENDER3D_DEFAULT_NORMAL;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// a quad's corners in loop order as a strip, the one place that knows the order
|
|
60
|
+
function render3DQuadStrip(a, b, c, d) { return [a, b, d, c]; }
|
|
61
|
+
|
|
62
|
+
// per corner values (colors, uvs) into strip order, a single value passes through
|
|
63
|
+
function render3DQuadValues(v) { return isArray(v) ? render3DQuadStrip(...v) : v; }
|
|
64
|
+
|
|
65
|
+
// 3D draws are only valid during the pass with a live shader
|
|
66
|
+
function render3DCanDraw()
|
|
67
|
+
{
|
|
68
|
+
if (!render3D.program) return false;
|
|
69
|
+
ASSERT(render3D.isRendering,
|
|
70
|
+
'3D draws are only valid during the 3D pass, draw from an EngineObject3D or render3D.onRenderOpaque');
|
|
71
|
+
return render3D.isRendering;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// the draw state fields a batch is drawn under; lights and fog are not captured, they are read live at flush
|
|
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'];
|
|
78
|
+
|
|
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
|
|
81
|
+
function render3DCaptureBatchState()
|
|
82
|
+
{
|
|
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;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// true when the current draw state differs from a captured one, so a pending batch must flush first
|
|
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)
|
|
110
|
+
{
|
|
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;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// run a function with some draw state fields overridden, restored afterward even on a throw
|
|
122
|
+
function render3DWithState(fields, fn)
|
|
123
|
+
{
|
|
124
|
+
const r = render3D, saved = {};
|
|
125
|
+
for (const key in fields)
|
|
126
|
+
saved[key] = r[key], r[key] = fields[key];
|
|
127
|
+
try { return fn(); }
|
|
128
|
+
finally { Object.assign(r, saved); }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// which side of the 2D scene an object draws on, its own flag or the plugin default
|
|
132
|
+
function render3DIsAfter2D(o) { return !!(o.renderAfter2D ?? render3D.renderAfter2D); }
|
|
133
|
+
|
|
134
|
+
// a size given as a number or a vec3
|
|
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); }
|
|
145
|
+
|
|
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} */
|
|
149
|
+
function render3DMatrix(matrix)
|
|
150
|
+
{
|
|
151
|
+
if (matrix instanceof Vector3)
|
|
152
|
+
return buildMatrix(matrix);
|
|
153
|
+
ASSERT(matrix instanceof Matrix4, 'takes a Matrix4, or a Vector3 for a position');
|
|
154
|
+
return matrix;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// the matrix that keeps normals pointing out when an object is scaled unevenly
|
|
158
|
+
function render3DNormalMatrix(matrix) { return matrix.copy().invert().transpose(); }
|
|
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
|
+
|
|
164
|
+
// a column of a matrix as a direction: 0 is the right axis, 4 up, 8 back
|
|
165
|
+
function render3DAxis(m, i) { return vec3(m[i], m[i+1], m[i+2]); }
|
|
166
|
+
|
|
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)
|
|
169
|
+
{
|
|
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]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// the largest axis scale of a matrix, how much it grows a bounding sphere
|
|
175
|
+
function render3DMaxScale(m)
|
|
176
|
+
{
|
|
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;
|
|
178
|
+
}
|
|
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
|
+
|
|
194
|
+
// a quad as a strip from its center and half axes, the same corner order as render3DQuadStrip
|
|
195
|
+
function render3DQuadAxes(center, right, up)
|
|
196
|
+
{
|
|
197
|
+
return [center.subtract(right).add(up), center.subtract(right).subtract(up),
|
|
198
|
+
center.add(right).add(up), center.add(right).subtract(up)];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// set the draw state for an object's render3D, or the defaults for the stage callbacks
|
|
202
|
+
function render3DSetObjectState(o)
|
|
203
|
+
{
|
|
204
|
+
const r = render3D;
|
|
205
|
+
const emissive = o?.emissive || 0;
|
|
206
|
+
ASSERT(isNumber(emissive) && emissive >= 0, 'emissive must be a number, 0 or more', emissive);
|
|
207
|
+
r.lighting = true;
|
|
208
|
+
r.emissive = emissive;
|
|
209
|
+
r.additive = !!o?.additive;
|
|
210
|
+
r.specular = o?.specular || 0;
|
|
211
|
+
r.receiveShadow = !o || o.receiveShadow;
|
|
212
|
+
r.cullBackFaces = r.mirrored = false; // each mesh sets these as it draws
|
|
213
|
+
r.pixelated = !!o?.pixelated;
|
|
214
|
+
ASSERT(!o?.shader || o.shader instanceof Shader, 'shader must be a Shader, not the snippet itself');
|
|
215
|
+
r.shader = o?.shader || undefined; // null is no shader too, so it batches with none
|
|
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
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// draw objects each with the draw state set from its own flags, then reset to the defaults
|
|
222
|
+
function render3DDrawObjects(objects)
|
|
223
|
+
{
|
|
224
|
+
for (const o of objects)
|
|
225
|
+
{
|
|
226
|
+
render3DSetObjectState(o);
|
|
227
|
+
o.render3D();
|
|
228
|
+
}
|
|
229
|
+
render3DSetObjectState();
|
|
230
|
+
}
|
|
231
|
+
|
|
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)
|
|
234
|
+
{
|
|
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;
|
|
242
|
+
}
|
|
243
|
+
|
|
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)
|
|
247
|
+
{
|
|
248
|
+
const r = render3D;
|
|
249
|
+
if (mesh.instanceCount && (mesh.instanceTextureInfo !== textureInfo || render3DStateChanged(mesh.instanceState)))
|
|
250
|
+
render3DFlushInstances(mesh);
|
|
251
|
+
if (!mesh.instanceCount)
|
|
252
|
+
{
|
|
253
|
+
mesh.instanceTextureInfo = textureInfo;
|
|
254
|
+
mesh.instanceState = render3DCaptureBatchState();
|
|
255
|
+
r.instanceMeshes.push(mesh);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// room for one more, doubling as the batch grows
|
|
259
|
+
const data = mesh.instanceData, k = mesh.instanceCount++ * RENDER3D_INSTANCE_FLOATS;
|
|
260
|
+
if (!data || data.length < k + RENDER3D_INSTANCE_FLOATS)
|
|
261
|
+
{
|
|
262
|
+
const grown = new Float32Array(max(64 * RENDER3D_INSTANCE_FLOATS, data ? data.length * 2 : 0));
|
|
263
|
+
data && grown.set(data);
|
|
264
|
+
mesh.instanceData = grown;
|
|
265
|
+
}
|
|
266
|
+
return k;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// draw the pending batches, or just one mesh's, each as a single instanced call
|
|
270
|
+
function render3DFlushInstances(only)
|
|
271
|
+
{
|
|
272
|
+
const r = render3D, gl = glContext;
|
|
273
|
+
for (const mesh of only ? [only] : r.instanceMeshes)
|
|
274
|
+
{
|
|
275
|
+
const count = mesh.instanceCount;
|
|
276
|
+
mesh.instanceCount = 0;
|
|
277
|
+
if (!count || !mesh.buffer) continue;
|
|
278
|
+
|
|
279
|
+
// the per instance values on top of the constant attributes, then the mesh under them
|
|
280
|
+
// a ring of buffers with fresh storage each time, so the driver never waits for a draw still reading one
|
|
281
|
+
const buffers = r.instanceBuffers, buffer = buffers[r.instanceBufferIndex = (r.instanceBufferIndex + 1) % buffers.length];
|
|
282
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
283
|
+
gl.bufferData(gl.ARRAY_BUFFER, mesh.instanceData, gl.DYNAMIC_DRAW, 0, count * RENDER3D_INSTANCE_FLOATS);
|
|
284
|
+
render3DDrawInstanced(mesh, buffer, count, mesh.instanceTextureInfo, mesh.instanceState);
|
|
285
|
+
}
|
|
286
|
+
if (!only)
|
|
287
|
+
r.instanceMeshes.length = 0;
|
|
288
|
+
else
|
|
289
|
+
{
|
|
290
|
+
const i = r.instanceMeshes.indexOf(only);
|
|
291
|
+
i < 0 || r.instanceMeshes.splice(i, 1);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
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
|
+
|
|
321
|
+
// forget the pending batches, for a frame that threw or a lost context
|
|
322
|
+
function render3DClearInstances()
|
|
323
|
+
{
|
|
324
|
+
for (const mesh of render3D.instanceMeshes)
|
|
325
|
+
mesh.instanceCount = 0;
|
|
326
|
+
render3D.instanceMeshes.length = 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// the live objects drawn on one side of the 2D scene
|
|
330
|
+
function render3DLayerObjects(after2D)
|
|
331
|
+
{
|
|
332
|
+
return /** @type {Array<EngineObject3D>} */ (engineObjects.filter(o=>
|
|
333
|
+
!o.destroyed && o instanceof EngineObject3D && render3DIsAfter2D(o) === after2D));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// the Light3D objects the shader gets this frame: directional lights light the whole scene so they come first,
|
|
337
|
+
// then the point lights nearest the camera
|
|
338
|
+
function render3DCollectLights()
|
|
339
|
+
{
|
|
340
|
+
// a light switched off by its radius or its alpha is left out, so it cannot take one of the few slots
|
|
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)));
|
|
343
|
+
if (lights.length > RENDER3D_MAX_LIGHTS)
|
|
344
|
+
{
|
|
345
|
+
// distances cached once, getWorldPos3D walks the parent chain and the sort asks many times
|
|
346
|
+
const cameraPos = render3D.camera.pos, distances = new Map;
|
|
347
|
+
for (const light of lights)
|
|
348
|
+
distances.set(light, light.directional ? -1 : light.getWorldPos3D().distanceSquared(cameraPos));
|
|
349
|
+
lights.sort((a, b)=> distances.get(a) - distances.get(b));
|
|
350
|
+
lights.length = RENDER3D_MAX_LIGHTS;
|
|
351
|
+
}
|
|
352
|
+
return lights;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// unit circle directions for a number of sides, [cos, sin, cos, sin, ...] including the closing point, cached
|
|
356
|
+
const render3DCircleCache = new Map;
|
|
357
|
+
function render3DCircle(sides)
|
|
358
|
+
{
|
|
359
|
+
sides |= 0;
|
|
360
|
+
let circle = render3DCircleCache.get(sides);
|
|
361
|
+
if (!circle)
|
|
362
|
+
{
|
|
363
|
+
circle = new Float32Array(sides * 2 + 2);
|
|
364
|
+
for (let i = 0; i <= sides; ++i)
|
|
365
|
+
{
|
|
366
|
+
const a = i / sides * 2 * PI;
|
|
367
|
+
circle[i*2] = cos(a), circle[i*2 + 1] = sin(a);
|
|
368
|
+
}
|
|
369
|
+
render3DCircleCache.set(sides, circle);
|
|
370
|
+
}
|
|
371
|
+
return circle;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// the rotation that points -Z along a direction, as vec3(pitch, yaw, 0); a zero direction keeps the current one
|
|
375
|
+
function render3DLookRotation(direction, current)
|
|
376
|
+
{
|
|
377
|
+
const d = direction.normalize();
|
|
378
|
+
if (!d.lengthSquared()) return current;
|
|
379
|
+
if (abs(d.x) + abs(d.z) < 1e-9) // straight up or down has no yaw of its own
|
|
380
|
+
return vec3(d.y > 0 ? PI / 2 : -PI / 2, current.y, 0);
|
|
381
|
+
return vec3(Math.asin(clamp(d.y, -1, 1)), atan2(-d.x, -d.z), 0);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
385
|
+
/**
|
|
386
|
+
* Render3D Plugin - The 3D renderer, camera, lights, shadows and fog
|
|
387
|
+
* - There is one of these, in the global render3D
|
|
388
|
+
* - It draws the 3D scene before gameRender, so 2D drawing lands on top
|
|
389
|
+
* - Set renderAfter2D to draw the 3D scene over the 2D scene instead
|
|
390
|
+
* - Settings like lighting and specular are read as each thing draws
|
|
391
|
+
* - Every object sets them from its own flags, so you rarely touch them
|
|
392
|
+
* @memberof Render3D
|
|
393
|
+
* @example
|
|
394
|
+
* new Render3DPlugin;
|
|
395
|
+
* render3D.camera.pos = vec3(0, 5, 10);
|
|
396
|
+
* render3D.camera.lookAt(vec3());
|
|
397
|
+
* new EngineObject3D(vec3(), buildBox());
|
|
398
|
+
*/
|
|
399
|
+
class Render3DPlugin
|
|
400
|
+
{
|
|
401
|
+
/** Create the global 3D renderer, call in gameInit */
|
|
402
|
+
constructor()
|
|
403
|
+
{
|
|
404
|
+
ASSERT(!render3D, 'Render3D plugin already initialized');
|
|
405
|
+
render3D = this;
|
|
406
|
+
ASSERT(Object.keys(render3DCaptureBatchState()).join() === RENDER3D_STATE_FIELDS.join(),
|
|
407
|
+
'the batch state functions must list RENDER3D_STATE_FIELDS');
|
|
408
|
+
|
|
409
|
+
/** @property {Camera3D} - The camera */
|
|
410
|
+
this.camera = new Camera3D;
|
|
411
|
+
|
|
412
|
+
// lights and fog
|
|
413
|
+
/** @property {Vector3} - Direction toward the sun, where its light comes from, like a directional Light3D;
|
|
414
|
+
* read at each draw, and any length will do, the shading and the shadows normalize it themselves;
|
|
415
|
+
* the sun is the one light that casts shadows */
|
|
416
|
+
this.sunDirection = vec3(-.3, 1, .5);
|
|
417
|
+
/** @property {Color} - Sunlight color */
|
|
418
|
+
this.sunColor = WHITE.copy();
|
|
419
|
+
/** @property {Color} - Ambient light color, from above when ambientGroundColor is set */
|
|
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;
|
|
426
|
+
/** @property {Color|undefined} - Fog color, uses canvasClearColor when undefined
|
|
427
|
+
* @type {Color|undefined} */
|
|
428
|
+
this.fogColor = undefined;
|
|
429
|
+
/** @property {number} - Distance from the camera where fog starts */
|
|
430
|
+
this.fogStart = 0;
|
|
431
|
+
/** @property {number} - Distance from the camera where fog is total, 0 disables fog */
|
|
432
|
+
this.fogEnd = 0;
|
|
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 */
|
|
435
|
+
this.gravity = vec3();
|
|
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} */
|
|
439
|
+
this.softShadowHeight = 0;
|
|
440
|
+
/** @property {boolean} - Default for every builder's smooth argument: true for smooth vertex normals, false for
|
|
441
|
+
* flat faces */
|
|
442
|
+
this.smoothShading = false;
|
|
443
|
+
|
|
444
|
+
// shadows
|
|
445
|
+
/** @property {boolean} - Cast real shadows from the sun, off by default and free when off */
|
|
446
|
+
this.shadows = false;
|
|
447
|
+
/** @property {number} - Size of the shadow map in pixels, bigger is sharper and slower */
|
|
448
|
+
this.shadowMapSize = 1024;
|
|
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 */
|
|
452
|
+
this.shadowRange = 40;
|
|
453
|
+
/** @property {Vector3|undefined} - Center of the shadowed area, read each frame, undefined follows the camera
|
|
454
|
+
* @type {Vector3|undefined} */
|
|
455
|
+
this.shadowCenter = undefined;
|
|
456
|
+
/** @property {number} - Stops surfaces shadowing themselves, raise for speckles, lower if shadows drift off */
|
|
457
|
+
this.shadowBias = .003;
|
|
458
|
+
/** @property {number} - How much to blur the shadow edges */
|
|
459
|
+
this.shadowSoftness = 1;
|
|
460
|
+
|
|
461
|
+
// draw state, read at each draw
|
|
462
|
+
/** @property {boolean} - Apply lighting, when false draws plain vertex color times texture and casts no shadow;
|
|
463
|
+
* off for billboards, lines, ribbons and soft discs, an object sets emissive instead */
|
|
464
|
+
this.lighting = true;
|
|
465
|
+
/** @property {number} - How much a surface lights itself, set per object by its emissive */
|
|
466
|
+
this.emissive = 0;
|
|
467
|
+
/** @property {boolean} - Additive blending instead of alpha, in the transparent stage */
|
|
468
|
+
this.additive = false;
|
|
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 */
|
|
472
|
+
this.depthTest = true;
|
|
473
|
+
/** @property {boolean} - Write to the depth buffer, owned by the stages: on for opaque, off for transparent */
|
|
474
|
+
this.depthWrite = true;
|
|
475
|
+
// batch state set by drawMesh from each mesh: whether its back faces are skipped, off for strips so they
|
|
476
|
+
// show from both sides, and whether its transform mirrors it so the other winding is the front
|
|
477
|
+
this.cullBackFaces = false;
|
|
478
|
+
this.mirrored = false;
|
|
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 */
|
|
481
|
+
this.specular = 0;
|
|
482
|
+
/** @property {Shader|undefined} - Custom Shader for the next draws, set from each object's shader; undefined
|
|
483
|
+
* draws with the plugin's own
|
|
484
|
+
* @type {Shader|undefined} */
|
|
485
|
+
this.shader = undefined;
|
|
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 */
|
|
488
|
+
this.receiveShadow = true;
|
|
489
|
+
|
|
490
|
+
// the pass
|
|
491
|
+
/** @property {Function|undefined} - Draw solid world here, it runs again for shadows so only draw in it
|
|
492
|
+
* @type {Function|undefined} */
|
|
493
|
+
this.onRenderOpaque = undefined;
|
|
494
|
+
/** @property {Function|undefined} - Draw see through things here, like glows, billboards and soft shadows
|
|
495
|
+
* @type {Function|undefined} */
|
|
496
|
+
this.onRenderTransparent = undefined;
|
|
497
|
+
/** @property {Mesh|undefined} - Sky dome from buildSky or setSky, drawn around the camera behind everything
|
|
498
|
+
* @type {Mesh|undefined} */
|
|
499
|
+
this.sky = undefined;
|
|
500
|
+
/** @property {boolean} - Draw the 3D scene on top of the 2D scene instead of under it */
|
|
501
|
+
this.renderAfter2D = false;
|
|
502
|
+
/** @property {boolean} - Draw see through things far to near so they blend correctly */
|
|
503
|
+
this.sortTransparent = true;
|
|
504
|
+
/** @property {boolean} - Skip meshes whose bounding sphere is outside the view */
|
|
505
|
+
this.frustumCulling = true;
|
|
506
|
+
/** @property {boolean} - Draw every use of a mesh in the opaque stage as one instanced call, mesh.instanced
|
|
507
|
+
* overrides it per mesh */
|
|
508
|
+
this.instancing = true;
|
|
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 */
|
|
511
|
+
this.mipmaps = true;
|
|
512
|
+
/** @property {boolean} - Draw state: keep texture pixels hard edged, no mipmaps and no blending between them,
|
|
513
|
+
* set per object by pixelated */
|
|
514
|
+
this.pixelated = false;
|
|
515
|
+
/** @property {number} - Anisotropic filtering for textures seen at an angle, 1 to 16, 1 is off; needs mipmaps */
|
|
516
|
+
this.anisotropy = 4;
|
|
517
|
+
|
|
518
|
+
// shared meshes, every object using one draws in the same batch
|
|
519
|
+
/** @property {Mesh} - A box of size 1 that drawBox uses, for any object that is a box; set the object's scale3D
|
|
520
|
+
* and color instead of editing the mesh, which would change every box that uses it */
|
|
521
|
+
this.boxMesh = buildBox();
|
|
522
|
+
/** @property {Mesh} - A smooth sphere of diameter 1 that drawSphere uses, shared the same way as boxMesh */
|
|
523
|
+
this.sphereMesh = buildSphere(1, 16, 8, true);
|
|
524
|
+
/** @property {Mesh} - A flat square of size 1 facing +Y, seen from above only, for floors, water and decals;
|
|
525
|
+
* stand it up with the object's rotation3D, and size it with scale3D */
|
|
526
|
+
this.planeMesh = buildGrid();
|
|
527
|
+
this.planeMesh.doubleSided = false;
|
|
528
|
+
/** @property {Mesh} - The same square seen and lit from both sides, for signs, cards and leaves */
|
|
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;
|
|
535
|
+
|
|
536
|
+
// read only
|
|
537
|
+
/** @property {boolean} - True while the 3D pass is running, 3D draws are only valid then */
|
|
538
|
+
this.isRendering = false;
|
|
539
|
+
/** @property {boolean} - True while the shadow map is being drawn, draws go to the depth only shader */
|
|
540
|
+
this.shadowPass = false;
|
|
541
|
+
/** @property {Matrix4} - This frame's view matrix */
|
|
542
|
+
this.viewMatrix = new Matrix4;
|
|
543
|
+
/** @property {Matrix4} - This frame's projection matrix */
|
|
544
|
+
this.projectionMatrix = new Matrix4;
|
|
545
|
+
/** @property {Matrix4} - This frame's combined view projection */
|
|
546
|
+
this.viewProjection = new Matrix4;
|
|
547
|
+
/** @property {Matrix4} - This frame's light view projection for the shadow map */
|
|
548
|
+
this.shadowMatrix = new Matrix4;
|
|
549
|
+
/** @property {Vector3} - Camera right axis this frame */
|
|
550
|
+
this.cameraRight = vec3(1, 0, 0);
|
|
551
|
+
/** @property {Vector3} - Camera up axis this frame */
|
|
552
|
+
this.cameraUp = vec3(0, 1, 0);
|
|
553
|
+
/** @property {Vector3} - Camera forward axis this frame */
|
|
554
|
+
this.cameraForward = vec3(0, 0, -1);
|
|
555
|
+
this.cameraBack = vec3(0, 0, 1); // its opposite, the normal of camera facing draws
|
|
556
|
+
|
|
557
|
+
// internal state
|
|
558
|
+
this.blend = false; // blending on, set by the stages
|
|
559
|
+
/** @type {Array<Array<number>>} */
|
|
560
|
+
this.frustumPlanes = []; // the view as six inward planes [x, y, z, w]
|
|
561
|
+
/** @type {Array<Array<number>>} */
|
|
562
|
+
this.shadowPlanes = []; // the shadow map's box as six planes
|
|
563
|
+
/** @type {WebGLProgram|undefined} */
|
|
564
|
+
this.program = undefined; // the main program, undefined when not available
|
|
565
|
+
/** @type {WebGLProgram|undefined} */
|
|
566
|
+
this.currentProgram = undefined; // the program in use during a pass, a Shader's or the main one
|
|
567
|
+
this.lightCount = 0; // Light3D objects sent this pass
|
|
568
|
+
/** @type {WebGLProgram|undefined} */
|
|
569
|
+
this.shadowShader = undefined;
|
|
570
|
+
/** @type {WebGLVertexArrayObject|undefined} */
|
|
571
|
+
this.vao = undefined;
|
|
572
|
+
/** @type {WebGLTexture|undefined} */
|
|
573
|
+
this.whiteTexture = undefined; // 1x1 white for untextured draws
|
|
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} */
|
|
577
|
+
this.samplerKey = undefined; // the settings the samplers were made for, they are rebuilt when it changes
|
|
578
|
+
/** @type {WebGLTexture|undefined} */
|
|
579
|
+
this.shadowTexture = undefined;
|
|
580
|
+
/** @type {WebGLFramebuffer|undefined} */
|
|
581
|
+
this.shadowFramebuffer = undefined;
|
|
582
|
+
this.shadowTextureSize = 0;
|
|
583
|
+
this.contextGeneration = 0; // counts context losses, a mesh uploaded under an older one uploads again
|
|
584
|
+
this.uniforms = new Map; // uniform locations by program
|
|
585
|
+
/** @type {Object<string, Array<number>>} */
|
|
586
|
+
this.uniformValues = {}; // last values sent for the cached vec4 uniforms
|
|
587
|
+
this.shadowMapDrawn = false; // the shadow map is drawn by the first pass of the frame
|
|
588
|
+
this.passIsDefault = true; // the running pass is the default layer, the only one shadowed
|
|
589
|
+
this.lightPositions = new Float32Array(RENDER3D_MAX_LIGHTS * 4); // Light3D uniforms, filled each pass
|
|
590
|
+
this.lightColors = new Float32Array(RENDER3D_MAX_LIGHTS * 4);
|
|
591
|
+
|
|
592
|
+
// the stream of immediate mode draws
|
|
593
|
+
/** @type {WebGLBuffer|undefined} */
|
|
594
|
+
this.streamBuffer = undefined;
|
|
595
|
+
/** @type {Array<WebGLBuffer>} */
|
|
596
|
+
this.instanceBuffers = []; // the per instance values of the batches being drawn, used in turn
|
|
597
|
+
this.instanceBufferIndex = 0;
|
|
598
|
+
/** @type {Array<Mesh>} */
|
|
599
|
+
this.instanceMeshes = []; // meshes with a batch pending this stage
|
|
600
|
+
/** @type {Array<Array<number>>} */
|
|
601
|
+
this.attribValues = []; // last values sent for the cached constant attributes
|
|
602
|
+
this.streamData = new ArrayBuffer(RENDER3D_MAX_STREAM_VERTS * RENDER3D_VERTEX_BYTES);
|
|
603
|
+
this.streamFloats = new Float32Array(this.streamData);
|
|
604
|
+
this.streamInts = new Uint32Array(this.streamData);
|
|
605
|
+
this.streamCount = 0;
|
|
606
|
+
/** @type {TextureInfo|undefined} */
|
|
607
|
+
this.streamTileInfo = undefined;
|
|
608
|
+
this.streamState = undefined; // captured state the pending batch was drawn under
|
|
609
|
+
/** @type {Mesh|undefined} */
|
|
610
|
+
this.capture = undefined; // the mesh a bake is filling
|
|
611
|
+
/** @type {Array<{distance: number, state: Object, draw: function(): void}>|undefined} */
|
|
612
|
+
this.transparentQueue = undefined; // draws queued during the transparent stage, replayed far to near
|
|
613
|
+
|
|
614
|
+
render3DInitGL();
|
|
615
|
+
engineAddPlugin(undefined, render3DRender, render3DContextLost, render3DContextRestored, render3DPreRender);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
///////////////////////////////////////////////////////////////////////////
|
|
619
|
+
// Matrices and picking
|
|
620
|
+
|
|
621
|
+
/** Rebuild the view and projection matrices from the camera, called automatically each frame
|
|
622
|
+
* @param {number} [aspect] - Width over height, defaults to the main canvas */
|
|
623
|
+
updateMatrices(aspect=mainCanvasSize.y ? mainCanvasSize.x / mainCanvasSize.y : 1)
|
|
624
|
+
{
|
|
625
|
+
const camera = this.camera;
|
|
626
|
+
if (camera.align2D)
|
|
627
|
+
camera.update2D();
|
|
628
|
+
const cameraMatrix = camera.getMatrix();
|
|
629
|
+
this.viewMatrix = cameraMatrix.copy().invert();
|
|
630
|
+
this.projectionMatrix = camera.getProjectionMatrix(aspect);
|
|
631
|
+
this.viewProjection = this.projectionMatrix.copy().multiply(this.viewMatrix);
|
|
632
|
+
const m = cameraMatrix.m;
|
|
633
|
+
this.cameraRight = render3DAxis(m, 0);
|
|
634
|
+
this.cameraUp = render3DAxis(m, 4);
|
|
635
|
+
this.cameraBack = render3DAxis(m, 8); // the normal of anything facing the camera
|
|
636
|
+
this.cameraForward = this.cameraBack.scale(-1);
|
|
637
|
+
this.frustumPlanes = render3DFrustumPlanes(this.viewProjection);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** Where a world point lands on screen as -1 to 1 across and up, with z as depth
|
|
641
|
+
* - Uses this frame's camera, call updateMatrices first if the camera just moved
|
|
642
|
+
* @param {Vector3} pos
|
|
643
|
+
* @return {Vector3|undefined} - undefined when behind the camera or closer than the near plane */
|
|
644
|
+
worldToClip(pos)
|
|
645
|
+
{
|
|
646
|
+
const m = this.viewProjection.m;
|
|
647
|
+
const w = m[3]*pos.x + m[7]*pos.y + m[11]*pos.z + m[15];
|
|
648
|
+
const z = (m[2]*pos.x + m[6]*pos.y + m[10]*pos.z + m[14]) / w;
|
|
649
|
+
if (w <= 0 || z < -1)
|
|
650
|
+
return; // behind the camera, or in front of the near plane
|
|
651
|
+
return vec3(
|
|
652
|
+
(m[0]*pos.x + m[4]*pos.y + m[8]*pos.z + m[12]) / w,
|
|
653
|
+
(m[1]*pos.x + m[5]*pos.y + m[9]*pos.z + m[13]) / w, z);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Project a world point to screen space pixels, same space as mousePosScreen
|
|
657
|
+
* - The opposite of screenToRay, and it takes the same canvas so the pair agree
|
|
658
|
+
* @param {Vector3} pos
|
|
659
|
+
* @param {Vector2} [canvasSize] - Defaults to the main canvas size, as in screenToRay;
|
|
660
|
+
* the projection is whatever updateMatrices last built, which screenToRay does for its canvas
|
|
661
|
+
* @return {Vector2|undefined} - undefined when behind the camera or closer than the near plane */
|
|
662
|
+
worldToScreen(pos, canvasSize=mainCanvasSize)
|
|
663
|
+
{
|
|
664
|
+
const clip = this.worldToClip(pos);
|
|
665
|
+
if (!clip)
|
|
666
|
+
return;
|
|
667
|
+
return vec2((clip.x + 1) / 2 * canvasSize.x, (1 - clip.y) / 2 * canvasSize.y);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Get the world ray under a screen position, for clicking on things in 3D
|
|
671
|
+
* - Uses the camera where it is right now, so it is fine to call from gameUpdate
|
|
672
|
+
* - It brings the view matrices up to date for that canvas, so worldToScreen stays its exact opposite
|
|
673
|
+
* @param {Vector2} screenPos - Same space as mousePosScreen
|
|
674
|
+
* @param {Vector2} [canvasSize] - Defaults to the main canvas size
|
|
675
|
+
* @return {Ray3D} - Starts at the camera with a unit direction, or on the near plane when orthographic */
|
|
676
|
+
screenToRay(screenPos, canvasSize=mainCanvasSize)
|
|
677
|
+
{
|
|
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;
|
|
680
|
+
const aspect = width / height, camera = this.camera;
|
|
681
|
+
// bring the matrices up to date for this canvas, so worldToScreen and this agree on where things are
|
|
682
|
+
this.updateMatrices(aspect);
|
|
683
|
+
const clipX = screenPos.x / width * 2 - 1;
|
|
684
|
+
const clipY = 1 - screenPos.y / height * 2;
|
|
685
|
+
// the screen offset moves a parallel ray's origin, or bends a perspective ray's direction
|
|
686
|
+
const h = camera.orthographic ? camera.orthographic / 2 : tan(camera.fov / 2);
|
|
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
|
|
689
|
+
return camera.orthographic
|
|
690
|
+
? new Ray3D(camera.pos.add(offset).add(this.cameraForward.scale(camera.near)), this.cameraForward.copy())
|
|
691
|
+
: new Ray3D(camera.pos.copy(), this.cameraForward.add(offset).normalize());
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** Where a screen position lands on a flat ground plane, for top down games; use HeightMap.raycast for terrain
|
|
695
|
+
* @param {Vector2} screenPos - Same space as mousePosScreen
|
|
696
|
+
* @param {number} [groundHeight] - World height of the ground plane
|
|
697
|
+
* @param {Vector2} [canvasSize] - Defaults to the main canvas size, as in screenToRay
|
|
698
|
+
* @return {Vector3|undefined} - undefined when the ray misses the plane */
|
|
699
|
+
screenToGround(screenPos, groundHeight=0, canvasSize=mainCanvasSize)
|
|
700
|
+
{
|
|
701
|
+
const ray = this.screenToRay(screenPos, canvasSize);
|
|
702
|
+
const t = raycastPlane(ray, vec3(0, groundHeight, 0), RENDER3D_DEFAULT_NORMAL);
|
|
703
|
+
return t === undefined ? undefined : ray.getPosition(t);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Find the nearest object under a screen position or along a ray, for clicking on things
|
|
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
|
|
709
|
+
* - engineObjectsRaycast3D is the other half of this, every object along a ray instead of the nearest
|
|
710
|
+
* @param {Vector2|Ray3D} from - A screen position like mousePosScreen, or a ray to look along
|
|
711
|
+
* @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
|
|
712
|
+
* @return {{object: EngineObject3D, distance: number}|undefined} */
|
|
713
|
+
pick(from, objects=engineObjects)
|
|
714
|
+
{
|
|
715
|
+
const ray = from instanceof Ray3D ? from : this.screenToRay(from);
|
|
716
|
+
let nearest;
|
|
717
|
+
for (const o of objects)
|
|
718
|
+
{
|
|
719
|
+
const distance = render3DRaycastObject(ray, o);
|
|
720
|
+
if (distance !== undefined && (!nearest || distance < nearest.distance))
|
|
721
|
+
nearest = {object: /** @type {EngineObject3D} */ (o), distance}; // only a 3D object has a distance
|
|
722
|
+
}
|
|
723
|
+
return nearest;
|
|
724
|
+
}
|
|
725
|
+
|
|
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
|
|
728
|
+
* @param {Sound} sound
|
|
729
|
+
* @param {Vector3} pos3D
|
|
730
|
+
* @param {number} [volume]
|
|
731
|
+
* @param {number} [pitch]
|
|
732
|
+
* @param {number} [randomnessScale] - How much to scale pitch randomness
|
|
733
|
+
* @param {boolean} [loop]
|
|
734
|
+
* @param {boolean} [paused] - Start it paused
|
|
735
|
+
* @return {SoundInstance|undefined} - undefined when out of range or sound is off */
|
|
736
|
+
playSound(sound, pos3D, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
|
|
737
|
+
{
|
|
738
|
+
// the 3D range fade and pan, Sound.play with no position does the rest
|
|
739
|
+
ASSERT(sound instanceof Sound, 'sound must be a Sound');
|
|
740
|
+
ASSERT(isVector3(pos3D), 'pos3D must be a vec3');
|
|
741
|
+
const offset = pos3D.subtract(this.camera.pos), range = sound.range;
|
|
742
|
+
if (range)
|
|
743
|
+
{
|
|
744
|
+
const distance = offset.length();
|
|
745
|
+
if (distance > range)
|
|
746
|
+
return; // out of range
|
|
747
|
+
const taperRange = range * sound.taper;
|
|
748
|
+
if (distance > taperRange)
|
|
749
|
+
volume *= percent(distance, range, taperRange);
|
|
750
|
+
}
|
|
751
|
+
const instance = sound.play(undefined, volume, pitch, randomnessScale, loop, paused);
|
|
752
|
+
instance?.setPan(offset.normalize().dot(this.cameraRight));
|
|
753
|
+
return instance;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Play a sound on a loop at a 3D position, the same as playSound with loop on
|
|
757
|
+
* - Its volume and pan are set when it starts, change or stop it through the SoundInstance returned
|
|
758
|
+
* @param {Sound} sound
|
|
759
|
+
* @param {Vector3} pos3D
|
|
760
|
+
* @param {number} [volume]
|
|
761
|
+
* @param {number} [pitch]
|
|
762
|
+
* @param {number} [randomnessScale] - How much to scale pitch randomness
|
|
763
|
+
* @return {SoundInstance|undefined} - undefined when out of range or sound is off */
|
|
764
|
+
playSoundLoop(sound, pos3D, volume=1, pitch=1, randomnessScale=1)
|
|
765
|
+
{ return this.playSound(sound, pos3D, volume, pitch, randomnessScale, true); }
|
|
766
|
+
|
|
767
|
+
/** Is any part of a sphere on screen this frame, the test that skips meshes the camera cannot see
|
|
768
|
+
* - While the shadow map is drawing it tests the shadow area instead
|
|
769
|
+
* @param {Vector3} center
|
|
770
|
+
* @param {number} radius
|
|
771
|
+
* @return {boolean} */
|
|
772
|
+
isSphereVisible(center, radius) { return render3DSphereVisible(center.x, center.y, center.z, radius); }
|
|
773
|
+
|
|
774
|
+
///////////////////////////////////////////////////////////////////////////
|
|
775
|
+
// Meshes and the stream
|
|
776
|
+
|
|
777
|
+
/** Draw a mesh with the current draw state, batched with its other uses in the opaque stage when instancing is on
|
|
778
|
+
* @param {Mesh} mesh
|
|
779
|
+
* @param {Matrix4|Vector3} [matrix] - Object transform, or just a position to draw it at
|
|
780
|
+
* @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile or the whole texture
|
|
781
|
+
* @param {Color} [color] - Tint
|
|
782
|
+
* @return {void} */
|
|
783
|
+
drawMesh(mesh, matrix=RENDER3D_IDENTITY, tileInfo, color=WHITE)
|
|
784
|
+
{
|
|
785
|
+
matrix = render3DMatrix(matrix);
|
|
786
|
+
ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
|
|
787
|
+
'tileInfo must be a TileInfo or TextureInfo, it comes before color');
|
|
788
|
+
ASSERT(isColor(color), 'color must be a Color');
|
|
789
|
+
if (this.capture)
|
|
790
|
+
return void this.capture.combine(mesh, matrix, color);
|
|
791
|
+
if (this.transparentQueue)
|
|
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
|
+
}
|
|
797
|
+
if (!render3DCanDraw()) return;
|
|
798
|
+
if (this.shadowPass && !this.lighting) return; // unlit things cast no shadow
|
|
799
|
+
render3DMeshUpload(mesh);
|
|
800
|
+
if (!mesh.bufferCount) return;
|
|
801
|
+
const m = matrix.m;
|
|
802
|
+
if (this.frustumCulling && !render3DSphereVisible(m[12], m[13], m[14], mesh.radius * render3DMaxStretch(m)))
|
|
803
|
+
return;
|
|
804
|
+
// the mesh says whether its back faces can be skipped, and a mirroring transform, one with a negative
|
|
805
|
+
// determinant, turns the winding around so the other one is its front
|
|
806
|
+
const cullBackFaces = this.cullBackFaces, mirrored = this.mirrored;
|
|
807
|
+
this.cullBackFaces = !mesh.doubleSided;
|
|
808
|
+
this.mirrored = render3DMirrors(matrix);
|
|
809
|
+
// the stage draws the batch at its end
|
|
810
|
+
if (!this.blend && this.depthTest && (mesh.instanced ?? this.instancing))
|
|
811
|
+
render3DInstance(mesh, matrix, tileInfo, color);
|
|
812
|
+
else
|
|
813
|
+
{
|
|
814
|
+
this.flush();
|
|
815
|
+
render3DFlushBeforeOverlay();
|
|
816
|
+
render3DSetDrawUniforms(matrix, tileInfo, color);
|
|
817
|
+
render3DBindMesh(mesh);
|
|
818
|
+
glContext.drawElements(glContext.TRIANGLES, mesh.bufferCount, mesh.indexType, 0);
|
|
819
|
+
++drawCount;
|
|
820
|
+
primitiveCount += mesh.bufferCount / 3;
|
|
821
|
+
}
|
|
822
|
+
this.cullBackFaces = cullBackFaces, this.mirrored = mirrored;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** Draw a triangle strip, batched into the stream with the current draw state
|
|
826
|
+
* - Strip order: the first three points make a triangle, then each point makes another with the two before it
|
|
827
|
+
* - List the first three points counter clockwise as seen from the front, or the face points away
|
|
828
|
+
* and may vanish when back faces are culled
|
|
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
|
|
831
|
+
* @param {Array<Vector3>} points - In strip order
|
|
832
|
+
* @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
|
|
833
|
+
* @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, 0-1 across the tile
|
|
834
|
+
* @param {Color|Array<Color>} [colors] - One for all or one per point, vertex colors come before the texture
|
|
835
|
+
* @param {TileInfo|TextureInfo} [tileInfo] - Texture for this strip
|
|
836
|
+
* @return {void} */
|
|
837
|
+
drawStrip(points, normals, uvs, colors, tileInfo)
|
|
838
|
+
{
|
|
839
|
+
if (this.capture)
|
|
840
|
+
{
|
|
841
|
+
this.capture.addStrip(points, normals, uvs, colors);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (this.transparentQueue)
|
|
845
|
+
{
|
|
846
|
+
// sort by the center of the strip
|
|
847
|
+
let x = 0, y = 0, z = 0;
|
|
848
|
+
for (const p of points)
|
|
849
|
+
x += p.x, y += p.y, z += p.z;
|
|
850
|
+
return this.queueTransparent(vec3(x, y, z).scale(1 / points.length),
|
|
851
|
+
()=> this.drawStrip(points, normals, uvs, colors, tileInfo));
|
|
852
|
+
}
|
|
853
|
+
ASSERT(isArray(points) && points.length > 2, 'strip needs at least 3 points');
|
|
854
|
+
const n = points.length, count = render3DStripCount(n);
|
|
855
|
+
const uvRect = render3DBeginStrip(count, tileInfo);
|
|
856
|
+
if (!uvRect) return;
|
|
857
|
+
|
|
858
|
+
// the tile rect is applied to each uv now, so the whole texture maps at flush
|
|
859
|
+
const floats = this.streamFloats, ints = this.streamInts;
|
|
860
|
+
const normalArray = isArray(normals), uvArray = isArray(uvs), colorArray = isArray(colors);
|
|
861
|
+
const rgba = colorArray ? 0 : (colors || WHITE).rgbaInt();
|
|
862
|
+
for (let k = 0; k < count; ++k)
|
|
863
|
+
{
|
|
864
|
+
const i = render3DStripIndex(k, n), p = points[i];
|
|
865
|
+
const uv = uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV;
|
|
866
|
+
render3DWriteVertex(floats, ints, this.streamCount++ * RENDER3D_VERTEX_FLOATS, p.x, p.y, p.z,
|
|
867
|
+
normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL,
|
|
868
|
+
uvRect.x + uv.x * uvRect.w, uvRect.y + uv.y * uvRect.h, colorArray ? colors[i].rgbaInt() : rgba);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** Draw a strip with lighting off, for camera facing shapes where the light direction means nothing
|
|
873
|
+
* @param {Array<Vector3>} points - Strip order
|
|
874
|
+
* @param {Vector3|Array<Vector3>} [normals]
|
|
875
|
+
* @param {Vector2|Array<Vector2>} [uvs]
|
|
876
|
+
* @param {Color|Array<Color>} [colors]
|
|
877
|
+
* @param {TileInfo|TextureInfo} [tileInfo] */
|
|
878
|
+
drawStripUnlit(points, normals, uvs, colors, tileInfo)
|
|
879
|
+
{ render3DWithState({lighting: false}, ()=> this.drawStrip(points, normals, uvs, colors, tileInfo)); }
|
|
880
|
+
|
|
881
|
+
/** Draw the pending stream vertices as one strip with the state they were drawn under, called automatically when needed */
|
|
882
|
+
flush()
|
|
883
|
+
{
|
|
884
|
+
if (!this.streamCount || !render3DCanDraw()) return;
|
|
885
|
+
const gl = glContext;
|
|
886
|
+
render3DSetDrawUniforms(RENDER3D_IDENTITY, this.streamTileInfo, WHITE, RENDER3D_FULL_UV_RECT, this.streamState);
|
|
887
|
+
render3DBindVertexBuffer(this.streamBuffer);
|
|
888
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.streamFloats, 0, this.streamCount * RENDER3D_VERTEX_FLOATS);
|
|
889
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, this.streamCount);
|
|
890
|
+
++drawCount;
|
|
891
|
+
primitiveCount += this.streamCount;
|
|
892
|
+
this.streamCount = 0;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** Build a mesh once out of draw calls, instead of redrawing the shapes every frame
|
|
896
|
+
* - Call the same drawStrip, drawQuad and drawBox calls inside, and get a mesh back
|
|
897
|
+
* - Strips inside a bake ignore their tileInfo, the finished mesh picks the texture when it draws
|
|
898
|
+
* - drawMesh, drawBox and drawSphere copy their mesh in, moved and tinted, their tileInfo dropped too
|
|
899
|
+
* - The mesh skips its back faces like any, set doubleSided when what was drawn is open
|
|
900
|
+
* @param {Function} drawFunction
|
|
901
|
+
* @return {Mesh} */
|
|
902
|
+
bake(drawFunction)
|
|
903
|
+
{
|
|
904
|
+
this.flush();
|
|
905
|
+
ASSERT(!this.capture, 'bake cannot be nested');
|
|
906
|
+
const mesh = this.capture = new Mesh;
|
|
907
|
+
try { drawFunction(); }
|
|
908
|
+
finally { this.capture = undefined; }
|
|
909
|
+
return mesh;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
///////////////////////////////////////////////////////////////////////////
|
|
913
|
+
// The stages, run by the pass
|
|
914
|
+
|
|
915
|
+
/** Draw a layer's objects, solid ones first and see through ones after, called automatically
|
|
916
|
+
* - The main layer also draws the sky, the render callbacks and the debug shapes
|
|
917
|
+
* @param {Array<EngineObject3D>} objects
|
|
918
|
+
* @param {boolean} [isDefault] */
|
|
919
|
+
renderStages(objects, isDefault=true)
|
|
920
|
+
{
|
|
921
|
+
const opaque = [], transparent = [];
|
|
922
|
+
for (const o of objects)
|
|
923
|
+
(o.transparent || o.additive ? transparent : opaque).push(o);
|
|
924
|
+
|
|
925
|
+
isDefault && this.sky && this.drawSky();
|
|
926
|
+
|
|
927
|
+
// opaque: no blending, depth writes on, by render order
|
|
928
|
+
this.blend = false;
|
|
929
|
+
this.depthWrite = true;
|
|
930
|
+
const byOrder = (a, b)=> a.renderOrder - b.renderOrder;
|
|
931
|
+
opaque.sort(byOrder);
|
|
932
|
+
transparent.sort(byOrder);
|
|
933
|
+
render3DDrawObjects(opaque);
|
|
934
|
+
isDefault && this.onRenderOpaque?.();
|
|
935
|
+
this.flush();
|
|
936
|
+
render3DFlushInstances();
|
|
937
|
+
|
|
938
|
+
// transparent: blending on, depth writes off, every draw queued then replayed far to near
|
|
939
|
+
this.blend = true;
|
|
940
|
+
this.depthWrite = false;
|
|
941
|
+
this.transparentQueue = this.sortTransparent ? [] : undefined;
|
|
942
|
+
try
|
|
943
|
+
{
|
|
944
|
+
render3DDrawObjects(transparent);
|
|
945
|
+
for (const o of objects)
|
|
946
|
+
if (o.softShadow)
|
|
947
|
+
{
|
|
948
|
+
// the shadow grows with the object, by the same scale picking and culling
|
|
949
|
+
// measure it at, so one size set once holds however the object is scaled
|
|
950
|
+
const m = render3DObjectMatrix(o);
|
|
951
|
+
this.drawSoftShadow(m.getTranslation(), o.softShadow * render3DMaxScale(m.m), this.softShadowHeight);
|
|
952
|
+
}
|
|
953
|
+
isDefault && this.onRenderTransparent?.();
|
|
954
|
+
}
|
|
955
|
+
finally { this.flushTransparentQueue(); }
|
|
956
|
+
isDefault && render3DRenderDebug();
|
|
957
|
+
this.flush();
|
|
958
|
+
|
|
959
|
+
// leave the fields at the opaque defaults for anything reading them outside the pass
|
|
960
|
+
render3DSetObjectState();
|
|
961
|
+
this.blend = false;
|
|
962
|
+
this.depthWrite = true;
|
|
963
|
+
}
|
|
964
|
+
|
|
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
|
|
968
|
+
* @param {Vector3} pos - Where the draw is, for sorting
|
|
969
|
+
* @param {function(): void} draw
|
|
970
|
+
* @return {void} */
|
|
971
|
+
queueTransparent(pos, draw)
|
|
972
|
+
{
|
|
973
|
+
if (!this.transparentQueue)
|
|
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});
|
|
982
|
+
}
|
|
983
|
+
|
|
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 */
|
|
986
|
+
flushTransparentQueue()
|
|
987
|
+
{
|
|
988
|
+
const queue = this.transparentQueue;
|
|
989
|
+
if (!queue) return;
|
|
990
|
+
this.transparentQueue = undefined;
|
|
991
|
+
queue.sort((a, b)=> b.distance - a.distance);
|
|
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); }
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** Draw render3D.sky around the camera, unlit, unfogged and behind everything, called automatically by the pass */
|
|
1003
|
+
drawSky()
|
|
1004
|
+
{
|
|
1005
|
+
this.flush();
|
|
1006
|
+
// the dome only has to sit between the clip planes, the pass draws it first with no depth test;
|
|
1007
|
+
// a far plane at Infinity has no midpoint, so put it a long way out instead
|
|
1008
|
+
const {near, far} = this.camera;
|
|
1009
|
+
const radius = far == Infinity ? near * 1e4 : (near + far) / 2;
|
|
1010
|
+
render3DWithState({lighting: false, blend: false, depthTest: false, depthWrite: false, fogEnd: 0, shader: undefined}, ()=>
|
|
1011
|
+
this.drawMesh(this.sky, buildMatrix(this.camera.pos, undefined, vec3(radius))));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/** Rebuild the light's view projection around the shadow center, called automatically each frame shadows are on */
|
|
1015
|
+
updateShadowMatrix()
|
|
1016
|
+
{
|
|
1017
|
+
ASSERT(this.shadowRange > 0, 'shadowRange must be positive');
|
|
1018
|
+
const range = this.shadowRange > 0 ? this.shadowRange : 1, half = range / 2;
|
|
1019
|
+
const toSun = this.sunDirection.normalize();
|
|
1020
|
+
const center = this.shadowCenter || this.camera.pos.add(this.cameraForward.scale(half * .8));
|
|
1021
|
+
const view = Matrix4.lookAt(center.add(toSun.scale(range)), center).invert();
|
|
1022
|
+
// move the light's view in whole pixel steps so shadow edges do not crawl as the camera moves
|
|
1023
|
+
const texel = range / (this.shadowTextureSize || this.shadowMapSize), m = view.m; // no texture in headless mode
|
|
1024
|
+
m[12] = round(m[12] / texel) * texel;
|
|
1025
|
+
m[13] = round(m[13] / texel) * texel;
|
|
1026
|
+
this.shadowMatrix = Matrix4.orthographic(-half, half, -half, half, 0, range * 2).multiply(view);
|
|
1027
|
+
this.shadowPlanes = render3DFrustumPlanes(this.shadowMatrix);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
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
|
|
1032
|
+
* @param {Color} [topColor] - Straight up
|
|
1033
|
+
* @param {Color} [horizonColor] - Level with the camera
|
|
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
|
|
1036
|
+
* @return {Mesh} - The dome, also in render3D.sky */
|
|
1037
|
+
setSky(topColor=hsl(.6, .8, .55), horizonColor=hsl(.6, 1, .9), bottomColor=horizonColor, ambient=.5)
|
|
1038
|
+
{
|
|
1039
|
+
this.sky?.dispose();
|
|
1040
|
+
this.sky = buildSky(topColor, horizonColor, bottomColor);
|
|
1041
|
+
this.fogColor = horizonColor.copy();
|
|
1042
|
+
this.ambientColor = topColor.scale(ambient, 1);
|
|
1043
|
+
this.ambientGroundColor = bottomColor.scale(ambient, 1);
|
|
1044
|
+
return this.sky;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/** Set where fog starts and ends, and its color
|
|
1048
|
+
* @param {number} fogStart - Distance from the camera where fog starts
|
|
1049
|
+
* @param {number} fogEnd - Distance where fog is total, 0 disables fog
|
|
1050
|
+
* @param {Color} [fogColor] - Leaves the color alone when not passed, setSky sets it to the horizon */
|
|
1051
|
+
setFog(fogStart, fogEnd, fogColor)
|
|
1052
|
+
{
|
|
1053
|
+
this.fogStart = fogStart;
|
|
1054
|
+
this.fogEnd = fogEnd;
|
|
1055
|
+
if (fogColor)
|
|
1056
|
+
this.fogColor = fogColor.copy();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
///////////////////////////////////////////////////////////////////////////
|
|
1060
|
+
// Immediate mode shapes
|
|
1061
|
+
|
|
1062
|
+
/** Draw a box, untextured, for blocking out a scene without meshes or objects
|
|
1063
|
+
* @param {Vector3} pos - Center
|
|
1064
|
+
* @param {Vector3|number} [size] - Full size, a number for a cube
|
|
1065
|
+
* @param {Color} [color]
|
|
1066
|
+
* @param {Vector3} [rotation] - vec3(pitch, yaw, roll) */
|
|
1067
|
+
drawBox(pos, size=1, color=WHITE, rotation)
|
|
1068
|
+
{
|
|
1069
|
+
this.drawMesh(this.boxMesh, buildMatrix(pos, rotation, render3DSize3(size)), undefined, color);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/** Draw a sphere, untextured and smooth shaded
|
|
1073
|
+
* @param {Vector3} pos - Center
|
|
1074
|
+
* @param {number} [size] - Diameter
|
|
1075
|
+
* @param {Color} [color] */
|
|
1076
|
+
drawSphere(pos, size=1, color=WHITE)
|
|
1077
|
+
{
|
|
1078
|
+
this.drawMesh(this.sphereMesh, buildMatrix(pos, undefined, vec3(size)), undefined, color);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/** Draw a flat square that always faces the camera, unlit so it keeps its own colors
|
|
1082
|
+
* - Draw it from onRenderTransparent or a transparent object so it can fade
|
|
1083
|
+
* @param {Vector3} pos - Center
|
|
1084
|
+
* @param {Vector2} [size] - World units
|
|
1085
|
+
* @param {TileInfo|TextureInfo} [tileInfo]
|
|
1086
|
+
* @param {Color} [color]
|
|
1087
|
+
* @param {number} [angle] - Rotation in the camera plane, counter clockwise
|
|
1088
|
+
* @param {boolean} [upright] - Stand on world up and only turn to face the camera, for sprites on the ground
|
|
1089
|
+
* @return {void} */
|
|
1090
|
+
drawBillboard(pos, size=vec2(1), tileInfo, color=WHITE, angle=0, upright=false)
|
|
1091
|
+
{
|
|
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);
|
|
1095
|
+
if (this.transparentQueue) // sort by the exact position, a shadow under it sorts by the floor
|
|
1096
|
+
{
|
|
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));
|
|
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);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/** Draw a quad from four corners in loop order, counter clockwise seen from the front, a is the top left of the texture
|
|
1126
|
+
* @param {Vector3} a
|
|
1127
|
+
* @param {Vector3} b
|
|
1128
|
+
* @param {Vector3} c
|
|
1129
|
+
* @param {Vector3} d
|
|
1130
|
+
* @param {TileInfo|TextureInfo} [tileInfo]
|
|
1131
|
+
* @param {Color|Array<Color>} [color] - One for all or one per corner */
|
|
1132
|
+
drawQuad(a, b, c, d, tileInfo, color=WHITE)
|
|
1133
|
+
{
|
|
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);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/** Draw a triangle, counter clockwise from outside is the front
|
|
1142
|
+
* @param {Vector3} a
|
|
1143
|
+
* @param {Vector3} b
|
|
1144
|
+
* @param {Vector3} c
|
|
1145
|
+
* @param {Color} [color] */
|
|
1146
|
+
drawTriangle(a, b, c, color=WHITE)
|
|
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();
|
|
1150
|
+
this.drawStrip([a, b, c], render3DFaceNormal(a, b, c), undefined, color);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/** Draw a line as a camera facing ribbon, unlit
|
|
1154
|
+
* @param {Vector3} posA
|
|
1155
|
+
* @param {Vector3} posB
|
|
1156
|
+
* @param {number} [width]
|
|
1157
|
+
* @param {Color} [color] */
|
|
1158
|
+
drawLine(posA, posB, width=.1, color=WHITE)
|
|
1159
|
+
{
|
|
1160
|
+
this.drawRibbon([posA, posB], width, undefined, color);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/** Draw a ribbon along a path, unlit and visible from both sides; width and color can change along it
|
|
1164
|
+
* - The texture runs along the length, u from the first point to the last
|
|
1165
|
+
* - A path that ends where it starts is a loop, and joins with no seam
|
|
1166
|
+
* @param {Array<Vector3>} points - Center line in order, at least two
|
|
1167
|
+
* @param {number|Array<number>} [width] - Full width, one for all or one per point
|
|
1168
|
+
* @param {TileInfo|TextureInfo} [tileInfo]
|
|
1169
|
+
* @param {Color|Array<Color>} [color] - One for all or one per point
|
|
1170
|
+
* @param {Vector3|Array<Vector3>} [side] - Direction across the ribbon, one for all or one per point, default
|
|
1171
|
+
* faces the camera */
|
|
1172
|
+
drawRibbon(points, width=.1, tileInfo, color=WHITE, side)
|
|
1173
|
+
{
|
|
1174
|
+
const count = points.length;
|
|
1175
|
+
ASSERT(count > 1, 'a ribbon needs at least two points');
|
|
1176
|
+
ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
|
|
1177
|
+
'tileInfo must be a TileInfo or TextureInfo, it comes before color');
|
|
1178
|
+
const strip = [], uvs = tileInfo ? [] : undefined, colors = [], forward = this.cameraForward;
|
|
1179
|
+
let across = vec3(1, 0, 0); // kept from the last point where the direction vanishes
|
|
1180
|
+
// a loop's two ends take their direction across the join, so they meet edge to edge
|
|
1181
|
+
const loop = count > 2 && points[0].distanceSquared(points[count - 1]) < 1e-12;
|
|
1182
|
+
for (let i = 0; i < count; ++i)
|
|
1183
|
+
{
|
|
1184
|
+
const p = points[i];
|
|
1185
|
+
const w = isArray(width) ? width[i] : width;
|
|
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();
|
|
1189
|
+
const s = side && (isArray(side) ? side[i] : side);
|
|
1190
|
+
// across the path in the camera plane unless a side is given
|
|
1191
|
+
const next = points[i < count - 1 ? i + 1 : loop ? 1 : i];
|
|
1192
|
+
const last = points[i > 0 ? i - 1 : loop ? count - 2 : i];
|
|
1193
|
+
const dir = s || next.subtract(last).cross(forward);
|
|
1194
|
+
if (dir.lengthSquared() > 1e-12)
|
|
1195
|
+
across = dir.normalize();
|
|
1196
|
+
const half = across.scale(w / 2);
|
|
1197
|
+
strip.push(p.add(half), p.subtract(half));
|
|
1198
|
+
uvs?.push(vec2(i / (count - 1), 0), vec2(i / (count - 1), 1));
|
|
1199
|
+
colors.push(c, c);
|
|
1200
|
+
}
|
|
1201
|
+
render3DWithState({lighting: false, cullBackFaces: false},
|
|
1202
|
+
()=> this.drawStrip(strip, forward.scale(-1), uvs, colors, tileInfo));
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/** Draw a disc that fades to transparent at the rim, unlit, for glows, puffs and sky dots
|
|
1206
|
+
* @param {Vector3} pos - Center
|
|
1207
|
+
* @param {number} [size] - Diameter
|
|
1208
|
+
* @param {Color} [color]
|
|
1209
|
+
* @param {Vector3} [normal] - Facing direction, faces the camera by default
|
|
1210
|
+
* @param {number} [sides]
|
|
1211
|
+
* @return {void} */
|
|
1212
|
+
drawSoftDisc(pos, size=1, color=WHITE, normal=this.cameraBack, sides=16)
|
|
1213
|
+
{
|
|
1214
|
+
render3DAssertBlending();
|
|
1215
|
+
if (this.transparentQueue && !this.capture)
|
|
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
|
+
}
|
|
1220
|
+
// basis in the disc's plane
|
|
1221
|
+
const n = normal.normalize();
|
|
1222
|
+
const helper = abs(n.y) < .9 ? vec3(0, 1, 0) : vec3(1, 0, 0);
|
|
1223
|
+
const u = helper.cross(n).normalize(), w = u.cross(n);
|
|
1224
|
+
render3DDrawSoftDisc(size / 2, color, sides, n, (c, s, r)=>
|
|
1225
|
+
vec3(pos.x + (u.x * c + w.x * s) * r, pos.y + (u.y * c + w.y * s) * r, pos.z + (u.z * c + w.z * s) * r));
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/** Draw a soft round shadow on the ground under something, much cheaper than a real shadow
|
|
1229
|
+
* - Draw it from onRenderTransparent or from a transparent object
|
|
1230
|
+
* @param {Vector3} pos - Position of the thing casting the shadow
|
|
1231
|
+
* @param {number} [size] - Diameter
|
|
1232
|
+
* @param {number|HeightMap|function(number, number): number} [floorHeight] - Height of the ground, a HeightMap, or
|
|
1233
|
+
* (x, z) => y to follow terrain
|
|
1234
|
+
* @param {Color} [color]
|
|
1235
|
+
* @param {number} [lift] - How far above the ground to draw, raise it if the shadow cuts into rough ground
|
|
1236
|
+
* @return {void} */
|
|
1237
|
+
drawSoftShadow(pos, size=1, floorHeight=0, color=RENDER3D_SHADOW_COLOR, lift=.02)
|
|
1238
|
+
{
|
|
1239
|
+
render3DAssertBlending();
|
|
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);
|
|
1244
|
+
if (this.transparentQueue && !this.capture) // sort from the floor, under whatever casts it
|
|
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
|
+
}
|
|
1250
|
+
render3DDrawSoftDisc(size / 2, color, 16, RENDER3D_DEFAULT_NORMAL, (c, s, r)=>
|
|
1251
|
+
{
|
|
1252
|
+
const x = pos.x + c * r, z = pos.z + s * r;
|
|
1253
|
+
return vec3(x, height(x, z) + lift, z);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function render3DAssertBlending()
|
|
1259
|
+
{
|
|
1260
|
+
const r = render3D;
|
|
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');
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// draw the three rings of a soft disc as unlit strips, pointAt(cos, sin, radius) gives the world point
|
|
1266
|
+
function render3DDrawSoftDisc(radius, color, sides, normal, pointAt)
|
|
1267
|
+
{
|
|
1268
|
+
const alpha = [1, .9, .7, 0], circle = render3DCircle(sides); // alpha by ring, center to rim
|
|
1269
|
+
for (let k = 0; k < 3; ++k)
|
|
1270
|
+
{
|
|
1271
|
+
const points = [], colors = [];
|
|
1272
|
+
const c0 = color.withAlpha(color.a * alpha[k]), c1 = color.withAlpha(color.a * alpha[k+1]);
|
|
1273
|
+
const r0 = radius * k / 3, r1 = radius * (k + 1) / 3;
|
|
1274
|
+
for (let i = 0; i <= sides; ++i)
|
|
1275
|
+
{
|
|
1276
|
+
const c = circle[i*2], s = circle[i*2 + 1];
|
|
1277
|
+
points.push(pointAt(c, s, r1), pointAt(c, s, r0));
|
|
1278
|
+
colors.push(c1, c0);
|
|
1279
|
+
}
|
|
1280
|
+
render3D.drawStripUnlit(points, normal, undefined, colors);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1285
|
+
/**
|
|
1286
|
+
* Camera3D - Position, rotation and lens for the 3D view
|
|
1287
|
+
* - Looks down its -Z axis, rotation is vec3(pitch, yaw, roll)
|
|
1288
|
+
* @memberof Render3D
|
|
1289
|
+
*/
|
|
1290
|
+
class Camera3D
|
|
1291
|
+
{
|
|
1292
|
+
/** Create a camera, looking down -Z from z=10 by default */
|
|
1293
|
+
constructor()
|
|
1294
|
+
{
|
|
1295
|
+
/** @property {Vector3} - World position */
|
|
1296
|
+
this.pos = vec3(0, 0, 10);
|
|
1297
|
+
/** @property {Vector3} - Euler rotation, vec3(pitch, yaw, roll) in radians */
|
|
1298
|
+
this.rotation = vec3();
|
|
1299
|
+
/** @property {number} - Vertical field of view in radians */
|
|
1300
|
+
this.fov = PI/3;
|
|
1301
|
+
/** @property {number} - Near clip distance */
|
|
1302
|
+
this.near = .1;
|
|
1303
|
+
/** @property {number} - Far clip distance, Infinity is allowed for a perspective view */
|
|
1304
|
+
this.far = 1e3;
|
|
1305
|
+
/** @property {number} - Visible height in world units for an orthographic view, 0 is perspective */
|
|
1306
|
+
this.orthographic = 0;
|
|
1307
|
+
/** @property {boolean} - Line the 3D camera up with the 2D camera, so 3D things at z=0 sit on the 2D sprites */
|
|
1308
|
+
this.align2D = false;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/** Returns the camera's world transform
|
|
1312
|
+
* @return {Matrix4} */
|
|
1313
|
+
getMatrix() { return buildMatrix(this.pos, this.rotation); }
|
|
1314
|
+
|
|
1315
|
+
/** Returns the view matrix, world to camera space
|
|
1316
|
+
* @return {Matrix4} */
|
|
1317
|
+
getViewMatrix() { return this.getMatrix().invert(); }
|
|
1318
|
+
|
|
1319
|
+
/** Returns the projection matrix
|
|
1320
|
+
* @param {number} aspect - Width over height
|
|
1321
|
+
* @return {Matrix4} */
|
|
1322
|
+
getProjectionMatrix(aspect)
|
|
1323
|
+
{
|
|
1324
|
+
const h = this.orthographic / 2, w = h * aspect;
|
|
1325
|
+
return h ? Matrix4.orthographic(-w, w, -h, h, this.near, this.far) :
|
|
1326
|
+
Matrix4.perspective(this.fov, aspect, this.near, this.far);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/** Returns the direction the camera looks
|
|
1330
|
+
* @return {Vector3} */
|
|
1331
|
+
getForward() { return render3DAxis(this.getMatrix().m, 8).scale(-1); }
|
|
1332
|
+
|
|
1333
|
+
/** Returns the camera's right axis
|
|
1334
|
+
* @return {Vector3} */
|
|
1335
|
+
getRight() { return render3DAxis(this.getMatrix().m, 0); }
|
|
1336
|
+
|
|
1337
|
+
/** Returns the camera's up axis
|
|
1338
|
+
* @return {Vector3} */
|
|
1339
|
+
getUp() { return render3DAxis(this.getMatrix().m, 4); }
|
|
1340
|
+
|
|
1341
|
+
/** Point the camera at a target, sets pitch and yaw and clears roll
|
|
1342
|
+
* @param {Vector3} target */
|
|
1343
|
+
lookAt(target) { this.rotation = render3DLookRotation(target.subtract(this.pos), this.rotation); }
|
|
1344
|
+
|
|
1345
|
+
/** Put the camera on an orbit around a target, looking at it
|
|
1346
|
+
* @param {Vector3} target
|
|
1347
|
+
* @param {number} distance
|
|
1348
|
+
* @param {number} yaw - Radians around Y
|
|
1349
|
+
* @param {number} [pitch] - Radians above the horizon */
|
|
1350
|
+
orbit(target, distance, yaw, pitch=.5)
|
|
1351
|
+
{
|
|
1352
|
+
const r = cos(pitch) * distance;
|
|
1353
|
+
this.pos = target.add(vec3(sin(yaw) * r, sin(pitch) * distance, cos(yaw) * r));
|
|
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));
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/** Chase a target from an offset, easing toward it, and look at it
|
|
1359
|
+
* @param {Vector3} target
|
|
1360
|
+
* @param {Vector3} offset - Where to sit relative to the target
|
|
1361
|
+
* @param {number} [percent] - How far to move toward the spot each call, 1 snaps */
|
|
1362
|
+
follow(target, offset, percent=1)
|
|
1363
|
+
{
|
|
1364
|
+
this.pos = this.pos.lerp(target.add(offset), percent);
|
|
1365
|
+
this.lookAt(target);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/** Line the 3D camera up with the 2D camera, called automatically when align2D is set
|
|
1369
|
+
* @param {number} [canvasHeight] - Defaults to the main canvas height */
|
|
1370
|
+
update2D(canvasHeight=mainCanvasSize.y)
|
|
1371
|
+
{
|
|
1372
|
+
const halfHeight = canvasHeight / 2 / cameraScale; // half visible height in world units
|
|
1373
|
+
const distance = halfHeight / tan(this.fov/2);
|
|
1374
|
+
// a zoomed out 2D camera sits a long way back, far enough to fall past the far plane and
|
|
1375
|
+
// clip the whole scene away, which looks like nothing rendering at all
|
|
1376
|
+
ASSERT(!canvasHeight || distance < this.far,
|
|
1377
|
+
'align2D needs this camera distance to match the 2D view, raise camera.far past it', distance);
|
|
1378
|
+
this.orthographic &&= halfHeight * 2; // an orthographic camera stays orthographic and shows the same height
|
|
1379
|
+
this.pos = vec3(cameraPos.x, cameraPos.y, distance);
|
|
1380
|
+
this.rotation = vec3(0, 0, -cameraAngle); // 2D angles turn the other way
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1385
|
+
// GL setup, shaders and the frame hooks
|
|
1386
|
+
|
|
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]>} */
|
|
1389
|
+
const RENDER3D_ATTRIBS = [[0, 3, 5126, false, 0], [1, 3, 5126, false, 12], [2, 2, 5126, false, 24], [3, 4, 5121, true, 32]];
|
|
1390
|
+
|
|
1391
|
+
// the vertex shader, shared by the plugin's program and every Shader's
|
|
1392
|
+
// attributes: p position, n normal, t uv, c color, at fixed slots the depth shader also uses
|
|
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
|
|
1398
|
+
const RENDER3D_VERTEX_SOURCE =
|
|
1399
|
+
'#version 300 es\n' +
|
|
1400
|
+
'precision highp float;' +
|
|
1401
|
+
'uniform mat4 viewProj,lightViewProj;' +
|
|
1402
|
+
RENDER3D_VERTEX_INPUTS +
|
|
1403
|
+
'out vec3 P,N;out vec2 T,L;out vec4 C,S;' +
|
|
1404
|
+
'void main(){' +
|
|
1405
|
+
'vec4 w=mat4(m0,m1,m2,m3)*vec4(p,1.);' +
|
|
1406
|
+
'gl_Position=viewProj*w;' +
|
|
1407
|
+
'P=w.xyz;' +
|
|
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;' +
|
|
1410
|
+
'T=uvRect.xy+t*uvRect.zw;' +
|
|
1411
|
+
'L=t;' +
|
|
1412
|
+
'C=c*tint;' +
|
|
1413
|
+
'S=lightViewProj*w;' +
|
|
1414
|
+
'}';
|
|
1415
|
+
|
|
1416
|
+
// the names a Shader's snippet can use in 3D, over the plugin's own uniforms and varyings
|
|
1417
|
+
const RENDER3D_SNIPPET_NAMES =
|
|
1418
|
+
'uniform float iTime;uniform vec3 iResolution;\n' +
|
|
1419
|
+
'#define iChannel0 tex\n' +
|
|
1420
|
+
'#define localUV L\n' +
|
|
1421
|
+
'#define worldPos P\n' +
|
|
1422
|
+
'#define worldNormal N\n' +
|
|
1423
|
+
'#define sunDirection (-lightDir.xyz)\n' +
|
|
1424
|
+
'#define sunColor lightColor.rgb\n' +
|
|
1425
|
+
'#define ambientColor ambientFog.rgb\n' +
|
|
1426
|
+
'#define ambientGroundColor ambientGround.rgb\n' +
|
|
1427
|
+
'#define lightCount extraLightCount\n' +
|
|
1428
|
+
'#define lights extraLights\n' +
|
|
1429
|
+
'#define lightColors extraLightColors\n';
|
|
1430
|
+
|
|
1431
|
+
// the fragment shader; given a Shader's snippet, its mainImage replaces the texture sample and all else is the same
|
|
1432
|
+
// uniforms: lightDir (xyz the way the sunlight travels, w = emissive, 1 or more skips the lighting),
|
|
1433
|
+
// lightColor (the sun's rgb, a = specular), ambientFog (rgb, a = fogEnd), fogColor (rgb, a = fogStart),
|
|
1434
|
+
// cameraPos, tex, shadowMap, shadowParams (x = shadows on, y = bias, z = blur step in texture space,
|
|
1435
|
+
// w = how the draw finishes: 1 opaque and alpha tested, 0 blended, -1 additive)
|
|
1436
|
+
function render3DFragmentSource(fragmentCode)
|
|
1437
|
+
{
|
|
1438
|
+
return '#version 300 es\n' +
|
|
1439
|
+
'precision highp float;' +
|
|
1440
|
+
'uniform vec4 lightDir,lightColor,ambientFog,ambientGround,fogColor,shadowParams;' +
|
|
1441
|
+
'uniform vec4 extraLights[' + RENDER3D_MAX_LIGHTS + '],extraLightColors[' + RENDER3D_MAX_LIGHTS + '];' +
|
|
1442
|
+
'uniform int extraLightCount;' +
|
|
1443
|
+
'uniform vec3 cameraPos;' +
|
|
1444
|
+
'uniform sampler2D tex;' +
|
|
1445
|
+
'uniform bool premultipliedTexture;' + // is the texture a render target, which holds premultiplied color
|
|
1446
|
+
'uniform highp sampler2DShadow shadowMap;' +
|
|
1447
|
+
'in vec3 P,N;in vec2 T,L;in vec4 C,S;' +
|
|
1448
|
+
'out vec4 o;' +
|
|
1449
|
+
// the sun shadow at this fragment, 0 to 1: the light's depth map with a 3x3 blur, outside the map is lit
|
|
1450
|
+
'float shadow(){' +
|
|
1451
|
+
'if(shadowParams.x<=0.)return 1.;' +
|
|
1452
|
+
'vec3 q=S.xyz/S.w*.5+.5;' +
|
|
1453
|
+
'if(any(greaterThanEqual(abs(q-.5),vec3(.5))))return 1.;' +
|
|
1454
|
+
'q.z-=shadowParams.y;' +
|
|
1455
|
+
'float s=0.;' +
|
|
1456
|
+
'for(int x=-1;x<=1;++x)for(int y=-1;y<=1;++y)' +
|
|
1457
|
+
's+=texture(shadowMap,vec3(q.xy+vec2(x,y)*shadowParams.z,q.z));' +
|
|
1458
|
+
'return s/9.;}' +
|
|
1459
|
+
(fragmentCode ? RENDER3D_SNIPPET_NAMES + fragmentCode + '\n' : '') +
|
|
1460
|
+
'void main(){' +
|
|
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
|
|
1463
|
+
'if(shadowParams.w>0.&&t.a<.5)discard;' + // an opaque draw drops see through texels, as the shadow map does
|
|
1464
|
+
'vec4 c=C*t;' +
|
|
1465
|
+
'float e=lightDir.w;' +
|
|
1466
|
+
'if(e<1.){' +
|
|
1467
|
+
'vec3 n=dot(N,N)>0.?normalize(N):vec3(0,1,0);' +
|
|
1468
|
+
'if(!gl_FrontFacing)n=-n;' + // only a double sided mesh shows a back face, light it on the side that is seen
|
|
1469
|
+
'float nl=dot(n,-lightDir.xyz);' +
|
|
1470
|
+
'float s=shadow();' +
|
|
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);' +
|
|
1476
|
+
'for(int i=0;i<' + RENDER3D_MAX_LIGHTS + ';++i){' +
|
|
1477
|
+
'if(i>=extraLightCount)break;' +
|
|
1478
|
+
'vec4 L=extraLights[i];' +
|
|
1479
|
+
'bool directional=L.w<0.;' +
|
|
1480
|
+
'vec3 v=directional?L.xyz:L.xyz-P;' +
|
|
1481
|
+
'float d=length(v);' +
|
|
1482
|
+
'float a=directional?1.:max(0.,1.-d/L.w);' +
|
|
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);' +
|
|
1488
|
+
'}' +
|
|
1489
|
+
'c.rgb*=l*(1.-e)+e;' + // lit, blended toward its own color by how emissive it is
|
|
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
|
|
1492
|
+
'if(lightColor.a>0.){' +
|
|
1493
|
+
'vec3 r=reflect(lightDir.xyz,n);' +
|
|
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);' +
|
|
1495
|
+
'}}else c.rgb*=e;' + // fully emissive: its own color, or brighter, with no lighting to work out
|
|
1496
|
+
'if(ambientFog.a>0.){' +
|
|
1497
|
+
'float z=distance(cameraPos,P);' +
|
|
1498
|
+
'c.rgb=mix(c.rgb,shadowParams.w<0.?vec3(0):fogColor.rgb,smoothstep(fogColor.a,ambientFog.a,z));' +
|
|
1499
|
+
'}' +
|
|
1500
|
+
'o=vec4(c.rgb,shadowParams.w>0.?1.:c.a);' + // an opaque draw stays opaque whatever the tint alpha says
|
|
1501
|
+
'}';
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// a Shader's 3D program, compiled the first time a draw needs it
|
|
1505
|
+
function render3DShaderProgram(shader)
|
|
1506
|
+
{
|
|
1507
|
+
ASSERT(shader instanceof Shader, 'render3D.shader must be a Shader, not the snippet itself');
|
|
1508
|
+
return shader.program3D ||= glCreateProgram(RENDER3D_VERTEX_SOURCE, render3DFragmentSource(shader.fragmentCode));
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// make a program current for the pass and send it the pass uniforms: the matrices, the camera and the lights,
|
|
1512
|
+
// plus the time and canvas size for a Shader's program; the per draw uniform cache starts over
|
|
1513
|
+
function render3DUseProgram(program)
|
|
1514
|
+
{
|
|
1515
|
+
const gl = glContext, r = render3D;
|
|
1516
|
+
gl.useProgram(r.currentProgram = program);
|
|
1517
|
+
r.uniformValues = {};
|
|
1518
|
+
gl.uniformMatrix4fv(render3DUniform('viewProj'), false, r.viewProjection.m);
|
|
1519
|
+
gl.uniformMatrix4fv(render3DUniform('lightViewProj'), false, r.shadowMatrix.m);
|
|
1520
|
+
gl.uniform1i(render3DUniform('tex'), 0);
|
|
1521
|
+
gl.uniform1i(render3DUniform('shadowMap'), 1);
|
|
1522
|
+
const c = r.camera.pos;
|
|
1523
|
+
gl.uniform3f(render3DUniform('cameraPos'), c.x, c.y, c.z);
|
|
1524
|
+
gl.uniform1i(render3DUniform('extraLightCount'), r.lightCount);
|
|
1525
|
+
if (r.lightCount)
|
|
1526
|
+
{
|
|
1527
|
+
gl.uniform4fv(render3DUniform('extraLights'), r.lightPositions, 0, r.lightCount * 4);
|
|
1528
|
+
gl.uniform4fv(render3DUniform('extraLightColors'), r.lightColors, 0, r.lightCount * 4);
|
|
1529
|
+
}
|
|
1530
|
+
if (program !== r.program)
|
|
1531
|
+
{
|
|
1532
|
+
gl.uniform1f(render3DUniform('iTime'), time);
|
|
1533
|
+
gl.uniform3f(render3DUniform('iResolution'), glCanvas.width, glCanvas.height, 1);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function render3DInitGL()
|
|
1538
|
+
{
|
|
1539
|
+
if (headlessMode) return;
|
|
1540
|
+
if (!glEnable || !glContext)
|
|
1541
|
+
{
|
|
1542
|
+
console.warn('Render3DPlugin: WebGL not enabled, construct the plugin in gameInit with glEnable set');
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
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
|
|
1547
|
+
r.uniforms = new Map;
|
|
1548
|
+
r.uniformValues = {};
|
|
1549
|
+
render3DShadowCut = undefined; // the shadow shader is new too
|
|
1550
|
+
r.attribValues = []; // a fresh context has its own attribute defaults, so nothing sent before it counts
|
|
1551
|
+
|
|
1552
|
+
// the shader, see RENDER3D_VERTEX_SOURCE and render3DFragmentSource
|
|
1553
|
+
r.program = glCreateProgram(RENDER3D_VERTEX_SOURCE, render3DFragmentSource());
|
|
1554
|
+
|
|
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
|
|
1558
|
+
r.shadowShader = glCreateProgram(
|
|
1559
|
+
'#version 300 es\n' +
|
|
1560
|
+
'precision highp float;' +
|
|
1561
|
+
'uniform mat4 viewProj;' +
|
|
1562
|
+
RENDER3D_VERTEX_INPUTS +
|
|
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.);}'
|
|
1565
|
+
,
|
|
1566
|
+
'#version 300 es\n' +
|
|
1567
|
+
'precision highp float;' +
|
|
1568
|
+
'uniform sampler2D tex;' +
|
|
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;}'
|
|
1572
|
+
);
|
|
1573
|
+
|
|
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
|
|
1576
|
+
r.vao = gl.createVertexArray();
|
|
1577
|
+
gl.bindVertexArray(r.vao);
|
|
1578
|
+
for (const [location] of RENDER3D_ATTRIBS)
|
|
1579
|
+
gl.enableVertexAttribArray(location);
|
|
1580
|
+
|
|
1581
|
+
// the stream buffer
|
|
1582
|
+
r.streamBuffer = gl.createBuffer();
|
|
1583
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, r.streamBuffer);
|
|
1584
|
+
gl.bufferData(gl.ARRAY_BUFFER, r.streamData.byteLength, gl.DYNAMIC_DRAW);
|
|
1585
|
+
r.streamCount = 0;
|
|
1586
|
+
r.instanceBuffers = [gl.createBuffer(), gl.createBuffer(), gl.createBuffer()];
|
|
1587
|
+
|
|
1588
|
+
// white texture for untextured draws, and a one texel shadow map that keeps the shadow sampler valid until shadows
|
|
1589
|
+
// are on
|
|
1590
|
+
r.whiteTexture = glCreateTexture();
|
|
1591
|
+
|
|
1592
|
+
r.samplers = new Map;
|
|
1593
|
+
r.samplerKey = undefined;
|
|
1594
|
+
render3DUpdateShadowMap(1);
|
|
1595
|
+
|
|
1596
|
+
// hand the engine back its own buffer, vertex array and program, the 2D batch was flushed before they changed
|
|
1597
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer);
|
|
1598
|
+
glSetInstancedMode(true);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function render3DContextLost()
|
|
1602
|
+
{
|
|
1603
|
+
const r = render3D;
|
|
1604
|
+
r.program = r.currentProgram = r.shadowShader = r.vao = r.streamBuffer = r.whiteTexture = undefined;
|
|
1605
|
+
for (const shader of glShaderObjects)
|
|
1606
|
+
shader.program3D = undefined; // compiled again by the next draw
|
|
1607
|
+
r.lightCount = 0;
|
|
1608
|
+
r.instanceBuffers = [];
|
|
1609
|
+
r.samplers = new Map;
|
|
1610
|
+
r.samplerKey = undefined;
|
|
1611
|
+
render3DClearInstances();
|
|
1612
|
+
r.shadowFramebuffer = r.shadowTexture = undefined;
|
|
1613
|
+
r.shadowTextureSize = 0;
|
|
1614
|
+
r.streamCount = 0;
|
|
1615
|
+
++r.contextGeneration; // every uploaded mesh is stale now, the soft dot is a TextureInfo the engine restores
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function render3DContextRestored()
|
|
1619
|
+
{
|
|
1620
|
+
render3DInitGL();
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// a uniform location, looked up once per program
|
|
1624
|
+
function render3DUniform(name, program=render3D.currentProgram)
|
|
1625
|
+
{
|
|
1626
|
+
const u = render3D.uniforms;
|
|
1627
|
+
let cache = u.get(program);
|
|
1628
|
+
cache || u.set(program, cache = {});
|
|
1629
|
+
return cache[name] ??= glContext.getUniformLocation(program, name);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// the model matrix, the tint and the uv rect as constant attributes for one draw
|
|
1633
|
+
function render3DDrawAttribs(m, tint, uvRect)
|
|
1634
|
+
{
|
|
1635
|
+
const gl = glContext;
|
|
1636
|
+
gl.vertexAttrib4f(4, m[0], m[1], m[2], m[3]);
|
|
1637
|
+
gl.vertexAttrib4f(5, m[4], m[5], m[6], m[7]);
|
|
1638
|
+
gl.vertexAttrib4f(6, m[8], m[9], m[10], m[11]);
|
|
1639
|
+
gl.vertexAttrib4f(7, m[12], m[13], m[14], m[15]);
|
|
1640
|
+
render3DAttrib4f(11, tint.r, tint.g, tint.b, tint.a);
|
|
1641
|
+
render3DAttrib4f(12, uvRect.x, uvRect.y, uvRect.w, uvRect.h);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// set a constant vec4 attribute only when its value changed since the last time
|
|
1645
|
+
function render3DAttrib4f(location, x, y, z, w)
|
|
1646
|
+
{
|
|
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)
|
|
1649
|
+
return;
|
|
1650
|
+
last[0] = x, last[1] = y, last[2] = z, last[3] = w;
|
|
1651
|
+
glContext.vertexAttrib4f(location, x, y, z, w);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// textures in 3D shrink into the distance far more than sprites do, so the pass samples them through their mipmaps;
|
|
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
|
|
1657
|
+
function render3DUpdateSamplers()
|
|
1658
|
+
{
|
|
1659
|
+
const r = render3D, key = tilesPixelated + ' ' + r.anisotropy;
|
|
1660
|
+
if (r.samplerKey === key) return;
|
|
1661
|
+
r.samplerKey = key;
|
|
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);
|
|
1681
|
+
const anisotropy = gl.getExtension('EXT_texture_filter_anisotropic');
|
|
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;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// bind the texture of a tile or texture, white when there is none or it is not loaded, with the 3D sampler that
|
|
1692
|
+
// matches its wrap mode; the first time a texture is used in 3D it gets its mipmaps
|
|
1693
|
+
function render3DBindTexture(tileInfo, state=render3D)
|
|
1694
|
+
{
|
|
1695
|
+
const gl = glContext, r = render3D;
|
|
1696
|
+
const textureInfo = render3DTextureOf(tileInfo);
|
|
1697
|
+
const texture = textureInfo?.glTexture || r.whiteTexture;
|
|
1698
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
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
|
|
1707
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// send a vec4 uniform of the main shader only when its value changed since the last send
|
|
1712
|
+
function render3DUniform4f(name, x, y, z, w)
|
|
1713
|
+
{
|
|
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)
|
|
1716
|
+
return;
|
|
1717
|
+
last[0] = x, last[1] = y, last[2] = z, last[3] = w;
|
|
1718
|
+
glContext.uniform4f(render3DUniform(name), x, y, z, w);
|
|
1719
|
+
}
|
|
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
|
+
|
|
1731
|
+
// bind a vertex buffer and point the attributes at it
|
|
1732
|
+
function render3DBindVertexBuffer(buffer)
|
|
1733
|
+
{
|
|
1734
|
+
const gl = glContext;
|
|
1735
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
1736
|
+
for (const a of RENDER3D_ATTRIBS)
|
|
1737
|
+
gl.vertexAttribPointer(a[0], a[1], a[2], a[3], RENDER3D_VERTEX_BYTES, a[4]);
|
|
1738
|
+
}
|
|
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
|
+
|
|
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
|
+
|
|
1769
|
+
// this returns one shared object, so read it before calling again
|
|
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;
|
|
1773
|
+
function render3DGetTileUVs(tileInfo)
|
|
1774
|
+
{
|
|
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))
|
|
1777
|
+
return RENDER3D_FULL_UV_RECT;
|
|
1778
|
+
const inv = tileInfo.textureInfo.sizeInverse, rect = render3DTileUVRect;
|
|
1779
|
+
const bleedX = inv.x * tileInfo.bleed, bleedY = inv.y * tileInfo.bleed;
|
|
1780
|
+
rect.x = tileInfo.pos.x * inv.x + bleedX;
|
|
1781
|
+
rect.y = tileInfo.pos.y * inv.y + bleedY;
|
|
1782
|
+
rect.w = tileInfo.size.x * inv.x - 2*bleedX;
|
|
1783
|
+
rect.h = tileInfo.size.y * inv.y - 2*bleedY;
|
|
1784
|
+
return rect;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
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
|
|
1789
|
+
// tileInfo may be a TileInfo, a TextureInfo, or undefined for the white texture
|
|
1790
|
+
// state is the plugin's current fields, or the captured state of a stream batch
|
|
1791
|
+
function render3DSetDrawUniforms(matrix, tileInfo, tint, uvRect, state=render3D)
|
|
1792
|
+
{
|
|
1793
|
+
const gl = glContext, r = render3D;
|
|
1794
|
+
|
|
1795
|
+
// the per draw values are constant vertex attributes, a batch turns on a per instance array over them
|
|
1796
|
+
uvRect ||= render3DGetTileUVs(tileInfo);
|
|
1797
|
+
render3DDrawAttribs(matrix.m, tint, uvRect);
|
|
1798
|
+
render3DBindTexture(tileInfo, state);
|
|
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
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// the program: a Shader's own, compiled by its first draw, or the plugin's; switching sends the pass uniforms
|
|
1809
|
+
const program = state.shader ? render3DShaderProgram(state.shader) : r.program;
|
|
1810
|
+
program === r.currentProgram || render3DUseProgram(program);
|
|
1811
|
+
|
|
1812
|
+
// blending, matches the engine's 2D blend functions
|
|
1813
|
+
if (state.blend)
|
|
1814
|
+
{
|
|
1815
|
+
gl.enable(gl.BLEND);
|
|
1816
|
+
const destBlend = state.additive ? gl.ONE : gl.ONE_MINUS_SRC_ALPHA;
|
|
1817
|
+
gl.blendFuncSeparate(gl.SRC_ALPHA, destBlend, gl.ONE, destBlend);
|
|
1818
|
+
}
|
|
1819
|
+
else
|
|
1820
|
+
gl.disable(gl.BLEND);
|
|
1821
|
+
|
|
1822
|
+
// depth and culling
|
|
1823
|
+
state.depthTest ? gl.enable(gl.DEPTH_TEST) : gl.disable(gl.DEPTH_TEST);
|
|
1824
|
+
gl.depthMask(state.depthWrite);
|
|
1825
|
+
state.cullBackFaces ? gl.enable(gl.CULL_FACE) : gl.disable(gl.CULL_FACE);
|
|
1826
|
+
gl.frontFace(state.mirrored ? gl.CCW : gl.CW); // the pass's strips read clockwise, a mirror turns that around
|
|
1827
|
+
|
|
1828
|
+
// lights, fog and shadows are scene state read at draw time, sent only when they change
|
|
1829
|
+
// the shader takes the way the sunlight travels, away from the sun
|
|
1830
|
+
const s = r.sunDirection, sl = -(s.length() || 1), lc = r.sunColor, ac = r.ambientColor, fc = r.fogColor || canvasClearColor;
|
|
1831
|
+
render3DUniform4f('lightDir', s.x / sl, s.y / sl, s.z / sl, state.lighting ? state.emissive : 1);
|
|
1832
|
+
render3DUniform4f('lightColor', lc.r, lc.g, lc.b, state.specular);
|
|
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
|
+
|
|
1838
|
+
render3DUniform4f('fogColor', fc.r, fc.g, fc.b, r.fogStart);
|
|
1839
|
+
// how the fragment shader finishes: 1 drops see through texels and keeps the draw opaque,
|
|
1840
|
+
// 0 blends them away instead, and -1 is additive, which has to fade into fog differently
|
|
1841
|
+
const blendMode = state.blend ? (state.additive ? -1 : 0) : 1;
|
|
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)));
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// the six flat sides of the camera's visible box, each as [x, y, z, w] facing inward
|
|
1851
|
+
// a point is inside when x*px + y*py + z*pz + w is zero or more
|
|
1852
|
+
function render3DFrustumPlanes(matrix)
|
|
1853
|
+
{
|
|
1854
|
+
const m = matrix.m, planes = [];
|
|
1855
|
+
for (let i = 0; i < 3; ++i)
|
|
1856
|
+
for (const sign of [1, -1])
|
|
1857
|
+
{
|
|
1858
|
+
const p = [m[3] + sign * m[i], m[7] + sign * m[4+i], m[11] + sign * m[8+i], m[15] + sign * m[12+i]];
|
|
1859
|
+
const l = hypot(p[0], p[1], p[2]) || 1;
|
|
1860
|
+
planes.push(p.map(v=> v / l));
|
|
1861
|
+
}
|
|
1862
|
+
return planes;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// the preRender hook, before gameRender: the layer under the 2D scene
|
|
1866
|
+
function render3DPreRender()
|
|
1867
|
+
{
|
|
1868
|
+
const r = render3D;
|
|
1869
|
+
r.updateMatrices();
|
|
1870
|
+
r.shadowMapDrawn = false;
|
|
1871
|
+
render3DRenderPass(false);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// the render hook, after gameRenderPost: the layer on top of the 2D scene
|
|
1875
|
+
function render3DRender()
|
|
1876
|
+
{
|
|
1877
|
+
render3DRenderPass(true);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
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
|
|
1882
|
+
// the layer matching render3D.renderAfter2D is the default and always runs, the other only when an object asks for it
|
|
1883
|
+
function render3DRenderPass(after2D)
|
|
1884
|
+
{
|
|
1885
|
+
const gl = glContext, r = render3D;
|
|
1886
|
+
if (!r.program || !glEnable) return; // headless, gl disabled, or context lost
|
|
1887
|
+
render3DUpdateSamplers();
|
|
1888
|
+
ASSERT(!r.fogEnd || r.fogStart < r.fogEnd, 'fogStart must be less than fogEnd');
|
|
1889
|
+
ASSERT(!glRenderTarget, 'the 3D pass needs the canvas depth buffer, it can not draw into a render target');
|
|
1890
|
+
const isDefault = after2D === !!r.renderAfter2D, objects = render3DLayerObjects(after2D);
|
|
1891
|
+
if (!isDefault && !objects.length) return;
|
|
1892
|
+
r.passIsDefault = isDefault;
|
|
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();
|
|
1896
|
+
|
|
1897
|
+
// a previous frame that threw must not leave anything pending
|
|
1898
|
+
r.streamCount = 0;
|
|
1899
|
+
r.capture = r.transparentQueue = undefined;
|
|
1900
|
+
render3DClearInstances();
|
|
1901
|
+
|
|
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
|
|
1904
|
+
const lights = render3DCollectLights();
|
|
1905
|
+
r.lightCount = lights.length;
|
|
1906
|
+
const positions = r.lightPositions, colors = r.lightColors;
|
|
1907
|
+
lights.forEach((light, i)=>
|
|
1908
|
+
{
|
|
1909
|
+
const p = light.directional ? light.getWorldPos3D().normalize() : light.getWorldPos3D();
|
|
1910
|
+
ASSERT(!light.directional || p.lengthSquared(),
|
|
1911
|
+
'a directional light shines from its position toward the origin, so it cannot sit on the origin');
|
|
1912
|
+
const c = light.color, k = i * 4;
|
|
1913
|
+
positions[k] = p.x, positions[k+1] = p.y, positions[k+2] = p.z;
|
|
1914
|
+
positions[k+3] = light.directional ? -1 : max(0, light.radius); // a negative radius marks a direction
|
|
1915
|
+
colors[k] = c.r, colors[k+1] = c.g, colors[k+2] = c.b, colors[k+3] = c.a * light.intensity;
|
|
1916
|
+
});
|
|
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
|
+
|
|
1931
|
+
r.isRendering = true;
|
|
1932
|
+
try
|
|
1933
|
+
{
|
|
1934
|
+
// the shadow map from the light once a frame, then the stages sample it
|
|
1935
|
+
if (r.shadows && !r.shadowMapDrawn)
|
|
1936
|
+
{
|
|
1937
|
+
render3DRenderShadowMap();
|
|
1938
|
+
r.shadowMapDrawn = true;
|
|
1939
|
+
}
|
|
1940
|
+
render3DUseProgram(r.program); // after the shadow map, so the light matrix it sends is this frame's
|
|
1941
|
+
r.renderStages(objects, isDefault);
|
|
1942
|
+
}
|
|
1943
|
+
finally
|
|
1944
|
+
{
|
|
1945
|
+
// hand the state back to the engine's 2D batching, even when a draw threw
|
|
1946
|
+
r.isRendering = false;
|
|
1947
|
+
r.currentProgram = undefined; // the engine's 2D program takes over below
|
|
1948
|
+
r.streamCount = 0;
|
|
1949
|
+
r.capture = r.transparentQueue = undefined;
|
|
1950
|
+
gl.disable(gl.DEPTH_TEST);
|
|
1951
|
+
gl.disable(gl.CULL_FACE);
|
|
1952
|
+
gl.depthMask(true);
|
|
1953
|
+
gl.frontFace(gl.CCW);
|
|
1954
|
+
gl.bindSampler(0, null); // back to the textures' own filtering for 2D
|
|
1955
|
+
if (glActiveTexture)
|
|
1956
|
+
gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
|
|
1957
|
+
// ARRAY_BUFFER is not part of VAO state in WebGL2, so bindVertexArray alone would not restore it
|
|
1958
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer);
|
|
1959
|
+
glSetInstancedMode(true);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// create the shadow map depth texture and framebuffer at a size, or keep them when the size matches
|
|
1964
|
+
function render3DUpdateShadowMap(size)
|
|
1965
|
+
{
|
|
1966
|
+
const gl = glContext, r = render3D;
|
|
1967
|
+
ASSERT(size > 0, 'shadowMapSize must be positive');
|
|
1968
|
+
if (r.shadowTexture && r.shadowTextureSize === size) return;
|
|
1969
|
+
r.shadowTexture && gl.deleteTexture(r.shadowTexture);
|
|
1970
|
+
r.shadowFramebuffer && gl.deleteFramebuffer(r.shadowFramebuffer);
|
|
1971
|
+
const texture = r.shadowTexture = gl.createTexture();
|
|
1972
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
1973
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
1974
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, size, size, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);
|
|
1975
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); // smooth filtering softens shadow edges for free
|
|
1976
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1977
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1978
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1979
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE);
|
|
1980
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_COMPARE_FUNC, gl.LEQUAL);
|
|
1981
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
1982
|
+
const framebuffer = r.shadowFramebuffer = gl.createFramebuffer();
|
|
1983
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
1984
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texture, 0);
|
|
1985
|
+
gl.drawBuffers([gl.NONE]); // depth only
|
|
1986
|
+
gl.readBuffer(gl.NONE);
|
|
1987
|
+
ASSERT(gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE,
|
|
1988
|
+
'shadow map framebuffer is incomplete, try a smaller shadowMapSize');
|
|
1989
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
1990
|
+
r.shadowTextureSize = size;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// draw the lit opaque casters from the light into the shadow map with the depth only shader
|
|
1994
|
+
function render3DRenderShadowMap()
|
|
1995
|
+
{
|
|
1996
|
+
const gl = glContext, r = render3D;
|
|
1997
|
+
render3DUpdateShadowMap(r.shadowMapSize | 0);
|
|
1998
|
+
r.updateShadowMatrix();
|
|
1999
|
+
|
|
2000
|
+
// the map can not be read while it is drawn
|
|
2001
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
2002
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
2003
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
2004
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, r.shadowFramebuffer);
|
|
2005
|
+
gl.viewport(0, 0, r.shadowTextureSize, r.shadowTextureSize);
|
|
2006
|
+
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
2007
|
+
gl.useProgram(r.shadowShader);
|
|
2008
|
+
gl.uniformMatrix4fv(render3DUniform('viewProj', r.shadowShader), false, r.shadowMatrix.m);
|
|
2009
|
+
gl.enable(gl.DEPTH_TEST);
|
|
2010
|
+
gl.depthMask(true);
|
|
2011
|
+
gl.disable(gl.BLEND);
|
|
2012
|
+
gl.disable(gl.CULL_FACE);
|
|
2013
|
+
|
|
2014
|
+
r.shadowPass = true;
|
|
2015
|
+
try
|
|
2016
|
+
{
|
|
2017
|
+
// see through objects cast only when textured, their alpha cuts the shadow out
|
|
2018
|
+
const casters = render3DLayerObjects(!!r.renderAfter2D).filter(o=>
|
|
2019
|
+
o.castShadow && !o.additive && (!o.transparent || o.tileInfo));
|
|
2020
|
+
render3DDrawObjects(casters);
|
|
2021
|
+
r.onRenderOpaque?.();
|
|
2022
|
+
r.flush();
|
|
2023
|
+
render3DFlushInstances();
|
|
2024
|
+
}
|
|
2025
|
+
finally
|
|
2026
|
+
{
|
|
2027
|
+
// back to the frame with the map ready to sample
|
|
2028
|
+
r.shadowPass = false;
|
|
2029
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
2030
|
+
|
|
2031
|
+
// backing store size, mainCanvasSize is css pixels
|
|
2032
|
+
gl.viewport(0, 0, glCanvas.width, glCanvas.height);
|
|
2033
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
2034
|
+
gl.bindTexture(gl.TEXTURE_2D, r.shadowTexture);
|
|
2035
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2040
|
+
// Strips: every strip repeats its first point once at the start and its last
|
|
2041
|
+
// point once at the end. Those repeats make flat triangles with no area, which
|
|
2042
|
+
// are invisible, and they let one strip run straight into the next.
|
|
2043
|
+
// An odd count gets one more repeat at the end. Triangles in a strip alternate
|
|
2044
|
+
// which way they face, so keeping the count even keeps every strip facing out.
|
|
2045
|
+
|
|
2046
|
+
// make room in the stream for a strip of count vertices under the current state and texture, flushing a batch that
|
|
2047
|
+
// differs first; returns the uv rect to map the vertices with, or undefined when nothing can be drawn
|
|
2048
|
+
function render3DBeginStrip(count, tileInfo)
|
|
2049
|
+
{
|
|
2050
|
+
const r = render3D;
|
|
2051
|
+
if (!render3DCanDraw()) return;
|
|
2052
|
+
if (r.shadowPass && !r.lighting) return; // unlit things cast no shadow
|
|
2053
|
+
ASSERT(count <= RENDER3D_MAX_STREAM_VERTS, 'strip is too large for the stream, bake it into a mesh');
|
|
2054
|
+
if (count > RENDER3D_MAX_STREAM_VERTS) return;
|
|
2055
|
+
const textureInfo = render3DTextureOf(tileInfo);
|
|
2056
|
+
if (r.streamCount && (textureInfo !== r.streamTileInfo || render3DStateChanged(r.streamState)
|
|
2057
|
+
|| r.streamCount + count > RENDER3D_MAX_STREAM_VERTS))
|
|
2058
|
+
r.flush();
|
|
2059
|
+
render3DFlushBeforeOverlay();
|
|
2060
|
+
if (!r.streamCount)
|
|
2061
|
+
r.streamState = render3DCaptureBatchState();
|
|
2062
|
+
r.streamTileInfo = textureInfo;
|
|
2063
|
+
return render3DGetTileUVs(tileInfo);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
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)
|
|
2108
|
+
{
|
|
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;
|
|
2111
|
+
if (upright)
|
|
2112
|
+
{
|
|
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;
|
|
2117
|
+
}
|
|
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];
|
|
2129
|
+
return [
|
|
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),
|
|
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)];
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// how many vertices a strip of n points takes with its repeats, and which point vertex k of it is
|
|
2135
|
+
function render3DStripCount(n) { return n + 2 + (n & 1); }
|
|
2136
|
+
function render3DStripIndex(k, n) { return k < 1 ? 0 : k <= n ? k - 1 : n - 1; }
|
|
2137
|
+
|
|
2138
|
+
// walk a strip's vertices with the repeats applied, calling back with (point, normal, uv, color)
|
|
2139
|
+
// normals, uvs and colors may be one value for all points, an array per point, or undefined
|
|
2140
|
+
function render3DForEachStripVertex(points, normals, uvs, colors, callback)
|
|
2141
|
+
{
|
|
2142
|
+
ASSERT(isArray(points) && points.length > 2, 'strip needs at least 3 points');
|
|
2143
|
+
const n = points.length, count = render3DStripCount(n);
|
|
2144
|
+
const normalArray = isArray(normals), uvArray = isArray(uvs), colorArray = isArray(colors);
|
|
2145
|
+
for (let k = 0; k < count; ++k)
|
|
2146
|
+
{
|
|
2147
|
+
const i = render3DStripIndex(k, n);
|
|
2148
|
+
callback(points[i],
|
|
2149
|
+
normalArray ? normals[i] : normals || RENDER3D_DEFAULT_NORMAL,
|
|
2150
|
+
uvArray ? uvs[i] : uvs || RENDER3D_DEFAULT_UV,
|
|
2151
|
+
colorArray ? colors[i] : colors || WHITE);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// write one vertex into a packed buffer at float index j
|
|
2156
|
+
function render3DWriteVertex(floats, ints, j, x, y, z, n, u, v, rgba)
|
|
2157
|
+
{
|
|
2158
|
+
floats[j] = x; floats[j+1] = y; floats[j+2] = z;
|
|
2159
|
+
floats[j+3] = n.x; floats[j+4] = n.y; floats[j+5] = n.z;
|
|
2160
|
+
floats[j+6] = u; floats[j+7] = v;
|
|
2161
|
+
ints[j+8] = rgba;
|
|
2162
|
+
}
|
|
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
|
+
|
|
2207
|
+
// reorder a convex polygon's points, counter clockwise from outside, into one triangle strip
|
|
2208
|
+
function render3DPolygonStrip(points)
|
|
2209
|
+
{
|
|
2210
|
+
const strip = [points[0]];
|
|
2211
|
+
for (let i = 1, j = points.length - 1; i <= j; ++i, --j)
|
|
2212
|
+
{
|
|
2213
|
+
strip.push(points[i]);
|
|
2214
|
+
if (i !== j)
|
|
2215
|
+
strip.push(points[j]);
|
|
2216
|
+
}
|
|
2217
|
+
return strip;
|
|
2218
|
+
}
|
|
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
|
+
|
|
2235
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2236
|
+
|
|
2237
|
+
// frees the GPU buffer of a mesh that is garbage collected without dispose, some time after it goes; it holds the
|
|
2238
|
+
// buffer and its context, never the mesh, or the mesh could not be collected, and dispose unregisters the mesh
|
|
2239
|
+
const render3DMeshBuffers = typeof FinalizationRegistry == 'undefined' ? undefined :
|
|
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
|
+
}
|
|
2259
|
+
|
|
2260
|
+
/**
|
|
2261
|
+
* Mesh - Triangles with positions, normals, uvs and colors, uploaded once and drawn by matrix
|
|
2262
|
+
* - Build with addStrip, addQuad, combine or the shape builders, then render each frame
|
|
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
|
|
2267
|
+
* - The GPU buffer is created lazily on first render and dropped by dispose, or freed once the mesh is garbage
|
|
2268
|
+
* collected, so dispose is only needed to free it right away, like for a mesh rebuilt often
|
|
2269
|
+
* @memberof Render3D
|
|
2270
|
+
* @example
|
|
2271
|
+
* const mesh = buildLathe([[0, -1], [1, 0], [0, 1]], 4); // octahedron
|
|
2272
|
+
* mesh.render(buildMatrix(vec3(0, 1, 0)), undefined, RED);
|
|
2273
|
+
*/
|
|
2274
|
+
class Mesh
|
|
2275
|
+
{
|
|
2276
|
+
/** Create an empty mesh */
|
|
2277
|
+
constructor()
|
|
2278
|
+
{
|
|
2279
|
+
/** @property {Array<Vector3>} - Vertex positions, in strip order or one per vertex of an indexed mesh
|
|
2280
|
+
* @type {Array<Vector3>} */
|
|
2281
|
+
this.points = [];
|
|
2282
|
+
/** @property {Array<Vector3>} - Vertex normals
|
|
2283
|
+
* @type {Array<Vector3>} */
|
|
2284
|
+
this.normals = [];
|
|
2285
|
+
/** @property {Array<Vector2>} - Vertex texture coords, 0-1 across the tile
|
|
2286
|
+
* @type {Array<Vector2>} */
|
|
2287
|
+
this.uvs = [];
|
|
2288
|
+
/** @property {Array<Color>} - Vertex colors
|
|
2289
|
+
* @type {Array<Color>} */
|
|
2290
|
+
this.colors = [];
|
|
2291
|
+
/** @property {WebGLBuffer|undefined} - GPU vertex buffer, created by upload
|
|
2292
|
+
* @type {WebGLBuffer|undefined} */
|
|
2293
|
+
this.buffer = undefined;
|
|
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 */
|
|
2298
|
+
this.bufferCount = 0;
|
|
2299
|
+
this.indexType = 0; // gl.UNSIGNED_SHORT, or UNSIGNED_INT past 65535 vertices
|
|
2300
|
+
/** @property {boolean} - The mesh changed and needs uploading again, set it yourself if you edit the arrays */
|
|
2301
|
+
this.dirty = false;
|
|
2302
|
+
/** @property {boolean|undefined} - Draw every use of this mesh in the opaque stage as one instanced call,
|
|
2303
|
+
* undefined follows render3D.instancing
|
|
2304
|
+
* @type {boolean|undefined} */
|
|
2305
|
+
this.instanced = undefined;
|
|
2306
|
+
/** @property {boolean} - Draw both sides, each lit as the side that is seen; off skips the faces pointing away,
|
|
2307
|
+
* which is faster and right for closed shapes, the open builders like buildGrid and buildRibbon turn it on */
|
|
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;
|
|
2330
|
+
this.instanceCount = 0; // draws waiting in this mesh's batch, with their values, texture and draw state
|
|
2331
|
+
/** @type {Float32Array|undefined} */
|
|
2332
|
+
this.instanceData = undefined;
|
|
2333
|
+
/** @property {number} - Bounding sphere radius around the origin, for culling and picking, computed by upload */
|
|
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;
|
|
2338
|
+
this.contextGeneration = 0; // the context the buffer belongs to, see render3D.contextGeneration
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/** Number of vertices in the mesh
|
|
2342
|
+
* @return {number} */
|
|
2343
|
+
get vertexCount() { return this.points.length; }
|
|
2344
|
+
|
|
2345
|
+
/** Add a triangle strip, joined to the previous one by invisible flat triangles so one mesh holds many strips
|
|
2346
|
+
* - Strip order: the first three points make a triangle, then each point makes another with the two before it
|
|
2347
|
+
* - List the first three points counter clockwise as seen from the front, or the face points away
|
|
2348
|
+
* and may vanish when back faces are culled
|
|
2349
|
+
* @param {Array<Vector3>} points - Strip order
|
|
2350
|
+
* @param {Vector3|Array<Vector3>} [normals] - One for all or one per point, default up
|
|
2351
|
+
* @param {Vector2|Array<Vector2>} [uvs] - One for all or one per point, default zero
|
|
2352
|
+
* @param {Color|Array<Color>} [colors] - One for all or one per point, default white
|
|
2353
|
+
* @return {Mesh} */
|
|
2354
|
+
addStrip(points, normals, uvs, colors)
|
|
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
|
+
}
|
|
2362
|
+
render3DForEachStripVertex(points, normals, uvs, colors, (p, n, uv, c)=>
|
|
2363
|
+
{
|
|
2364
|
+
this.points.push(p);
|
|
2365
|
+
this.normals.push(n);
|
|
2366
|
+
this.uvs.push(uv);
|
|
2367
|
+
this.colors.push(c);
|
|
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;
|
|
2417
|
+
this.dirty = true;
|
|
2418
|
+
return this;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
/** Add a flat quad from four corners in loop order, counter clockwise seen from the front, a is the top left of the texture
|
|
2422
|
+
* @param {Vector3} a
|
|
2423
|
+
* @param {Vector3} b
|
|
2424
|
+
* @param {Vector3} c
|
|
2425
|
+
* @param {Vector3} d
|
|
2426
|
+
* @param {Color|Array<Color>} [color] - One for all or one per corner
|
|
2427
|
+
* @param {Array<Vector2>} [uvs] - One per corner, default across the tile
|
|
2428
|
+
* @return {Mesh} */
|
|
2429
|
+
addQuad(a, b, c, d, color, uvs)
|
|
2430
|
+
{
|
|
2431
|
+
return this.addStrip(render3DQuadStrip(a, b, c, d), render3DFaceNormal(a, b, c, d),
|
|
2432
|
+
uvs ? render3DQuadValues(uvs) : RENDER3D_QUAD_UVS, render3DQuadValues(color));
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
/** Append another mesh transformed by a matrix, for building one shape out of several
|
|
2436
|
+
* @param {Mesh} mesh
|
|
2437
|
+
* @param {Matrix4|Vector3} [matrix] - Transform, or just a position to move it to
|
|
2438
|
+
* @param {Color} [color] - Multiplies the appended vertex colors
|
|
2439
|
+
* @return {Mesh} */
|
|
2440
|
+
combine(mesh, matrix=RENDER3D_IDENTITY, color=WHITE)
|
|
2441
|
+
{
|
|
2442
|
+
matrix = render3DMatrix(matrix); // most parts only need moving into place
|
|
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)
|
|
2458
|
+
{
|
|
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));
|
|
2470
|
+
}
|
|
2471
|
+
this.doubleSided ||= mesh.doubleSided; // an open part leaves the whole mesh open
|
|
2472
|
+
this.vertexKeys = undefined; // the new entries have no keys
|
|
2473
|
+
this.dirty = true;
|
|
2474
|
+
return this;
|
|
2475
|
+
}
|
|
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
|
+
|
|
2534
|
+
/** Scale every uv, so a whole texture repeats across the mesh when its TextureInfo wraps
|
|
2535
|
+
* @param {Vector2|number} scale - Repeats across and up, a number for both
|
|
2536
|
+
* @return {Mesh} */
|
|
2537
|
+
scaleUVs(scale)
|
|
2538
|
+
{
|
|
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));
|
|
2542
|
+
this.dirty = true;
|
|
2543
|
+
return this;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
/** Move, turn or scale every vertex in place, normals follow along
|
|
2547
|
+
* @param {Matrix4|Vector3} matrix - Transform, or just an offset to move by
|
|
2548
|
+
* @return {Mesh} */
|
|
2549
|
+
transform(matrix)
|
|
2550
|
+
{
|
|
2551
|
+
matrix = render3DMatrix(matrix);
|
|
2552
|
+
const normalMatrix = render3DNormalMatrix(matrix);
|
|
2553
|
+
for (let i = 0; i < this.points.length; ++i)
|
|
2554
|
+
{
|
|
2555
|
+
this.points[i] = matrix.transformPoint(this.points[i]);
|
|
2556
|
+
// a mesh built by hand may have no normals yet, and then there is nothing to turn
|
|
2557
|
+
this.normals[i] &&= normalMatrix.transformDirection(this.normals[i]).normalize();
|
|
2558
|
+
}
|
|
2559
|
+
render3DMirrors(matrix) && render3DFlipWinding(this); // a mirror would leave the faces pointing in
|
|
2560
|
+
this.dirty = true;
|
|
2561
|
+
return this;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
/** Turn the mesh inside out so it is lit and drawn from within, for rooms and domes
|
|
2565
|
+
* @return {Mesh} */
|
|
2566
|
+
flipNormals()
|
|
2567
|
+
{
|
|
2568
|
+
render3DFlipWinding(this);
|
|
2569
|
+
this.normals = this.normals.map(n=> n.scale(-1));
|
|
2570
|
+
this.dirty = true;
|
|
2571
|
+
return this;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
/** Set every vertex color
|
|
2575
|
+
* @param {Color} color
|
|
2576
|
+
* @return {Mesh} */
|
|
2577
|
+
setColor(color)
|
|
2578
|
+
{
|
|
2579
|
+
// one per point, not one per color already there, so a mesh built by hand with no
|
|
2580
|
+
// colors gets them instead of quietly staying white
|
|
2581
|
+
this.colors = this.points.map(()=> color);
|
|
2582
|
+
this.dirty = true;
|
|
2583
|
+
return this;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
/** Measure the axis aligned box around the vertices
|
|
2587
|
+
* @return {{min: Vector3, max: Vector3}} */
|
|
2588
|
+
getBounds()
|
|
2589
|
+
{
|
|
2590
|
+
if (!this.points.length)
|
|
2591
|
+
return {min: vec3(), max: vec3()};
|
|
2592
|
+
const lo = vec3(Infinity), hi = vec3(-Infinity);
|
|
2593
|
+
for (const p of this.points)
|
|
2594
|
+
{
|
|
2595
|
+
lo.x = min(lo.x, p.x); lo.y = min(lo.y, p.y); lo.z = min(lo.z, p.z);
|
|
2596
|
+
hi.x = max(hi.x, p.x); hi.y = max(hi.y, p.y); hi.z = max(hi.z, p.z);
|
|
2597
|
+
}
|
|
2598
|
+
return {min: lo, max: hi};
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
/** Move the mesh so the center of its bounds is on the origin
|
|
2602
|
+
* @return {Mesh} */
|
|
2603
|
+
center()
|
|
2604
|
+
{
|
|
2605
|
+
const bounds = this.getBounds();
|
|
2606
|
+
return this.transform(bounds.min.add(bounds.max).scale(-.5));
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
/** Scale the mesh evenly so its largest extent is a size, for loaded models of unknown units
|
|
2610
|
+
* @param {number} [size]
|
|
2611
|
+
* @return {Mesh} */
|
|
2612
|
+
fit(size=1)
|
|
2613
|
+
{
|
|
2614
|
+
const bounds = this.getBounds();
|
|
2615
|
+
const extent = bounds.max.subtract(bounds.min);
|
|
2616
|
+
const scale = size / (max(extent.x, extent.y, extent.z) || 1);
|
|
2617
|
+
return this.transform(Matrix4.scaling(vec3(scale)));
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
/** Measure the bounding sphere around the origin into radius, called by upload
|
|
2621
|
+
* @return {number} */
|
|
2622
|
+
computeRadius()
|
|
2623
|
+
{
|
|
2624
|
+
let r = 0;
|
|
2625
|
+
for (const p of this.points)
|
|
2626
|
+
r = max(r, p.lengthSquared());
|
|
2627
|
+
this.bounds = this.getBounds(); // measured with it, a pick tests a mesh's box after its sphere
|
|
2628
|
+
return this.radius = r ** .5;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
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
|
|
2634
|
+
* @return {Mesh} */
|
|
2635
|
+
computeNormals(smooth=false)
|
|
2636
|
+
{
|
|
2637
|
+
if (this.indices)
|
|
2638
|
+
{
|
|
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
|
|
2684
|
+
// a zero normal means a flat triangle joining two strips, so skip it
|
|
2685
|
+
faceNormals.push(normal.lengthSquared() ? normal.normalize(i & 1 ? 1 : -1) : undefined);
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
// then hand those to the vertices, shared around a position or kept per face
|
|
2689
|
+
const normals = points.map(()=> RENDER3D_DEFAULT_NORMAL);
|
|
2690
|
+
if (smooth)
|
|
2691
|
+
{
|
|
2692
|
+
// add up the face normals meeting at each position, each weighted by its corner angle so a cube
|
|
2693
|
+
// corner averages its three faces evenly however the strips cut them, then normalize
|
|
2694
|
+
const sums = new Map;
|
|
2695
|
+
faceNormals.forEach((f, i)=> f && [0, 1, 2].forEach(j=>
|
|
2696
|
+
render3DAddCornerNormal(sums, points[i+j], points[i+(j+1)%3], points[i+(j+2)%3], f)));
|
|
2697
|
+
for (let i = 0; i < n; ++i)
|
|
2698
|
+
normals[i] = (sums.get(render3DPlaceKey(points[i])) || RENDER3D_DEFAULT_NORMAL).normalize();
|
|
2699
|
+
}
|
|
2700
|
+
else
|
|
2701
|
+
// every triangle writes its own three corners, so the only vertices left with the default
|
|
2702
|
+
// are the repeats at the ends of a strip, which no triangle with any area uses
|
|
2703
|
+
faceNormals.forEach((f, i)=> f && (normals[i] = normals[i+1] = normals[i+2] = f));
|
|
2704
|
+
|
|
2705
|
+
this.normals = normals;
|
|
2706
|
+
this.vertexKeys = undefined; // flat normals tell entries at one place apart
|
|
2707
|
+
this.dirty = true;
|
|
2708
|
+
return this;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
/** Pack the vertices and create the GPU buffer, called automatically by render
|
|
2712
|
+
* @return {Mesh} */
|
|
2713
|
+
upload()
|
|
2714
|
+
{
|
|
2715
|
+
this.computeRadius();
|
|
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)
|
|
2720
|
+
{
|
|
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
|
+
}
|
|
2734
|
+
}
|
|
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
|
|
2740
|
+
this.buffer = gl.createBuffer();
|
|
2741
|
+
this.indexBuffer = gl.createBuffer();
|
|
2742
|
+
this.bufferCount = indices.length;
|
|
2743
|
+
this.indexType = wide ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
|
|
2744
|
+
this.dirty = false;
|
|
2745
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
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);
|
|
2749
|
+
this.contextGeneration = render3D.contextGeneration;
|
|
2750
|
+
const buffers = {buffer: this.buffer, indexBuffer: this.indexBuffer, generation: this.contextGeneration};
|
|
2751
|
+
render3DMeshBuffers?.register(this, buffers, this);
|
|
2752
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, glArrayBuffer); // the engine's 2D batch writes through this binding
|
|
2753
|
+
return this;
|
|
2754
|
+
}
|
|
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
|
+
|
|
2833
|
+
/** Draw the mesh with the current draw state, batched with its other uses in the opaque stage
|
|
2834
|
+
* @param {Matrix4|Vector3} [matrix] - Object transform, or just a position to draw it at
|
|
2835
|
+
* @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile or the whole texture
|
|
2836
|
+
* @param {Color} [color] - Tint */
|
|
2837
|
+
render(matrix, tileInfo, color) { render3D?.drawMesh(this, matrix, tileInfo, color); }
|
|
2838
|
+
|
|
2839
|
+
/** Delete the GPU buffer now, the CPU arrays stay so the mesh can be rendered again
|
|
2840
|
+
* - Optional, the buffer is freed anyway once the mesh is garbage collected, this frees it right away */
|
|
2841
|
+
dispose()
|
|
2842
|
+
{
|
|
2843
|
+
if (!this.buffer) return;
|
|
2844
|
+
render3DMeshBuffers?.unregister(this); // freed here, so not again when the mesh is collected
|
|
2845
|
+
// a buffer from a context that was lost is gone with it, and the new context refuses to delete it
|
|
2846
|
+
if (this.contextGeneration === render3D?.contextGeneration)
|
|
2847
|
+
glContext?.deleteBuffer(this.buffer), glContext?.deleteBuffer(this.indexBuffer);
|
|
2848
|
+
this.buffer = this.indexBuffer = undefined;
|
|
2849
|
+
this.bufferCount = 0;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2854
|
+
// Shape builders, all centered on the origin so buildMatrix does placement
|
|
2855
|
+
// Sizes are full sizes like buildBox and the 2D drawCircle, sides go around an axis and rings along it
|
|
2856
|
+
|
|
2857
|
+
/**
|
|
2858
|
+
* Spin a flat outline around the Y axis to make a round shape, like a vase or a wheel
|
|
2859
|
+
* - profile is [[radius, y], ...] from bottom to top
|
|
2860
|
+
* - A profile that ends where it starts makes a closed ring like a donut
|
|
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
|
|
2864
|
+
* @param {Array<Array<number>>} profile
|
|
2865
|
+
* @param {number} [sides] - Around the axis
|
|
2866
|
+
* @param {boolean} [smooth] - Defaults to render3D.smoothShading
|
|
2867
|
+
* @param {boolean} [capped] - Close the ends that have a radius with flat discs
|
|
2868
|
+
* @return {Mesh}
|
|
2869
|
+
* @memberof Render3D
|
|
2870
|
+
* @example
|
|
2871
|
+
* const vase = buildLathe([[0, -1], [.8, -.3], [.9, .2], [.4, .6], [0, 1]], 12);
|
|
2872
|
+
*/
|
|
2873
|
+
function buildLathe(profile, sides=16, smooth=render3D?.smoothShading, capped=true)
|
|
2874
|
+
{
|
|
2875
|
+
ASSERT(isArray(profile) && profile.length > 1, 'lathe profile needs at least 2 points');
|
|
2876
|
+
sides |= 0;
|
|
2877
|
+
ASSERT(sides > 2, 'lathe needs at least 3 sides');
|
|
2878
|
+
const mesh = new Mesh;
|
|
2879
|
+
const rings = profile.length;
|
|
2880
|
+
const point = (i, a)=> vec3(sin(a) * profile[i][0], profile[i][1], cos(a) * profile[i][0]);
|
|
2881
|
+
|
|
2882
|
+
// 2D outward normal of each profile segment, in (radius, y) space
|
|
2883
|
+
const segmentNormal = (i)=>
|
|
2884
|
+
{
|
|
2885
|
+
const [r0, y0] = profile[i], [r1, y1] = profile[i+1];
|
|
2886
|
+
const n = vec2(y1 - y0, r0 - r1);
|
|
2887
|
+
return n.length() ? n.normalize() : vec2(1, 0);
|
|
2888
|
+
};
|
|
2889
|
+
// vertex normal: average of the adjacent segment normals, across the seam when the profile is closed,
|
|
2890
|
+
// and a closed profile needs no caps
|
|
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;
|
|
2893
|
+
const segmentLength = (i)=> hypot(profile[i+1][0] - profile[i][0], profile[i+1][1] - profile[i][1]);
|
|
2894
|
+
const vertexNormal = (i)=>
|
|
2895
|
+
{
|
|
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
|
|
2898
|
+
if (!closed && (!i || i == rings - 1) && abs(profile[i][0]) < 1e-9)
|
|
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
|
+
}
|
|
2904
|
+
// otherwise the neighbors weighted by their length, so a short band does not tilt a long wall
|
|
2905
|
+
let n = vec2();
|
|
2906
|
+
const add = (s)=> n = n.add(segmentNormal(s).scale(segmentLength(s)));
|
|
2907
|
+
if (i > 0) add(i - 1);
|
|
2908
|
+
else if (closed) add(rings - 2);
|
|
2909
|
+
if (i < rings - 1) add(i);
|
|
2910
|
+
else if (closed) add(0);
|
|
2911
|
+
return n.length() ? n.normalize() : vec2(1, 0);
|
|
2912
|
+
};
|
|
2913
|
+
const normal3D = (n, a)=> vec3(sin(a) * n.x, n.y, cos(a) * n.x);
|
|
2914
|
+
|
|
2915
|
+
// v runs along the profile by arc length
|
|
2916
|
+
const lengths = [0];
|
|
2917
|
+
for (let i = 1; i < rings; ++i)
|
|
2918
|
+
lengths[i] = lengths[i-1] + hypot(profile[i][0] - profile[i-1][0], profile[i][1] - profile[i-1][1]);
|
|
2919
|
+
const total = lengths[rings - 1] || 1;
|
|
2920
|
+
const v = (i)=> 1 - lengths[i] / total;
|
|
2921
|
+
|
|
2922
|
+
for (let i = 0; i + 1 < rings; ++i)
|
|
2923
|
+
{
|
|
2924
|
+
if (smooth)
|
|
2925
|
+
{
|
|
2926
|
+
// one ribbon around the ring pair, top point then bottom point per column
|
|
2927
|
+
const points = [], normals = [], uvs = [];
|
|
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;
|
|
2933
|
+
for (let j = 0; j <= sides; ++j)
|
|
2934
|
+
{
|
|
2935
|
+
const a = j / sides * 2 * PI, u = j / sides;
|
|
2936
|
+
points.push(point(i + 1, a), point(i, a));
|
|
2937
|
+
normals.push(normal3D(n1, a + turn1), normal3D(n0, a + turn0));
|
|
2938
|
+
uvs.push(vec2(u, v(i + 1)), vec2(u, v(i)));
|
|
2939
|
+
}
|
|
2940
|
+
mesh.addStrip(points, normals, uvs);
|
|
2941
|
+
}
|
|
2942
|
+
else
|
|
2943
|
+
{
|
|
2944
|
+
// one quad per side with its face normal
|
|
2945
|
+
const n = segmentNormal(i);
|
|
2946
|
+
for (let j = 0; j < sides; ++j)
|
|
2947
|
+
{
|
|
2948
|
+
const a0 = j / sides * 2 * PI, a1 = (j + 1) / sides * 2 * PI;
|
|
2949
|
+
const u0 = j / sides, u1 = (j + 1) / sides;
|
|
2950
|
+
mesh.addStrip(
|
|
2951
|
+
[point(i + 1, a0), point(i, a0), point(i + 1, a1), point(i, a1)],
|
|
2952
|
+
normal3D(n, (a0 + a1) / 2),
|
|
2953
|
+
[vec2(u0, v(i + 1)), vec2(u0, v(i)), vec2(u1, v(i + 1)), vec2(u1, v(i))]);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
// flat discs close the ends that have a radius, a hard edge even when the sides are smooth
|
|
2959
|
+
if (capped && !closed)
|
|
2960
|
+
for (const [i, up] of /** @type {Array<[number, boolean]>} */ ([[0, false], [rings - 1, true]]))
|
|
2961
|
+
{
|
|
2962
|
+
if (abs(profile[i][0]) < 1e-9) continue; // a pole has no cap
|
|
2963
|
+
const points = [], uvs = [];
|
|
2964
|
+
for (let j = 0; j < sides; ++j)
|
|
2965
|
+
{
|
|
2966
|
+
const a = (up ? j : -j) / sides * 2 * PI; // counter clockwise seen from outside
|
|
2967
|
+
points.push(point(i, a));
|
|
2968
|
+
uvs.push(vec2(sin(a) * .5 + .5, cos(a) * .5 + .5));
|
|
2969
|
+
}
|
|
2970
|
+
mesh.addStrip(render3DPolygonStrip(points), vec3(0, up ? 1 : -1, 0), render3DPolygonStrip(uvs));
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
// an end left open shows the inside, so it is seen from both sides
|
|
2974
|
+
mesh.doubleSided = !closed && !capped && (abs(profile[0][0]) > 1e-9 || abs(profile[rings-1][0]) > 1e-9);
|
|
2975
|
+
return mesh;
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
/**
|
|
2979
|
+
* Build a sphere centered on the origin
|
|
2980
|
+
* @param {number} [size] - Diameter
|
|
2981
|
+
* @param {number} [sides] - Around
|
|
2982
|
+
* @param {number} [rings] - Top to bottom
|
|
2983
|
+
* @param {boolean} [smooth] - Defaults to render3D.smoothShading
|
|
2984
|
+
* @return {Mesh}
|
|
2985
|
+
* @memberof Render3D
|
|
2986
|
+
*/
|
|
2987
|
+
function buildSphere(size=1, sides=16, rings=8, smooth=render3D?.smoothShading)
|
|
2988
|
+
{
|
|
2989
|
+
ASSERT(rings > 1, 'sphere needs at least 2 rings');
|
|
2990
|
+
const profile = [];
|
|
2991
|
+
for (let i = 0; i <= rings; ++i)
|
|
2992
|
+
{
|
|
2993
|
+
const a = i / rings * PI - PI/2;
|
|
2994
|
+
profile.push([cos(a) * size / 2, sin(a) * size / 2]);
|
|
2995
|
+
}
|
|
2996
|
+
return buildLathe(profile, sides, smooth);
|
|
2997
|
+
}
|
|
2998
|
+
|
|
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)
|
|
3025
|
+
{
|
|
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);
|
|
3029
|
+
}
|
|
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
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
|
|
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])
|
|
3055
|
+
{
|
|
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
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
return mesh;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
/**
|
|
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
|
|
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
|
|
3081
|
+
* @return {Mesh}
|
|
3082
|
+
* @memberof Render3D
|
|
3083
|
+
*/
|
|
3084
|
+
function buildBox(size=1, bevel=0, bevelSegments=1)
|
|
3085
|
+
{
|
|
3086
|
+
ASSERT(isNumber(bevel) && bevel >= 0, 'bevel must be a number, 0 or more');
|
|
3087
|
+
const half = render3DSize3(size).scale(.5);
|
|
3088
|
+
// each face: normal, right axis, up axis (right cross up = normal)
|
|
3089
|
+
const faces = [
|
|
3090
|
+
[vec3(0, 0, 1), vec3(1, 0, 0), vec3(0, 1, 0)],
|
|
3091
|
+
[vec3(0, 0, -1), vec3(-1, 0, 0), vec3(0, 1, 0)],
|
|
3092
|
+
[vec3(1, 0, 0), vec3(0, 0, -1), vec3(0, 1, 0)],
|
|
3093
|
+
[vec3(-1, 0, 0), vec3(0, 0, 1), vec3(0, 1, 0)],
|
|
3094
|
+
[vec3(0, 1, 0), vec3(1, 0, 0), vec3(0, 0, -1)],
|
|
3095
|
+
[vec3(0, -1, 0), vec3(1, 0, 0), vec3(0, 0, 1)],
|
|
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;
|
|
3101
|
+
for (const [n, r, u] of faces)
|
|
3102
|
+
{
|
|
3103
|
+
const center = n.multiply(half);
|
|
3104
|
+
const right = r.multiply(half), up = u.multiply(half);
|
|
3105
|
+
mesh.addStrip(render3DQuadAxes(center, right, up), n, RENDER3D_QUAD_UVS);
|
|
3106
|
+
}
|
|
3107
|
+
return mesh;
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
/**
|
|
3111
|
+
* Build a heightfield grid in the XZ plane centered on the origin
|
|
3112
|
+
* - smooth rounds the lighting across cells and colors each corner
|
|
3113
|
+
* - flat lights and colors each cell on its own, so a checkerboard stays crisp
|
|
3114
|
+
* - doubleSided, a sheet seen from both sides; turn it off for ground only ever seen from above
|
|
3115
|
+
* - One cell is a plain square, render3D.planeMesh and planeMeshDoubleSided are shared ones
|
|
3116
|
+
* @param {Vector2|number} [size] - World size along X and Z, a number for a square
|
|
3117
|
+
* @param {Vector2|number} [segments] - Cells along X and Z, a number for both
|
|
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
|
|
3122
|
+
* @return {Mesh}
|
|
3123
|
+
* @memberof Render3D
|
|
3124
|
+
* @example
|
|
3125
|
+
* const ground = buildGrid(vec2(20), 10, (x, z)=> (floor(x / 2) + floor(z / 2)) & 1 ? GRAY : WHITE); // 2 unit checks
|
|
3126
|
+
*/
|
|
3127
|
+
function buildGrid(size=vec2(1), segments=1, color, heightFunction, smooth=heightFunction && render3D?.smoothShading)
|
|
3128
|
+
{
|
|
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');
|
|
3134
|
+
const mesh = new Mesh;
|
|
3135
|
+
const segmentsX = segments.x, segmentsZ = segments.y;
|
|
3136
|
+
const cellX = size.x / segmentsX, cellZ = size.y / segmentsZ;
|
|
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);
|
|
3157
|
+
for (let j = 0; j < segmentsZ; ++j)
|
|
3158
|
+
{
|
|
3159
|
+
const below = row(j + 1);
|
|
3160
|
+
if (smooth)
|
|
3161
|
+
{
|
|
3162
|
+
// one ribbon per row with vertex normals from the slope
|
|
3163
|
+
const points = [], normals = [], uvs = [], colors = [];
|
|
3164
|
+
for (let i = 0; i <= segmentsX; ++i)
|
|
3165
|
+
{
|
|
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]);
|
|
3170
|
+
}
|
|
3171
|
+
mesh.addStrip(points, normals, uvs, colors);
|
|
3172
|
+
}
|
|
3173
|
+
else
|
|
3174
|
+
{
|
|
3175
|
+
// one quad per cell with its face normal and one color sampled at its center
|
|
3176
|
+
for (let i = 0; i < segmentsX; ++i)
|
|
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
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
mesh.doubleSided = true; // a sheet, seen from both sides; terrain seen only from above can turn it off
|
|
3198
|
+
return mesh;
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
/**
|
|
3202
|
+
* Build a sky dome: a sphere colored by direction, wound to be seen from inside
|
|
3203
|
+
* - set it as render3D.sky and the pass draws it around the camera behind everything
|
|
3204
|
+
* @param {Color} [topColor] - Straight up
|
|
3205
|
+
* @param {Color} [horizonColor] - Level with the camera
|
|
3206
|
+
* @param {Color} [bottomColor] - Straight down, what a camera looking at the ground sees past its edge; defaults to the
|
|
3207
|
+
* horizon color
|
|
3208
|
+
* @param {number} [sides] - Around
|
|
3209
|
+
* @param {number} [rings] - Top to bottom
|
|
3210
|
+
* @return {Mesh}
|
|
3211
|
+
* @memberof Render3D
|
|
3212
|
+
*/
|
|
3213
|
+
function buildSky(topColor=hsl(.6, .8, .55), horizonColor=hsl(.6, 1, .9), bottomColor=horizonColor, sides=16, rings=8)
|
|
3214
|
+
{
|
|
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));
|
|
3218
|
+
return mesh;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3222
|
+
/**
|
|
3223
|
+
* EngineObject3D - An EngineObject with a 3D transform and a mesh
|
|
3224
|
+
* - Set pos3D, rotation3D and scale3D instead of the 2D pos, size and angle
|
|
3225
|
+
* - Gets update, children, timers, destroy and renderOrder from EngineObject
|
|
3226
|
+
* - velocity3D is added to pos3D each frame, along with render3D.gravity and damping once it has a mass
|
|
3227
|
+
* - Objects face -Z, the same way the camera does, so lookAt turns them to face a point
|
|
3228
|
+
* - The 2D pos and velocity are still there but nothing draws them
|
|
3229
|
+
* - These inherited fields are 2D only and do nothing here: angle, angleVelocity, angleDamping,
|
|
3230
|
+
* additiveColor, drawSize, mirror, clampSpeed, friction and groundObject
|
|
3231
|
+
* - The inherited shader works here as in 2D, and with emissive at 1 its snippet does its own lighting
|
|
3232
|
+
* - Set sync2D for a 2D game with 3D looks, pos and angle then drive pos3D and rotation3D,
|
|
3233
|
+
* which is the one way those 2D fields reach a 3D object
|
|
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
|
|
3237
|
+
* - Its tile and raycast halves are 2D only so they default off here, and a child sits solid collision out
|
|
3238
|
+
* - A sync2D object collides in 2D instead, which needs the 2D size set as well as size3D
|
|
3239
|
+
* - setMesh swaps the mesh and frees the old one, for text and terrain that get built again
|
|
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
|
|
3242
|
+
* - The 2D offset arguments of addChild do nothing here, set the child's pos3D
|
|
3243
|
+
* @extends EngineObject
|
|
3244
|
+
* @memberof Render3D
|
|
3245
|
+
* @example
|
|
3246
|
+
* class Spinner extends EngineObject3D
|
|
3247
|
+
* {
|
|
3248
|
+
* constructor(pos) { super(pos, buildBox(), undefined, RED); }
|
|
3249
|
+
* update() { this.rotation3D.y += .02; }
|
|
3250
|
+
* }
|
|
3251
|
+
*/
|
|
3252
|
+
class EngineObject3D extends EngineObject
|
|
3253
|
+
{
|
|
3254
|
+
/** Create a 3D object and add it to the object list
|
|
3255
|
+
* @param {Vector3} [pos3D] - World space position
|
|
3256
|
+
* @param {Mesh} [mesh] - Mesh to draw, undefined draws nothing
|
|
3257
|
+
* @param {TileInfo|TextureInfo} [tileInfo] - Texture, mesh uvs map across the tile; a whole TextureInfo becomes
|
|
3258
|
+
* the tile that covers it
|
|
3259
|
+
* @param {Color} [color] - Tint */
|
|
3260
|
+
constructor(pos3D=vec3(), mesh, tileInfo, color=WHITE)
|
|
3261
|
+
{
|
|
3262
|
+
ASSERT(!tileInfo || tileInfo instanceof TileInfo || tileInfo instanceof TextureInfo,
|
|
3263
|
+
'tileInfo must be a TileInfo or TextureInfo, it comes before color');
|
|
3264
|
+
// a whole texture is stored as the tile that covers it, with no padding or bleed to trim
|
|
3265
|
+
// the edges, so this is always a TileInfo like the 2D one and the object stays an EngineObject
|
|
3266
|
+
if (tileInfo instanceof TextureInfo)
|
|
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);
|
|
3270
|
+
ASSERT(isVector3(pos3D), 'pos3D must be a vec3');
|
|
3271
|
+
ASSERT(!mesh || mesh instanceof Mesh, 'mesh must be a Mesh or undefined');
|
|
3272
|
+
this.mass = 0; // static: no 2D physics, and no 3D gravity until a mass is set
|
|
3273
|
+
|
|
3274
|
+
/** @property {Vector3} - World space position, local to the parent when attached to an EngineObject3D */
|
|
3275
|
+
this.pos3D = pos3D.copy();
|
|
3276
|
+
/** @property {Vector3} - Rotation vec3(pitch, yaw, roll) in radians, local to the parent when attached to an
|
|
3277
|
+
* EngineObject3D */
|
|
3278
|
+
this.rotation3D = vec3();
|
|
3279
|
+
/** @property {Vector3} - Scale, local to the parent when attached to an EngineObject3D */
|
|
3280
|
+
this.scale3D = vec3(1);
|
|
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 */
|
|
3283
|
+
this.velocity3D = vec3();
|
|
3284
|
+
/** @property {Vector3} - Added to rotation3D each frame by the engine before update, angleDamping is 2D only */
|
|
3285
|
+
this.angleVelocity3D = vec3();
|
|
3286
|
+
/** @property {Mesh|undefined} - Mesh to draw
|
|
3287
|
+
* @type {Mesh|undefined} */
|
|
3288
|
+
this.mesh = mesh;
|
|
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 */
|
|
3298
|
+
this.softShadow = 0;
|
|
3299
|
+
/** @property {boolean} - A sprite stands on world up instead of tilting toward the camera */
|
|
3300
|
+
this.upright = false;
|
|
3301
|
+
/** @property {boolean} - Keep this object's texture pixels hard edged, for pixel art that should not blur or bleed */
|
|
3302
|
+
this.pixelated = false;
|
|
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 */
|
|
3305
|
+
this.sync2D = false;
|
|
3306
|
+
/** @property {boolean} - Draw in the transparent stage, blended and sorted far to near with depth writes off;
|
|
3307
|
+
* on for a sprite */
|
|
3308
|
+
this.transparent = !mesh && !!tileInfo;
|
|
3309
|
+
/** @property {boolean} - Additive blending, in the transparent stage */
|
|
3310
|
+
this.additive = false;
|
|
3311
|
+
/** @property {number} - How much it lights itself: 0 is lit as normal, 1 is its own color with no shading, for
|
|
3312
|
+
* lamps and glowing things, between is partly self lit, and above 1 is brighter than its color, for bloom */
|
|
3313
|
+
this.emissive = 0;
|
|
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 */
|
|
3316
|
+
this.specular = 0;
|
|
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 */
|
|
3320
|
+
this.castShadow = true;
|
|
3321
|
+
/** @property {boolean} - Collide as the sphere that fits size3D instead of as the size3D box, so it rolls
|
|
3322
|
+
* around corners */
|
|
3323
|
+
this.collideAsSphere3D = false;
|
|
3324
|
+
/** @property {boolean} - Darkened by the shadow map when render3D.shadows is on */
|
|
3325
|
+
this.receiveShadow = true;
|
|
3326
|
+
/** @property {boolean|undefined} - Draw this object over the 2D scene, undefined uses render3D.renderAfter2D
|
|
3327
|
+
* @type {boolean|undefined} */
|
|
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
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
/** Move by the 3D velocities and push out of solids, called automatically each frame before update, like the 2D physics
|
|
3344
|
+
* - update runs once every object has moved and collided, so bounce off anything else there, it lands before the draw
|
|
3345
|
+
* - Override this and call super to change how the object moves itself
|
|
3346
|
+
* - A sync2D object runs the 2D physics as well, and collides there instead */
|
|
3347
|
+
updatePhysics()
|
|
3348
|
+
{
|
|
3349
|
+
// a sync2D object collides in 2D, which measures the 2D size, and that starts at zero on a 3D object
|
|
3350
|
+
ASSERT(!this.sync2D || !this.collideSolidObjects || (this.size.x && this.size.y),
|
|
3351
|
+
'a sync2D object collides in 2D, so give it a 2D size as well as a size3D', this.size);
|
|
3352
|
+
if (this.sync2D)
|
|
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;
|
|
3356
|
+
render3DMove(this);
|
|
3357
|
+
oldPos && render3DCollideLevel(this, oldPos);
|
|
3358
|
+
// the engine only runs this for objects that own where they are, a child rides along with its parent
|
|
3359
|
+
if (this.collideSolidObjects && !this.sync2D)
|
|
3360
|
+
render3DCollideSolid(this);
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
/** Move a child by its own velocities, bring a sync2D object's pos3D up to its 2D pos, then update the children,
|
|
3364
|
+
* called automatically each frame
|
|
3365
|
+
* @param {boolean} [updateChildren] - Also update the children's transforms */
|
|
3366
|
+
updateTransforms(updateChildren=true)
|
|
3367
|
+
{
|
|
3368
|
+
if (!paused)
|
|
3369
|
+
{
|
|
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
|
+
}
|
|
3377
|
+
}
|
|
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();
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
/** Set how this object collides, the same flags as in 2D
|
|
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
|
|
3391
|
+
* - A sync2D object collides in 2D instead, against the 2D size, so set that as well as size3D
|
|
3392
|
+
* @param {boolean} [collideSolidObjects] - Take part in solid collision
|
|
3393
|
+
* @param {boolean} [isSolid] - Block other objects, a pair where neither one blocks passes through;
|
|
3394
|
+
* blocking needs collideSolidObjects, so isSolid on its own is not allowed
|
|
3395
|
+
* @param {boolean} [collideLevel] - Collide with the level, the height maps and voxel maps, or the 2D tile layers
|
|
3396
|
+
* for a sync2D object
|
|
3397
|
+
* @param {boolean} [collideRaycast] - Raycasts, 2D only; 3D has render3D.pick and engineObjectsRaycast3D */
|
|
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; }
|
|
3406
|
+
|
|
3407
|
+
/** Returns the world position
|
|
3408
|
+
* @return {Vector3} */
|
|
3409
|
+
getWorldPos3D() { return render3DObjectMatrix(this).getTranslation(); }
|
|
3410
|
+
|
|
3411
|
+
/** Returns the direction the object faces, its -Z axis in the world
|
|
3412
|
+
* @return {Vector3} */
|
|
3413
|
+
getForward3D() { return render3DAxis(render3DObjectMatrix(this).m, 8).normalize(-1); }
|
|
3414
|
+
|
|
3415
|
+
/** Returns the object's right axis in the world
|
|
3416
|
+
* @return {Vector3} */
|
|
3417
|
+
getRight3D() { return render3DAxis(render3DObjectMatrix(this).m, 0).normalize(); }
|
|
3418
|
+
|
|
3419
|
+
/** Returns the object's up axis in the world
|
|
3420
|
+
* @return {Vector3} */
|
|
3421
|
+
getUp3D() { return render3DAxis(render3DObjectMatrix(this).m, 4).normalize(); }
|
|
3422
|
+
|
|
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
|
|
3426
|
+
* @return {Matrix4} */
|
|
3427
|
+
getMatrix() { return render3DObjectMatrix(this).copy(); }
|
|
3428
|
+
|
|
3429
|
+
/** Turn the object so its -Z axis points at a world space target, sets pitch and yaw and clears roll
|
|
3430
|
+
* @param {Vector3} target */
|
|
3431
|
+
lookAt(target)
|
|
3432
|
+
{
|
|
3433
|
+
// rotation3D is local to the parent, so a child has to aim at the target from the parent's point of view
|
|
3434
|
+
const parent = this.parent instanceof EngineObject3D ? this.parent : undefined;
|
|
3435
|
+
const local = parent ? parent.getMatrix().invert().transformPoint(target) : target;
|
|
3436
|
+
this.rotation3D = render3DLookRotation(local.subtract(this.pos3D), this.rotation3D);
|
|
3437
|
+
}
|
|
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
|
+
|
|
3492
|
+
/** Draw a different mesh and free the GPU buffer of the one it replaces
|
|
3493
|
+
* - For a mesh built again when something changes, like a score, a rebuilt terrain or a loaded model
|
|
3494
|
+
* - A mesh another object is still drawing is left alone, since builders are often shared
|
|
3495
|
+
* - Freeing one held somewhere else only costs it an upload, the points it was built from stay
|
|
3496
|
+
* @param {Mesh} [mesh] - The mesh to draw from now on, undefined to draw nothing
|
|
3497
|
+
* @return {Mesh|undefined} - The mesh passed in */
|
|
3498
|
+
setMesh(mesh)
|
|
3499
|
+
{
|
|
3500
|
+
ASSERT(!mesh || mesh instanceof Mesh, 'mesh must be a Mesh or undefined');
|
|
3501
|
+
const old = this.mesh;
|
|
3502
|
+
this.mesh = mesh;
|
|
3503
|
+
// nothing to free and nothing to look for when it was never uploaded
|
|
3504
|
+
if (old && old !== mesh && old.buffer && !engineObjects.some(o=> /** @type {EngineObject3D} */ (o).mesh === old))
|
|
3505
|
+
old.dispose();
|
|
3506
|
+
return mesh;
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
/** 2D rendering is skipped, the mesh is drawn by render3D during the 3D pass */
|
|
3510
|
+
render() {}
|
|
3511
|
+
|
|
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} */
|
|
3514
|
+
render3D()
|
|
3515
|
+
{
|
|
3516
|
+
// an opaque draw comes out solid however low its alpha is, so a fade with no flag looks like nothing happened
|
|
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);
|
|
3521
|
+
if (this.mesh)
|
|
3522
|
+
render3D.drawMesh(this.mesh, matrix, this.tileInfo, this.color);
|
|
3523
|
+
else if (this.tileInfo)
|
|
3524
|
+
{
|
|
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);
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
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
|
+
|
|
3578
|
+
// move an object by its 3D velocities, an object with mass falling with render3D.gravity and slowing by its damping
|
|
3579
|
+
function render3DMove(o)
|
|
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;
|
|
3583
|
+
if (o.mass && !o.sync2D) // a 2D driven object gets the 2D gravity instead
|
|
3584
|
+
{
|
|
3585
|
+
// damped first and gravity added after, the order EngineObject.updatePhysics uses,
|
|
3586
|
+
// so the same mass, damping and gravity fall the same way in both
|
|
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;
|
|
3589
|
+
}
|
|
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);
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
// where a solid object is in the world and what it collides as: the sphere that fits size3D, or the size3D box,
|
|
3604
|
+
// each grown by the object's scale
|
|
3605
|
+
// only objects that own where they are take part, so pos3D is already world space, and however the object is
|
|
3606
|
+
// turned its axes come out as long as its scale makes them; building the transform to read that back off it
|
|
3607
|
+
// costs six trig calls and a matrix for every pair tested, which is the whole cost of a crowded scene
|
|
3608
|
+
function render3DSolidShape(o)
|
|
3609
|
+
{
|
|
3610
|
+
ASSERT(!o.parent, 'a child rides along with its parent, it has no world pos3D of its own to collide with');
|
|
3611
|
+
const s = o.size3D, k = o.scale3D;
|
|
3612
|
+
const kx = abs(k.x), ky = abs(k.y), kz = abs(k.z);
|
|
3613
|
+
if (o.collideAsSphere3D)
|
|
3614
|
+
return {pos: o.pos3D.copy(), radius: max(s.x, s.y, s.z) / 2 * max(kx, ky, kz)};
|
|
3615
|
+
return {pos: o.pos3D.copy(), size: vec3(s.x * kx, s.y * ky, s.z * kz)};
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
// how far a solid shape can reach from its own center, for a quick reject before the exact test
|
|
3619
|
+
// it has to be the shape's own radius, or a wider one: a box reaches to its corner, and a sphere
|
|
3620
|
+
// takes the largest scale the same way render3DSolidShape does, or the reject would skip real touches
|
|
3621
|
+
function render3DSolidReach(o)
|
|
3622
|
+
{
|
|
3623
|
+
const s = o.size3D, k = o.scale3D;
|
|
3624
|
+
const kx = abs(k.x), ky = abs(k.y), kz = abs(k.z);
|
|
3625
|
+
if (o.collideAsSphere3D)
|
|
3626
|
+
return max(s.x, s.y, s.z) / 2 * max(kx, ky, kz);
|
|
3627
|
+
return hypot(s.x * kx, s.y * ky, s.z * kz) / 2;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
// what it takes to move shape a clear of shape b, whichever pair of shapes they are, or undefined for no touch
|
|
3631
|
+
function render3DSolidPush(a, b)
|
|
3632
|
+
{
|
|
3633
|
+
if (!a.size) // a is a sphere
|
|
3634
|
+
return b.size ? collideSphereBox(a.pos, a.radius, b.pos, b.size)
|
|
3635
|
+
: collideSphereSphere(a.pos, a.radius, b.pos, b.radius);
|
|
3636
|
+
if (!b.size) // only b is, so push b out of a and turn it around
|
|
3637
|
+
{
|
|
3638
|
+
const push = collideSphereBox(b.pos, b.radius, a.pos, a.size);
|
|
3639
|
+
return push && push.scale(-1);
|
|
3640
|
+
}
|
|
3641
|
+
return collideBoxBox3D(a.pos, a.size, b.pos, b.size);
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
// push a solid object out of the solids before it in the engine's list of them, so each pair is resolved once:
|
|
3645
|
+
// the ones after it update later and test against it then, and an object that is not in the list yet, because it
|
|
3646
|
+
// turned collision on this frame, tests them all itself and is not tested back
|
|
3647
|
+
// one pair per test is half the work of the 2D solver, which tests both directions; the difference only shows
|
|
3648
|
+
// when a collideWithObject destroys some third object, whose own turn then finds the pair already gone
|
|
3649
|
+
function render3DCollideSolid(a)
|
|
3650
|
+
{
|
|
3651
|
+
let shapeA = render3DSolidShape(a);
|
|
3652
|
+
const reachA = render3DSolidReach(a);
|
|
3653
|
+
for (const b of engineObjectsCollide)
|
|
3654
|
+
{
|
|
3655
|
+
if (b === a) break;
|
|
3656
|
+
// a child is part of its parent
|
|
3657
|
+
if (b.destroyed || !(b instanceof EngineObject3D) || b.parent || b.sync2D) continue;
|
|
3658
|
+
if (!a.isSolid && !b.isSolid) continue; // neither one blocks, so they pass through each other
|
|
3659
|
+
|
|
3660
|
+
// the pairs nowhere near each other are almost all of them in a scene of any size, so
|
|
3661
|
+
// settle those with one distance check instead of building a shape for each
|
|
3662
|
+
const p = shapeA.pos, q = b.pos3D, reach = reachA + render3DSolidReach(b);
|
|
3663
|
+
const dx = p.x - q.x, dy = p.y - q.y, dz = p.z - q.z;
|
|
3664
|
+
if (dx*dx + dy*dy + dz*dz > reach*reach)
|
|
3665
|
+
continue;
|
|
3666
|
+
|
|
3667
|
+
const push = render3DSolidPush(shapeA, render3DSolidShape(b));
|
|
3668
|
+
if (!push) continue;
|
|
3669
|
+
|
|
3670
|
+
// both objects hear about it, and either one can take the touch over
|
|
3671
|
+
const resolveA = a.collideWithObject(b, push);
|
|
3672
|
+
const resolveB = b.collideWithObject(a, push.scale(-1));
|
|
3673
|
+
if (!resolveA || !resolveB) continue;
|
|
3674
|
+
|
|
3675
|
+
// heavier objects move less, mass 0 stays put; then bounce apart when moving toward each other
|
|
3676
|
+
const total = a.mass + b.mass;
|
|
3677
|
+
const weightA = !a.mass ? 0 : !b.mass ? 1 : b.mass / total;
|
|
3678
|
+
const weightB = !b.mass ? 0 : !a.mass ? 1 : a.mass / total;
|
|
3679
|
+
a.pos3D = a.pos3D.add(push.scale(weightA));
|
|
3680
|
+
b.pos3D = b.pos3D.subtract(push.scale(weightB));
|
|
3681
|
+
if (weightA)
|
|
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
|
|
3685
|
+
const normal = push.normalize();
|
|
3686
|
+
if (weightA && a.velocity3D.dot(normal) < 0)
|
|
3687
|
+
a.velocity3D = a.velocity3D.reflect(normal, a.restitution);
|
|
3688
|
+
if (weightB && b.velocity3D.dot(normal) > 0)
|
|
3689
|
+
b.velocity3D = b.velocity3D.reflect(normal, b.restitution);
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
/**
|
|
3694
|
+
* Collect the EngineObject3D objects whose boxes overlap a sphere or a box, the 3D twin of engineObjectsCollect
|
|
3695
|
+
* - Boxes are axis aligned around the world position, rotation3D is ignored; lights, emitters and trails have no size
|
|
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
|
|
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
|
|
3701
|
+
* @return {Array<EngineObject3D>}
|
|
3702
|
+
* @memberof Render3D
|
|
3703
|
+
*/
|
|
3704
|
+
function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=false)
|
|
3705
|
+
{
|
|
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);
|
|
3709
|
+
const collected = [];
|
|
3710
|
+
for (const o of objects)
|
|
3711
|
+
{
|
|
3712
|
+
if (!(o instanceof EngineObject3D) || o.destroyed) continue;
|
|
3713
|
+
// the box in world space, scaled by the object and its parents
|
|
3714
|
+
const m = render3DObjectMatrix(o).m, s = o.size3D;
|
|
3715
|
+
if (!(s.x || s.y || s.z)) continue;
|
|
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);
|
|
3730
|
+
}
|
|
3731
|
+
return collected;
|
|
3732
|
+
}
|
|
3733
|
+
|
|
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
|
|
3736
|
+
function render3DRaycastObject(ray, o)
|
|
3737
|
+
{
|
|
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);
|
|
3747
|
+
if (!(radius > 0)) return; // nothing to hit
|
|
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;
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
/**
|
|
3798
|
+
* Collect every EngineObject3D a ray passes through, nearest first, the 3D twin of engineObjectsRaycast
|
|
3799
|
+
* - The ray has no end, so everything along it counts however far away it is
|
|
3800
|
+
* - Use render3D.pick for the nearest one on its own, with the distance to it
|
|
3801
|
+
* @param {Ray3D} ray - From render3D.screenToRay, or any ray
|
|
3802
|
+
* @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
|
|
3803
|
+
* @return {Array<EngineObject3D>}
|
|
3804
|
+
* @memberof Render3D
|
|
3805
|
+
*/
|
|
3806
|
+
function engineObjectsRaycast3D(ray, objects=engineObjects)
|
|
3807
|
+
{
|
|
3808
|
+
const hits = [];
|
|
3809
|
+
for (const o of objects)
|
|
3810
|
+
{
|
|
3811
|
+
const distance = render3DRaycastObject(ray, o);
|
|
3812
|
+
if (distance !== undefined) // only a 3D object has a distance
|
|
3813
|
+
hits.push({o: /** @type {EngineObject3D} */ (o), distance});
|
|
3814
|
+
}
|
|
3815
|
+
return hits.sort((a, b)=> a.distance - b.distance).map(hit=> hit.o);
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
/**
|
|
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
|
|
3824
|
+
* @param {Array<EngineObject>} [objects] - Defaults to every object
|
|
3825
|
+
* @param {boolean} [testCenters] - Test only each object's center, see engineObjectsCollect3D
|
|
3826
|
+
* @memberof Render3D
|
|
3827
|
+
*/
|
|
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
|
+
}
|
|
4006
|
+
|
|
4007
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4008
|
+
/**
|
|
4009
|
+
* Light3D - A light that is an EngineObject3D, so it can move, follow a parent or be destroyed like anything else
|
|
4010
|
+
* - A point light: it lights what is near it and fades out by its radius, DirectionalLight3D shines from far away
|
|
4011
|
+
* - Only the sun, render3D.sunDirection, casts shadows; these light and make highlights without one
|
|
4012
|
+
* - Only the 8 lights nearest the camera are used each frame
|
|
4013
|
+
* - radius is where the light fades out, and it fades fast, so a small radius wants a higher intensity
|
|
4014
|
+
* - intensity multiplies the color, above 1 for a light brighter than white
|
|
4015
|
+
* - radius is a world distance, so scale3D does not change it
|
|
4016
|
+
* - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
|
|
4017
|
+
* - Draws nothing itself, add a glow with drawSoftDisc or a small emissive mesh if it should be seen
|
|
4018
|
+
* @extends EngineObject3D
|
|
4019
|
+
* @memberof Render3D
|
|
4020
|
+
* @example
|
|
4021
|
+
* const torch = new Light3D(vec3(0, 3, 0), 10, hsl(.1, 1, .65));
|
|
4022
|
+
*/
|
|
4023
|
+
class Light3D extends EngineObject3D
|
|
4024
|
+
{
|
|
4025
|
+
/** Create a point light
|
|
4026
|
+
* @param {Vector3} [pos3D] - Where it is
|
|
4027
|
+
* @param {number} [radius] - Distance where the light fades to nothing
|
|
4028
|
+
* @param {Color} [color] - Light color, its alpha fades it
|
|
4029
|
+
* @param {number} [intensity] - Brightness, multiplies the color, above 1 is brighter than white */
|
|
4030
|
+
constructor(pos3D=vec3(), radius=5, color=WHITE, intensity=1)
|
|
4031
|
+
{
|
|
4032
|
+
super(pos3D, undefined, undefined, color);
|
|
4033
|
+
ASSERT(radius >= 0, 'light radius cannot be negative, 0 is an off switch like an alpha of 0');
|
|
4034
|
+
ASSERT(intensity >= 0, 'light intensity cannot be negative, 0 is an off switch');
|
|
4035
|
+
this.size3D = vec3(); // not a solid thing to pick or collect
|
|
4036
|
+
/** @property {number} - Distance where the light fades to nothing */
|
|
4037
|
+
this.radius = radius;
|
|
4038
|
+
/** @property {number} - Brightness, multiplies the color, above 1 is brighter than white */
|
|
4039
|
+
this.intensity = intensity;
|
|
4040
|
+
/** @property {boolean} - Shine from far away, from its position toward the origin, instead of out from its
|
|
4041
|
+
* position with a falloff; DirectionalLight3D sets it */
|
|
4042
|
+
this.directional = false;
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
/** Lights draw nothing */
|
|
4046
|
+
render3D() {}
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4050
|
+
/**
|
|
4051
|
+
* DirectionalLight3D - A Light3D that shines from far away with no falloff, like sunlight
|
|
4052
|
+
* - It shines from its position toward the origin, like a three.js DirectionalLight: only the direction to it
|
|
4053
|
+
* counts, so moving it or its parent swings the light around; parent it to a sun in the sky and it follows
|
|
4054
|
+
* - It cannot sit on the origin, since that leaves no direction
|
|
4055
|
+
* - Like every Light3D it casts no shadow, only the sun, render3D.sunDirection, does
|
|
4056
|
+
* @extends Light3D
|
|
4057
|
+
* @memberof Render3D
|
|
4058
|
+
* @example
|
|
4059
|
+
* const fill = new DirectionalLight3D(vec3(-1, 1, 1), hsl(.6, .5, .3)); // from the back left and above
|
|
4060
|
+
*/
|
|
4061
|
+
class DirectionalLight3D extends Light3D
|
|
4062
|
+
{
|
|
4063
|
+
/** Create a directional light
|
|
4064
|
+
* @param {Vector3} [pos3D] - Where it shines from, toward the origin
|
|
4065
|
+
* @param {Color} [color] - Light color, its alpha fades it
|
|
4066
|
+
* @param {number} [intensity] - Brightness, multiplies the color, above 1 is brighter than white */
|
|
4067
|
+
constructor(pos3D=vec3(0, 1, 0), color=WHITE, intensity=1)
|
|
4068
|
+
{
|
|
4069
|
+
super(pos3D, 0, color, intensity);
|
|
4070
|
+
this.directional = true;
|
|
4071
|
+
}
|
|
4072
|
+
}
|