@woosh/meep-engine 2.40.1 → 2.42.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.
Files changed (75) hide show
  1. package/core/collection/HashMap.d.ts +2 -0
  2. package/core/collection/HashMap.js +22 -0
  3. package/core/geom/3d/apply_mat4_transform_to_v3_array.js +2 -4
  4. package/core/geom/3d/normal/hemioct/encode_unit3_hemioct.js +5 -0
  5. package/core/geom/3d/normal/octahedron/decode_octahedron_to_unit.js +31 -0
  6. package/core/geom/3d/normal/octahedron/encode_unit_to_octahedron.js +33 -0
  7. package/core/geom/3d/normal/octahedron/encoding.spec.js +29 -0
  8. package/core/geom/3d/sphere/sphere_radius_sqr_from_v3_array_transformed.js +28 -0
  9. package/core/geom/3d/topology/struct/BinaryTopology.js +112 -0
  10. package/core/geom/Quaternion.js +14 -0
  11. package/core/math/sign_not_zero.js +8 -0
  12. package/core/parser/simple/SimpleParser.js +3 -86
  13. package/core/parser/simple/readUnsignedInteger.js +66 -0
  14. package/core/parser/simple/skipWhitespace.js +21 -0
  15. package/engine/EngineHarness.js +13 -3
  16. package/engine/asset/AssetDescription.spec.js +27 -0
  17. package/engine/asset/loaders/GLTFAssetLoader.js +5 -3
  18. package/engine/ecs/foliage/ImpostorFoliage.js +4 -0
  19. package/engine/ecs/transform/Transform.js +23 -3
  20. package/engine/graphics/ecs/decal/v2/Decal.d.ts +11 -0
  21. package/engine/graphics/ecs/decal/v2/Decal.js +50 -0
  22. package/engine/graphics/ecs/decal/v2/FPDecalSystem.d.ts +8 -0
  23. package/engine/graphics/ecs/decal/v2/FPDecalSystem.js +213 -0
  24. package/engine/graphics/ecs/decal/v2/prototypeDecalSystem.js +237 -0
  25. package/engine/graphics/ecs/mesh-v2/ShadedGeometry.js +8 -1
  26. package/engine/graphics/ecs/mesh-v2/build_three_object.js +4 -0
  27. package/engine/graphics/ecs/mesh-v2/three_object_to_entity_composition.js +22 -7
  28. package/engine/graphics/filter/ImageFilter.js +2 -1
  29. package/engine/graphics/geometry/MikkT/MikkTSpace.js +466 -305
  30. package/engine/graphics/geometry/MikkT/MikkTSpace.spec.js +7 -1
  31. package/engine/{asset/loaders/geometry/geometry → graphics/geometry/buffered}/computeBufferAttributeHash.js +1 -1
  32. package/engine/{asset/loaders/geometry/geometry → graphics/geometry/buffered}/computeGeometryEquality.js +2 -2
  33. package/engine/{asset/loaders/geometry/geometry → graphics/geometry/buffered}/computeGeometryHash.js +1 -1
  34. package/engine/graphics/geometry/optimization/merge/merge_geometry_hierarchy.js +2 -2
  35. package/engine/graphics/impostors/card_cluster/FacePlaneAssignment.js +30 -0
  36. package/engine/graphics/impostors/card_cluster/README.md +13 -0
  37. package/engine/graphics/impostors/octahedral/ImpostorBaker.js +345 -0
  38. package/engine/graphics/impostors/octahedral/ImpostorCaptureType.js +10 -0
  39. package/engine/graphics/impostors/octahedral/ImpostorDescription.js +69 -0
  40. package/engine/graphics/impostors/octahedral/README.md +30 -0
  41. package/engine/graphics/impostors/octahedral/bake/compute_bounding_sphere.js +45 -0
  42. package/engine/graphics/impostors/octahedral/bake/compute_bounding_sphere_radius_only.js +37 -0
  43. package/engine/graphics/impostors/octahedral/bake/prepare_bake_material.js +117 -0
  44. package/engine/graphics/impostors/octahedral/grid/HemiOctahedralUvEncoder.js +20 -0
  45. package/engine/graphics/impostors/octahedral/grid/OctahedralUvEncoder.js +25 -0
  46. package/engine/graphics/impostors/octahedral/grid/UvEncoder.js +21 -0
  47. package/engine/graphics/impostors/octahedral/prototypeBaker.js +237 -0
  48. package/engine/graphics/impostors/octahedral/shader/BakeShaderStandard.js +196 -0
  49. package/engine/graphics/impostors/octahedral/shader/ImpostorShaderStandard.js +306 -0
  50. package/engine/graphics/impostors/octahedral/shader/ImpostorShaderV0.js +349 -0
  51. package/engine/graphics/impostors/octahedral/shader/ImpostorShaderV1.js +74 -0
  52. package/engine/graphics/impostors/octahedral/shader/glsl/common.glsl +206 -0
  53. package/engine/graphics/impostors/octahedral/shader/glsl/v1/common.glsl +209 -0
  54. package/engine/graphics/impostors/octahedral/shader/glsl/v1/flagment.glsl +80 -0
  55. package/engine/graphics/impostors/octahedral/shader/glsl/v1/vertex.glsl +350 -0
  56. package/engine/graphics/micron/convert_three_object_to_micron.js +2 -2
  57. package/engine/graphics/micron/plugin/MicronRenderPlugin.js +2 -2
  58. package/engine/graphics/micron/render/v1/getTransformedPositionsCached.js +1 -1
  59. package/engine/graphics/particles/node-based/codegen/CodeGenerator.js +1 -1
  60. package/engine/graphics/particles/node-based/nodes/noise/CurlNoiseNode.js +1 -1
  61. package/engine/graphics/render/forward_plus/LightManager.js +1 -1
  62. package/engine/graphics/render/forward_plus/materials/FP_SHADER_CHUNK_ACCUMULATION.js +1 -3
  63. package/engine/graphics/render/forward_plus/materials/FP_SHADER_CHUNK_PREAMBLE.js +2 -1
  64. package/engine/graphics/render/forward_plus/model/Decal.js +19 -2
  65. package/engine/graphics/render/visibility/hiz/buildCanvasViewFromTexture.js +32 -10
  66. package/engine/graphics/shaders/ScreenSpaceQuadShader.js +4 -14
  67. package/engine/graphics/shaders/glsl_gen_swizzled_read.js +39 -0
  68. package/engine/graphics/texture/sampler/Sampler2D.js +10 -10
  69. package/engine/graphics/texture/sampler/prototypeSamplerFiltering.js +117 -11
  70. package/engine/graphics/texture/sampler/resize/sampler2d_downsample_mipmap.js +66 -0
  71. package/engine/graphics/texture/sampler/sampler2_d_scale_down_lanczos.js +2 -2
  72. package/engine/ui/GUIEngine.js +8 -1
  73. package/package.json +1 -1
  74. package/view/tooltip/gml/parser/readReferenceToken.js +1 -1
  75. package/engine/asset/GameAssetManager.js +0 -137
