@zephyr3d/scene 0.9.24 → 0.9.25
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/dist/index.d.ts +52 -43
- package/dist/index.js +1 -1
- package/dist/material/shader/helper.js +435 -435
- package/dist/material/shader/helper.js.map +1 -1
- package/dist/render/cluster_light.js +4 -4
- package/dist/render/cluster_light.js.map +1 -1
- package/dist/scene/terrain-cm/grass.js +270 -249
- package/dist/scene/terrain-cm/grass.js.map +1 -1
- package/dist/utility/serialization/scene/terrain.js +88 -69
- package/dist/utility/serialization/scene/terrain.js.map +1 -1
- package/dist/values.js +56 -56
- package/dist/values.js.map +1 -1
- package/package.json +1 -1
|
@@ -31,30 +31,40 @@ import '../../shaders/atmosphere.js';
|
|
|
31
31
|
import '../../render/rendergraph/scene_render_context.js';
|
|
32
32
|
import { ClipmapGrassMaterial } from './grassmaterial.js';
|
|
33
33
|
|
|
34
|
-
const MAX_INSTANCES_PER_NODE = 2048;
|
|
35
34
|
const INSTANCE_BYTES = 4 * 4;
|
|
35
|
+
/** Number of placement cells along each axis of a grass tile */ const TILE_CELLS = 64;
|
|
36
|
+
/** Default number of placement cells per density map texel along each axis */ const DEFAULT_CELLS_PER_TEXEL = 2;
|
|
37
|
+
const MAX_CELLS_PER_TEXEL = 8;
|
|
36
38
|
/**
|
|
37
|
-
*
|
|
39
|
+
* Deterministic 2D integer hash, returns a value in [0, 1).
|
|
40
|
+
* Grass placement derives everything (jitter, survival threshold, rotation)
|
|
41
|
+
* from this hash so that blade positions are stable across edits and reloads.
|
|
42
|
+
*/ function hashCell(x, z, seed) {
|
|
43
|
+
let h = Math.imul(x, 0x27d4eb2d) ^ Math.imul(z, 0x165667b1) ^ Math.imul(seed, 0x9e3779b9) | 0;
|
|
44
|
+
h = Math.imul(h ^ h >>> 15, 0x85ebca6b);
|
|
45
|
+
h = Math.imul(h ^ h >>> 13, 0xc2b2ae35);
|
|
46
|
+
h ^= h >>> 16;
|
|
47
|
+
return (h >>> 0) / 4294967296;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Grass blade instance buffer for a single tile
|
|
38
51
|
* @internal
|
|
39
52
|
*/ class GrassInstances extends Disposable {
|
|
40
|
-
|
|
53
|
+
_numInstances;
|
|
41
54
|
_baseVertexBuffer;
|
|
42
55
|
_indexBuffer;
|
|
43
56
|
_instanceBuffer;
|
|
44
57
|
_primitive;
|
|
45
58
|
constructor(baseVertexBuffer, indexBuffer){
|
|
46
59
|
super();
|
|
47
|
-
this.
|
|
60
|
+
this._numInstances = 0;
|
|
48
61
|
this._baseVertexBuffer = new DRef(baseVertexBuffer);
|
|
49
62
|
this._indexBuffer = new DRef(indexBuffer);
|
|
50
63
|
this._instanceBuffer = new DRef();
|
|
51
64
|
this._primitive = new DRef();
|
|
52
65
|
}
|
|
53
66
|
get numInstances() {
|
|
54
|
-
return this.
|
|
55
|
-
}
|
|
56
|
-
get instanceBuffer() {
|
|
57
|
-
return this._instanceBuffer.get();
|
|
67
|
+
return this._numInstances;
|
|
58
68
|
}
|
|
59
69
|
setBaseVertexBuffer(baseVertexBuffer) {
|
|
60
70
|
if (baseVertexBuffer !== this._baseVertexBuffer.get()) {
|
|
@@ -62,8 +72,25 @@ const INSTANCE_BYTES = 4 * 4;
|
|
|
62
72
|
this._primitive.dispose();
|
|
63
73
|
}
|
|
64
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Sets the packed instance data (x, y, sin, cos per instance)
|
|
77
|
+
*/ setData(data, count) {
|
|
78
|
+
this._numInstances = count;
|
|
79
|
+
if (count === 0) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const device = getDevice();
|
|
83
|
+
const bytesRequired = count * INSTANCE_BYTES;
|
|
84
|
+
let buffer = this._instanceBuffer.get();
|
|
85
|
+
if (!buffer || buffer.byteLength < bytesRequired) {
|
|
86
|
+
buffer = device.createVertexBuffer('tex1_f32x4', new Uint8Array(nextPowerOf2(bytesRequired)));
|
|
87
|
+
this._instanceBuffer.set(buffer);
|
|
88
|
+
this._primitive.dispose();
|
|
89
|
+
}
|
|
90
|
+
buffer.bufferSubData(0, data.subarray(0, count * 4));
|
|
91
|
+
}
|
|
65
92
|
draw() {
|
|
66
|
-
if (this.
|
|
93
|
+
if (this._numInstances > 0) {
|
|
67
94
|
if (!this._primitive.get()) {
|
|
68
95
|
const primitive = new Primitive();
|
|
69
96
|
primitive.setVertexBuffer(this._baseVertexBuffer.get());
|
|
@@ -74,58 +101,8 @@ const INSTANCE_BYTES = 4 * 4;
|
|
|
74
101
|
primitive.indexCount = this._indexBuffer.get().length;
|
|
75
102
|
this._primitive.set(primitive);
|
|
76
103
|
}
|
|
77
|
-
this._primitive.get().drawInstanced(this.
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
updateBuffers() {
|
|
81
|
-
const device = getDevice();
|
|
82
|
-
if (this._instances.length === 0) {
|
|
83
|
-
this._instanceBuffer.set(null);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
if (this._instances.length > 0 && !this._instanceBuffer.get()) {
|
|
87
|
-
this._instanceBuffer.set(device.createVertexBuffer('tex1_f32x4', new Uint8Array(this._instances.length * INSTANCE_BYTES)));
|
|
88
|
-
}
|
|
89
|
-
let buffer = this._instanceBuffer.get();
|
|
90
|
-
const currentBytes = buffer.byteLength;
|
|
91
|
-
const bytesRequired = this._instances.length * INSTANCE_BYTES;
|
|
92
|
-
if (currentBytes < bytesRequired) {
|
|
93
|
-
buffer = device.createVertexBuffer('tex1_f32x4', new Uint8Array(nextPowerOf2(bytesRequired)));
|
|
94
|
-
this._instanceBuffer.set(buffer);
|
|
95
|
-
this._primitive.dispose();
|
|
96
|
-
}
|
|
97
|
-
const data = new Float32Array(4 * this._instances.length);
|
|
98
|
-
for(let i = 0; i < this._instances.length; i++){
|
|
99
|
-
data[i * 4 + 0] = this._instances[i].x;
|
|
100
|
-
data[i * 4 + 1] = this._instances[i].y;
|
|
101
|
-
data[i * 4 + 2] = Math.sin(this._instances[i].angle);
|
|
102
|
-
data[i * 4 + 3] = Math.cos(this._instances[i].angle);
|
|
103
|
-
}
|
|
104
|
-
buffer.bufferSubData(0, data);
|
|
105
|
-
}
|
|
106
|
-
removeInstances(minX, minZ, maxX, maxZ, num) {
|
|
107
|
-
if (num <= 0) {
|
|
108
|
-
return 0;
|
|
109
|
-
}
|
|
110
|
-
let removed = 0;
|
|
111
|
-
for(let i = this._instances.length - 1; i >= 0; i--){
|
|
112
|
-
const instance = this._instances[i];
|
|
113
|
-
if (instance.x >= minX && instance.x <= maxX && instance.y >= minZ && instance.y <= maxZ) {
|
|
114
|
-
this._instances.splice(i, 1);
|
|
115
|
-
removed++;
|
|
116
|
-
if (removed === num) {
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (removed > 0) {
|
|
122
|
-
this.updateBuffers();
|
|
104
|
+
this._primitive.get().drawInstanced(this._numInstances);
|
|
123
105
|
}
|
|
124
|
-
return removed;
|
|
125
|
-
}
|
|
126
|
-
addInstances(instances) {
|
|
127
|
-
this._instances.push(...instances);
|
|
128
|
-
this.updateBuffers();
|
|
129
106
|
}
|
|
130
107
|
onDispose() {
|
|
131
108
|
super.onDispose();
|
|
@@ -137,34 +114,53 @@ const INSTANCE_BYTES = 4 * 4;
|
|
|
137
114
|
}
|
|
138
115
|
/**
|
|
139
116
|
* Grass layer class
|
|
117
|
+
*
|
|
140
118
|
* @public
|
|
141
119
|
*/ class GrassLayer extends Disposable {
|
|
142
120
|
static _indexBuffer = new DRef();
|
|
121
|
+
static _cullAABB = new AABB();
|
|
122
|
+
static _visibleTiles = [];
|
|
123
|
+
static _instanceData = new Float32Array(TILE_CELLS * TILE_CELLS * 4);
|
|
143
124
|
_material;
|
|
144
|
-
|
|
125
|
+
_seed;
|
|
145
126
|
_bladeWidth;
|
|
146
127
|
_bladeHeight;
|
|
147
128
|
_baseVertexBuffer;
|
|
129
|
+
_densityWidth;
|
|
130
|
+
_densityHeight;
|
|
131
|
+
_cellsPerTexel;
|
|
132
|
+
_densityMap;
|
|
133
|
+
_tiles;
|
|
134
|
+
_tilesX;
|
|
135
|
+
_tilesZ;
|
|
136
|
+
_numBlades;
|
|
148
137
|
/**
|
|
149
138
|
* Creates an instance of GrassLayer
|
|
150
139
|
* @param terrain - Clipmap terrain object
|
|
140
|
+
* @param seed - Seed for deterministic blade placement, usually the layer index
|
|
151
141
|
* @param bladeWidth - Grass blade width
|
|
152
142
|
* @param bladeHeight - Grass blade height
|
|
153
143
|
* @param albedoMap - Albedo texture for the blade
|
|
154
|
-
*/ constructor(terrain, bladeWidth, bladeHeight, albedoMap){
|
|
144
|
+
*/ constructor(terrain, seed, bladeWidth, bladeHeight, albedoMap){
|
|
155
145
|
super();
|
|
156
146
|
this._material = new DRef(new ClipmapGrassMaterial(terrain));
|
|
157
147
|
this._material.get().albedoTexture = albedoMap ?? null;
|
|
158
148
|
if (albedoMap) {
|
|
159
149
|
this._material.get().setTextureSize(albedoMap.width, albedoMap.height);
|
|
160
150
|
}
|
|
151
|
+
this._seed = seed;
|
|
161
152
|
this._bladeWidth = bladeWidth;
|
|
162
153
|
this._bladeHeight = bladeHeight;
|
|
163
154
|
this._baseVertexBuffer = new DRef(this.createBaseVertexBuffer(this._bladeWidth, this._bladeHeight));
|
|
164
|
-
this.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
155
|
+
this._densityWidth = Math.max(1, terrain.sizeX);
|
|
156
|
+
this._densityHeight = Math.max(1, terrain.sizeZ);
|
|
157
|
+
this._cellsPerTexel = DEFAULT_CELLS_PER_TEXEL;
|
|
158
|
+
this._densityMap = new Uint8Array(this._densityWidth * this._densityHeight);
|
|
159
|
+
this._tiles = new Map();
|
|
160
|
+
this._tilesX = 0;
|
|
161
|
+
this._tilesZ = 0;
|
|
162
|
+
this._numBlades = 0;
|
|
163
|
+
this.updateTileGrid();
|
|
168
164
|
}
|
|
169
165
|
/** @internal */ updateMaterial() {
|
|
170
166
|
this._material.get().uniformChanged();
|
|
@@ -184,21 +180,93 @@ const INSTANCE_BYTES = 4 * 4;
|
|
|
184
180
|
*/ getAlbedoMap() {
|
|
185
181
|
return this._material.get().albedoTexture;
|
|
186
182
|
}
|
|
183
|
+
/** How many grass blades are currently generated in this layer */ get numBlades() {
|
|
184
|
+
return this._numBlades;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Density map of this layer, one byte per texel (0 = no grass, 255 = full density).
|
|
188
|
+
* Texel (x, z) covers the normalized terrain region [x/w..(x+1)/w, z/h..(z+1)/h].
|
|
189
|
+
* After modifying the data, call {@link GrassLayer.updateDensityRegion} to
|
|
190
|
+
* regenerate the affected blade instances.
|
|
191
|
+
*/ get densityMap() {
|
|
192
|
+
return this._densityMap;
|
|
193
|
+
}
|
|
194
|
+
/** Width of the density map in texels */ get densityMapWidth() {
|
|
195
|
+
return this._densityWidth;
|
|
196
|
+
}
|
|
197
|
+
/** Height of the density map in texels */ get densityMapHeight() {
|
|
198
|
+
return this._densityHeight;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Number of placement cells per density map texel along each axis.
|
|
202
|
+
* The maximum blade count per texel is the square of this value.
|
|
203
|
+
*/ get cellsPerTexel() {
|
|
204
|
+
return this._cellsPerTexel;
|
|
205
|
+
}
|
|
206
|
+
set cellsPerTexel(val) {
|
|
207
|
+
val = Math.max(1, Math.min(MAX_CELLS_PER_TEXEL, val | 0));
|
|
208
|
+
if (val !== this._cellsPerTexel) {
|
|
209
|
+
this._cellsPerTexel = val;
|
|
210
|
+
this.rebuild();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Replaces the density map and regenerates all blade instances
|
|
215
|
+
* @param width - Density map width in texels
|
|
216
|
+
* @param height - Density map height in texels
|
|
217
|
+
* @param cellsPerTexel - Placement cells per texel along each axis
|
|
218
|
+
* @param data - Density data, one byte per texel
|
|
219
|
+
*/ setDensityData(width, height, cellsPerTexel, data) {
|
|
220
|
+
if (width < 1 || height < 1 || width * height !== data.length) {
|
|
221
|
+
console.error('Invalid grass density data');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this._densityWidth = width;
|
|
225
|
+
this._densityHeight = height;
|
|
226
|
+
this._cellsPerTexel = Math.max(1, Math.min(MAX_CELLS_PER_TEXEL, cellsPerTexel | 0));
|
|
227
|
+
this._densityMap = data;
|
|
228
|
+
this.rebuild();
|
|
229
|
+
}
|
|
187
230
|
/**
|
|
188
|
-
*
|
|
189
|
-
* @param
|
|
190
|
-
|
|
191
|
-
|
|
231
|
+
* Regenerates blade instances for all tiles overlapping a density map region
|
|
232
|
+
* @param minTexelX - Minimum x texel of the region (inclusive)
|
|
233
|
+
* @param minTexelZ - Minimum z texel of the region (inclusive)
|
|
234
|
+
* @param maxTexelX - Maximum x texel of the region (exclusive)
|
|
235
|
+
* @param maxTexelZ - Maximum z texel of the region (exclusive)
|
|
236
|
+
*/ updateDensityRegion(minTexelX, minTexelZ, maxTexelX, maxTexelZ) {
|
|
237
|
+
const k = this._cellsPerTexel;
|
|
238
|
+
// expand by one texel to cover the bilinear sampling footprint
|
|
239
|
+
const cx0 = Math.max(0, (minTexelX - 1) * k);
|
|
240
|
+
const cz0 = Math.max(0, (minTexelZ - 1) * k);
|
|
241
|
+
const cx1 = Math.min(this._densityWidth * k, (maxTexelX + 1) * k);
|
|
242
|
+
const cz1 = Math.min(this._densityHeight * k, (maxTexelZ + 1) * k);
|
|
243
|
+
if (cx1 <= cx0 || cz1 <= cz0) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const tx0 = Math.floor(cx0 / TILE_CELLS);
|
|
247
|
+
const tz0 = Math.floor(cz0 / TILE_CELLS);
|
|
248
|
+
const tx1 = Math.min(this._tilesX, Math.ceil(cx1 / TILE_CELLS));
|
|
249
|
+
const tz1 = Math.min(this._tilesZ, Math.ceil(cz1 / TILE_CELLS));
|
|
250
|
+
for(let tz = tz0; tz < tz1; tz++){
|
|
251
|
+
for(let tx = tx0; tx < tx1; tx++){
|
|
252
|
+
this.generateTile(tx, tz);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
192
255
|
}
|
|
193
256
|
/**
|
|
194
|
-
*
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
this.
|
|
257
|
+
* Regenerates blade instances for the entire layer
|
|
258
|
+
*/ rebuild() {
|
|
259
|
+
for (const tile of this._tiles.values()){
|
|
260
|
+
tile.dispose();
|
|
261
|
+
}
|
|
262
|
+
this._tiles.clear();
|
|
263
|
+
this._numBlades = 0;
|
|
264
|
+
this.updateTileGrid();
|
|
265
|
+
for(let tz = 0; tz < this._tilesZ; tz++){
|
|
266
|
+
for(let tx = 0; tx < this._tilesX; tx++){
|
|
267
|
+
this.generateTile(tx, tz);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
202
270
|
}
|
|
203
271
|
/** Grass blade width in this layer */ get bladeWidth() {
|
|
204
272
|
return this._bladeWidth;
|
|
@@ -221,7 +289,92 @@ const INSTANCE_BYTES = 4 * 4;
|
|
|
221
289
|
this._bladeWidth = width;
|
|
222
290
|
this._bladeHeight = height;
|
|
223
291
|
this._baseVertexBuffer.set(this.createBaseVertexBuffer(this._bladeWidth, this._bladeHeight));
|
|
224
|
-
|
|
292
|
+
for (const tile of this._tiles.values()){
|
|
293
|
+
tile.setBaseVertexBuffer(this._baseVertexBuffer.get());
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** @internal */ updateTileGrid() {
|
|
298
|
+
this._tilesX = Math.ceil(this._densityWidth * this._cellsPerTexel / TILE_CELLS);
|
|
299
|
+
this._tilesZ = Math.ceil(this._densityHeight * this._cellsPerTexel / TILE_CELLS);
|
|
300
|
+
}
|
|
301
|
+
/** @internal */ sampleDensity(u, v) {
|
|
302
|
+
const w = this._densityWidth;
|
|
303
|
+
const h = this._densityHeight;
|
|
304
|
+
const x = u * w - 0.5;
|
|
305
|
+
const z = v * h - 0.5;
|
|
306
|
+
const ix = Math.floor(x);
|
|
307
|
+
const iz = Math.floor(z);
|
|
308
|
+
const fx = x - ix;
|
|
309
|
+
const fz = z - iz;
|
|
310
|
+
const x0 = Math.min(Math.max(ix, 0), w - 1);
|
|
311
|
+
const x1 = Math.min(Math.max(ix + 1, 0), w - 1);
|
|
312
|
+
const z0 = Math.min(Math.max(iz, 0), h - 1);
|
|
313
|
+
const z1 = Math.min(Math.max(iz + 1, 0), h - 1);
|
|
314
|
+
const d = this._densityMap;
|
|
315
|
+
const a = d[z0 * w + x0] + (d[z0 * w + x1] - d[z0 * w + x0]) * fx;
|
|
316
|
+
const b = d[z1 * w + x0] + (d[z1 * w + x1] - d[z1 * w + x0]) * fx;
|
|
317
|
+
return (a + (b - a) * fz) * (1 / 255);
|
|
318
|
+
}
|
|
319
|
+
/** @internal */ regionIsEmpty(cx0, cz0, cx1, cz1) {
|
|
320
|
+
const k = this._cellsPerTexel;
|
|
321
|
+
const x0 = Math.max(0, Math.floor(cx0 / k) - 1);
|
|
322
|
+
const z0 = Math.max(0, Math.floor(cz0 / k) - 1);
|
|
323
|
+
const x1 = Math.min(this._densityWidth, Math.ceil(cx1 / k) + 1);
|
|
324
|
+
const z1 = Math.min(this._densityHeight, Math.ceil(cz1 / k) + 1);
|
|
325
|
+
for(let z = z0; z < z1; z++){
|
|
326
|
+
const rowBase = z * this._densityWidth;
|
|
327
|
+
for(let x = x0; x < x1; x++){
|
|
328
|
+
if (this._densityMap[rowBase + x] !== 0) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
/** @internal */ generateTile(tx, tz) {
|
|
336
|
+
const k = this._cellsPerTexel;
|
|
337
|
+
const cw = this._densityWidth * k;
|
|
338
|
+
const ch = this._densityHeight * k;
|
|
339
|
+
const cx0 = tx * TILE_CELLS;
|
|
340
|
+
const cz0 = tz * TILE_CELLS;
|
|
341
|
+
const cx1 = Math.min(cx0 + TILE_CELLS, cw);
|
|
342
|
+
const cz1 = Math.min(cz0 + TILE_CELLS, ch);
|
|
343
|
+
const key = tz * this._tilesX + tx;
|
|
344
|
+
let tile = this._tiles.get(key);
|
|
345
|
+
// fast path: nothing to generate if there is no tile yet and no density
|
|
346
|
+
if (!tile && this.regionIsEmpty(cx0, cz0, cx1, cz1)) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const data = GrassLayer._instanceData;
|
|
350
|
+
const seed = this._seed * 4;
|
|
351
|
+
let count = 0;
|
|
352
|
+
for(let cz = cz0; cz < cz1; cz++){
|
|
353
|
+
for(let cx = cx0; cx < cx1; cx++){
|
|
354
|
+
const u = (cx + hashCell(cx, cz, seed)) / cw;
|
|
355
|
+
const v = (cz + hashCell(cx, cz, seed + 1)) / ch;
|
|
356
|
+
if (this.sampleDensity(u, v) > hashCell(cx, cz, seed + 2)) {
|
|
357
|
+
const angle = hashCell(cx, cz, seed + 3) * Math.PI * 2;
|
|
358
|
+
data[count * 4 + 0] = u;
|
|
359
|
+
data[count * 4 + 1] = v;
|
|
360
|
+
data[count * 4 + 2] = Math.sin(angle);
|
|
361
|
+
data[count * 4 + 3] = Math.cos(angle);
|
|
362
|
+
count++;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
this._numBlades += count - (tile?.numInstances ?? 0);
|
|
367
|
+
if (count === 0) {
|
|
368
|
+
if (tile) {
|
|
369
|
+
tile.dispose();
|
|
370
|
+
this._tiles.delete(key);
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
if (!tile) {
|
|
374
|
+
tile = new GrassInstances(this._baseVertexBuffer.get(), GrassLayer._getIndexBuffer());
|
|
375
|
+
this._tiles.set(key, tile);
|
|
376
|
+
}
|
|
377
|
+
tile.setData(data, count);
|
|
225
378
|
}
|
|
226
379
|
}
|
|
227
380
|
/** @internal */ static _getIndexBuffer() {
|
|
@@ -323,16 +476,49 @@ const INSTANCE_BYTES = 4 * 4;
|
|
|
323
476
|
], vertices);
|
|
324
477
|
}
|
|
325
478
|
/** @internal */ draw(ctx, region, minY, maxY) {
|
|
479
|
+
const visible = GrassLayer._visibleTiles;
|
|
480
|
+
visible.length = 0;
|
|
481
|
+
const camera = ctx.camera;
|
|
482
|
+
const cullAABB = GrassLayer._cullAABB;
|
|
483
|
+
const cellsW = this._densityWidth * this._cellsPerTexel;
|
|
484
|
+
const cellsH = this._densityHeight * this._cellsPerTexel;
|
|
485
|
+
const rx = region.x;
|
|
486
|
+
const rz = region.y;
|
|
487
|
+
const rw = region.z - region.x;
|
|
488
|
+
const rh = region.w - region.y;
|
|
489
|
+
for (const [key, tile] of this._tiles){
|
|
490
|
+
const tx = key % this._tilesX;
|
|
491
|
+
const tz = (key - tx) / this._tilesX;
|
|
492
|
+
const u0 = tx * TILE_CELLS / cellsW;
|
|
493
|
+
const v0 = tz * TILE_CELLS / cellsH;
|
|
494
|
+
const u1 = Math.min(1, (tx + 1) * TILE_CELLS / cellsW);
|
|
495
|
+
const v1 = Math.min(1, (tz + 1) * TILE_CELLS / cellsH);
|
|
496
|
+
cullAABB.minPoint.setXYZ(rx + u0 * rw, minY, rz + v0 * rh);
|
|
497
|
+
cullAABB.maxPoint.setXYZ(rx + u1 * rw, maxY, rz + v1 * rh);
|
|
498
|
+
const clipState = camera.clipMask ? cullAABB.getClipStateWithFrustumMask(camera.frustum, camera.clipMask) : cullAABB.getClipStateWithFrustum(camera.frustum);
|
|
499
|
+
if (clipState !== ClipState.NOT_CLIPPED) {
|
|
500
|
+
visible.push(tile);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (visible.length === 0) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
326
506
|
this._material.get().apply(ctx);
|
|
327
507
|
for(let pass = 0; pass < this._material.get().numPasses; pass++){
|
|
328
508
|
this._material.get().bind(ctx.device, pass);
|
|
329
|
-
|
|
509
|
+
for (const tile of visible){
|
|
510
|
+
tile.draw();
|
|
511
|
+
}
|
|
330
512
|
}
|
|
513
|
+
visible.length = 0;
|
|
331
514
|
}
|
|
332
515
|
/** @internal */ onDispose() {
|
|
333
516
|
super.onDispose();
|
|
334
517
|
this._material.dispose();
|
|
335
|
-
this.
|
|
518
|
+
for (const tile of this._tiles.values()){
|
|
519
|
+
tile.dispose();
|
|
520
|
+
}
|
|
521
|
+
this._tiles.clear();
|
|
336
522
|
this._baseVertexBuffer.dispose();
|
|
337
523
|
}
|
|
338
524
|
}
|
|
@@ -341,7 +527,6 @@ const INSTANCE_BYTES = 4 * 4;
|
|
|
341
527
|
* @public
|
|
342
528
|
*/ class GrassRenderer extends Disposable {
|
|
343
529
|
_terrain;
|
|
344
|
-
_numGrassBlades;
|
|
345
530
|
_layers;
|
|
346
531
|
/**
|
|
347
532
|
* Creates an instance of GrassRenderer
|
|
@@ -349,7 +534,6 @@ const INSTANCE_BYTES = 4 * 4;
|
|
|
349
534
|
*/ constructor(terrain){
|
|
350
535
|
super();
|
|
351
536
|
this._terrain = new DWeakRef(terrain);
|
|
352
|
-
this._numGrassBlades = 0;
|
|
353
537
|
this._layers = [];
|
|
354
538
|
}
|
|
355
539
|
/** @internal */ updateMaterial() {
|
|
@@ -358,7 +542,7 @@ const INSTANCE_BYTES = 4 * 4;
|
|
|
358
542
|
}
|
|
359
543
|
}
|
|
360
544
|
/** How many grass blades */ get numGrassBlades() {
|
|
361
|
-
return this.
|
|
545
|
+
return this._layers.reduce((sum, layer)=>sum + layer.numBlades, 0);
|
|
362
546
|
}
|
|
363
547
|
/** How many grass layers */ get numLayers() {
|
|
364
548
|
return this._layers.length;
|
|
@@ -377,7 +561,7 @@ const INSTANCE_BYTES = 4 * 4;
|
|
|
377
561
|
* @param albedoMap - Albedo texture of grass blades in this layer
|
|
378
562
|
* @returns Index of the added grass layer
|
|
379
563
|
*/ addLayer(bladeWidth, bladeHeight, albedoMap) {
|
|
380
|
-
const layer = new GrassLayer(this._terrain.get(), bladeWidth, bladeHeight, albedoMap);
|
|
564
|
+
const layer = new GrassLayer(this._terrain.get(), this._layers.length, bladeWidth, bladeHeight, albedoMap);
|
|
381
565
|
this._layers.push(layer);
|
|
382
566
|
return this._layers.length - 1;
|
|
383
567
|
}
|
|
@@ -427,34 +611,6 @@ const INSTANCE_BYTES = 4 * 4;
|
|
|
427
611
|
console.error(`Invalid grass layer: ${layer}`);
|
|
428
612
|
}
|
|
429
613
|
}
|
|
430
|
-
/**
|
|
431
|
-
* Add grass instances to the grass layer at given index
|
|
432
|
-
* @param layer - Index of the grass layer
|
|
433
|
-
* @param instances - Grass blade instances to add
|
|
434
|
-
*/ addInstances(layer, instances) {
|
|
435
|
-
const grassLayer = this._layers[layer];
|
|
436
|
-
if (!grassLayer) {
|
|
437
|
-
console.error(`Invalid grass layer: ${layer}`);
|
|
438
|
-
} else {
|
|
439
|
-
this._numGrassBlades += grassLayer.addInstances(instances);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* Remove grass instances from the grass layer at given index within a region
|
|
444
|
-
* @param layer - Index of the grass layer
|
|
445
|
-
* @param minX - Minimum x of the region
|
|
446
|
-
* @param minZ - Minimum z of the region
|
|
447
|
-
* @param maxX - Maximum x of the region
|
|
448
|
-
* @param maxZ - Maximum z of the region
|
|
449
|
-
* @param num - How many grass blades to remove
|
|
450
|
-
*/ removeInstances(layer, minX, minZ, maxX, maxZ, num) {
|
|
451
|
-
const grassLayer = this._layers[layer];
|
|
452
|
-
if (!grassLayer) {
|
|
453
|
-
console.error(`Invalid grass layer: ${layer}`);
|
|
454
|
-
} else {
|
|
455
|
-
grassLayer.removeInstances(minX, minZ, maxX, maxZ, num);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
614
|
/** @internal */ draw(ctx) {
|
|
459
615
|
const bv = this._terrain.get().getWorldBoundingVolume().toAABB();
|
|
460
616
|
const minY = bv.minPoint.y;
|
|
@@ -471,141 +627,6 @@ const INSTANCE_BYTES = 4 * 4;
|
|
|
471
627
|
}
|
|
472
628
|
}
|
|
473
629
|
}
|
|
474
|
-
/** @internal */ class GrassQuadtreeNode extends Disposable {
|
|
475
|
-
static _cullAABB = new AABB();
|
|
476
|
-
_grassInstances;
|
|
477
|
-
_children;
|
|
478
|
-
_baseVertexBuffer;
|
|
479
|
-
_indexBuffer;
|
|
480
|
-
_minX;
|
|
481
|
-
_minZ;
|
|
482
|
-
_maxX;
|
|
483
|
-
_maxZ;
|
|
484
|
-
constructor(baseVertexBuffer, indexBuffer){
|
|
485
|
-
super();
|
|
486
|
-
this._baseVertexBuffer = new DRef(baseVertexBuffer);
|
|
487
|
-
this._indexBuffer = new DRef(indexBuffer);
|
|
488
|
-
this._grassInstances = new DRef(new GrassInstances(baseVertexBuffer, indexBuffer));
|
|
489
|
-
this._children = null;
|
|
490
|
-
this._minX = 0;
|
|
491
|
-
this._minZ = 0;
|
|
492
|
-
this._maxX = 1;
|
|
493
|
-
this._maxZ = 1;
|
|
494
|
-
}
|
|
495
|
-
get grassInstances() {
|
|
496
|
-
return this._grassInstances.get();
|
|
497
|
-
}
|
|
498
|
-
get children() {
|
|
499
|
-
return this._children;
|
|
500
|
-
}
|
|
501
|
-
draw(camera, region, minY, maxY, skipClipTest) {
|
|
502
|
-
if (!skipClipTest) {
|
|
503
|
-
const cullAABB = GrassQuadtreeNode._cullAABB;
|
|
504
|
-
const x = region.x;
|
|
505
|
-
const z = region.y;
|
|
506
|
-
const dx = region.z - x;
|
|
507
|
-
const dz = region.w - z;
|
|
508
|
-
cullAABB.minPoint.setXYZ(x + this._minX * dx, minY, z + this._minZ * dz);
|
|
509
|
-
cullAABB.maxPoint.setXYZ(x + this._maxX * dx, maxY, z + this._maxZ * dz);
|
|
510
|
-
const clipState = camera.clipMask ? cullAABB.getClipStateWithFrustumMask(camera.frustum, camera.clipMask) : cullAABB.getClipStateWithFrustum(camera.frustum);
|
|
511
|
-
if (clipState === ClipState.NOT_CLIPPED) {
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
skipClipTest = clipState === ClipState.A_INSIDE_B;
|
|
515
|
-
}
|
|
516
|
-
if (this._grassInstances.get().numInstances > 0) {
|
|
517
|
-
this._grassInstances.get().draw();
|
|
518
|
-
}
|
|
519
|
-
if (this._children) {
|
|
520
|
-
for (const child of this._children){
|
|
521
|
-
child.draw(camera, region, minY, maxY, skipClipTest);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
setBaseVertexBuffer(baseVertexBuffer) {
|
|
526
|
-
if (baseVertexBuffer !== this._baseVertexBuffer.get()) {
|
|
527
|
-
this._baseVertexBuffer.set(baseVertexBuffer);
|
|
528
|
-
this._grassInstances.get().setBaseVertexBuffer(baseVertexBuffer);
|
|
529
|
-
if (this._children) {
|
|
530
|
-
for (const child of this._children){
|
|
531
|
-
child.setBaseVertexBuffer(baseVertexBuffer);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
removeInstances(minX, minZ, maxX, maxZ, numInstances) {
|
|
537
|
-
let n = Math.min(this._grassInstances.get().numInstances, numInstances);
|
|
538
|
-
if (n <= 0) {
|
|
539
|
-
return 0;
|
|
540
|
-
}
|
|
541
|
-
let removed = 0;
|
|
542
|
-
if (this._children) {
|
|
543
|
-
for (const child of this._children){
|
|
544
|
-
if (child._minX < maxX && child._minZ < maxZ && child._maxX > minX && child._maxZ > minZ) {
|
|
545
|
-
removed += child.removeInstances(minX, minZ, maxX, maxZ, n);
|
|
546
|
-
n -= removed;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
if (n > 0) {
|
|
551
|
-
removed += this._grassInstances.get().removeInstances(minX, minZ, maxX, maxZ, n);
|
|
552
|
-
}
|
|
553
|
-
return removed;
|
|
554
|
-
}
|
|
555
|
-
addInstances(instances) {
|
|
556
|
-
if (instances.length === 0) {
|
|
557
|
-
return 0;
|
|
558
|
-
}
|
|
559
|
-
let n = Math.min(instances.length, MAX_INSTANCES_PER_NODE - this._grassInstances.get().numInstances);
|
|
560
|
-
this._grassInstances.get().addInstances(instances.slice(0, n));
|
|
561
|
-
if (n < instances.length) {
|
|
562
|
-
if (!this._children) {
|
|
563
|
-
this._children = [
|
|
564
|
-
new GrassQuadtreeNode(this._baseVertexBuffer.get(), this._indexBuffer.get()),
|
|
565
|
-
new GrassQuadtreeNode(this._baseVertexBuffer.get(), this._indexBuffer.get()),
|
|
566
|
-
new GrassQuadtreeNode(this._baseVertexBuffer.get(), this._indexBuffer.get()),
|
|
567
|
-
new GrassQuadtreeNode(this._baseVertexBuffer.get(), this._indexBuffer.get())
|
|
568
|
-
];
|
|
569
|
-
this._children[0]._minX = this._minX;
|
|
570
|
-
this._children[0]._minZ = this._minZ;
|
|
571
|
-
this._children[0]._maxX = (this._minX + this._maxX) * 0.5;
|
|
572
|
-
this._children[0]._maxZ = (this._minZ + this._maxZ) * 0.5;
|
|
573
|
-
this._children[1]._minX = this._children[0]._maxX;
|
|
574
|
-
this._children[1]._minZ = this._minZ;
|
|
575
|
-
this._children[1]._maxX = this._maxX;
|
|
576
|
-
this._children[1]._maxZ = this._children[0]._maxZ;
|
|
577
|
-
this._children[2]._minX = this._minX;
|
|
578
|
-
this._children[2]._minZ = this._children[0]._maxZ;
|
|
579
|
-
this._children[2]._maxX = this._children[0]._maxX;
|
|
580
|
-
this._children[2]._maxZ = this._maxZ;
|
|
581
|
-
this._children[3]._minX = this._children[0]._maxX;
|
|
582
|
-
this._children[3]._minZ = this._children[0]._maxZ;
|
|
583
|
-
this._children[3]._maxX = this._maxX;
|
|
584
|
-
this._children[3]._maxZ = this._maxZ;
|
|
585
|
-
}
|
|
586
|
-
instances = instances.slice(n);
|
|
587
|
-
for (const child of this._children){
|
|
588
|
-
if (instances.length > 0) {
|
|
589
|
-
const childInstances = instances.filter((val)=>val.x >= child._minX && val.y >= child._minZ && val.x < child._maxX && val.y < child._maxZ);
|
|
590
|
-
n += child.addInstances(childInstances);
|
|
591
|
-
instances = instances.filter((val)=>!(val.x >= child._minX && val.y >= child._minZ && val.x < child._maxX && val.y < child._maxZ));
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
return n;
|
|
596
|
-
}
|
|
597
|
-
onDispose() {
|
|
598
|
-
super.onDispose();
|
|
599
|
-
this._baseVertexBuffer.dispose();
|
|
600
|
-
this._indexBuffer.dispose();
|
|
601
|
-
this._grassInstances.dispose();
|
|
602
|
-
if (this._children) {
|
|
603
|
-
for (const child of this._children){
|
|
604
|
-
child.dispose();
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
630
|
|
|
610
|
-
export { GrassInstances, GrassLayer,
|
|
631
|
+
export { GrassInstances, GrassLayer, GrassRenderer };
|
|
611
632
|
//# sourceMappingURL=grass.js.map
|