@call-me-sensei/toonlab 0.1.1
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/ATTRIBUTION.md +40 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/package.json +97 -0
- package/src/character/characterRig.js +353 -0
- package/src/character/freestyleSwimClip.js +541 -0
- package/src/character/index.js +4 -0
- package/src/character/modelLoader.js +433 -0
- package/src/core/materialRoles.js +438 -0
- package/src/core/presetDocuments.js +110 -0
- package/src/core/shaderBackend.js +24 -0
- package/src/debrisgen/debrisFields.js +544 -0
- package/src/debrisgen/debrisGenerator.js +1929 -0
- package/src/debrisgen/debrisPalettes.js +71 -0
- package/src/debrisgen/debrisPhysics.js +198 -0
- package/src/debrisgen/debrisPresets.js +155 -0
- package/src/debrisgen/debrisSettings.js +333 -0
- package/src/debrisgen/debrisTextures.js +380 -0
- package/src/debrisgen/index.js +5 -0
- package/src/debug/fieldValues.js +95 -0
- package/src/debug/index.js +7 -0
- package/src/debug/settingsPanel.js +170 -0
- package/src/environment/environmentAmbientProbe.js +160 -0
- package/src/environment/environmentMaterialAdapter.js +336 -0
- package/src/environment/environmentMaterialClassifier.js +178 -0
- package/src/environment/environmentPlanarReflection.js +201 -0
- package/src/environment/environmentPresets.js +443 -0
- package/src/environment/environmentRigs.js +684 -0
- package/src/environment/environmentSettings.js +504 -0
- package/src/environment/environmentShaderMaterials.js +318 -0
- package/src/environment/environmentSunShadowPass.js +268 -0
- package/src/environment/environmentTextureResolver.js +161 -0
- package/src/environment/environmentTimeOfDay.js +167 -0
- package/src/environment/environmentVertexAo.js +188 -0
- package/src/environment/index.js +8 -0
- package/src/environment/scanAssetStylize.js +103 -0
- package/src/index.js +21 -0
- package/src/loaders/index.js +1 -0
- package/src/post/index.js +2 -0
- package/src/post/postProcessing.js +1087 -0
- package/src/rockgen/export/glbExport.js +177 -0
- package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
- package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
- package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
- package/src/rockgen/index.js +14 -0
- package/src/rockgen/mesh/meshAttributes.js +374 -0
- package/src/rockgen/mesh/meshDocument.js +158 -0
- package/src/rockgen/mesh/surfaceNets.js +329 -0
- package/src/rockgen/noise/cellularNoise3.js +92 -0
- package/src/rockgen/noise/prng.js +62 -0
- package/src/rockgen/noise/simplexNoise3.js +89 -0
- package/src/rockgen/noise/valueNoise3.js +87 -0
- package/src/rockgen/rockDocument.js +255 -0
- package/src/rockgen/rockHelpers.js +8 -0
- package/src/rockgen/rockgenPresets.js +559 -0
- package/src/rockgen/rockgenSettings.js +756 -0
- package/src/rockgen/sdf/fieldCompiler.js +544 -0
- package/src/rockgen/sdf/sculptEdits.js +66 -0
- package/src/rockgen/sdf/sdfModifiers.js +45 -0
- package/src/rockgen/sdf/sdfOps.js +37 -0
- package/src/rockgen/sdf/sdfPrimitives.js +88 -0
- package/src/shaders-tsl/anime.js +963 -0
- package/src/shaders-tsl/chunks/character-color.js +64 -0
- package/src/shaders-tsl/chunks/character-highlights.js +174 -0
- package/src/shaders-tsl/chunks/character-lighting.js +309 -0
- package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
- package/src/shaders-tsl/chunks/character-roles.js +52 -0
- package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
- package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
- package/src/shaders-tsl/chunks/character-skinning.js +140 -0
- package/src/shaders-tsl/chunks/environment-color.js +47 -0
- package/src/shaders-tsl/chunks/environment-debug.js +79 -0
- package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
- package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
- package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
- package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
- package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
- package/src/shaders-tsl/chunks/water-color.js +190 -0
- package/src/shaders-tsl/chunks/water-common.js +124 -0
- package/src/shaders-tsl/chunks/water-foam.js +93 -0
- package/src/shaders-tsl/chunks/water-lighting.js +116 -0
- package/src/shaders-tsl/chunks/water-ripple.js +64 -0
- package/src/shaders-tsl/chunks/water-waves.js +100 -0
- package/src/shaders-tsl/environment-ao-overlay.js +60 -0
- package/src/shaders-tsl/environment.js +680 -0
- package/src/shaders-tsl/flower.js +267 -0
- package/src/shaders-tsl/grass.js +193 -0
- package/src/shaders-tsl/post-composite.js +461 -0
- package/src/shaders-tsl/sky.js +175 -0
- package/src/shaders-tsl/tree-leaf.js +264 -0
- package/src/shaders-tsl/water-breaker.js +423 -0
- package/src/shaders-tsl/water-kelp.js +171 -0
- package/src/shaders-tsl/water-rain.js +107 -0
- package/src/shaders-tsl/water-simulation.js +140 -0
- package/src/shaders-tsl/water-splash.js +298 -0
- package/src/shaders-tsl/water.js +509 -0
- package/src/sky/index.js +2 -0
- package/src/sky/stylizedSky.js +341 -0
- package/src/toon/characterRenderPasses.js +646 -0
- package/src/toon/index.js +4 -0
- package/src/toon/settings/alphaSettings.js +170 -0
- package/src/toon/settings/averageShadowSettings.js +181 -0
- package/src/toon/settings/baseTextureSettings.js +133 -0
- package/src/toon/settings/celShadeSettings.js +96 -0
- package/src/toon/settings/contactShadowSettings.js +114 -0
- package/src/toon/settings/eyeHighlightSettings.js +187 -0
- package/src/toon/settings/faceLightingSettings.js +135 -0
- package/src/toon/settings/furSettings.js +112 -0
- package/src/toon/settings/glitterSettings.js +102 -0
- package/src/toon/settings/hairHighlightSettings.js +325 -0
- package/src/toon/settings/indirectLightSettings.js +244 -0
- package/src/toon/settings/localLightSettings.js +176 -0
- package/src/toon/settings/materialMapSettings.js +299 -0
- package/src/toon/settings/outlineSettings.js +263 -0
- package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
- package/src/toon/settings/rimLightSettings.js +268 -0
- package/src/toon/settings/sceneShadowSettings.js +147 -0
- package/src/toon/settings/selfShadowSettings.js +221 -0
- package/src/toon/settings/shadowColorSettings.js +238 -0
- package/src/toon/settings/skinToneSettings.js +150 -0
- package/src/toon/settings/specularSettings.js +314 -0
- package/src/toon/settings/stickerSettings.js +102 -0
- package/src/toon/toonMaterialAdapter.js +2235 -0
- package/src/toon/toonSettings.js +860 -0
- package/src/vegetation/flowerSpecies.js +260 -0
- package/src/vegetation/index.js +10 -0
- package/src/vegetation/stylizedBush.js +136 -0
- package/src/vegetation/stylizedFlower.js +213 -0
- package/src/vegetation/stylizedFlowers.js +297 -0
- package/src/vegetation/stylizedGrass.js +491 -0
- package/src/vegetation/stylizedTree.js +2449 -0
- package/src/vegetation/stylizedTreeFoliage.js +691 -0
- package/src/vegetation/treeExport.js +300 -0
- package/src/vegetation/treeRecipe.js +647 -0
- package/src/water/index.js +2 -0
- package/src/water/water.js +20 -0
- package/src/water/waterBreakerSystem.js +591 -0
- package/src/water/waterInteraction.js +193 -0
- package/src/water/waterMaterial.js +263 -0
- package/src/water/waterRain.js +136 -0
- package/src/water/waterRippleSimulation.js +227 -0
- package/src/water/waterScenePasses.js +415 -0
- package/src/water/waterSettings.js +1138 -0
- package/src/water/waterSplashSystem.js +251 -0
- package/src/water/waterSurface.js +475 -0
- package/src/water/waterVegetation.js +83 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
|
|
3
|
+
import { createGrassNodeMaterial } from '../shaders-tsl/grass.js';
|
|
4
|
+
|
|
5
|
+
const pushScratch = new THREE.Vector3();
|
|
6
|
+
|
|
7
|
+
function setSrgbColor(color, rgb) {
|
|
8
|
+
color.setRGB(rgb[0], rgb[1], rgb[2], THREE.SRGBColorSpace);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function cleanObject(value) {
|
|
12
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function finiteNumber(value, fallback, { min = -Infinity, max = Infinity } = {}) {
|
|
16
|
+
const number = Number(value);
|
|
17
|
+
if (!Number.isFinite(number)) return fallback;
|
|
18
|
+
return Math.min(max, Math.max(min, number));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function colorArray(value, fallback) {
|
|
22
|
+
if (value?.isColor) return [value.r, value.g, value.b];
|
|
23
|
+
if (Array.isArray(value) && value.length >= 3) {
|
|
24
|
+
const next = value.slice(0, 3).map(Number);
|
|
25
|
+
return next.every(Number.isFinite) ? next : fallback.slice();
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === 'number' || typeof value === 'string') {
|
|
28
|
+
try {
|
|
29
|
+
const color = new THREE.Color(value);
|
|
30
|
+
return [color.r, color.g, color.b];
|
|
31
|
+
} catch {
|
|
32
|
+
return fallback.slice();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return fallback.slice();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function vectorArray(value, fallback, size) {
|
|
39
|
+
const keys = ['x', 'y', 'z', 'w'];
|
|
40
|
+
const read = (index) => {
|
|
41
|
+
if (Array.isArray(value)) return Number(value[index]);
|
|
42
|
+
if (value && typeof value === 'object') return Number(value[keys[index]]);
|
|
43
|
+
return NaN;
|
|
44
|
+
};
|
|
45
|
+
const next = Array.from({ length: size }, (_, index) => read(index));
|
|
46
|
+
return next.every(Number.isFinite) ? next : fallback.slice(0, size);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Default grass-field settings. Every value equals the field's historical
|
|
51
|
+
* hardcoded/constructor default, so `new StylizedGrassField({ placements })`
|
|
52
|
+
* renders identically to previous releases.
|
|
53
|
+
*/
|
|
54
|
+
export const DEFAULT_GRASS_SETTINGS = Object.freeze({
|
|
55
|
+
backlitStrength: 0.3,
|
|
56
|
+
baseColor: Object.freeze([0.42, 0.68, 0.24]),
|
|
57
|
+
bladeHeightRange: Object.freeze([0.16, 0.42]),
|
|
58
|
+
bladeWidthRange: Object.freeze([0.05, 0.085]),
|
|
59
|
+
cloudShadowCoverage: 0.45,
|
|
60
|
+
cloudShadowScale: 0.012,
|
|
61
|
+
cloudShadowStrength: 0,
|
|
62
|
+
cloudShadowVelocity: Object.freeze([0.02, 0.006]),
|
|
63
|
+
gustFrequency: 0.35,
|
|
64
|
+
gustSpeed: 1.6,
|
|
65
|
+
pushRadius: 0.9,
|
|
66
|
+
shadowStrength: 0.9,
|
|
67
|
+
shadowTint: Object.freeze([0.42, 0.47, 0.62]),
|
|
68
|
+
skyColor: Object.freeze([0.62, 0.78, 0.95]),
|
|
69
|
+
sunColor: Object.freeze([1.0, 0.96, 0.84]),
|
|
70
|
+
sunDirection: Object.freeze([0.35, 0.72, 0.42]),
|
|
71
|
+
tipColor: Object.freeze([0.74, 0.9, 0.42]),
|
|
72
|
+
windDirection: Object.freeze([1, 0.3]),
|
|
73
|
+
windSpeed: 1.0,
|
|
74
|
+
windStrength: 0.16,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validates, clamps, and merges partial grass options over
|
|
79
|
+
* {@link DEFAULT_GRASS_SETTINGS}. Unknown keys are ignored; malformed values
|
|
80
|
+
* fall back to their defaults. `createGrassSettings()` deep-equals the
|
|
81
|
+
* defaults object.
|
|
82
|
+
*
|
|
83
|
+
* @param {Object} [options] Partial settings (legacy constructor options are
|
|
84
|
+
* the same flat shape, so they work unchanged).
|
|
85
|
+
* @returns {Object} A complete, plain grass settings object.
|
|
86
|
+
*/
|
|
87
|
+
export function createGrassSettings(options = {}) {
|
|
88
|
+
const source = cleanObject(options);
|
|
89
|
+
const base = DEFAULT_GRASS_SETTINGS;
|
|
90
|
+
return {
|
|
91
|
+
backlitStrength: finiteNumber(source.backlitStrength, base.backlitStrength, { min: 0 }),
|
|
92
|
+
baseColor: colorArray(source.baseColor, base.baseColor),
|
|
93
|
+
bladeHeightRange: vectorArray(source.bladeHeightRange, base.bladeHeightRange, 2),
|
|
94
|
+
bladeWidthRange: vectorArray(source.bladeWidthRange, base.bladeWidthRange, 2),
|
|
95
|
+
cloudShadowCoverage: finiteNumber(source.cloudShadowCoverage, base.cloudShadowCoverage, { min: 0, max: 1 }),
|
|
96
|
+
cloudShadowScale: finiteNumber(source.cloudShadowScale, base.cloudShadowScale, { min: 0.0001 }),
|
|
97
|
+
cloudShadowStrength: finiteNumber(source.cloudShadowStrength, base.cloudShadowStrength, { min: 0, max: 1 }),
|
|
98
|
+
cloudShadowVelocity: vectorArray(source.cloudShadowVelocity, base.cloudShadowVelocity, 2),
|
|
99
|
+
gustFrequency: finiteNumber(source.gustFrequency, base.gustFrequency, { min: 0 }),
|
|
100
|
+
gustSpeed: finiteNumber(source.gustSpeed, base.gustSpeed, { min: 0 }),
|
|
101
|
+
pushRadius: finiteNumber(source.pushRadius, base.pushRadius, { min: 0 }),
|
|
102
|
+
shadowStrength: finiteNumber(source.shadowStrength, base.shadowStrength, { min: 0, max: 1 }),
|
|
103
|
+
shadowTint: colorArray(source.shadowTint, base.shadowTint),
|
|
104
|
+
skyColor: colorArray(source.skyColor, base.skyColor),
|
|
105
|
+
sunColor: colorArray(source.sunColor, base.sunColor),
|
|
106
|
+
sunDirection: vectorArray(source.sunDirection, base.sunDirection, 3),
|
|
107
|
+
tipColor: colorArray(source.tipColor, base.tipColor),
|
|
108
|
+
windDirection: vectorArray(source.windDirection, base.windDirection, 2),
|
|
109
|
+
windSpeed: finiteNumber(source.windSpeed, base.windSpeed),
|
|
110
|
+
windStrength: finiteNumber(source.windStrength, base.windStrength, { min: 0 }),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Panel group metadata for the grass settings, in display order. Settings
|
|
116
|
+
* themselves stay flat; each group lists which flat keys it owns via
|
|
117
|
+
* {@link GRASS_SETTING_FIELD_SCHEMA}.
|
|
118
|
+
*/
|
|
119
|
+
export const GRASS_SETTING_GROUPS = Object.freeze([
|
|
120
|
+
Object.freeze({
|
|
121
|
+
description: 'Random blade dimensions baked into the instance attributes when the field is built. Construction-only.',
|
|
122
|
+
id: 'blades',
|
|
123
|
+
label: 'Blades',
|
|
124
|
+
}),
|
|
125
|
+
Object.freeze({
|
|
126
|
+
description: 'Per-blade wind sway and the traveling gust bands that ripple across the field.',
|
|
127
|
+
id: 'wind',
|
|
128
|
+
label: 'Wind',
|
|
129
|
+
}),
|
|
130
|
+
Object.freeze({
|
|
131
|
+
description: 'Blade palette and sun/sky response, including the backlit glow on blades between the camera and the sun.',
|
|
132
|
+
id: 'lighting',
|
|
133
|
+
label: 'Lighting',
|
|
134
|
+
}),
|
|
135
|
+
Object.freeze({
|
|
136
|
+
description: 'Scene-shadow darkening and the drifting procedural cloud shadows over the field.',
|
|
137
|
+
id: 'shadows',
|
|
138
|
+
label: 'Shadows',
|
|
139
|
+
}),
|
|
140
|
+
Object.freeze({
|
|
141
|
+
description: 'Character push-away response around the push target.',
|
|
142
|
+
id: 'interaction',
|
|
143
|
+
label: 'Interaction',
|
|
144
|
+
}),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
const GRASS_FIELD_DEFINITIONS = Object.freeze({
|
|
148
|
+
blades: {
|
|
149
|
+
bladeHeightRange: {
|
|
150
|
+
description: 'Min/max blade height in meters for placements without an explicit height. Construction-only: baked into instance attributes.',
|
|
151
|
+
label: 'Blade Height Range',
|
|
152
|
+
type: 'vector2',
|
|
153
|
+
},
|
|
154
|
+
bladeWidthRange: {
|
|
155
|
+
description: 'Min/max blade width in meters for placements without an explicit width. Construction-only: baked into instance attributes.',
|
|
156
|
+
label: 'Blade Width Range',
|
|
157
|
+
type: 'vector2',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
wind: {
|
|
161
|
+
windDirection: {
|
|
162
|
+
description: 'Horizontal (XZ) heading the wind blows toward. Magnitude does not matter; use wind strength for amplitude.',
|
|
163
|
+
label: 'Wind Direction',
|
|
164
|
+
type: 'vector2',
|
|
165
|
+
},
|
|
166
|
+
windSpeed: {
|
|
167
|
+
description: 'How fast the per-blade sway oscillates.',
|
|
168
|
+
label: 'Wind Speed',
|
|
169
|
+
range: { max: 4, min: 0, step: 0.01 },
|
|
170
|
+
type: 'number',
|
|
171
|
+
},
|
|
172
|
+
windStrength: {
|
|
173
|
+
description: 'How far blade tips bend with the wind.',
|
|
174
|
+
label: 'Wind Strength',
|
|
175
|
+
range: { max: 1, min: 0, step: 0.005 },
|
|
176
|
+
type: 'number',
|
|
177
|
+
},
|
|
178
|
+
gustFrequency: {
|
|
179
|
+
description: 'Spatial frequency of the traveling gust bands; higher packs gust waves closer together.',
|
|
180
|
+
label: 'Gust Frequency',
|
|
181
|
+
range: { max: 2, min: 0, step: 0.01 },
|
|
182
|
+
type: 'number',
|
|
183
|
+
},
|
|
184
|
+
gustSpeed: {
|
|
185
|
+
description: 'How fast gust bands travel across the field.',
|
|
186
|
+
label: 'Gust Speed',
|
|
187
|
+
range: { max: 6, min: 0, step: 0.01 },
|
|
188
|
+
type: 'number',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
lighting: {
|
|
192
|
+
baseColor: {
|
|
193
|
+
description: 'Blade color at the root.',
|
|
194
|
+
label: 'Base Color',
|
|
195
|
+
type: 'color',
|
|
196
|
+
},
|
|
197
|
+
tipColor: {
|
|
198
|
+
description: 'Blade color at the tip; blades gradient from base to tip.',
|
|
199
|
+
label: 'Tip Color',
|
|
200
|
+
type: 'color',
|
|
201
|
+
},
|
|
202
|
+
sunDirection: {
|
|
203
|
+
description: 'World-space direction toward the sun (normalized on apply). Match your main directional light.',
|
|
204
|
+
label: 'Sun Direction',
|
|
205
|
+
type: 'vector3',
|
|
206
|
+
},
|
|
207
|
+
sunColor: {
|
|
208
|
+
description: 'Sunlight tint applied to lit blades.',
|
|
209
|
+
label: 'Sun Color',
|
|
210
|
+
type: 'color',
|
|
211
|
+
},
|
|
212
|
+
skyColor: {
|
|
213
|
+
description: 'Ambient sky tint mixed into shaded blades.',
|
|
214
|
+
label: 'Sky Color',
|
|
215
|
+
type: 'color',
|
|
216
|
+
},
|
|
217
|
+
backlitStrength: {
|
|
218
|
+
description: 'Translucent backlight boost when the camera looks toward the sun through the blades.',
|
|
219
|
+
label: 'Backlit Strength',
|
|
220
|
+
range: { max: 2, min: 0, step: 0.01 },
|
|
221
|
+
type: 'number',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
shadows: {
|
|
225
|
+
shadowStrength: {
|
|
226
|
+
description: 'How strongly renderer shadow maps (trees, rocks, the character) darken blades.',
|
|
227
|
+
label: 'Shadow Strength',
|
|
228
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
229
|
+
type: 'number',
|
|
230
|
+
},
|
|
231
|
+
shadowTint: {
|
|
232
|
+
description: 'Color a fully shadowed blade is multiplied by (cool and dark so grass matches the terrain shadow response).',
|
|
233
|
+
label: 'Shadow Tint',
|
|
234
|
+
type: 'color',
|
|
235
|
+
},
|
|
236
|
+
cloudShadowStrength: {
|
|
237
|
+
description: 'How strongly drifting procedural cloud shadows darken the field. 0 disables the effect.',
|
|
238
|
+
label: 'Cloud Shadow Strength',
|
|
239
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
240
|
+
type: 'number',
|
|
241
|
+
},
|
|
242
|
+
cloudShadowCoverage: {
|
|
243
|
+
description: 'Fraction of the field covered by cloud shadow at any moment.',
|
|
244
|
+
label: 'Cloud Shadow Coverage',
|
|
245
|
+
range: { max: 1, min: 0, step: 0.01 },
|
|
246
|
+
type: 'number',
|
|
247
|
+
},
|
|
248
|
+
cloudShadowScale: {
|
|
249
|
+
description: 'World-to-noise scale of the cloud shadow pattern; smaller values give larger cloud shapes.',
|
|
250
|
+
label: 'Cloud Shadow Scale',
|
|
251
|
+
range: { max: 0.1, min: 0.001, step: 0.001 },
|
|
252
|
+
type: 'number',
|
|
253
|
+
},
|
|
254
|
+
cloudShadowVelocity: {
|
|
255
|
+
description: 'Cloud shadow drift in noise-space units per second (world drift = velocity / scale).',
|
|
256
|
+
label: 'Cloud Shadow Velocity',
|
|
257
|
+
type: 'vector2',
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
interaction: {
|
|
261
|
+
pushRadius: {
|
|
262
|
+
description: 'Radius in meters around the push target within which blades bend away.',
|
|
263
|
+
label: 'Push Radius',
|
|
264
|
+
range: { max: 3, min: 0, step: 0.01 },
|
|
265
|
+
type: 'number',
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
function createGrassFieldMetadata(group, key, field) {
|
|
271
|
+
const defaultValue = DEFAULT_GRASS_SETTINGS[key];
|
|
272
|
+
return Object.freeze({
|
|
273
|
+
defaultValue: Array.isArray(defaultValue) ? [...defaultValue] : defaultValue,
|
|
274
|
+
description: field.description,
|
|
275
|
+
group: group.id,
|
|
276
|
+
id: `${group.id}.${key}`,
|
|
277
|
+
key,
|
|
278
|
+
label: field.label,
|
|
279
|
+
optionLabels: field.optionLabels ?? null,
|
|
280
|
+
options: field.options ?? null,
|
|
281
|
+
range: field.range ?? null,
|
|
282
|
+
serializable: field.serializable ?? true,
|
|
283
|
+
type: field.type,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Field metadata (id/group/key/label/description/type/range/defaultValue/
|
|
289
|
+
* serializable) per settings group, in the shape consumed by
|
|
290
|
+
* `createSettingsPanel`. Keys are the flat {@link DEFAULT_GRASS_SETTINGS}
|
|
291
|
+
* keys.
|
|
292
|
+
*/
|
|
293
|
+
export const GRASS_SETTING_FIELD_SCHEMA = Object.freeze(
|
|
294
|
+
Object.fromEntries(
|
|
295
|
+
GRASS_SETTING_GROUPS.map((group) => [
|
|
296
|
+
group.id,
|
|
297
|
+
Object.freeze(
|
|
298
|
+
Object.fromEntries(
|
|
299
|
+
Object.entries(GRASS_FIELD_DEFINITIONS[group.id] ?? {})
|
|
300
|
+
.map(([key, field]) => [key, createGrassFieldMetadata(group, key, field)]),
|
|
301
|
+
),
|
|
302
|
+
),
|
|
303
|
+
]),
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// Dense instanced grass: procedural tapered blades with wind sway and a
|
|
308
|
+
// push-away radius around a character. One draw call for the whole field;
|
|
309
|
+
// emitting is a one-time attribute fill, animation is entirely in the vertex
|
|
310
|
+
// shader. No texture assets.
|
|
311
|
+
//
|
|
312
|
+
// const grass = new StylizedGrassField({
|
|
313
|
+
// placements: points.map((p) => ({ x: p.x, y: terrainHeight(p), z: p.z })),
|
|
314
|
+
// });
|
|
315
|
+
// scene.add(grass);
|
|
316
|
+
// grass.setPushTarget(characterObject3D);
|
|
317
|
+
// grass.update(delta); // each frame
|
|
318
|
+
// grass.applySettings({ windStrength: 0.3, cloudShadowStrength: 0.5 });
|
|
319
|
+
//
|
|
320
|
+
// Options are a flat settings object (see DEFAULT_GRASS_SETTINGS) plus
|
|
321
|
+
// `placements`; legacy individual constructor options are the same keys, so
|
|
322
|
+
// existing callers keep working unchanged.
|
|
323
|
+
export class StylizedGrassField extends THREE.Mesh {
|
|
324
|
+
constructor(options = {}) {
|
|
325
|
+
const { placements = [] } = cleanObject(options);
|
|
326
|
+
const settings = createGrassSettings(options);
|
|
327
|
+
const { bladeHeightRange, bladeWidthRange } = settings;
|
|
328
|
+
|
|
329
|
+
const blade = new THREE.PlaneGeometry(1, 1, 1, 3);
|
|
330
|
+
blade.translate(0, 0.5, 0);
|
|
331
|
+
|
|
332
|
+
const geometry = new THREE.InstancedBufferGeometry();
|
|
333
|
+
geometry.index = blade.index;
|
|
334
|
+
geometry.setAttribute('position', blade.attributes.position);
|
|
335
|
+
geometry.setAttribute('uv', blade.attributes.uv);
|
|
336
|
+
|
|
337
|
+
const count = Math.max(placements.length, 1);
|
|
338
|
+
const origins = new Float32Array(count * 3);
|
|
339
|
+
const infos = new Float32Array(count * 4);
|
|
340
|
+
placements.forEach((placement, i) => {
|
|
341
|
+
origins[i * 3] = placement.x ?? 0;
|
|
342
|
+
origins[i * 3 + 1] = placement.y ?? 0;
|
|
343
|
+
origins[i * 3 + 2] = placement.z ?? 0;
|
|
344
|
+
infos[i * 4] = placement.height ??
|
|
345
|
+
THREE.MathUtils.lerp(bladeHeightRange[0], bladeHeightRange[1], Math.random());
|
|
346
|
+
infos[i * 4 + 1] = placement.phase ?? Math.random();
|
|
347
|
+
infos[i * 4 + 2] = placement.width ??
|
|
348
|
+
THREE.MathUtils.lerp(bladeWidthRange[0], bladeWidthRange[1], Math.random());
|
|
349
|
+
infos[i * 4 + 3] = Math.random() * Math.PI * 2;
|
|
350
|
+
});
|
|
351
|
+
geometry.setAttribute('iOrigin', new THREE.InstancedBufferAttribute(origins, 3));
|
|
352
|
+
geometry.setAttribute('iInfo', new THREE.InstancedBufferAttribute(infos, 4));
|
|
353
|
+
geometry.instanceCount = placements.length;
|
|
354
|
+
geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(), 1e5);
|
|
355
|
+
|
|
356
|
+
const material = createGrassNodeMaterial(settings);
|
|
357
|
+
setSrgbColor(material.uniforms.uBaseColor.value, settings.baseColor);
|
|
358
|
+
setSrgbColor(material.uniforms.uTipColor.value, settings.tipColor);
|
|
359
|
+
setSrgbColor(material.uniforms.uSunColor.value, settings.sunColor);
|
|
360
|
+
setSrgbColor(material.uniforms.uSkyColor.value, settings.skyColor);
|
|
361
|
+
setSrgbColor(material.uniforms.uShadowTint.value, settings.shadowTint);
|
|
362
|
+
|
|
363
|
+
super(geometry, material);
|
|
364
|
+
this.name = 'StylizedGrassField';
|
|
365
|
+
this.frustumCulled = false;
|
|
366
|
+
this.receiveShadow = true;
|
|
367
|
+
this.pushTarget = null;
|
|
368
|
+
this.settings = settings;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Runtime re-tune: merges `options` into the current settings and pushes
|
|
373
|
+
* every material-driven value (wind, palette, sun, shadows, push radius)
|
|
374
|
+
* into the uniforms. `bladeHeightRange` / `bladeWidthRange` are baked into
|
|
375
|
+
* the instance attributes at construction and are construction-only; new
|
|
376
|
+
* values are stored but do not reshape existing blades.
|
|
377
|
+
*
|
|
378
|
+
* @param {Object} [options] Partial flat settings, same keys as
|
|
379
|
+
* {@link DEFAULT_GRASS_SETTINGS}.
|
|
380
|
+
* @returns {Object} The updated settings object.
|
|
381
|
+
*/
|
|
382
|
+
applySettings(options = {}) {
|
|
383
|
+
const merged = { ...this.settings };
|
|
384
|
+
for (const [key, value] of Object.entries(cleanObject(options))) {
|
|
385
|
+
if (value !== undefined) merged[key] = value;
|
|
386
|
+
}
|
|
387
|
+
const settings = createGrassSettings(merged);
|
|
388
|
+
this.settings = settings;
|
|
389
|
+
|
|
390
|
+
const uniforms = this.material.uniforms;
|
|
391
|
+
uniforms.uShadowStrength.value = settings.shadowStrength;
|
|
392
|
+
uniforms.uWindDirection.value.set(settings.windDirection[0], settings.windDirection[1]);
|
|
393
|
+
uniforms.uWindSpeed.value = settings.windSpeed;
|
|
394
|
+
uniforms.uWindStrength.value = settings.windStrength;
|
|
395
|
+
uniforms.uGustFrequency.value = settings.gustFrequency;
|
|
396
|
+
uniforms.uGustSpeed.value = settings.gustSpeed;
|
|
397
|
+
uniforms.uPushRadius.value = settings.pushRadius;
|
|
398
|
+
uniforms.uBacklitStrength.value = settings.backlitStrength;
|
|
399
|
+
uniforms.uCloudShadowStrength.value = settings.cloudShadowStrength;
|
|
400
|
+
uniforms.uCloudShadowCoverage.value = settings.cloudShadowCoverage;
|
|
401
|
+
uniforms.uCloudShadowScale.value = settings.cloudShadowScale;
|
|
402
|
+
uniforms.uCloudShadowVelocity.value.set(
|
|
403
|
+
settings.cloudShadowVelocity[0], settings.cloudShadowVelocity[1]);
|
|
404
|
+
uniforms.uSunDirection.value.set(...settings.sunDirection).normalize();
|
|
405
|
+
setSrgbColor(uniforms.uBaseColor.value, settings.baseColor);
|
|
406
|
+
setSrgbColor(uniforms.uTipColor.value, settings.tipColor);
|
|
407
|
+
setSrgbColor(uniforms.uSunColor.value, settings.sunColor);
|
|
408
|
+
setSrgbColor(uniforms.uSkyColor.value, settings.skyColor);
|
|
409
|
+
setSrgbColor(uniforms.uShadowTint.value, settings.shadowTint);
|
|
410
|
+
return this.settings;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
setWind({ direction, speed, strength, gustFrequency, gustSpeed } = {}) {
|
|
414
|
+
this.applySettings({
|
|
415
|
+
gustFrequency,
|
|
416
|
+
gustSpeed,
|
|
417
|
+
windDirection: direction,
|
|
418
|
+
windSpeed: speed,
|
|
419
|
+
windStrength: strength,
|
|
420
|
+
});
|
|
421
|
+
return this;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
setSun({ direction, color, sky } = {}) {
|
|
425
|
+
this.applySettings({
|
|
426
|
+
skyColor: sky,
|
|
427
|
+
sunColor: color,
|
|
428
|
+
sunDirection: direction,
|
|
429
|
+
});
|
|
430
|
+
return this;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Scene-shadow response: strength lerps the renderer shadow mask, tint is
|
|
434
|
+
// the color a fully shadowed blade is multiplied by.
|
|
435
|
+
setSceneShadow({ strength, tint } = {}) {
|
|
436
|
+
this.applySettings({
|
|
437
|
+
shadowStrength: strength,
|
|
438
|
+
shadowTint: tint,
|
|
439
|
+
});
|
|
440
|
+
return this;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Drifting procedural cloud shadows over the field. strength 0 disables.
|
|
444
|
+
// velocity is uv-space drift per second (worldDrift = velocity / scale).
|
|
445
|
+
setCloudShadow({ strength, coverage, scale, velocity } = {}) {
|
|
446
|
+
this.applySettings({
|
|
447
|
+
cloudShadowCoverage: coverage,
|
|
448
|
+
cloudShadowScale: scale,
|
|
449
|
+
cloudShadowStrength: strength,
|
|
450
|
+
cloudShadowVelocity: velocity,
|
|
451
|
+
});
|
|
452
|
+
return this;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Collapse blades between start and end meters from the camera so distant,
|
|
456
|
+
// fog-swallowed grass stops costing fill rate. Pass nothing to disable.
|
|
457
|
+
setDistanceFade({ start = 1e6, end } = {}) {
|
|
458
|
+
const uniforms = this.material.uniforms;
|
|
459
|
+
uniforms.uFadeStart.value = start;
|
|
460
|
+
uniforms.uFadeEnd.value = Number.isFinite(end) ? Math.max(end, start + 0.01) : start + 1;
|
|
461
|
+
return this;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// target: Object3D | (outVector3) => position | { x, y, z } | null.
|
|
465
|
+
setPushTarget(target) {
|
|
466
|
+
this.pushTarget = target;
|
|
467
|
+
return this;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
update(delta) {
|
|
471
|
+
const uniforms = this.material.uniforms;
|
|
472
|
+
uniforms.uTime.value += Math.min(Math.max(delta ?? 0.016, 0), 0.1);
|
|
473
|
+
const target = this.pushTarget;
|
|
474
|
+
if (!target) {
|
|
475
|
+
uniforms.uPushPosition.value.set(0, -1e5, 0);
|
|
476
|
+
} else if (typeof target === 'function') {
|
|
477
|
+
const resolved = target(pushScratch);
|
|
478
|
+
if (resolved && Number.isFinite(resolved.x)) uniforms.uPushPosition.value.copy(resolved);
|
|
479
|
+
} else if (target.isObject3D) {
|
|
480
|
+
uniforms.uPushPosition.value.copy(target.getWorldPosition(pushScratch));
|
|
481
|
+
} else if (Number.isFinite(target.x)) {
|
|
482
|
+
uniforms.uPushPosition.value.set(target.x, target.y ?? 0, target.z ?? 0);
|
|
483
|
+
}
|
|
484
|
+
return this;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
dispose() {
|
|
488
|
+
this.geometry.dispose();
|
|
489
|
+
this.material.dispose();
|
|
490
|
+
}
|
|
491
|
+
}
|