@@ -14,7 +14,13 @@ test('single triangle', () => {
14
14
 
15
15
  ctx.geometry_buffer_index = [0, 1, 2];
16
16
  ctx.geometry_buffer_vertex_position = [1, 2, 3, 4, 5, 6, 7, 8, 9];
17
- ctx.geometry_buffer_vertex_normal = [0, 1, 0, 0, 1, 0, 0, 1, 0];
17
+
18
+ ctx.geometry_buffer_vertex_normal = [
19
+ 0, 1, 0,
20
+ 0, 1, 0,
21
+ 0, 1, 0
22
+ ];
23
+
18
24
  ctx.geometry_buffer_vertex_uv = [0, 0, 0, 0, 0, 0];
19
25
 
20
26
  genTangSpace(ctx);
@@ -1,4 +1,4 @@
1
- import { computeStridedIntegerArrayHash } from "../../../../computeStridedIntegerArrayHash.js";
1
+ import { computeStridedIntegerArrayHash } from "../../../computeStridedIntegerArrayHash.js";
2
2
 
3
3
  /**
4
4
  *
@@ -1,5 +1,5 @@
1
- import { isArrayEqualStrict } from "../../../../../core/collection/array/isArrayEqualStrict.js";
2
- import { is_typed_array_equals } from "../../../../../core/collection/array/typed/is_typed_array_equals.js";
1
+ import { isArrayEqualStrict } from "../../../../core/collection/array/isArrayEqualStrict.js";
2
+ import { is_typed_array_equals } from "../../../../core/collection/array/typed/is_typed_array_equals.js";
3
3
 
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { computeBufferAttributeHash } from "./computeBufferAttributeHash.js";
2
- import { computeStringHash } from "../../../../../core/primitives/strings/computeStringHash.js";
2
+ import { computeStringHash } from "../../../../core/primitives/strings/computeStringHash.js";
3
3
 
4
4
  /**
5
5
  *
@@ -1,7 +1,7 @@
1
1
  import { StaticMaterialCache } from "../../../../asset/loaders/material/StaticMaterialCache.js";
2
2
  import { HashMap } from "../../../../../core/collection/HashMap.js";
3
- import { computeGeometryEquality } from "../../../../asset/loaders/geometry/geometry/computeGeometryEquality.js";
4
- import { computeGeometryHash } from "../../../../asset/loaders/geometry/geometry/computeGeometryHash.js";
3
+ import { computeGeometryEquality } from "../../buffered/computeGeometryEquality.js";
4
+ import { computeGeometryHash } from "../../buffered/computeGeometryHash.js";
5
5
  import { array_copy } from "../../../../../core/collection/array/copyArray.js";
6
6
  import { mat4 } from "gl-matrix";
7
7
  import { Group, Matrix4, Mesh } from "three";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * A geometry is separated into individual triangles, with each triangle projected onto a single plane.
3
+ * This class contains and fully describes such assignment
4
+ */
5
+ export class FacePlaneAssignment {
6
+ /**
7
+ * Flat plane data array
8
+ * Format: [normal_0_x, normal_0_y, normal_0_z, plane_0_offset, normal_1_x, ... ]
9
+ * @type {number[]}
10
+ */
11
+ planes = [];
12
+
13
+ /**
14
+ *
15
+ * @type {number}
16
+ */
17
+ plane_count = 0;
18
+
19
+ /**
20
+ *
21
+ * @type {THREE.BufferGeometry|null}
22
+ */
23
+ geometry = null;
24
+
25
+ /**
26
+ * Indices of planes associated with each face (triangle) for a given face index
27
+ * @type {number[]}
28
+ */
29
+ face_assignments = [];
30
+ }
@@ -0,0 +1,13 @@
1
+ # Card Cluster Impostors
2
+
3
+ The system is fairly trivial in concept. It generates a specified number of *cards* (quads) along with textures to represent original mesh.
4
+
5
+ The idea for this system was inspired by Sean Feeley's talk at GDC 2019 "Interactive Wind and Vegetation in 'God of War'", where he briefly explained how impostors work in their engine.
6
+
7
+ ---
8
+ ### References
9
+
10
+ 1. [2003] "Billboard Clouds for Extreme Model Simplification" by Xavier Decoret, Fr ´ edo Durand, Franc ´ ¸ois X. Sillion, Julie Dorsey
11
+ 2. [2005] "Extreme Model Simplification for Forest Rendering" by Anton L. Fuhrmann, Eike Umlauf and Stephan Mantler
12
+ 3. [2005] "Tree Rendering with Billboard Clouds" by Ismael Garcia, Mateu Sbert, and László Szirmay-Kalos
13
+ 4. [2009] "Implementation of an Improved Billboard Cloud Algorithm" by Christian Luksch
@@ -0,0 +1,345 @@
1
+ import { ImpostorCaptureType } from "./ImpostorCaptureType.js";
2
+ import { assert } from "../../../../core/assert.js";
3
+ import { isPowerOfTwo } from "../../../../core/math/isPowerOrTwo.js";
4
+ import { Mesh, OrthographicCamera, Scene, Vector4, WebGLMultipleRenderTargets } from "three";
5
+ import { ImpostorDescription } from "./ImpostorDescription.js";
6
+ import { mat4, vec3 } from "gl-matrix";
7
+ import { BakeShaderStandard } from "./shader/BakeShaderStandard.js";
8
+ import { Sampler2D } from "../../texture/sampler/Sampler2D.js";
9
+ import Signal from "../../../../core/events/signal/Signal.js";
10
+ import { HashMap } from "../../../../core/collection/HashMap.js";
11
+ import { computeMaterialHash } from "../../../asset/loaders/material/computeMaterialHash.js";
12
+ import { computeMaterialEquality } from "../../../asset/loaders/material/computeMaterialEquality.js";
13
+ import { UvEncoder } from "./grid/UvEncoder.js";
14
+ import { OctahedralUvEncoder } from "./grid/OctahedralUvEncoder.js";
15
+ import { HemiOctahedralUvEncoder } from "./grid/HemiOctahedralUvEncoder.js";
16
+ import { prepare_bake_material } from "./bake/prepare_bake_material.js";
17
+ import { collectIteratorValueToArray } from "../../../../core/collection/IteratorUtils.js";
18
+ import { compute_bounding_sphere } from "./bake/compute_bounding_sphere.js";
19
+
20
+ export class ImpostorBaker {
21
+
22
+ /**
23
+ *
24
+ * @type {THREE.WebGLRenderer|null}
25
+ * @private
26
+ */
27
+ _renderer = null;
28
+
29
+ /**
30
+ *
31
+ * @param {THREE.WebGLRenderer} v
32
+ */
33
+ set renderer(v) {
34
+ this._renderer = v;
35
+ }
36
+
37
+
38
+ /**
39
+ *
40
+ * @param {number[]|vec4} bounding_sphere
41
+ * @param {{mesh:ShadedGeometry, transform:mat4}[]} objects
42
+ * @param {number} resolution
43
+ * @param {number} frames
44
+ * @param {UvEncoder} encoder
45
+ */
46
+ bake_internal({
47
+ bounding_sphere,
48
+ objects,
49
+ resolution,
50
+ frames,
51
+ encoder
52
+ }) {
53
+
54
+ const distance = bounding_sphere[3];
55
+
56
+ assert.isNumber(distance, 'distance');
57
+ assert.isNonNegativeInteger(resolution, 'resolution');
58
+ assert.isNonNegativeInteger(frames, 'frames');
59
+
60
+ const unit_sphere_direction = [];
61
+
62
+ const cam = new OrthographicCamera();
63
+ const rt = new WebGLMultipleRenderTargets(resolution, resolution, 3);
64
+ rt.scissorTest = false;
65
+ rt.stencilBuffer = false;
66
+
67
+ rt.texture[0].name = 'diffuse+alpha';
68
+ rt.texture[1].name = 'normal+depth';
69
+ rt.texture[2].name = 'orm'; // Occlusion, Roughness, Metalness
70
+
71
+ const renderer = this._renderer;
72
+
73
+ // remember render state
74
+ const _rt = renderer.getRenderTarget();
75
+ const _vp = new Vector4();
76
+ renderer.getViewport(_vp);
77
+ const _autoClear = renderer.autoClear;
78
+ const _pixelRatio = renderer.getPixelRatio();
79
+
80
+ const gl = this._renderer.getContext();
81
+
82
+ // Baking should be done without anti-aliasing, so we make sure to disable it
83
+ const _enabled_antialias = gl.isEnabled(gl.SAMPLE_ALPHA_TO_COVERAGE);
84
+
85
+ if (_enabled_antialias) {
86
+ gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
87
+ }
88
+
89
+
90
+ // set new render state
91
+ renderer.autoClear = false;
92
+ renderer.setRenderTarget(rt);
93
+ renderer.setClearColor(0xFFFFFF, 0);
94
+ renderer.clearColor();
95
+ renderer.setPixelRatio(1);
96
+
97
+ const frame_width = resolution / frames;
98
+ const frame_height = resolution / frames;
99
+
100
+ const object_count = objects.length;
101
+
102
+ // construct scene
103
+ const scene = new Scene();
104
+
105
+ scene.autoUpdate = false;
106
+ scene.matrixAutoUpdate = false;
107
+ scene.matrixWorldNeedsUpdate = false;
108
+
109
+ const id = new ImpostorDescription();
110
+ id.frame_count = frames;
111
+ id.resolution = resolution;
112
+
113
+ /**
114
+ * Fired when all the rendering is done to indicate that cleanup should happen
115
+ * @type {Signal}
116
+ */
117
+ const cleanup_signal = new Signal();
118
+
119
+ /**
120
+ *
121
+ * @type {HashMap<Material, BakeShaderStandard>}
122
+ */
123
+ const bake_material_map = new HashMap({
124
+ keyHashFunction: computeMaterialHash,
125
+ keyEqualityFunction: computeMaterialEquality
126
+ });
127
+
128
+ const max_anisotropic_filtering_level = renderer.capabilities.getMaxAnisotropy();
129
+
130
+ for (let k = 0; k < object_count; k++) {
131
+ const object = objects[k];
132
+
133
+ const source_mesh = object.mesh;
134
+
135
+ // ensure tangents are generated
136
+ //buffer_geometry_ensure_tangents(source_mesh.geometry);
137
+
138
+ const source_material = source_mesh.material;
139
+
140
+ // obtain material for baking
141
+ const bake_material = bake_material_map.getOrCompute(source_material, (source_material) => {
142
+
143
+ const bake_material = new BakeShaderStandard();
144
+
145
+ // prepare bake material to match the source material
146
+ prepare_bake_material({
147
+ bake_material: bake_material,
148
+ source_material: source_material,
149
+ cleanup_signal: cleanup_signal,
150
+ anisotropy: max_anisotropic_filtering_level
151
+ });
152
+
153
+ bake_material.uniforms.uAtlasResolution.value.set(resolution, resolution);
154
+
155
+ cleanup_signal.addOne(bake_material.dispose, bake_material);
156
+
157
+ id.source_material_count++;
158
+
159
+ return bake_material;
160
+ });
161
+
162
+
163
+ const geometry = source_mesh.geometry;
164
+ const mesh = new Mesh(geometry, bake_material);
165
+
166
+ mesh.matrixAutoUpdate = false;
167
+ mesh.matrixWorldNeedsUpdate = false;
168
+ mesh.frustumCulled = false;
169
+
170
+ mat4.copy(mesh.matrixWorld.elements, object.transform);
171
+
172
+ scene.add(mesh);
173
+
174
+ // update stats
175
+ id.source_geometry_polygon_count += geometry.getIndex().count / 3;
176
+ id.source_geometry_vertex_count += geometry.getAttribute('position').count;
177
+ id.source_instance_count++;
178
+ }
179
+
180
+
181
+ const bake_material_array = collectIteratorValueToArray([], bake_material_map.values());
182
+
183
+ // traverse octahedron
184
+ const max_frame_index = frames - 1;
185
+
186
+ for (let i = 0; i < frames; i++) {
187
+ const octahedron_u = max_frame_index > 0 ? (i / max_frame_index) : 0;
188
+
189
+ for (let j = 0; j < frames; j++) {
190
+
191
+ const octahedron_v = max_frame_index > 0 ? (j / max_frame_index) : 0;
192
+
193
+ // compute vector direction where to place camera
194
+ encoder.uv_to_unit(unit_sphere_direction, [octahedron_u, octahedron_v]);
195
+
196
+ // offset by the radius of the sphere
197
+ const camera_px = distance * unit_sphere_direction[0] + bounding_sphere[0];
198
+ const camera_py = distance * unit_sphere_direction[1] + bounding_sphere[1];
199
+ const camera_pz = distance * unit_sphere_direction[2] + bounding_sphere[2];
200
+
201
+ // console.log(`UV:${octahedron_u.toFixed(2)},${octahedron_v.toFixed(2)}\t V3:${unit_sphere_direction.map(n => n.toFixed(2)).join(', ')}`);
202
+
203
+ // construct projection matrix
204
+ cam.left = -distance;
205
+ cam.right = distance;
206
+ cam.top = distance;
207
+ cam.bottom = -distance;
208
+ cam.near = 0;
209
+ cam.far = distance * 2;
210
+
211
+ cam.position.set(
212
+ camera_px,
213
+ camera_py,
214
+ camera_pz
215
+ );
216
+
217
+ cam.lookAt(bounding_sphere[0], bounding_sphere[1], bounding_sphere[2]);
218
+ cam.updateProjectionMatrix();
219
+
220
+ // update materials
221
+ for (let k = 0; k < bake_material_array.length; k++) {
222
+ const mat = bake_material_array[k];
223
+
224
+ mat.uniforms.projection_params.value.set(
225
+ 0, 0, 0, 1 / cam.far
226
+ );
227
+ mat.uniformsNeedUpdate = true;
228
+ }
229
+
230
+ //TODO consider doing super-sampling for some textures for better-looking results
231
+
232
+ /*
233
+ TODO it's possible to reduce bounding sphere in some cases where thin geometry would produce 0 texels
234
+ in the actual render, but we can't know that without doing the rendering first.
235
+ Consider doing a cheap pre-pass, rendering out pixels and checking if we can crop the bounding sphere further
236
+ */
237
+
238
+ renderer.setViewport(i * frame_width, j * frame_height, frame_width, frame_height);
239
+ renderer.render(scene, cam);
240
+ }
241
+ }
242
+
243
+ /*
244
+ TODO dilate non-color textures to prevent artifacts when sampling around the edges
245
+ */
246
+
247
+ //restore renderer state
248
+ renderer.setRenderTarget(_rt);
249
+ renderer.setViewport(_vp);
250
+ renderer.autoClear = _autoClear;
251
+ renderer.setPixelRatio(_pixelRatio);
252
+ if (_enabled_antialias) {
253
+ gl.enable(gl.SAMPLE_ALPHA_TO_COVERAGE);
254
+ }
255
+
256
+ // cleanup
257
+ cleanup_signal.send0();
258
+
259
+ // const sampler = convertTexture2Sampler2D(rt.texture);
260
+
261
+
262
+ id.atlas = Sampler2D.uint8(1, resolution, resolution);
263
+ // id.atlas = sampler;
264
+ id.rt = rt;
265
+
266
+ return id;
267
+ }
268
+
269
+ /**
270
+ *
271
+ * @param {{mesh:ShadedGeometry, transform:mat4}[]} objects objects that should make up the impostor. Baking will be done around the origin, so make sure to position meshes accordingly to make good use of texture space.
272
+ * @param {number} frames how many views to capture for the atlas in each X and Y direction. Setting this to 4 will result in 16 (4*4) views captured, higher value will sacrifice detail but result in transitions being more smooth
273
+ * @param {number} resolution resolution of the final asset, higher = more detail
274
+ * @param {ImpostorCaptureType} type
275
+ * @returns {ImpostorDescription}
276
+ */
277
+ bake({
278
+ objects,
279
+ frames = 12,
280
+ resolution = 1024,
281
+ type = ImpostorCaptureType.FullSphere
282
+ }) {
283
+
284
+ console.time('bake');
285
+
286
+ assert.defined(objects, 'object');
287
+ assert.isNonNegativeInteger(frames, 'frames');
288
+ assert.isNonNegativeInteger(resolution, 'resolution');
289
+ assert.greaterThanOrEqual(frames, 1, 'number of frames must be >= 1');
290
+
291
+ if (frames > resolution) {
292
+ throw new Error(`Frame count(=${frames}) is greater than the resolution(=${resolution}) of the bake texture, packing description is unachievable`);
293
+ }
294
+
295
+ const frame_resolution = resolution / frames;
296
+
297
+ if (frames >= resolution / 4) {
298
+ console.warn(`Bake configuration will produce less frames, this will likely result in a useless impostor that contains too little texel density to properly represent the underlying scene. [frames=${frames}, resolution=${resolution}, resulting density = ${Math.pow(frame_resolution, 2)} texels per frame]`);
299
+ }
300
+
301
+ if (resolution % frames !== 0 && frame_resolution < 16) {
302
+ console.warn(`To get results at low frame resolutions number of frames should be a divisor of resolution, instead got [frames=${frames}, resolution=${resolution}, resolution/frames=${frame_resolution}]`);
303
+ }
304
+
305
+ if (!isPowerOfTwo(resolution)) {
306
+ throw new Error(` 'resolution' must be a power of two, instead was '${resolution}'`);
307
+ }
308
+
309
+ if (frames < 2) {
310
+ console.warn(`Frame count is set to ${frames}, which is too low to be useful. Consider values of 2 and above. A good default is 12`);
311
+ }
312
+
313
+ if (this._renderer === null) {
314
+ throw new Error('No renderer attached. Renderer is required for baking.');
315
+ }
316
+
317
+ const bounding_sphere = compute_bounding_sphere(objects);
318
+
319
+ // we need to compute bounding sphere around origin of the object
320
+ let encoder;
321
+ if (type === ImpostorCaptureType.FullSphere) {
322
+ encoder = new OctahedralUvEncoder();
323
+ } else if (type === ImpostorCaptureType.Hemisphere) {
324
+ encoder = new HemiOctahedralUvEncoder();
325
+ } else {
326
+ throw new Error(`Unsupported capture mode '${type}'`);
327
+ }
328
+
329
+ const r = this.bake_internal({
330
+ resolution,
331
+ frames,
332
+ objects,
333
+ bounding_sphere,
334
+ encoder
335
+ });
336
+
337
+ r.capture_type = type;
338
+ r.sphere_radius = bounding_sphere[3];
339
+ vec3.copy(r.offset,bounding_sphere);
340
+
341
+ console.timeEnd('bake');
342
+
343
+ return r;
344
+ }
345
+ }
@@ -0,0 +1,10 @@
1
+ export const ImpostorCaptureType = {
2
+ /**
3
+ * Capture from all sides
4
+ */
5
+ FullSphere: 0,
6
+ /**
7
+ * Only capture from the top hemisphere, good for things that will never be seen from "below", such as trees, houses etc.
8
+ */
9
+ Hemisphere: 1
10
+ };
@@ -0,0 +1,69 @@
1
+ import { ImpostorCaptureType } from "./ImpostorCaptureType.js";
2
+
3
+ export class ImpostorDescription {
4
+ /**
5
+ * How many XY frames should be captured
6
+ * More frames will result in less distortion, but lower texel density (lower resolution for each individual frame)
7
+ * @type {number}
8
+ */
9
+ frame_count = 16;
10
+
11
+ /**
12
+ * How the impostor is being captured
13
+ * @type {number}
14
+ */
15
+ capture_type = ImpostorCaptureType.FullSphere;
16
+
17
+ /**
18
+ * Resolution of the texture atlas used for baking. All "frames" will be packed here
19
+ * Must be power of two
20
+ * @type {number}
21
+ */
22
+ resolution = 1024;
23
+
24
+ /**
25
+ * Radius of baking sphere, this is centered on origin
26
+ * @type {number}
27
+ */
28
+ sphere_radius = 0;
29
+
30
+ /**
31
+ * Baking offset
32
+ * @type {number[]}
33
+ */
34
+ offset = [0, 0, 0];
35
+
36
+ /**
37
+ * Actual baked data
38
+ * @type {Sampler2D|null}
39
+ */
40
+ atlas = null;
41
+
42
+ /**
43
+ * Source scene from which impostor was baked
44
+ * @type {number}
45
+ */
46
+ source_geometry_polygon_count = 0;
47
+ /**
48
+ * @type {number}
49
+ */
50
+ source_geometry_vertex_count = 0;
51
+ /**
52
+ *
53
+ * @type {number}
54
+ */
55
+ source_material_count = 0;
56
+ /**
57
+ *
58
+ * @type {number}
59
+ */
60
+ source_instance_count = 0;
61
+
62
+
63
+ /**
64
+ * TODO: replace with something non-three.js specific
65
+ * @deprecated use non-three.js specific accessors instead
66
+ * @type {THREE.WebGLMultipleRenderTargets}
67
+ */
68
+ rt = null;
69
+ }
@@ -0,0 +1,30 @@
1
+ # Octahedral Impostor system
2
+
3
+ This system is based on the idea presented by Ryan Brucks
4
+
5
+ Conceptually, the system has 2 parts:
6
+ 1. Baking. This is where we take the source mesh, and prepare assets for impostor. These assets are mainly textures, copied from materials of the source meshes as well as depth fields.
7
+ 2. Drawing. Assets prepared during "Baking" part are being rendered, using a simple geometry (6 triangles), and performing ray marching as well as shading inside the pixel shader.
8
+
9
+ This is inherently a deferred rendering technique, where an impostor asset is represented as a collection of pre-rendered G-Buffers from different view angles, those "frames" are looked up at runtime in the shader to create the best approximation of a G-Buffer for the given projection transform, and the shading is performed. Depth is written out as well, this allows impostors to not only exist in color space, but also in depth space. The fact that impostors write depth gives them ability to be used for shadow mapping as well.
10
+
11
+ ## Limitations
12
+ * Materials. Currently only PBR rendering is supported with the following attributes recorded
13
+ * Albedo (vertex colors are included as well)
14
+ * Normal
15
+ * Metalness
16
+ * Roughness
17
+ * Ambient Occlusion
18
+ * Animations are not supported, neither vertex, nor pixel-shader based ones. The whole impostor can move around and be transformed, but the contents are static and pre-baked.
19
+ * When camera moves too close to the impostor, or an extreme projection is applied - artefacts will become visible. In case of camera getting too close - parts of the mesh that are occluded by outer shell will not become visible in the impostor, as that information is simply not captured. In cases of extreme projection - certain parts of the captured G-Buffer may undergo warping and stretching converting a handful of baked pixel to a larger number of on-screen pixels, creating pixelation and smearing. Note that impostors are not a replacement for your meshes, so use them sensibly.
20
+
21
+
22
+ ---
23
+ references:
24
+
25
+ - [2021] [Rendering a Sphere on a Quad](https://bgolus.medium.com/rendering-a-sphere-on-a-quad-13c92025570c) by Ben Golus
26
+ - [2018] [Octahedral Impostors](https://shaderbits.com/blog/octahedral-impostors) by Ryan Brucks
27
+ - [2018] https://github.com/xraxra/IMP
28
+ - [2018] https://github.com/wojtekpil/Godot-Octahedral-Impostors
29
+ - [2011] BN11 : "Real-time Realistic Rendering and Lighting of Forests" by Eric Bruneton, Fabrice Neyret
30
+ - [2007] TSK07: "Fast (Spherical) Light Field Rendering with Per-Pixel Depth." by Severin Todt, Christof Rezk-Salama and Andreas Kolb
@@ -0,0 +1,45 @@
1
+ import { Miniball } from "../../../../../core/geom/packing/miniball/Miniball.js";
2
+ import { PointSet } from "../../../../../core/geom/packing/miniball/PointSet.js";
3
+ import { apply_mat4_transform_to_v3_array } from "../../../../../core/geom/3d/apply_mat4_transform_to_v3_array.js";
4
+
5
+ /**
6
+ *
7
+ * @param {{mesh:ShadedGeometry, transform:mat4}[]} objects
8
+ */
9
+ export function compute_bounding_sphere(objects) {
10
+ let total_point_count = 0;
11
+ const object_count = objects.length;
12
+
13
+ for (let i = 0; i < object_count; i++) {
14
+ const object = objects[i];
15
+
16
+ const geometry = object.mesh.geometry;
17
+
18
+ const attribute = geometry.getAttribute('position');
19
+
20
+ total_point_count += attribute.count;
21
+ }
22
+
23
+ const final_buffer = new Float32Array(total_point_count * 3);
24
+
25
+ let pointer = 0;
26
+
27
+ for (let i = 0; i < object_count; i++) {
28
+ const object = objects[i];
29
+
30
+ const geometry = object.mesh.geometry;
31
+
32
+ const attribute = geometry.getAttribute('position');
33
+
34
+ const source_length = attribute.array.length;
35
+
36
+ apply_mat4_transform_to_v3_array(attribute.array, 0, final_buffer, pointer, attribute.count, object.transform);
37
+
38
+ pointer += source_length;
39
+ }
40
+
41
+ const points = new PointSet(total_point_count, 3, final_buffer);
42
+ const miniball = new Miniball(points);
43
+
44
+ return [...miniball.center(), miniball.radius()];
45
+ }
@@ -0,0 +1,37 @@
1
+ import { max2 } from "../../../../../core/math/max2.js";
2
+ import {
3
+ sphere_radius_sqr_from_v3_array_transformed
4
+ } from "../../../../../core/geom/3d/sphere/sphere_radius_sqr_from_v3_array_transformed.js";
5
+
6
+ /**
7
+ * Computes radius of bounding sphere around origin for given group of objects
8
+ *
9
+ * NOTE: This may seem pointless, but by doing this computation on per-vertex basis we get the tightest possible bounds,
10
+ * allowing us to get the best possible texture space utilization.
11
+ * @param {{mesh:ShadedGeometry, transform:mat4}[]} objects
12
+ * @returns {number}
13
+ */
14
+ export function compute_bounding_sphere_radius_only(objects) {
15
+ let distance_sqr = 0;
16
+
17
+ const n = objects.length;
18
+ for (let i = 0; i < n; i++) {
19
+ const object = objects[i];
20
+
21
+ const attribute = object.mesh.geometry.getAttribute('position');
22
+
23
+ const array = attribute.array;
24
+
25
+ const transform = object.transform;
26
+
27
+ const n = array.length;
28
+
29
+ const radius2 = sphere_radius_sqr_from_v3_array_transformed(array, n, transform);
30
+
31
+ distance_sqr = max2(distance_sqr, radius2);
32
+
33
+ }
34
+
35
+
36
+ return Math.sqrt(distance_sqr);
37
+ }