diff --git a/extensions/community/NatureElements.json b/extensions/community/NatureElements.json new file mode 100644 index 000000000..71c18fd38 --- /dev/null +++ b/extensions/community/NatureElements.json @@ -0,0 +1,7750 @@ +{ + "author": "Your Name", + "category": "3D", + "dimension": "", + "extensionNamespace": "FoliageSwaying", + "fullName": "Nature elements", + "gdevelopVersion": "", + "helpPath": "/nature-elements", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWdyYXNzIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDIwSDJWMThINy43NUM3IDE1LjE5IDQuODEgMTMgMiAxMi4yNkMyLjY0IDEyLjEgMy4zMSAxMiA0IDEyQzguNDIgMTIgMTIgMTUuNTggMTIgMjBNMjIgMTIuMjZDMjEuMzYgMTIuMSAyMC42OSAxMiAyMCAxMkMxNy4wNyAxMiAxNC41IDEzLjU4IDEzLjEyIDE1LjkzQzEzLjQxIDE2LjU5IDEzLjY1IDE3LjI4IDEzLjc5IDE4QzEzLjkyIDE4LjY1IDE0IDE5LjMyIDE0IDIwSDIyVjE4SDE2LjI0QzE3IDE1LjE5IDE5LjE5IDEzIDIyIDEyLjI2TTE1LjY0IDExQzE2LjQyIDguOTMgMTcuODcgNy4xOCAxOS43MyA2QzE1LjQ0IDYuMTYgMTIgOS42NyAxMiAxNFYxNEMxMi45NSAxMi43NSAxNC4yIDExLjcyIDE1LjY0IDExTTExLjQyIDguODVDMTAuNTggNi42NiA4Ljg4IDQuODkgNi43IDRDOC4xNCA1Ljg2IDkgOC4xOCA5IDEwLjcxQzkgMTAuOTIgOC45NyAxMS4xMiA4Ljk2IDExLjMyQzkuMzkgMTEuNTYgOS43OSAxMS44NCAxMC4xOCAxMi4xNEMxMC4zOSAxMC45NiAxMC44MyA5Ljg1IDExLjQyIDguODVaIiAvPjwvc3ZnPg==", + "name": "NatureElements", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg", + "shortDescription": "Adds realistic wind-based foliage swaying to 3D objects.", + "version": "1.0.31", + "description": [ + "## Features:", + "", + "* Real-time foliage wind animation for 3D scenes", + "* Multiple sway types: **Grass**, **Bush**, **Tree Trunk** (dead tree), and **Tree Leaves** (flowers coming soon)", + "* Works with both **regular meshes** and **GPU-instanced foliage** for better performance", + "* Global wind controls: `strength`, `speed`, and `wind direction`", + "* Optional **gust system** with texture-driven gust masks", + "* Optional **visual tuning**: gradient tint, saturation/contrast, PBR tweaks", + "* Distance-based fading/culling for better performance", + "* Frustum-aware visibility updates for GPU-instanced objects (renders only what should be visible)", + "* Automatic foliage shader/material patching (including shadow materials)", + "* Runtime-safe live updates for wind/gust parameters", + "* Scene/object cleanup logic to reduce memory leaks and stale resources", + "", + "", + "## Comes with:", + "", + "* **Update foliage sway** action — sets wind parameters (run it every frame)", + "* **Set wind gust** action — sets gust parameters (set at the beginning of the scene)", + "* **Foliage swaying** behavior — sets individual parameters for your 3D foliage object (grass, tree, bush)", + "", + "", + "## How to use:", + "", + "1. Import your 3D object (tree, bush, grass...) and add the **Foliage swaying** behavior to it.", + "", + "2. Choose from various object settings and parameters, then add the object to your scene.", + "![Foliage swaying behavior](https://i.imgur.com/dRBtniE.jpeg)", + "", + "3. Add the **Update foliage sway** action, choose its parameters, and run it every frame. ", + "![Update foliage sway action](https://i.imgur.com/A5eaxPZ.jpeg)", + "", + "4. Optionally use the **Set wind gust** action at the beginning of the scene. ", + "![Set wind gust action](https://i.imgur.com/TEwvtgB.jpeg)", + "", + "5. Play your scene.", + "", + "", + "## Current limitations, issues, and guides:", + "", + "* Collision is not yet supported for GPU instanced objects but is planned.", + "* The mesh complexity auto-detection (`Polyscale` parameter) is not perfect and can produce inconsistent sway intensity across assets. If you encounter overly strong or weak sways, experiment with the `Polyscale` value to make your asset's sway consistent. Use the debug option to find the auto-selected value, then increase or decrease it.", + "* Foliage material auto-detection works well and can usually be left empty, but there are edge cases with multi-material objects. If you don't have access to the source file of your 3D object, you can find your object's material name using the debug option.", + "* The optional wind gust texture can create natural-looking gusts and is worth creating. To make one yourself, use only black and red colors (black will be ignored and red will be the actual gust map). For the best results, create a tilable 512x512 image. See examples below. ", + "![Gust map examples](https://i.imgur.com/gElKT5y.png)", + "* PBR settings are currently not fully tested and should be treated as experimental.", + "", + "", + "## Future nice-to-haves:", + "* Polygon-based foliage scattering", + "* Collisions for instanced objects", + "* Split camera culling vs shadow caster culling to avoid edge cases (mostly early shadow pop-outs)", + "* Rewrite shader swaying logic to eliminate `PolyScale` completely", + "* Basic LOD system" + ], + "tags": [ + "3D", + "shader", + "foliage", + "wind", + "sway", + "animation", + "tree", + "grass", + "vegetation", + "nature", + "cozy" + ], + "authorIds": [ + "VNp7UkcF59OvG8pYLklh6IE3tBX2" + ], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Define helper classes JavaScript code.", + "fullName": "Define helper classes", + "functionType": "Action", + "name": "DefineHelperClasses", + "private": true, + "sentence": "Define helper classes JavaScript code.", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/**", + " * Nature Elements - Foliage Sway Helper Library", + " *", + " * Shared helper namespace for the FoliageSwaying behavior.", + " * Uses the current `FoliageSwaying` behavior identifier.", + " * Library code is attached to `gdjs._natureElementsFoliageSway`, while", + " * mutable runtime state lives on `runtimeScene._natureElementsFoliageSway`.", + " */", + "", + "", + " /** @typedef {{ material: THREE.Material, refCount: number, _ownedByFoliage?: boolean }} FoliageSharedMaterialEntry */", + " /** @typedef {THREE.Material & { alphaMap?: THREE.Texture|null, map?: THREE.Texture|null, alphaTest?: number, transparent?: boolean, opacity?: number, name?: string }} FoliageInspectableMaterial */", + " /** @typedef {{ timeWind: Set, fadeInterp: Set, gust: Set, shadowHost: Set, unknown: Set, rebuildNeeded: boolean }} FoliageMaterialBucketsShape */", + " /** @typedef {{ strength: number, scale: number, speed: number, threshold: number, contrast: number }} GustDefaults */", + " /** @typedef {{ kind: string, lastUsed: number }} FoliageObjectTypeCacheMeta */", + " /** @typedef {{ tick: number, setCount: number, meta: Map }} FoliageObjectTypeCacheState */", + " /** @typedef {string|number|boolean|null|undefined} FoliagePrimitiveValue */", + " /** @typedef {FoliagePrimitiveValue|gdjs.Variable} FoliageArgumentValue */", + " /** @typedef {{ getAsBoolean?: () => boolean, getAsNumber?: () => number, getAsString?: () => string }} FoliageArgumentVariableLike */", + " /** @typedef {{ getArgument: (name: string) => FoliageArgumentValue }} FoliageEventsFunctionContext */", + " /** @typedef {{ runtimeScene: gdjs.RuntimeScene|null, dt: number, time: number, windStrength: number, windSpeed: number, wx: number, wy: number, cam: THREE.Camera|null, camX: number, camY: number, camZ: number, frustum: THREE.Frustum|null, _frustumBuilt: boolean }} SharedFrameContext */", + " /** @typedef {{ _getmaterialName: () => string, _getswayType: () => string, _getuniformSway: () => boolean|string, _get_customLit: () => boolean|string, _getmetallic: () => number|string, _getroughness: () => number|string, _getspecular: () => number|string, _getnormalStrength: () => number|string, _getaoStrength: () => number|string, _getenvStrength: () => number|string, _getcullingMode: () => string, _gettwoSidedLighting: () => boolean|string, _getgpuInstancing: () => boolean|string, _getdistanceFadeEnabled: () => boolean|string, _getfadeStart: () => number|string, _getfadeEnd: () => number|string, _getpolyScale: () => number|string, _getignoreUV: () => boolean|string, _getgradStart: () => number|string, _getgradEnd: () => number|string, _get_useColorGrading: () => boolean|string, _getcolorTop: () => string, _getcolorBottom: () => string, _getuContrast: () => number|string, _getuSat: () => number|string, _getdebugOutput: () => boolean|string }} FoliageBehaviorGetterMethods */", + " /**", + " * Typed snapshot of behavior properties read once for a single runtime use-site.", + " * Consumers use this to keep property access centralized instead of scattering", + " * generated getter calls throughout creation, update, and cleanup code.", + " */", + " class FoliageBehaviorProps {", + " /**", + " * @param {FoliageBehavior} behavior", + " */", + " constructor(behavior) {", + " var materialName = behavior._getmaterialName();", + " var swayType = behavior._getswayType();", + " var cullingMode = behavior._getcullingMode();", + " var colorTop = behavior._getcolorTop();", + " var colorBottom = behavior._getcolorBottom();", + "", + " this.materialName = materialName == null ? \"\" : String(materialName);", + " this.swayType = swayType == null || String(swayType) === \"\" ? \"grassSway\" : String(swayType);", + " this.uniformSway = parseBool(behavior._getuniformSway(), true);", + " this.customLit = parseBool(behavior._get_customLit(), false);", + " this.metallic = readClamped(behavior._getmetallic(), 0, 0, 1);", + " this.roughness = readClamped(behavior._getroughness(), 1, 0, 1);", + " this.specular = readClamped(behavior._getspecular(), 0.1, 0, 1);", + " this.normalStrength = readClamped(behavior._getnormalStrength(), 1, 0, 1);", + " this.aoStrength = readClamped(behavior._getaoStrength(), 1, 0, 1);", + " this.envStrength = readClamped(behavior._getenvStrength(), 1, 0, 1);", + " this.cullingMode = cullingMode == null || String(cullingMode) === \"\" ? \"useSource\" : String(cullingMode);", + " this.twoSidedLighting = parseBool(behavior._gettwoSidedLighting(), true);", + " this.gpuInstancing = parseBool(behavior._getgpuInstancing(), false);", + " this.distanceFadeEnabled = parseBool(behavior._getdistanceFadeEnabled(), false);", + " this.fadeStart = readClamped(behavior._getfadeStart(), 1200, 0, 100000);", + " this.fadeEnd = readClamped(behavior._getfadeEnd(), 1600, 0, 100000);", + " this.polyScale = readClamped(behavior._getpolyScale(), 0, 0, 200);", + " this.ignoreUV = parseBool(behavior._getignoreUV(), false);", + " this.gradStart = readClamped(behavior._getgradStart(), 0.0, 0.0, 1.0);", + " this.gradEnd = readClamped(behavior._getgradEnd(), 1.0, 0.0, 1.0);", + " // Current GDevelop runtime generates getters for gradStart/gradEnd, but not", + " // for gradHeight. Keep the long-standing runtime default until the property", + " // is surfaced again by the generator/editor.", + " this.gradHeight = 1.0;", + " this.useColorGrading = parseBool(behavior._get_useColorGrading(), false);", + " this.colorTop = colorTop == null ? \"\" : String(colorTop);", + " this.colorBottom = colorBottom == null ? \"\" : String(colorBottom);", + " this.uContrast = readClamped(behavior._getuContrast(), 1.10, 0.0, 3.0);", + " this.uSat = readClamped(behavior._getuSat(), 1.35, 0.0, 3.0);", + " this.debugOutput = parseBool(behavior._getdebugOutput(), false);", + " }", + " }", + "", + " /**", + " * Per-frame material categorization used by update passes to touch only the", + " * materials that need a specific uniform or rebuild step.", + " */", + " class FoliageMaterialBuckets {", + " constructor() {", + " this.reset();", + " }", + "", + " reset() {", + " this.timeWind = new Set();", + " this.fadeInterp = new Set();", + " this.gust = new Set();", + " this.shadowHost = new Set();", + " this.unknown = new Set();", + " this.rebuildNeeded = true;", + " return this;", + " }", + "", + " addUnknown(mat) {", + " if (mat) this.unknown.add(mat);", + " }", + "", + " remove(mat) {", + " if (!mat) return;", + " this.timeWind.delete(mat);", + " this.fadeInterp.delete(mat);", + " this.gust.delete(mat);", + " this.shadowHost.delete(mat);", + " this.unknown.delete(mat);", + " }", + " }", + " /**", + " * Owner of shared foliage materials and their lifecycle bookkeeping.", + " * This keeps ref-count, active-material tracking, and shadow material disposal", + " * in one place instead of spreading that logic across events.", + " */", + " class FoliageSharedMaterialRegistry {", + " /**", + " * @param {FoliageSceneState} state", + " */", + " constructor(state) {", + " this.state = state;", + " }", + "", + " ensureBuckets() {", + " var state = this.state;", + " if (!(state._materialBuckets instanceof FoliageMaterialBuckets)) {", + " state._materialBuckets = new FoliageMaterialBuckets();", + " }", + " return state._materialBuckets;", + " }", + "", + " resetBuckets() {", + " var state = this.state;", + " state._materialBuckets = new FoliageMaterialBuckets();", + " return state._materialBuckets;", + " }", + "", + " registerActiveMaterial(mat) {", + " if (!mat) return;", + " var state = this.state;", + " state.activeMaterials.add(mat);", + " this.ensureBuckets().addUnknown(mat);", + " }", + "", + " unregisterActiveMaterial(mat) {", + " if (!mat) return;", + " var state = this.state;", + " state.activeMaterials.delete(mat);", + " var buckets = state._materialBuckets;", + " if (!buckets) return;", + " buckets.remove(mat);", + " }", + "", + " disposeShadowMaterials(mat) {", + " if (!mat || !mat.userData) return;", + " var ud = mat.userData;", + " var negVariant = ud._foliageDetNegVariant;", + " if (negVariant && negVariant !== mat) {", + " delete ud._foliageDetNegVariant;", + " this.unregisterActiveMaterial(negVariant);", + " this.disposeShadowMaterials(negVariant);", + " if (typeof negVariant.dispose === \"function\") {", + " try { negVariant.dispose(); } catch (eNeg) {}", + " }", + " }", + " var depthMat = ud._foliageDepthMat;", + " var distanceMat = ud._foliageDistanceMat;", + " if (depthMat && depthMat !== mat && typeof depthMat.dispose === \"function\") {", + " try { depthMat.dispose(); } catch (e) {}", + " }", + " if (distanceMat && distanceMat !== mat && distanceMat !== depthMat && typeof distanceMat.dispose === \"function\") {", + " try { distanceMat.dispose(); } catch (e) {}", + " }", + " delete ud._foliageDepthMat;", + " delete ud._foliageDistanceMat;", + " delete ud._foliageShadowOwned;", + " }", + "", + " markGustForRecompile() {", + " var state = this.state;", + " state.activeMaterials.forEach(function(mat) {", + " if (!mat) return;", + " mat.needsUpdate = true;", + " var ud = mat.userData;", + " if (!ud) return;", + " if (ud._foliageDepthMat) ud._foliageDepthMat.needsUpdate = true;", + " if (ud._foliageDistanceMat) ud._foliageDistanceMat.needsUpdate = true;", + " });", + " }", + " }", + " /** @typedef {{ __foliageSkipOnDestroy?: boolean, __foliageNonInstancedRegistered?: boolean, __foliageSharedKey?: string, __foliageSharedKeyTrunk?: string, __foliageInstancingGroupKey?: string, __foliageInstancingGroupKeyLeaves?: string, __foliageInstancingIndex?: number, __foliageQueued?: boolean, __foliageQueueId?: number }} FoliageBehaviorPrivateFields */", + " /** @typedef {gdjs.RuntimeBehavior & FoliageBehaviorPrivateFields & FoliageBehaviorGetterMethods} FoliageBehavior */", + " /** @typedef {THREE.Object3D & { isMesh?: boolean, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|THREE.Material[]|null, castShadow?: boolean, receiveShadow?: boolean, id?: number, name?: string }} FoliageMeshLike */", + " /** @typedef {{ zMin: number, zMax: number, relZMin: number, relZMax: number, hasGeom: boolean, totalVerts: number, totalTris: number, meshCount: number, slotCount: number, planeLikeCount: number, sizeLocalMin: THREE.Vector3, sizeLocalMax: THREE.Vector3, sizeWorldMin: THREE.Vector3, sizeWorldMax: THREE.Vector3, _meshIds?: Set }} FoliageDataRecord */", + " /** @typedef {{ meshName: string, materialIndex: number, materialRef: THREE.Material|null, materialName: string, alphaLikely: boolean, score: number }} FoliageObjectRecord */", + " /** @typedef {{ srcRef: THREE.Material|null, pickedId: string, matchMode: \"name\"|\"auto\", matchName: string }} FoliageMaterialSelection */", + " /** @typedef {{ repMesh?: THREE.Object3D|null, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, baseGroupKey?: string }} FoliagePendingPart */", + " /** @typedef {{ gdObj?: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string, getX?: () => number, getY?: () => number, getZ?: () => number, getScaleX?: () => number, getScaleY?: () => number, getScaleZ?: () => number, getRotationX?: () => number, getRotationY?: () => number, getRotationZ?: () => number, getAngle?: () => number, hide?: (enable?: boolean) => void, isHidden?: () => boolean, deleteFromScene?: (runtimeScene?: gdjs.RuntimeScene) => void })|null, threeObj?: THREE.Object3D|null, repMesh?: THREE.Object3D|null, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, parent?: THREE.Object3D|null, baseGroupKey?: string, groupKey?: string, swayType?: string, behavior?: FoliageBehavior|null, queueId?: number, parts?: FoliagePendingPart[] }} FoliagePendingItem */", + " /** @typedef {{ gdObj?: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string, getX?: () => number, getY?: () => number, getZ?: () => number, getScaleX?: () => number, getScaleY?: () => number, getScaleZ?: () => number, getRotationX?: () => number, getRotationY?: () => number, getRotationZ?: () => number, getAngle?: () => number, hide?: (enable?: boolean) => void, isHidden?: () => boolean, deleteFromScene?: (runtimeScene?: gdjs.RuntimeScene) => void })|null, threeObj?: THREE.Object3D|null, material?: THREE.Material|null, trunkMaterial?: THREE.Material|null, fadeStart?: number, fadeEnd?: number, fadeEnabled?: boolean, fadeBehavior?: FoliageBehavior|null, _wasHidden?: boolean, _parkedNoFade?: boolean, _firstHideWarmupDone?: boolean }} FoliageNonInstancedEntry */", + " /** @typedef {{ key?: string, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, parent?: THREE.Object3D|null, matricesBuffer?: Float32Array|null, centersXY?: Float32Array|null, centersZ?: Float32Array|null, cullRadii?: Float32Array|null, matrixCount?: number, aliveCount?: number, freeIndices?: number[], freeIndexSet?: Set, capacity?: number, mesh?: THREE.InstancedMesh|null, castShadow?: boolean, receiveShadow?: boolean, fadeEnabled?: boolean, fadeStart?: number, fadeEnd?: number, _fadeBehavior?: FoliageBehavior|null, _fadeSig?: string, _instanceCullRadius?: number, instanceFade?: Float32Array|null, instanceFadePrev?: Float32Array|null, _distanceCandidateIndices?: Uint32Array|null, _distanceCandidateCount?: number, _fadeDisabledApplied?: boolean, _lastFadeEnabled?: boolean, visibleCount?: number, instanceDetSign?: number, _srcGeometryRef?: THREE.BufferGeometry|null, _ownedGeometry?: THREE.BufferGeometry|null }} FoliageInstancingGroup */", + " /** @typedef {{ groups: Map, dirty: boolean, sceneTag: string|null, pending: FoliagePendingItem[], queueIdCounter: number, cancelledQueueIds: Set, foliageRoot: THREE.Object3D|null, _cachedSceneRoot: THREE.Object3D|null, _isReady?: boolean, _tmpRootSet?: Set|null, _tmpMat4?: THREE.Matrix4, _tmpObj3D?: THREE.Object3D, _tmpVec3_pos?: THREE.Vector3, _tmpVec3_scale?: THREE.Vector3, _tmpEuler?: THREE.Euler, _tmpQuat?: THREE.Quaternion, _tmpMat4_objWorld?: THREE.Matrix4, _tmpMat4_objWorldInv?: THREE.Matrix4, _tmpMat4_repWorld?: THREE.Matrix4, _repRelByMesh?: WeakMap, _itemsByParentRepMesh?: Map, _repRelMatrixCache?: Map, _tmpMat4_local?: THREE.Matrix4, _tmpMat4_foliageRootInv?: THREE.Matrix4 }} FoliageInstancingStateShape */", + " /** @typedef {{ records?: FoliageObjectRecord[], statsByRef?: Map, statsByName?: Map, selection?: FoliageMaterialSelection|null, _cachedGeometry?: THREE.BufferGeometry|null, _gpuGeometry?: THREE.BufferGeometry|null, _gpuGroupKey?: string, _sharedMaterialKey?: string, _resolvedSide?: THREE.Side, _gpuFastMode?: \"single\"|\"splitLeavesTree\", _gpuLeavesGeometry?: THREE.BufferGeometry|null, _gpuTrunkGeometry?: THREE.BufferGeometry|null, _gpuLeavesBaseGroupKey?: string, _gpuTrunkBaseGroupKey?: string, _gpuLeavesSharedMaterialKey?: string, _gpuTrunkSharedMaterialKey?: string, _gpuLeavesResolvedSide?: THREE.Side, _gpuTrunkResolvedSide?: THREE.Side }} FoliageObjectTypeCacheEntry */", + " /**", + " * Cached result of the automatic polyScale heuristic.", + " * polyScale is the sway-response multiplier used by the shader; when the", + " * behavior sets polyScale to 0, onCreated estimates it from asset size and complexity", + " * so different foliage meshes bend with a more comparable visual intensity.", + " * @typedef {{ polyScale: number, polyScaleRaw: number, sizeFactor: number, complexityFactor: number, vertexFactor: number, triFactor: number, structureFactor: number, planeFactor: number, responseGain: number, planeLikeRatio: number, baseByType: number, boundsMode: string }} FoliageAutoPolyScaleEntry", + " */", + " /** @typedef {{ isAlphaLikely: (mat: FoliageInspectableMaterial|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterial|null|undefined) => number }} FoliageObjectTypeAnalysisHelpers */", + " /** @typedef {{ patchedMaterials: WeakSet, sharedByKey: Map, activeMaterials: Set, materialRegistry: FoliageSharedMaterialRegistry, objectTypeCacheRegistry: FoliageObjectTypeCache, nonInstancedRegistry: FoliageNonInstancedRegistry, instancingState: FoliageInstancingState, instancingCoordinator: FoliageInstancingCoordinator, cullingCoordinator: FoliageCullingCoordinator, shadowUniformSync: FoliageShadowUniformSync, time: number, gustVersion: number, debugPrinted: Set, gustEnabled: boolean, gustStrength: number, gustScale: number, gustSpeed: number, gustThreshold: number, gustContrast: number, gustTexture: THREE.Texture|null, gustTextureKey: string, gustTextureResourceName: string, gustFallbackTex: THREE.Texture|null, _gustFallbackIsStripe: boolean, _gustDefineDirty: boolean, fadeUpdateHz?: number, cullAccum?: number, cullInterval?: number, _lastLoggedFadeHz?: number, lastCullTime?: number, _lastFadeUpdateHz?: number, lastCamX?: number, lastCamY?: number, lastCamZ?: number, _fadeParamsDirty?: boolean, objectTypeCache: Map, _objectTypeCacheState: FoliageObjectTypeCacheState, geometryBBoxCache: WeakMap, autoPolyScaleCache: Map, nonInstancedFadeObjects: FoliageNonInstancedEntry[], nonInstancedStaticObjects: FoliageNonInstancedEntry[], _nonInstancedStaticCheckAccum: number, instancing: FoliageInstancingState, GUST_DEFAULTS: GustDefaults, _materialBuckets: FoliageMaterialBucketsShape, _frameCtx: SharedFrameContext|null, _frustumShared: THREE.Frustum|null, _projViewMatrixShared: THREE.Matrix4|null, _behaviorPropsCache?: WeakMap|null, _tmpInstanceCullSphere?: { cx: number, cy: number, cz: number, radius: number }, _tmpInstanceCullSphereA?: { cx: number, cy: number, cz: number, radius: number }, _tmpInstanceCullSphereB?: { cx: number, cy: number, cz: number, radius: number }, _tmpInstanceCullSphereShared?: { cx: number, cy: number, cz: number, radius: number }, _tmpVec3NonInstancedFade: THREE.Vector3|null, _libReady: boolean, getBehaviorProps: (behavior: FoliageBehavior|null|undefined) => FoliageBehaviorProps|null, resetMaterialBuckets: () => void, registerActiveMaterial: (mat: THREE.Material|null|undefined) => void, unregisterActiveMaterial: (mat: THREE.Material|null|undefined) => void, disposeShadowMaterials: (mat: THREE.Material|null|undefined) => void, markGustForRecompile: () => void, ensureGustFallbackTex: () => THREE.Texture|null, disposeNonFallbackTex: (tex: THREE.Texture|null|undefined) => void, cleanupForSceneChange: () => void, _syncObjectTypeCacheAliases: () => FoliageSceneState, _syncNonInstancedAliases: () => FoliageSceneState, _syncInstancingAliases: () => FoliageSceneState, _readBoolArg: (efc: FoliageEventsFunctionContext, name: string, fallback: boolean) => boolean, _readNumArg: (efc: FoliageEventsFunctionContext, name: string, fallback: number) => number, _readStringArg: (efc: FoliageEventsFunctionContext, name: string, fallback: string) => string, buildFrameContext: (runtimeScene: gdjs.RuntimeScene, eventsFunctionContext: FoliageEventsFunctionContext) => SharedFrameContext, _ensureFrustum: (ctx: SharedFrameContext) => THREE.Frustum|null }} FoliageSceneStateShape */", + " /** @typedef {{ _libReady: boolean, getSceneState: (runtimeScene: gdjs.RuntimeScene) => FoliageSceneState, normalizeCullingMode: (v: FoliagePrimitiveValue) => { name: string, code: number }, resolveRenderSide: (sourceSide: THREE.Side|undefined|null, cullingModeName: string) => THREE.Side, buildSideSuffix: (cullingModeCode: number, renderSide: THREE.Side|undefined|null) => string, buildPbrSuffix: (customLitValue: boolean, metallicValue: number, roughnessValue: number, specularValue: number, normalStrengthValue: number, aoStrengthValue: number, envStrengthValue: number) => string, buildGpuFastCacheKey: (localSharedKey: string, localMaterialName: string, localSwayType: string, localDistanceFadeEnabled: boolean, localPbrSuffix: string, localSideSuffix: string, localTwoSidedLighting: boolean, localPolyScaleAutoMode: boolean) => string, parseColor: (str: string, fallbackHex: string) => THREE.Color, isAlphaLikely: (mat: FoliageInspectableMaterial|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterial|null|undefined) => number, sanitizeKeyPart: (s: string) => string, meshMatchesSelection: (mesh: FoliageMeshLike, selection: FoliageMaterialSelection, srcRef: THREE.Material|null, srcName: string) => boolean, findFirstMatchingMesh: (root: THREE.Object3D, selection: FoliageMaterialSelection, srcRef: THREE.Material|null, srcName: string) => FoliageMeshLike|null, findSplitTreeMeshes: (root: THREE.Object3D, trunkMatcher: (mesh: FoliageMeshLike) => boolean, leavesMatcher: (mesh: FoliageMeshLike) => boolean) => { trunk: FoliageMeshLike|null, leaves: FoliageMeshLike|null } }} FoliageExtensionApi */", + " /** @typedef {typeof gdjs & { _natureElementsFoliageSway?: FoliageExtensionApi }} FoliageGdjs */", + " /** @typedef {gdjs.RuntimeScene & { _natureElementsFoliageSway?: FoliageSceneState }} FoliageRuntimeScene */", + " ", + " /**", + " * Cache for expensive per-object-type mesh/material analysis.", + " * onCreated uses this to reuse selection, stats, and fast-path metadata across", + " * many runtime objects of the same source asset.", + " */", + " class FoliageObjectTypeCache {", + " constructor() {", + " /** @type {Map} */", + " this.entries = new Map();", + " /** @type {FoliageObjectTypeCacheState} */", + " this.state = { tick: 0, setCount: 0, meta: new Map() };", + " /** @type {WeakMap} */", + " this.geometryBBoxCache = new WeakMap();", + " /** @type {Map} */", + " this.autoPolyScaleCache = new Map();", + " /** @type {Set} */", + " this.debugPrinted = new Set();", + " this.reset();", + " }", + "", + " /**", + " * Drop cached object references so scene teardown does not keep stale geometry", + " * or material handles alive longer than necessary.", + " * @param {FoliageObjectTypeCacheEntry|null|undefined} entry", + " */", + " _clearEntryCachedRefs(entry) {", + " if (!entry) return;", + " if (entry.records) {", + " for (var ri = 0; ri < entry.records.length; ri++) {", + " entry.records[ri].materialRef = null;", + " }", + " }", + " entry._cachedGeometry = null;", + " entry._gpuGeometry = null;", + " }", + "", + " /**", + " * Reset all cache domains owned by this registry.", + " * @returns {void}", + " */", + " reset() {", + " if (this.entries && typeof this.entries.values === \"function\") {", + " for (var entry of this.entries.values()) {", + " this._clearEntryCachedRefs(entry);", + " }", + " }", + " this.entries = new Map();", + " this.state = { tick: 0, setCount: 0, meta: new Map() };", + " this.geometryBBoxCache = new WeakMap();", + " this.autoPolyScaleCache = new Map();", + " this.debugPrinted = new Set();", + " }", + "", + " /**", + " * Classify a cache key so pruning can cap each cache family independently.", + " * @param {string} key", + " * @returns {\"rop\"|\"stats\"|\"fast\"}", + " */", + " classifyKey(key) {", + " if (!key) return \"rop\";", + " if (key.indexOf(\"::GPU::FD\") !== -1) return \"fast\";", + " if (key.indexOf(\"::collectAndStats\") !== -1) return \"stats\";", + " return \"rop\";", + " }", + "", + " /**", + " * Mark a key as recently used for LRU-style pruning.", + " * @param {string} key", + " * @param {string} [kindHint]", + " * @returns {void}", + " */", + " touch(key, kindHint) {", + " var kind = kindHint || this.classifyKey(key);", + " this.state.tick++;", + " this.state.meta.set(key, { kind: kind, lastUsed: this.state.tick });", + " }", + "", + " /**", + " * Prune oversized cache families without touching keys that are still in active use.", + " * @param {boolean} force", + " * @returns {void}", + " */", + " pruneIfNeeded(force) {", + " this.state.setCount++;", + " if (!force && (this.state.setCount % 200) !== 0) return;", + " var oversByKind = { rop: 0, stats: 0, fast: 0 };", + " var counts = { rop: 0, stats: 0, fast: 0 };", + " var self = this;", + " this.state.meta.forEach(function(meta, key) {", + " if (!self.entries.has(key)) {", + " self.state.meta.delete(key);", + " return;", + " }", + " var kind = (meta && meta.kind) || self.classifyKey(key);", + " if (counts[kind] === undefined) counts[kind] = 0;", + " counts[kind]++;", + " });", + " oversByKind.rop = Math.max(0, (counts.rop || 0) - 512);", + " oversByKind.stats = Math.max(0, (counts.stats || 0) - 256);", + " oversByKind.fast = Math.max(0, (counts.fast || 0) - 1024);", + " if (!oversByKind.rop && !oversByKind.stats && !oversByKind.fast) return;", + " var candidates = [];", + " this.state.meta.forEach(function(meta, key) {", + " candidates.push({", + " key: key,", + " kind: (meta && meta.kind) || self.classifyKey(key),", + " lastUsed: meta && isFinite(meta.lastUsed) ? meta.lastUsed : 0", + " });", + " });", + " candidates.sort(function(a, b) { return a.lastUsed - b.lastUsed; });", + " for (var iPr = 0; iPr < candidates.length; iPr++) {", + " var c = candidates[iPr];", + " if (!oversByKind[c.kind]) continue;", + " self._clearEntryCachedRefs(self.entries.get(c.key));", + " self.entries.delete(c.key);", + " self.state.meta.delete(c.key);", + " oversByKind[c.kind]--;", + " if (!oversByKind.rop && !oversByKind.stats && !oversByKind.fast) break;", + " }", + " }", + "", + " /**", + " * @param {string} key", + " * @returns {FoliageObjectTypeCacheEntry|undefined}", + " */", + " get(key) {", + " var value = this.entries.get(key);", + " if (value !== undefined) this.touch(key);", + " return value;", + " }", + "", + " /**", + " * @param {string} key", + " * @param {FoliageObjectTypeCacheEntry} value", + " * @param {string|undefined} kindHint", + " * @returns {FoliageObjectTypeCacheEntry}", + " */", + " set(key, value, kindHint) {", + " this.entries.set(key, value);", + " this.touch(key, kindHint);", + " this.pruneIfNeeded(false);", + " return value;", + " }", + "", + " /**", + " * @param {THREE.BufferGeometry|null|undefined} geometry", + " * @returns {THREE.Box3|null}", + " */", + " getCachedBBox(geometry) {", + " if (!geometry) return null;", + " return this.geometryBBoxCache.get(geometry) || null;", + " }", + "", + " /**", + " * @param {THREE.BufferGeometry|null|undefined} geometry", + " * @param {THREE.Box3|null|undefined} bbox", + " * @returns {THREE.Box3|null|undefined}", + " */", + " setCachedBBox(geometry, bbox) {", + " if (!geometry || !bbox) return bbox;", + " this.geometryBBoxCache.set(geometry, bbox);", + " return bbox;", + " }", + "", + " /**", + " * @param {string} key", + " * @returns {FoliageAutoPolyScaleEntry|null|undefined}", + " */", + " getAutoPolyScale(key) {", + " return this.autoPolyScaleCache.get(key);", + " }", + "", + " /**", + " * Store a computed auto polyScale result so repeated objects of the same", + " * asset can skip the size/complexity analysis path.", + " * @param {string} key", + " * @param {FoliageAutoPolyScaleEntry} entry", + " * @returns {FoliageAutoPolyScaleEntry}", + " */", + " setAutoPolyScale(key, entry) {", + " this.autoPolyScaleCache.set(key, entry);", + " return entry;", + " }", + "", + " /**", + " * @param {string} key", + " * @returns {boolean}", + " */", + " hasDebugKey(key) {", + " return this.debugPrinted.has(key);", + " }", + "", + " /**", + " * @param {string} key", + " * @returns {void}", + " */", + " markDebugKey(key) {", + " this.debugPrinted.add(key);", + " }", + "", + " /**", + " * @param {FoliageObjectRecord[]} records", + " * @param {string} wantedName", + " * @returns {FoliageMaterialSelection|null}", + " */", + " resolveByName(records, wantedName) {", + " for (var i = 0; i < records.length; i++) {", + " var r = records[i];", + " if ((r.materialName || \"\") === wantedName) {", + " return {", + " srcRef: r.materialRef,", + " pickedId: wantedName,", + " matchMode: \"name\",", + " matchName: wantedName", + " };", + " }", + " }", + " return null;", + " }", + "", + " /**", + " * @param {FoliageObjectRecord[]} records", + " * @returns {FoliageMaterialSelection|null}", + " */", + " pickBest(records) {", + " var bestRec = null;", + " var bestScore = -9999;", + " var bestIndex = -1;", + " for (var i = 0; i < records.length; i++) {", + " var r = records[i];", + " var sc = r.score;", + " if (!bestRec) {", + " bestRec = r;", + " bestScore = sc;", + " bestIndex = i;", + " continue;", + " }", + " if (sc > bestScore) {", + " bestRec = r;", + " bestScore = sc;", + " bestIndex = i;", + " continue;", + " }", + " if (sc === bestScore) {", + " var a = r.alphaLikely ? 1 : 0;", + " var b = bestRec.alphaLikely ? 1 : 0;", + " if (a > b) {", + " bestRec = r;", + " bestScore = sc;", + " bestIndex = i;", + " }", + " }", + " }", + " if (!bestRec) return null;", + " var pickedId =", + " bestRec.materialName && String(bestRec.materialName).trim() !== \"\"", + " ? String(bestRec.materialName).trim()", + " : \"__AUTO_INDEX_\" + bestIndex;", + " return {", + " srcRef: bestRec.materialRef,", + " pickedId: pickedId,", + " matchMode: \"auto\",", + " matchName:", + " bestRec.materialName && String(bestRec.materialName).trim() !== \"\"", + " ? String(bestRec.materialName).trim()", + " : \"\"", + " };", + " }", + "", + " /**", + " * Traverse an object once and collect both material records and shared geometry stats", + " * that later selection and auto-poly-scale logic can reuse.", + " * @param {THREE.Object3D} root", + " * @param {{ isAlphaLikely: (mat: FoliageInspectableMaterial|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterial|null|undefined) => number }} helpers", + " * @returns {{ records: FoliageObjectRecord[], statsByRef: Map, statsByName: Map }}", + " */", + " collectAndStats(root, helpers) {", + " if (!root || typeof root.traverse !== \"function\") {", + " return { records: [], statsByRef: new Map(), statsByName: new Map() };", + " }", + " var isAlphaLikely = helpers && typeof helpers.isAlphaLikely === \"function\"", + " ? helpers.isAlphaLikely", + " : function() { return false; };", + " var scoreMaterial = helpers && typeof helpers.scoreMaterial === \"function\"", + " ? helpers.scoreMaterial", + " : function() { return -9999; };", + " var records = [];", + " var statsByRef = new Map();", + " var statsByName = new Map();", + " /** @returns {FoliageDataRecord} */", + " function ensureStats(map, key) {", + " var st = map.get(key);", + " if (!st) {", + " st = {", + " zMin: Infinity,", + " zMax: -Infinity,", + " relZMin: Infinity,", + " relZMax: -Infinity,", + " hasGeom: false,", + " totalVerts: 0,", + " totalTris: 0,", + " meshCount: 0,", + " slotCount: 0,", + " planeLikeCount: 0,", + " sizeLocalMin: new THREE.Vector3(Infinity, Infinity, Infinity),", + " sizeLocalMax: new THREE.Vector3(-Infinity, -Infinity, -Infinity),", + " sizeWorldMin: new THREE.Vector3(Infinity, Infinity, Infinity),", + " sizeWorldMax: new THREE.Vector3(-Infinity, -Infinity, -Infinity),", + " _meshIds: new Set()", + " };", + " map.set(key, st);", + " }", + " return st;", + " }", + " function markMeshUsage(st, meshKey) {", + " if (!st) return;", + " if (!st._meshIds || typeof st._meshIds.has !== \"function\") st._meshIds = new Set();", + " if (!st._meshIds.has(meshKey)) {", + " st._meshIds.add(meshKey);", + " st.meshCount++;", + " }", + " }", + " root.updateMatrixWorld(true);", + " var tmpBox = new THREE.Box3();", + " var tmpBoxLocal = new THREE.Box3();", + " var rootWorldInv = new THREE.Matrix4();", + " var meshToRoot = new THREE.Matrix4();", + " var hasRootWorldInv = true;", + " try { rootWorldInv.copy(root.matrixWorld).invert(); } catch (eRootInv) { hasRootWorldInv = false; }", + " var registry = this;", + " root.traverse(function(o) {", + " var mesh = /** @type {THREE.Object3D & { isMesh?: boolean, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|THREE.Material[]|null, id?: number, name?: string, matrixWorld: THREE.Matrix4 }} */ (o);", + " if (!mesh || !mesh.isMesh) return;", + " var meshName = mesh.name && String(mesh.name).trim() !== \"\" ? String(mesh.name).trim() : \"(unnamed mesh)\";", + " if (!mesh.material) return;", + " var geom = mesh.geometry;", + " var bbox = null;", + " var vertCount = 0;", + " var triCount = 0;", + " var meshKey = isFinite(mesh.id) ? (\"M:\" + mesh.id) : (\"MN:\" + meshName);", + " if (geom) {", + " if (geom.attributes && geom.attributes.position && isFinite(geom.attributes.position.count)) {", + " vertCount = geom.attributes.position.count;", + " }", + " if (geom.index && isFinite(geom.index.count)) {", + " triCount = geom.index.count / 3;", + " } else if (vertCount > 0) {", + " triCount = vertCount / 3;", + " }", + " var cachedBBox = registry.getCachedBBox(geom);", + " if (cachedBBox) {", + " bbox = cachedBBox;", + " } else {", + " if (geom.boundingBox) {", + " bbox = geom.boundingBox;", + " } else {", + " try { geom.computeBoundingBox(); } catch (e) {}", + " bbox = geom.boundingBox || null;", + " }", + " if (bbox) registry.setCachedBBox(geom, bbox);", + " }", + " }", + " var hasWorld = false;", + " var wMinX = 0, wMinY = 0, wMinZ = 0;", + " var wMaxX = 0, wMaxY = 0, wMaxZ = 0;", + " var hasLocal = false;", + " var lMinX = 0, lMinY = 0, lMinZ = 0;", + " var lMaxX = 0, lMaxY = 0, lMaxZ = 0;", + " var isPlaneLike = false;", + " var relMinZ = 0, relMaxZ = 0;", + " if (bbox) {", + " tmpBox.copy(bbox);", + " tmpBox.applyMatrix4(mesh.matrixWorld);", + " hasWorld = true;", + " wMinX = tmpBox.min.x; wMinY = tmpBox.min.y; wMinZ = tmpBox.min.z;", + " wMaxX = tmpBox.max.x; wMaxY = tmpBox.max.y; wMaxZ = tmpBox.max.z;", + " var meshOriginZ = mesh.matrixWorld.elements[14];", + " relMinZ = wMinZ - meshOriginZ;", + " relMaxZ = wMaxZ - meshOriginZ;", + " if (hasRootWorldInv) {", + " try {", + " meshToRoot.multiplyMatrices(rootWorldInv, mesh.matrixWorld);", + " tmpBoxLocal.copy(bbox);", + " tmpBoxLocal.applyMatrix4(meshToRoot);", + " hasLocal = true;", + " lMinX = tmpBoxLocal.min.x; lMinY = tmpBoxLocal.min.y; lMinZ = tmpBoxLocal.min.z;", + " lMaxX = tmpBoxLocal.max.x; lMaxY = tmpBoxLocal.max.y; lMaxZ = tmpBoxLocal.max.z;", + " } catch (eLocalAabb) {", + " hasLocal = false;", + " }", + " }", + " if (geom && geom.type === \"PlaneGeometry\") {", + " isPlaneLike = true;", + " } else {", + " var bx = bbox.max.x - bbox.min.x;", + " var by = bbox.max.y - bbox.min.y;", + " var bz = bbox.max.z - bbox.min.z;", + " var maxDim = Math.max(bx, by, bz);", + " if (isFinite(maxDim) && maxDim > 0) {", + " var minDim = Math.min(bx, by, bz);", + " var thinness = minDim / maxDim;", + " if (thinness < 0.03) isPlaneLike = true;", + " }", + " }", + " }", + " function updateStats(st, verts, tris) {", + " if (bbox) {", + " st.hasGeom = true;", + " if (bbox.min.z < st.zMin) st.zMin = bbox.min.z;", + " if (bbox.max.z > st.zMax) st.zMax = bbox.max.z;", + " }", + " if (verts) st.totalVerts += verts;", + " if (tris) st.totalTris += tris;", + " st.slotCount++;", + " if (isPlaneLike) st.planeLikeCount++;", + " markMeshUsage(st, meshKey);", + " if (hasLocal) {", + " if (lMinX < st.sizeLocalMin.x) st.sizeLocalMin.x = lMinX;", + " if (lMinY < st.sizeLocalMin.y) st.sizeLocalMin.y = lMinY;", + " if (lMinZ < st.sizeLocalMin.z) st.sizeLocalMin.z = lMinZ;", + " if (lMaxX > st.sizeLocalMax.x) st.sizeLocalMax.x = lMaxX;", + " if (lMaxY > st.sizeLocalMax.y) st.sizeLocalMax.y = lMaxY;", + " if (lMaxZ > st.sizeLocalMax.z) st.sizeLocalMax.z = lMaxZ;", + " }", + " if (hasWorld) {", + " if (wMinX < st.sizeWorldMin.x) st.sizeWorldMin.x = wMinX;", + " if (wMinY < st.sizeWorldMin.y) st.sizeWorldMin.y = wMinY;", + " if (wMinZ < st.sizeWorldMin.z) st.sizeWorldMin.z = wMinZ;", + " if (wMaxX > st.sizeWorldMax.x) st.sizeWorldMax.x = wMaxX;", + " if (wMaxY > st.sizeWorldMax.y) st.sizeWorldMax.y = wMaxY;", + " if (wMaxZ > st.sizeWorldMax.z) st.sizeWorldMax.z = wMaxZ;", + " if (relMinZ < st.relZMin) st.relZMin = relMinZ;", + " if (relMaxZ > st.relZMax) st.relZMax = relMaxZ;", + " }", + " }", + " if (Array.isArray(mesh.material)) {", + " var mats = mesh.material;", + " var vertShare = (vertCount && mats.length > 0) ? (vertCount / mats.length) : 0;", + " var triShare = (triCount && mats.length > 0) ? (triCount / mats.length) : 0;", + " for (var i = 0; i < mats.length; i++) {", + " var m = mats[i];", + " if (!m) continue;", + " var mName = m.name && String(m.name).trim() !== \"\" ? String(m.name).trim() : \"\";", + " records.push({", + " meshName: meshName,", + " materialIndex: i,", + " materialRef: m,", + " materialName: mName,", + " alphaLikely: isAlphaLikely(m),", + " score: scoreMaterial(m)", + " });", + " updateStats(ensureStats(statsByRef, m), vertShare, triShare);", + " if (mName) updateStats(ensureStats(statsByName, mName), vertShare, triShare);", + " }", + " } else {", + " var mSingle = mesh.material;", + " if (!mSingle) return;", + " var mNameSingle = mSingle.name && String(mSingle.name).trim() !== \"\" ? String(mSingle.name).trim() : \"\";", + " records.push({", + " meshName: meshName,", + " materialIndex: 0,", + " materialRef: mSingle,", + " materialName: mNameSingle,", + " alphaLikely: isAlphaLikely(mSingle),", + " score: scoreMaterial(mSingle)", + " });", + " updateStats(ensureStats(statsByRef, mSingle), vertCount, triCount);", + " if (mNameSingle) updateStats(ensureStats(statsByName, mNameSingle), vertCount, triCount);", + " }", + " });", + " statsByRef.forEach(function(stRefFinal) {", + " if (stRefFinal && stRefFinal._meshIds) delete stRefFinal._meshIds;", + " });", + " statsByName.forEach(function(stNameFinal) {", + " if (stNameFinal && stNameFinal._meshIds) delete stNameFinal._meshIds;", + " });", + " return { records: records, statsByRef: statsByRef, statsByName: statsByName };", + " }", + "", + " /**", + " * Resolve the best material selection for an object type, reusing cached", + " * full-object stats when possible so repeated onCreated calls avoid retraversal.", + " * @param {THREE.Object3D} root", + " * @param {string} wantedName", + " * @param {string} sharedKeyForCache", + " * @param {{ isAlphaLikely: (mat: FoliageInspectableMaterial|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterial|null|undefined) => number }} helpers", + " * @returns {FoliageObjectTypeCacheEntry}", + " */", + " resolveOrPick(root, wantedName, sharedKeyForCache, helpers) {", + " var collected = null;", + " if (sharedKeyForCache) {", + " var statsCacheKey = sharedKeyForCache + \"::collectAndStats\";", + " var cachedStats = this.get(statsCacheKey);", + " if (cachedStats && cachedStats.records && cachedStats.statsByRef && cachedStats.statsByName) {", + " collected = cachedStats;", + " } else {", + " collected = this.collectAndStats(root, helpers);", + " this.set(statsCacheKey, collected, \"stats\");", + " }", + " } else {", + " collected = this.collectAndStats(root, helpers);", + " }", + " var records = collected.records;", + " var selection = null;", + " if (wantedName && wantedName.trim() !== \"\") {", + " selection = this.resolveByName(records, wantedName.trim());", + " }", + " if (!selection) selection = this.pickBest(records);", + " return {", + " records: records,", + " selection: selection,", + " statsByRef: collected.statsByRef,", + " statsByName: collected.statsByName", + " };", + " }", + "", + " /**", + " * @param {string} objName", + " * @param {FoliageObjectRecord[]} records", + " * @returns {void}", + " */", + " debugPrintFromRecords(objName, records) {", + " try {", + " var matSet = new Set();", + " var meshSet = new Set();", + " for (var i = 0; i < records.length; i++) {", + " var r = records[i];", + " meshSet.add(r.meshName);", + " matSet.add(r.materialName && r.materialName !== \"\" ? r.materialName : \"(unnamed material)\");", + " }", + " console.log(\"[\" + objName + \"] \" + matSet.size + \" materials: \" + Array.from(matSet).join(\", \"));", + " } catch (e) {}", + " }", + " }", + " ", + " function parseBool(v, fallback) {", + " if (v === undefined || v === null) return fallback;", + " if (typeof v === \"boolean\") return v;", + " var s = String(v).trim().toLowerCase();", + " if (s === \"true\" || s === \"1\" || s === \"yes\" || s === \"on\") return true;", + " if (s === \"false\" || s === \"0\" || s === \"no\" || s === \"off\") return false;", + " return fallback;", + " }", + " ", + " function readClamped(v, fallback, lo, hi) {", + " var n = Number(v);", + " if (!isFinite(n)) n = fallback;", + " if (n < lo) n = lo;", + " if (n > hi) n = hi;", + " return n;", + " }", + "", + " function readArgumentRaw(eventsFunctionContext, name) {", + " if (!eventsFunctionContext || typeof eventsFunctionContext.getArgument !== \"function\") return undefined;", + " return eventsFunctionContext.getArgument(name);", + " }", + "", + " function readBoolArg(eventsFunctionContext, name, fallback) {", + " var raw = readArgumentRaw(eventsFunctionContext, name);", + " if (raw === undefined || raw === null) return fallback;", + " if (typeof raw === \"boolean\" || typeof raw === \"number\" || typeof raw === \"string\") {", + " return parseBool(raw, fallback);", + " }", + " var variable = /** @type {FoliageArgumentVariableLike} */ (raw);", + " if (typeof variable.getAsBoolean === \"function\") return !!variable.getAsBoolean();", + " if (typeof variable.getAsString === \"function\") return parseBool(variable.getAsString(), fallback);", + " return fallback;", + " }", + "", + " function readNumArg(eventsFunctionContext, name, fallback) {", + " var raw = readArgumentRaw(eventsFunctionContext, name);", + " if (raw === undefined || raw === null) return fallback;", + " /** @type {number} */", + " var n;", + " if (typeof raw === \"boolean\" || typeof raw === \"number\" || typeof raw === \"string\") {", + " n = Number(raw);", + " } else {", + " var variable = /** @type {FoliageArgumentVariableLike} */ (raw);", + " if (typeof variable.getAsNumber === \"function\") {", + " n = Number(variable.getAsNumber());", + " } else if (typeof variable.getAsString === \"function\") {", + " n = Number(variable.getAsString());", + " } else {", + " return fallback;", + " }", + " }", + " return isFinite(n) ? n : fallback;", + " }", + "", + " function readStringArg(eventsFunctionContext, name, fallback) {", + " var raw = readArgumentRaw(eventsFunctionContext, name);", + " if (raw === undefined || raw === null) return fallback;", + " if (typeof raw === \"boolean\" || typeof raw === \"number\" || typeof raw === \"string\") {", + " return String(raw);", + " }", + " var variable = /** @type {FoliageArgumentVariableLike} */ (raw);", + " if (typeof variable.getAsString === \"function\") {", + " var s = variable.getAsString();", + " return (s === undefined || s === null) ? fallback : String(s);", + " }", + " return fallback;", + " }", + " ", + " /**", + " * Grow capacities geometrically so instancing buffers do not reallocate on every small bump.", + " * @param {number} n", + " * @returns {number}", + " */", + " function nextPow2(n) {", + " n = n | 0;", + " if (n <= 1) return 1;", + " n--;", + " n |= n >> 1;", + " n |= n >> 2;", + " n |= n >> 4;", + " n |= n >> 8;", + " n |= n >> 16;", + " n++;", + " return n;", + " }", + " ", + " /**", + " * Build or reuse a per-frame snapshot of behavior properties.", + " * This keeps generated getter calls centralized and avoids rereading the same", + " * behavior repeatedly during one update frame.", + " * @param {FoliageBehavior|null|undefined} behavior", + " * @returns {FoliageBehaviorProps|null}", + " */", + " function getBehaviorProps(behavior) {", + " if (!behavior) return null;", + " var state = /** @type {FoliageSceneState|undefined|null} */ (this);", + " var cache = state && state._behaviorPropsCache && typeof state._behaviorPropsCache.get === \"function\" ? state._behaviorPropsCache : null;", + " if (!cache) return new FoliageBehaviorProps(behavior);", + " var cached = cache.get(behavior);", + " if (cached) return cached;", + " var props = new FoliageBehaviorProps(behavior);", + " cache.set(behavior, props);", + " return props;", + " }", + "", + " /**", + " * Normalize editor / legacy culling inputs into one canonical runtime mode.", + " * @param {FoliagePrimitiveValue} v", + " * @returns {{ name: string, code: number }}", + " */", + " function normalizeCullingMode(v) {", + " var s = String(v === undefined || v === null ? \"useSource\" : v).trim().toLowerCase();", + " if (s === \"1\" || s === \"backfacecullingon\" || s === \"backface_culling_on\" || s === \"on\") {", + " return { name: \"backfaceCullingOn\", code: 1 };", + " }", + " if (s === \"2\" || s === \"backfacecullingoff\" || s === \"backface_culling_off\" || s === \"off\") {", + " return { name: \"backfaceCullingOff\", code: 2 };", + " }", + " return { name: \"useSource\", code: 0 };", + " }", + "", + " /**", + " * @param {THREE.Side|undefined|null} sourceSide", + " * @param {string} cullingModeName", + " * @returns {THREE.Side}", + " */", + " function resolveRenderSide(sourceSide, cullingModeName) {", + " if (cullingModeName === \"backfaceCullingOn\") return THREE.FrontSide;", + " if (cullingModeName === \"backfaceCullingOff\") return THREE.DoubleSide;", + " return /** @type {THREE.Side} */ ((typeof sourceSide === \"number\") ? sourceSide : THREE.FrontSide);", + " }", + "", + " /**", + " * Build the cache-key suffix for side/culling variants that change GL state or shader behavior.", + " * @param {number} cullingModeCode", + " * @param {THREE.Side|undefined|null} renderSide", + " * @returns {string}", + " */", + " function buildSideSuffix(cullingModeCode, renderSide) {", + " var sd = (typeof renderSide === \"number\") ? renderSide : THREE.FrontSide;", + " return \"_CM\" + cullingModeCode + \"_SD\" + sd;", + " }", + "", + " /**", + " * Build the cache-key suffix for shader-relevant custom PBR options.", + " * @returns {string}", + " */", + " function buildPbrSuffix(customLitValue, metallicValue, roughnessValue, specularValue, normalStrengthValue, aoStrengthValue, envStrengthValue) {", + " if (!customLitValue) return \"_CL0\";", + " return \"_CL1_M\" + Math.round(metallicValue * 100) + \"_R\" + Math.round(roughnessValue * 100) + \"_S\" + Math.round(specularValue * 100) + \"_N\" + Math.round(normalStrengthValue * 100) + \"_AO\" + Math.round(aoStrengthValue * 100) + \"_E\" + Math.round(envStrengthValue * 100);", + " }", + "", + " /**", + " * Build the fast-path key used to reuse GPU-instancing analysis for repeated objects.", + " * @returns {string}", + " */", + " function buildGpuFastCacheKey(localSharedKey, localMaterialName, localSwayType, localDistanceFadeEnabled, localPbrSuffix, localSideSuffix, localTwoSidedLighting, localPolyScaleAutoMode) {", + " return localSharedKey + \"::\" + (localMaterialName || \"\") + \"::\" + localSwayType + \"::GPU::FD\" + (localDistanceFadeEnabled ? \"1\" : \"0\") + localPbrSuffix + localSideSuffix + \"_TSL\" + (localTwoSidedLighting ? \"1\" : \"0\") + \"_PA\" + (localPolyScaleAutoMode ? \"1\" : \"0\");", + " }", + "", + " /**", + " * Parse a GDevelop color string (`r;g;b`) into a linear-space Three.js color.", + " * @param {string} str", + " * @param {string} fallbackHex", + " * @returns {THREE.Color}", + " */", + " function parseColor(str, fallbackHex) {", + " try {", + " var c;", + " if (!str) {", + " c = new THREE.Color(fallbackHex);", + " return c;", + " }", + " var p = String(str).split(\";\");", + " if (p.length !== 3) return new THREE.Color(fallbackHex);", + "", + " c = new THREE.Color(+p[0] / 255, +p[1] / 255, +p[2] / 255);", + " if (c.convertSRGBToLinear) c.convertSRGBToLinear();", + " return c;", + " } catch (e) {", + " return new THREE.Color(fallbackHex);", + " }", + " }", + "", + " /**", + " * Heuristic used by object analysis to guess whether a material is leaf/card-like.", + " * @param {FoliageInspectableMaterial|null|undefined} mat", + " * @returns {boolean}", + " */", + " function isAlphaLikely(mat) {", + " if (!mat) return false;", + " if (mat.alphaMap) return true;", + " if (mat.alphaTest && mat.alphaTest > 0) return true;", + " if (mat.transparent === true) return true;", + " if (mat.opacity !== undefined && mat.opacity < 0.999) return true;", + " return false;", + " }", + "", + " /**", + " * Score materials for automatic foliage-part selection when no explicit name matches.", + " * Higher scores bias toward alpha-cut foliage materials and away from bark/trunk materials.", + " * @param {FoliageInspectableMaterial|null|undefined} mat", + " * @returns {number}", + " */", + " function scoreMaterial(mat) {", + " if (!mat) return -9999;", + " var name = String(mat.name || \"\").toLowerCase();", + "", + " var s = 0;", + " if (mat.alphaMap) s += 8;", + " if (mat.alphaTest && mat.alphaTest > 0) s += 7;", + " if (mat.transparent === true) s += 6;", + " if (mat.opacity !== undefined && mat.opacity < 0.999) s += 4;", + " if (mat.map) s += 1;", + "", + " if (name.includes(\"leaf\") || name.includes(\"leaves\") || name.includes(\"foliage\")) s += 5;", + " if (name.includes(\"grass\") || name.includes(\"bush\")) s += 3;", + "", + " if (name.includes(\"trunk\") || name.includes(\"bark\") || name.includes(\"wood\") || name.includes(\"stem\")) s -= 6;", + "", + " return s;", + " }", + "", + " function sanitizeKeyPart(s) {", + " if (!s) return \"(empty)\";", + " return String(s).replace(/::/g, \"_\").trim() || \"(empty)\";", + " }", + "", + " /**", + " * @param {FoliageMeshLike} mesh", + " * @param {FoliageMaterialSelection} selection", + " * @param {THREE.Material|null} srcRef", + " * @param {string} srcName", + " */", + " function meshMatchesSelection(mesh, selection, srcRef, srcName) {", + " if (!mesh || !mesh.isMesh || !mesh.geometry || !mesh.material) return false;", + "", + " if (!Array.isArray(mesh.material)) {", + " var mSingle = mesh.material;", + " if (!mSingle) return false;", + "", + " if (selection.matchMode === \"name\") {", + " return (mSingle.name || \"\") === selection.matchName;", + " }", + " if (mSingle === srcRef) return true;", + " return !!(srcName && (mSingle.name || \"\") === srcName && isAlphaLikely(mSingle));", + " }", + "", + " var mats = mesh.material;", + " for (var i = 0; i < mats.length; i++) {", + " var m = mats[i];", + " if (!m) continue;", + "", + " if (selection.matchMode === \"name\") {", + " if ((m.name || \"\") === selection.matchName) return true;", + " } else {", + " if (m === srcRef) return true;", + " if (srcName && (m.name || \"\") === srcName && isAlphaLikely(m)) return true;", + " }", + " }", + " return false;", + " }", + "", + " /**", + " * Find the first mesh that matches the resolved material selection for this object.", + " * @param {THREE.Object3D} root", + " * @param {FoliageMaterialSelection} selection", + " * @param {THREE.Material|null} srcRef", + " * @param {string} srcName", + " * @returns {FoliageMeshLike|null}", + " */", + " function findFirstMatchingMesh(root, selection, srcRef, srcName) {", + " /** @type {FoliageMeshLike|null} */", + " var found = null;", + " root.traverse(function(o) {", + " if (found) return;", + " var mesh = /** @type {FoliageMeshLike} */ (o);", + " if (meshMatchesSelection(mesh, selection, srcRef, srcName)) {", + " found = mesh;", + " }", + " });", + " return found;", + " }", + "", + " /**", + " * Resolve trunk and leaves meshes in one traversal for split-tree setups such as leavesSway.", + " * @param {THREE.Object3D} root", + " * @param {(mesh: FoliageMeshLike) => boolean} trunkMatcher", + " * @param {(mesh: FoliageMeshLike) => boolean} leavesMatcher", + " * @returns {{ trunk: FoliageMeshLike|null, leaves: FoliageMeshLike|null }}", + " */", + " function findSplitTreeMeshes(root, trunkMatcher, leavesMatcher) {", + " /** @type {FoliageMeshLike|null} */", + " var trunk = null;", + " /** @type {FoliageMeshLike|null} */", + " var leaves = null;", + " root.traverse(function(o) {", + " if (trunk && leaves) return;", + " var mesh = /** @type {FoliageMeshLike} */ (o);", + " if (!mesh || !mesh.isMesh || !mesh.geometry || !mesh.material) return;", + " if (!trunk && trunkMatcher(mesh)) trunk = mesh;", + " if (!leaves && leavesMatcher(mesh)) leaves = mesh;", + " });", + " return { trunk: trunk, leaves: leaves };", + " }", + "", + " /**", + " * Owner of non-instanced fade/static registrations and their cleanup rules.", + " * Update code delegates list transitions here so non-instanced lifecycle logic", + " * stays separate from the frame-level fade math.", + " */", + " class FoliageNonInstancedRegistry {", + " /**", + " * @param {FoliageSceneState} state", + " */", + " constructor(state) {", + " /** @type {FoliageSceneState} */", + " this.state = state;", + " /** @type {FoliageNonInstancedEntry[]} */", + " this.fadeEntries = [];", + " /** @type {FoliageNonInstancedEntry[]} */", + " this.staticEntries = [];", + " /** @type {number} */", + " this.staticCheckAccum = 0;", + " this.reset();", + " }", + "", + " /**", + " * Mirror registry-owned lists back onto scene-state compatibility aliases.", + " * @returns {FoliageNonInstancedRegistry}", + " */", + " syncAliases() {", + " var state = this.state;", + " if (!state) return this;", + " state.nonInstancedFadeObjects = this.fadeEntries;", + " state.nonInstancedStaticObjects = this.staticEntries;", + " state._nonInstancedStaticCheckAccum = this.staticCheckAccum;", + " return this;", + " }", + "", + " /**", + " * @returns {FoliageNonInstancedRegistry}", + " */", + " reset() {", + " this.fadeEntries = [];", + " this.staticEntries = [];", + " this.staticCheckAccum = 0;", + " return this.syncAliases();", + " }", + "", + " /**", + " * @param {\"fade\"|\"static\"|FoliageNonInstancedEntry[]|null|undefined} fromList", + " * @returns {FoliageNonInstancedEntry[]|null}", + " */", + " _getList(fromList) {", + " if (fromList === \"fade\") return this.fadeEntries;", + " if (fromList === \"static\") return this.staticEntries;", + " return Array.isArray(fromList) ? fromList : null;", + " }", + "", + " /**", + " * @param {FoliageNonInstancedEntry[]|null|undefined} list", + " * @param {FoliageNonInstancedEntry|null|undefined} entry", + " * @returns {boolean}", + " */", + " _listHas(list, entry) {", + " if (!list || !entry) return false;", + " for (var i = 0; i < list.length; i++) {", + " if (list[i] === entry) return true;", + " }", + " return false;", + " }", + "", + " /**", + " * @param {FoliageNonInstancedEntry[]|null|undefined} list", + " * @param {FoliageNonInstancedEntry|null|undefined} entry", + " * @param {boolean} swapRemove", + " * @returns {boolean}", + " */", + " _removeFromList(list, entry, swapRemove) {", + " if (!list || !entry) return false;", + " for (var i = list.length - 1; i >= 0; i--) {", + " if (list[i] !== entry) continue;", + " if (swapRemove) {", + " var lastIdx = list.length - 1;", + " if (i < lastIdx) list[i] = list[lastIdx];", + " list.pop();", + " } else {", + " list.splice(i, 1);", + " }", + " return true;", + " }", + " return false;", + " }", + "", + " /**", + " * @param {gdjs.RuntimeObject3D|null|undefined} gdObj", + " * @param {THREE.Object3D|null|undefined} threeObj", + " * @returns {FoliageNonInstancedEntry|null}", + " */", + " findEntry(gdObj, threeObj) {", + " var i = 0;", + " for (i = 0; i < this.fadeEntries.length; i++) {", + " var activeEntry = this.fadeEntries[i];", + " if (activeEntry && (activeEntry.gdObj === gdObj || activeEntry.threeObj === threeObj)) return activeEntry;", + " }", + " for (i = 0; i < this.staticEntries.length; i++) {", + " var staticEntry = this.staticEntries[i];", + " if (staticEntry && (staticEntry.gdObj === gdObj || staticEntry.threeObj === threeObj)) return staticEntry;", + " }", + " return null;", + " }", + "", + " /**", + " * @param {FoliageNonInstancedEntry|null|undefined} entry", + " * @returns {void}", + " */", + " clearRegistration(entry) {", + " if (!entry) return;", + " var behavior = entry.gdObj ? /** @type {FoliageBehavior|null} */ (entry.gdObj.getBehavior(\"FoliageSwaying\")) : null;", + " if (behavior && behavior.__foliageNonInstancedRegistered) delete behavior.__foliageNonInstancedRegistered;", + " if (entry.threeObj && entry.threeObj.userData) delete entry.threeObj.userData.__foliageNonInstancedRegistered;", + " }", + "", + " /**", + " * @param {FoliageNonInstancedEntry|null|undefined} entry", + " * @returns {void}", + " */", + " disposeEntryMaterials(entry) {", + " var state = this.state;", + " if (!entry || !state) return;", + " state.unregisterActiveMaterial(entry.material || null);", + " if (entry.material) {", + " state.disposeShadowMaterials(entry.material);", + " if (typeof entry.material.dispose === \"function\") {", + " try { entry.material.dispose(); } catch (e) {}", + " }", + " }", + " state.unregisterActiveMaterial(entry.trunkMaterial || null);", + " if (entry.trunkMaterial) {", + " state.disposeShadowMaterials(entry.trunkMaterial);", + " if (typeof entry.trunkMaterial.dispose === \"function\") {", + " try { entry.trunkMaterial.dispose(); } catch (e) {}", + " }", + " }", + " }", + "", + " /**", + " * @param {gdjs.RuntimeObject3D|null|undefined} gdObj", + " * @param {THREE.Object3D|null|undefined} threeObj", + " * @returns {number}", + " */", + " removeForObject(gdObj, threeObj) {", + " var removed = 0;", + " var removedEntries = new Set();", + " var lists = [this.fadeEntries, this.staticEntries];", + " for (var li = 0; li < lists.length; li++) {", + " var list = lists[li];", + " for (var i = list.length - 1; i >= 0; i--) {", + " var entry = list[i];", + " if (!entry || (entry.gdObj !== gdObj && entry.threeObj !== threeObj)) continue;", + " if (!removedEntries.has(entry)) {", + " this.disposeEntryMaterials(entry);", + " this.clearRegistration(entry);", + " removedEntries.add(entry);", + " }", + " list.splice(i, 1);", + " removed++;", + " }", + " }", + " this.syncAliases();", + " return removed;", + " }", + "", + " /**", + " * @param {FoliageNonInstancedEntry} entryData", + " * @returns {FoliageNonInstancedEntry|null}", + " */", + " upsertFadeEntry(entryData) {", + " var state = this.state;", + " if (!state || !entryData) return null;", + " var entry = this.findEntry(entryData.gdObj || null, entryData.threeObj || null);", + " if (entry) {", + " if (entry.material && entry.material !== entryData.material) {", + " state.unregisterActiveMaterial(entry.material);", + " state.disposeShadowMaterials(entry.material);", + " if (typeof entry.material.dispose === \"function\") {", + " try { entry.material.dispose(); } catch (eReplace) {}", + " }", + " }", + " if (entry.trunkMaterial && entry.trunkMaterial !== entryData.trunkMaterial) {", + " state.unregisterActiveMaterial(entry.trunkMaterial);", + " state.disposeShadowMaterials(entry.trunkMaterial);", + " if (typeof entry.trunkMaterial.dispose === \"function\") {", + " try { entry.trunkMaterial.dispose(); } catch (eReplaceTrunk) {}", + " }", + " }", + " entry.gdObj = entryData.gdObj || null;", + " entry.threeObj = entryData.threeObj || null;", + " entry.material = entryData.material || null;", + " entry.trunkMaterial = entryData.trunkMaterial || null;", + " entry.fadeStart = entryData.fadeStart;", + " entry.fadeEnd = entryData.fadeEnd;", + " entry.fadeEnabled = !!entryData.fadeEnabled;", + " entry.fadeBehavior = entryData.fadeBehavior || null;", + " entry._parkedNoFade = false;", + " if (typeof entry._firstHideWarmupDone !== \"boolean\") entry._firstHideWarmupDone = false;", + " this._removeFromList(this.staticEntries, entry, false);", + " if (!this._listHas(this.fadeEntries, entry)) this.fadeEntries.push(entry);", + " } else {", + " entry = {", + " gdObj: entryData.gdObj || null,", + " threeObj: entryData.threeObj || null,", + " material: entryData.material || null,", + " trunkMaterial: entryData.trunkMaterial || null,", + " fadeStart: entryData.fadeStart,", + " fadeEnd: entryData.fadeEnd,", + " fadeEnabled: !!entryData.fadeEnabled,", + " fadeBehavior: entryData.fadeBehavior || null,", + " _parkedNoFade: false,", + " _firstHideWarmupDone: false", + " };", + " this.fadeEntries.push(entry);", + " }", + " state.registerActiveMaterial(entry.material || null);", + " state.registerActiveMaterial(entry.trunkMaterial || null);", + " this.syncAliases();", + " return entry;", + " }", + "", + " /**", + " * @param {FoliageNonInstancedEntry|null|undefined} entry", + " * @returns {FoliageNonInstancedEntry|null}", + " */", + " parkEntry(entry) {", + " if (!entry) return null;", + " this._removeFromList(this.fadeEntries, entry, true);", + " if (!this._listHas(this.staticEntries, entry)) this.staticEntries.push(entry);", + " entry._parkedNoFade = true;", + " this.syncAliases();", + " return entry;", + " }", + "", + " /**", + " * @param {FoliageNonInstancedEntry|null|undefined} entry", + " * @returns {FoliageNonInstancedEntry|null}", + " */", + " reactivateEntry(entry) {", + " if (!entry) return null;", + " this._removeFromList(this.staticEntries, entry, false);", + " if (!this._listHas(this.fadeEntries, entry)) this.fadeEntries.push(entry);", + " entry._parkedNoFade = false;", + " this.syncAliases();", + " return entry;", + " }", + "", + " /**", + " * @param {FoliageNonInstancedEntry|null|undefined} entry", + " * @param {\"fade\"|\"static\"|FoliageNonInstancedEntry[]|null|undefined} fromList", + " * @returns {boolean}", + " */", + " removeInvalidEntry(entry, fromList) {", + " var list = this._getList(fromList);", + " if (!entry || !list) return false;", + " this.disposeEntryMaterials(entry);", + " this.clearRegistration(entry);", + " var removed = this._removeFromList(list, entry, list === this.fadeEntries);", + " this.syncAliases();", + " return removed;", + " }", + " }", + "", + " /**", + " * Mutable storage for instanced foliage groups, pending queue items, and the", + " * temporary math objects reused by the instancing pipeline.", + " */", + " class FoliageInstancingState {", + " /**", + " * @param {string|null|undefined} sceneTag", + " */", + " constructor(sceneTag) {", + " /** @type {boolean} */", + " this._isReady = false;", + " /** @type {Map} */", + " this.groups = new Map();", + " /** @type {boolean} */", + " this.dirty = false;", + " /** @type {string|null} */", + " this.sceneTag = null;", + " /** @type {FoliagePendingItem[]} */", + " this.pending = [];", + " /** @type {number} */", + " this.queueIdCounter = 0;", + " /** @type {Set} */", + " this.cancelledQueueIds = new Set();", + " /** @type {THREE.Object3D|null} */", + " this.foliageRoot = null;", + " /** @type {THREE.Object3D|null} */", + " this._cachedSceneRoot = null;", + " /** @type {Set} */", + " this._tmpRootSet = new Set();", + " /** @type {THREE.Matrix4} */", + " this._tmpMat4 = new THREE.Matrix4();", + " /** @type {THREE.Object3D} */", + " this._tmpObj3D = new THREE.Object3D();", + " /** @type {THREE.Vector3} */", + " this._tmpVec3_pos = new THREE.Vector3();", + " /** @type {THREE.Vector3} */", + " this._tmpVec3_scale = new THREE.Vector3(1, 1, 1);", + " /** @type {THREE.Euler} */", + " this._tmpEuler = new THREE.Euler(0, 0, 0, \"XYZ\");", + " /** @type {THREE.Quaternion} */", + " this._tmpQuat = new THREE.Quaternion();", + " /** @type {THREE.Matrix4} */", + " this._tmpMat4_objWorld = new THREE.Matrix4();", + " /** @type {THREE.Matrix4} */", + " this._tmpMat4_objWorldInv = new THREE.Matrix4();", + " /** @type {THREE.Matrix4} */", + " this._tmpMat4_repWorld = new THREE.Matrix4();", + " /** @type {WeakMap} */", + " this._repRelByMesh = new WeakMap();", + " /** @type {Map} */", + " this._itemsByParentRepMesh = new Map();", + " /** @type {Map} */", + " this._repRelMatrixCache = new Map();", + " /** @type {THREE.Matrix4} */", + " this._tmpMat4_local = new THREE.Matrix4();", + " /** @type {THREE.Matrix4} */", + " this._tmpMat4_foliageRootInv = new THREE.Matrix4();", + " this.reset(sceneTag);", + " }", + "", + " /**", + " * @param {string|null|undefined} sceneTag", + " * @returns {FoliageInstancingState}", + " */", + " reset(sceneTag) {", + " this._isReady = false;", + " this.disposeAllGroups();", + " if (this.foliageRoot) {", + " try {", + " if (this.foliageRoot.parent) this.foliageRoot.parent.remove(this.foliageRoot);", + " } catch (eRoot) {}", + " }", + " this.groups = new Map();", + " this.dirty = false;", + " this.sceneTag = sceneTag === undefined ? null : sceneTag;", + " this.pending = [];", + " this.queueIdCounter = 0;", + " this.cancelledQueueIds = new Set();", + " this.foliageRoot = null;", + " this._cachedSceneRoot = null;", + " this._tmpRootSet = new Set();", + " this._tmpMat4 = new THREE.Matrix4();", + " this._tmpObj3D = new THREE.Object3D();", + " this._tmpVec3_pos = new THREE.Vector3();", + " this._tmpVec3_scale = new THREE.Vector3(1, 1, 1);", + " this._tmpEuler = new THREE.Euler(0, 0, 0, \"XYZ\");", + " this._tmpQuat = new THREE.Quaternion();", + " this._tmpMat4_objWorld = new THREE.Matrix4();", + " this._tmpMat4_objWorldInv = new THREE.Matrix4();", + " this._tmpMat4_repWorld = new THREE.Matrix4();", + " this._repRelByMesh = new WeakMap();", + " this._itemsByParentRepMesh = new Map();", + " this._repRelMatrixCache = new Map();", + " this._tmpMat4_local = new THREE.Matrix4();", + " this._tmpMat4_foliageRootInv = new THREE.Matrix4();", + " this._isReady = true;", + " return this;", + " }", + "", + " /**", + " * Backfill internal state after scene reset or legacy compatibility paths.", + " * @returns {FoliageInstancingState}", + " */", + " ensureReady() {", + " if (this._isReady === true) return this;", + " if (!this.groups || typeof this.groups.get !== \"function\") this.groups = new Map();", + " if (typeof this.dirty !== \"boolean\") this.dirty = false;", + " if (this.sceneTag === undefined) this.sceneTag = null;", + " if (!Array.isArray(this.pending)) this.pending = [];", + " if (typeof this.queueIdCounter !== \"number\") this.queueIdCounter = 0;", + " if (!this.cancelledQueueIds || typeof this.cancelledQueueIds.has !== \"function\") this.cancelledQueueIds = new Set();", + " if (!this._tmpRootSet || typeof this._tmpRootSet.add !== \"function\") this._tmpRootSet = new Set();", + " if (!this._tmpMat4) this._tmpMat4 = new THREE.Matrix4();", + " if (!this._tmpObj3D) this._tmpObj3D = new THREE.Object3D();", + " if (!this._tmpVec3_pos) this._tmpVec3_pos = new THREE.Vector3();", + " if (!this._tmpVec3_scale) this._tmpVec3_scale = new THREE.Vector3(1, 1, 1);", + " if (!this._tmpEuler) this._tmpEuler = new THREE.Euler(0, 0, 0, \"XYZ\");", + " if (!this._tmpQuat) this._tmpQuat = new THREE.Quaternion();", + " if (!this._tmpMat4_objWorld) this._tmpMat4_objWorld = new THREE.Matrix4();", + " if (!this._tmpMat4_objWorldInv) this._tmpMat4_objWorldInv = new THREE.Matrix4();", + " if (!this._tmpMat4_repWorld) this._tmpMat4_repWorld = new THREE.Matrix4();", + " if (!this._repRelByMesh) this._repRelByMesh = new WeakMap();", + " if (!this._itemsByParentRepMesh || typeof this._itemsByParentRepMesh.clear !== \"function\") this._itemsByParentRepMesh = new Map();", + " if (!this._repRelMatrixCache || typeof this._repRelMatrixCache.clear !== \"function\") this._repRelMatrixCache = new Map();", + " if (!this._tmpMat4_local) this._tmpMat4_local = new THREE.Matrix4();", + " if (!this._tmpMat4_foliageRootInv) this._tmpMat4_foliageRootInv = new THREE.Matrix4();", + " this._isReady = true;", + " return this;", + " }", + "", + " /**", + " * @returns {number}", + " */", + " nextQueueId() {", + " this.ensureReady();", + " this.queueIdCounter += 1;", + " return this.queueIdCounter;", + " }", + "", + " /**", + " * @param {FoliagePendingItem} item", + " * @returns {FoliagePendingItem}", + " */", + " enqueue(item) {", + " this.ensureReady();", + " this.pending.push(item);", + " return item;", + " }", + "", + " /**", + " * @param {number} queueId", + " * @returns {boolean}", + " */", + " cancelQueueId(queueId) {", + " if (queueId === undefined || queueId === null || !isFinite(queueId)) return false;", + " this.ensureReady();", + " this.cancelledQueueIds.add(Number(queueId));", + " return true;", + " }", + "", + " /**", + " * @param {string} groupKey", + " * @param {Partial|undefined} seedData", + " * @returns {FoliageInstancingGroup}", + " */", + " ensureGroup(groupKey, seedData) {", + " this.ensureReady();", + " var g = this.groups.get(groupKey);", + " if (!g) {", + " g = {", + " key: groupKey,", + " geometry: null,", + " material: null,", + " parent: null,", + " matricesBuffer: null,", + " centersXY: null,", + " centersZ: null,", + " cullRadii: null,", + " matrixCount: 0,", + " aliveCount: 0,", + " freeIndices: [],", + " freeIndexSet: new Set(),", + " capacity: 0,", + " mesh: null,", + " castShadow: false,", + " receiveShadow: false,", + " fadeEnabled: false,", + " fadeStart: 0,", + " fadeEnd: 0,", + " _fadeBehavior: null,", + " _instanceCullRadius: 0,", + " instanceFade: null,", + " instanceFadePrev: null,", + " visibleCount: 0,", + " _srcGeometryRef: null,", + " _ownedGeometry: null", + " };", + " this.groups.set(groupKey, g);", + " }", + " if (seedData) {", + " for (var key in seedData) {", + " if (Object.prototype.hasOwnProperty.call(seedData, key) && seedData[key] !== undefined) {", + " g[key] = seedData[key];", + " }", + " }", + " }", + " if (!g.freeIndices) g.freeIndices = [];", + " if (!g.freeIndexSet || typeof g.freeIndexSet.has !== \"function\") g.freeIndexSet = new Set(g.freeIndices);", + " if (typeof g.matrixCount !== \"number\") g.matrixCount = 0;", + " if (typeof g.aliveCount !== \"number\") g.aliveCount = 0;", + " if (typeof g.capacity !== \"number\") g.capacity = 0;", + " if (!g.centersXY) g.centersXY = null;", + " if (!g.centersZ) g.centersZ = null;", + " if (!g.cullRadii) g.cullRadii = null;", + " return g;", + " }", + "", + " /**", + " * @returns {FoliageInstancingState}", + " */", + " markDirty() {", + " this.ensureReady();", + " this.dirty = true;", + " return this;", + " }", + "", + " /**", + " * @param {string} groupKey", + " * @returns {boolean}", + " */", + " disposeGroup(groupKey) {", + " this.ensureReady();", + " var g = this.groups.get(groupKey);", + " if (!g) return false;", + " try {", + " if (g.mesh) {", + " if (g.mesh.parent) g.mesh.parent.remove(g.mesh);", + " if (g._ownedGeometry && typeof g._ownedGeometry.dispose === \"function\") {", + " try { g._ownedGeometry.dispose(); } catch (eGeo) {}", + " }", + " if (g.mesh.dispose) {", + " try { g.mesh.dispose(); } catch (eMesh) {}", + " }", + " }", + " } catch (e) {}", + " g.mesh = null;", + " g._ownedGeometry = null;", + " g.matricesBuffer = null;", + " g.centersXY = null;", + " g.centersZ = null;", + " g.cullRadii = null;", + " g.instanceFade = null;", + " g.instanceFadePrev = null;", + " g._srcGeometryRef = null;", + " g._instanceCullRadius = 0;", + " g.matrixCount = 0;", + " g.aliveCount = 0;", + " g.freeIndices = [];", + " g.freeIndexSet = new Set();", + " this.groups.delete(groupKey);", + " return true;", + " }", + "", + " /**", + " * @returns {FoliageInstancingState}", + " */", + " disposeAllGroups() {", + " if (!this.groups || typeof this.groups.forEach !== \"function\") return this;", + " var keys = [];", + " this.groups.forEach(function(_group, key) {", + " keys.push(key);", + " });", + " for (var i = 0; i < keys.length; i++) {", + " this.disposeGroup(keys[i]);", + " }", + " this.groups.clear();", + " return this;", + " }", + "", + " /**", + " * @param {string} groupKey", + " * @param {number} instanceIndex", + " * @param {string|null|undefined} leavesGroupKey", + " * @returns {boolean}", + " */", + " freeIndex(groupKey, instanceIndex, leavesGroupKey) {", + " this.ensureReady();", + " var g = this.groups.get(groupKey);", + " var gLeaves = leavesGroupKey ? this.groups.get(leavesGroupKey) : null;", + " if (!g || typeof g.matrixCount !== \"number\") return false;", + " if (!isFinite(instanceIndex) || instanceIndex < 0 || instanceIndex >= g.matrixCount) return false;", + "", + " if (typeof g.aliveCount !== \"number\") {", + " var estimatedFree = g.freeIndices ? g.freeIndices.length : 0;", + " g.aliveCount = Math.max(0, g.matrixCount - estimatedFree);", + " }", + " if (gLeaves && typeof gLeaves.aliveCount !== \"number\") {", + " var estimatedFreeLeaves = gLeaves.freeIndices ? gLeaves.freeIndices.length : 0;", + " gLeaves.aliveCount = Math.max(0, gLeaves.matrixCount - estimatedFreeLeaves);", + " }", + " if (!g.freeIndexSet || typeof g.freeIndexSet.has !== \"function\") {", + " g.freeIndexSet = new Set();", + " if (g.freeIndices && Array.isArray(g.freeIndices)) {", + " for (var fi = 0; fi < g.freeIndices.length; fi++) g.freeIndexSet.add(g.freeIndices[fi]);", + " }", + " }", + "", + " if (!g.freeIndexSet.has(instanceIndex)) {", + " if (!g.freeIndices) g.freeIndices = [];", + " g.freeIndices.push(instanceIndex);", + " g.freeIndexSet.add(instanceIndex);", + " if (g.aliveCount > 0) g.aliveCount--;", + " if (gLeaves && gLeaves.aliveCount > 0) gLeaves.aliveCount--;", + " }", + "", + " if (g.aliveCount === 0) {", + " this.disposeGroup(groupKey);", + " if (leavesGroupKey && leavesGroupKey !== groupKey && gLeaves) this.disposeGroup(leavesGroupKey);", + " } else {", + " this.markDirty();", + " }", + " return true;", + " }", + " }", + "", + " /**", + " * Procedural owner for instancing work that operates on FoliageInstancingState.", + " * It translates queued runtime objects into persistent instanced groups and", + " * keeps per-instance transforms, fade data, and group rebuilds in sync.", + " */", + " class FoliageInstancingCoordinator {", + " /**", + " * @param {FoliageSceneState} state", + " */", + " constructor(state) {", + " /** @type {FoliageSceneState} */", + " this.state = state;", + " /** @type {number} */", + " this._DEG2RAD = Math.PI / 180;", + " }", + "", + " /**", + " * Refresh live fade settings from the behavior so queued/instanced groups", + " * keep using the current editor/runtime values without duplicating fade logic.", + " * @param {FoliageInstancingGroup|FoliageNonInstancedEntry|null|undefined} target", + " * @param {FoliageBehavior|null|undefined} behavior", + " * @param {boolean} defaultEnabled", + " * @returns {void}", + " */", + " _refreshLiveFadeState(target, behavior, defaultEnabled) {", + " if (!target) return;", + " var state = this.state;", + " var enabled = (target.fadeEnabled === undefined) ? defaultEnabled : !!target.fadeEnabled;", + " var start = target.fadeStart != null ? target.fadeStart : 1200;", + " var end = target.fadeEnd != null ? target.fadeEnd : 1600;", + " if (behavior) {", + " var fadePropsLive = state.getBehaviorProps(behavior);", + " if (fadePropsLive) {", + " enabled = !!fadePropsLive.distanceFadeEnabled;", + " start = fadePropsLive.fadeStart;", + " end = fadePropsLive.fadeEnd;", + " }", + " }", + " if (end <= start) end = start + 100;", + " target.fadeEnabled = !!enabled;", + " target.fadeStart = start;", + " target.fadeEnd = end;", + " }", + "", + " /**", + " * @param {THREE.Matrix4|null|undefined} mat", + " * @returns {number}", + " */", + " _getDetSignFromMatrix(mat) {", + " if (!mat || typeof mat.determinant !== \"function\") return 1;", + " var det = mat.determinant();", + " return (isFinite(det) && det < 0) ? -1 : 1;", + " }", + "", + " /**", + " * @param {number} detSign", + " * @returns {string}", + " */", + " _getDetSignSuffix(detSign) {", + " return detSign < 0 ? \"::DETNEG\" : \"::DETPOS\";", + " }", + "", + " /**", + " * @param {number} side", + " * @returns {number}", + " */", + " _flipMaterialSide(side) {", + " if (typeof THREE === \"undefined\") return side;", + " if (side === THREE.FrontSide) return THREE.BackSide;", + " if (side === THREE.BackSide) return THREE.FrontSide;", + " return side;", + " }", + "", + " /**", + " * @param {THREE.Material|null|undefined} sourceMat", + " * @returns {string}", + " */", + " _getDetVariantSourceSignature(sourceMat) {", + " if (!sourceMat) return \"null\";", + " var ud = sourceMat.userData || {};", + " var cfg = ud.foliageConfig || {};", + " var sourceDepth = ud._foliageDepthMat || null;", + " var sourceDistance = ud._foliageDistanceMat || null;", + " var topColor = cfg.topColor && typeof cfg.topColor.getHexString === \"function\" ? cfg.topColor.getHexString() : \"\";", + " var bottomColor = cfg.bottomColor && typeof cfg.bottomColor.getHexString === \"function\" ? cfg.bottomColor.getHexString() : \"\";", + " return [", + " sourceMat.side,", + " sourceMat.alphaTest,", + " sourceMat.transparent ? 1 : 0,", + " sourceMat.depthWrite ? 1 : 0,", + " cfg.swayType || \"\",", + " cfg.uniformSway ? 1 : 0,", + " cfg.phase,", + " topColor,", + " bottomColor,", + " cfg.sat,", + " cfg.contrast,", + " cfg.useGrad ? 1 : 0,", + " cfg.twoSidedLighting ? 1 : 0,", + " cfg.distanceFadeEnabled ? 1 : 0,", + " cfg.customLit ? 1 : 0,", + " cfg.metallic,", + " cfg.roughness,", + " cfg.specular,", + " cfg.normalStrength,", + " cfg.aoStrength,", + " cfg.envStrength,", + " cfg.cullingMode || \"\",", + " cfg.renderSide,", + " cfg.polyScale,", + " cfg.gradStart,", + " cfg.gradEnd,", + " cfg.ignoreUV ? 1 : 0,", + " cfg.gradLocalZMin,", + " cfg.gradLocalZMax,", + " sourceDepth ? sourceDepth.side : \"nd\",", + " sourceDistance ? sourceDistance.side : \"ns\"", + " ].join(\"|\");", + " }", + "", + " /**", + " * @param {THREE.Material|null|undefined} mat", + " * @param {number} detSign", + " * @returns {void}", + " */", + " _applyDetSignMetadata(mat, detSign) {", + " if (!mat) return;", + " var sign = detSign < 0 ? -1 : 1;", + " var ud = mat.userData || (mat.userData = {});", + " var cfg = ud.foliageConfig || (ud.foliageConfig = {});", + " cfg.instanceDetSign = sign;", + " ud._foliageDetGrouped = true;", + " ud._foliagePreparedDetSign = sign;", + " ud._foliageInstanceDetSign = sign;", + " var uniforms = ud.foliageUniforms;", + " if (uniforms && uniforms.uInstanceDetSign) {", + " uniforms.uInstanceDetSign.value = sign;", + " }", + " }", + "", + " /**", + " * @param {THREE.Material|null|undefined} sourceMat", + " * @param {THREE.Material|null|undefined} targetMat", + " * @param {number} detSign", + " * @param {THREE.Side|undefined|null} resolvedSide", + " * @returns {THREE.Material|null}", + " */", + " _syncSignedShadowVariant(sourceMat, targetMat, detSign, resolvedSide) {", + " if (!sourceMat || !targetMat) return null;", + " if (typeof targetMat.copy === \"function\") targetMat.copy(sourceMat);", + " targetMat.onBeforeCompile = sourceMat.onBeforeCompile;", + " targetMat.userData = Object.assign({}, sourceMat.userData || {});", + " delete targetMat.userData._foliageDepthMat;", + " delete targetMat.userData._foliageDistanceMat;", + " delete targetMat.userData._foliageShadowOwned;", + " var shadowCfg = sourceMat.userData && sourceMat.userData.foliageConfig", + " ? sourceMat.userData.foliageConfig", + " : null;", + " targetMat.userData.foliageConfig = Object.assign({}, shadowCfg || {});", + " targetMat.userData.foliageConfig.instanceDetSign = detSign < 0 ? -1 : 1;", + " targetMat.userData._foliageDetGrouped = true;", + " targetMat.userData._foliagePreparedDetSign = detSign < 0 ? -1 : 1;", + " targetMat.userData._foliageInstanceDetSign = detSign < 0 ? -1 : 1;", + " delete targetMat.userData.foliageUniforms;", + " delete targetMat.userData.__gustVersionApplied;", + " if (resolvedSide !== undefined && resolvedSide !== null) targetMat.side = resolvedSide;", + " var signKey = detSign < 0 ? -1 : 1;", + " var sourceProgramKey = \"\";", + " if (typeof sourceMat.customProgramCacheKey === \"function\") {", + " try {", + " sourceProgramKey = String(sourceMat.customProgramCacheKey.call(sourceMat) || \"\");", + " } catch (eProgramKey) {", + " sourceProgramKey = \"\";", + " }", + " }", + " var patchKey = \"raw\";", + " if (targetMat.userData.__foliageFadeOnlyShadowPatched) patchKey = \"fadeOnlyShadow\";", + " else if (targetMat.userData.__foliageShadowPatched) patchKey = \"foliageShadow\";", + " targetMat.customProgramCacheKey = function() {", + " var sideKey = resolvedSide !== undefined && resolvedSide !== null ? resolvedSide : this.side;", + " return [", + " sourceProgramKey,", + " \"FoliageSignedShadow\",", + " patchKey,", + " signKey,", + " sideKey", + " ].join(\"|\");", + " };", + " targetMat.needsUpdate = true;", + " return targetMat;", + " }", + "", + " /**", + " * Return a determinant-sign-specific material variant for instanced rendering.", + " * Mirrored instances need their own material/shadow state so side handling and", + " * lighting stay coherent instead of mixing positive and negative transforms.", + " * @param {THREE.Material|null|undefined} sourceMat", + " * @param {number} detSign", + " * @returns {THREE.Material|null}", + " */", + " _getSignedInstancedMaterial(sourceMat, detSign) {", + " if (!sourceMat) return null;", + " var state = this.state;", + " var sign = detSign < 0 ? -1 : 1;", + " this._applyDetSignMetadata(sourceMat, 1);", + " if (sign >= 0) {", + " return sourceMat;", + " }", + "", + " var sourceUd = sourceMat.userData || (sourceMat.userData = {});", + " var variantSignature = this._getDetVariantSourceSignature(sourceMat);", + " var negMat = sourceUd._foliageDetNegVariant;", + " if (negMat && negMat.userData && negMat.userData._foliageVariantSignature === variantSignature) {", + " return negMat;", + " }", + " if (!negMat) {", + " negMat = sourceMat.clone();", + " sourceUd._foliageDetNegVariant = negMat;", + " }", + "", + " if (typeof negMat.copy === \"function\") negMat.copy(sourceMat);", + " negMat.name = (sourceMat.name || \"Foliage\") + \"::DetNeg\";", + " negMat.onBeforeCompile = sourceMat.onBeforeCompile;", + " negMat.userData = Object.assign({}, sourceMat.userData || {});", + " delete negMat.userData._foliageDetNegVariant;", + " delete negMat.userData._foliageDepthMat;", + " delete negMat.userData._foliageDistanceMat;", + " delete negMat.userData._foliageShadowOwned;", + " var baseCfg = sourceMat.userData && sourceMat.userData.foliageConfig ? sourceMat.userData.foliageConfig : null;", + " negMat.userData.foliageConfig = Object.assign({}, baseCfg || {});", + " negMat.userData.foliageConfig.instanceDetSign = -1;", + " negMat.userData._foliageDetGrouped = true;", + " negMat.userData._foliagePreparedDetSign = -1;", + " negMat.userData._foliageInstanceDetSign = -1;", + " negMat.userData._foliageVariantSignature = variantSignature;", + " delete negMat.userData.foliageUniforms;", + " delete negMat.userData.__gustVersionApplied;", + " negMat.side = this._flipMaterialSide(sourceMat.side);", + "", + " var sourceDepth = sourceMat.userData ? sourceMat.userData._foliageDepthMat : null;", + " if (sourceDepth) {", + " var negDepth = sourceDepth.clone();", + " this._syncSignedShadowVariant(sourceDepth, negDepth, -1, negMat.side);", + " negMat.userData._foliageDepthMat = negDepth;", + " } else {", + " delete negMat.userData._foliageDepthMat;", + " }", + "", + " var sourceDistance = sourceMat.userData ? sourceMat.userData._foliageDistanceMat : null;", + " if (sourceDistance) {", + " var negDistance = sourceDistance.clone();", + " this._syncSignedShadowVariant(sourceDistance, negDistance, -1, negMat.side);", + " negMat.userData._foliageDistanceMat = negDistance;", + " } else {", + " delete negMat.userData._foliageDistanceMat;", + " }", + "", + " negMat.userData._foliageShadowOwned = !!(negMat.userData._foliageDepthMat || negMat.userData._foliageDistanceMat);", + " negMat.needsUpdate = true;", + " state.registerActiveMaterial(negMat);", + " return negMat;", + " }", + "", + " /**", + " * @param {string} label", + " * @param {string} baseGroupKey", + " * @param {number} detSign", + " * @param {FoliageBehavior|null|undefined} behavior", + " * @returns {void}", + " */", + " _logDetSignDiagnostic(label, baseGroupKey, detSign, behavior) {", + " var state = this.state;", + " if (!state || !state.debugPrinted || !behavior) return;", + " var props = state.getBehaviorProps(behavior);", + " if (!props || !props.debugOutput) return;", + " var sign = detSign < 0 ? -1 : 1;", + " var debugKey = \"det-sign::\" + label + \"::\" + baseGroupKey + \"::\" + sign;", + " if (state.debugPrinted.has(debugKey)) return;", + " state.debugPrinted.add(debugKey);", + " console.log(\"[Foliage det-sign] \" + label + \" key=\" + baseGroupKey + \" sign=\" + sign);", + " }", + "", + " /**", + " * @param {(gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string, getX?: () => number, getY?: () => number, getZ?: () => number, getScaleX?: () => number, getScaleY?: () => number, getScaleZ?: () => number, getRotationX?: () => number, getRotationY?: () => number, getRotationZ?: () => number, getAngle?: () => number, hide?: (enable?: boolean) => void, isHidden?: () => boolean, deleteFromScene?: (runtimeScene?: gdjs.RuntimeScene) => void })|null|undefined} gdObj", + " * @param {FoliageInstancingState} inst", + " * @param {THREE.Matrix4} outMat", + " * @returns {THREE.Matrix4}", + " */", + " composeObjectWorldFromGDevelop(gdObj, inst, outMat) {", + " if (!gdObj || !outMat) return outMat;", + " var pos = inst._tmpVec3_pos;", + " var scl = inst._tmpVec3_scale;", + " var eul = inst._tmpEuler;", + " var q = inst._tmpQuat;", + " var x = 0, y = 0, z = 0;", + " try { x = gdObj.getX ? gdObj.getX() : 0; } catch (e) {}", + " try { y = gdObj.getY ? gdObj.getY() : 0; } catch (e2) {}", + " try { z = gdObj.getZ ? gdObj.getZ() : 0; } catch (e3) {}", + " var sx = 1, sy = 1, sz = 1;", + " try { sx = gdObj.getScaleX ? gdObj.getScaleX() : 1; } catch (e4) {}", + " try { sy = gdObj.getScaleY ? gdObj.getScaleY() : 1; } catch (e5) {}", + " try { sz = gdObj.getScaleZ ? gdObj.getScaleZ() : 1; } catch (e6) {}", + " if (!isFinite(sx) || sx === 0) sx = 1;", + " if (!isFinite(sy) || sy === 0) sy = 1;", + " if (!isFinite(sz) || sz === 0) sz = 1;", + " var rx = 0, ry = 0, rz = 0;", + " try { rx = gdObj.getRotationX ? gdObj.getRotationX() : 0; } catch (e7) {}", + " try { ry = gdObj.getRotationY ? gdObj.getRotationY() : 0; } catch (e8) {}", + " try { rz = gdObj.getRotationZ ? gdObj.getRotationZ() : 0; } catch (e9) {}", + " if (!isFinite(rx)) rx = 0;", + " if (!isFinite(ry)) ry = 0;", + " if (!isFinite(rz) || (rx === 0 && ry === 0 && rz === 0)) {", + " try {", + " if (gdObj.getAngle) {", + " var a = gdObj.getAngle();", + " if (isFinite(a)) rz = a;", + " }", + " } catch (e10) {}", + " if (!isFinite(rz)) rz = 0;", + " }", + " pos.set(x, y, z);", + " scl.set(sx, sy, sz);", + " eul.set(rx * this._DEG2RAD, ry * this._DEG2RAD, rz * this._DEG2RAD, \"XYZ\");", + " q.setFromEuler(eul);", + " outMat.compose(pos, q, scl);", + " return outMat;", + " }", + "", + " /**", + " * @param {(gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string, getX?: () => number, getY?: () => number, getZ?: () => number, getScaleX?: () => number, getScaleY?: () => number, getScaleZ?: () => number, getRotationX?: () => number, getRotationY?: () => number, getRotationZ?: () => number, getAngle?: () => number, hide?: (enable?: boolean) => void, isHidden?: () => boolean, deleteFromScene?: (runtimeScene?: gdjs.RuntimeScene) => void })|null|undefined} gdObj", + " * @param {THREE.Object3D|null|undefined} repMesh", + " * @returns {THREE.Matrix4}", + " */", + " getOrComputeRepRelMatrix(gdObj, repMesh) {", + " var state = this.state;", + " var inst = state.instancingState || state.instancing || null;", + " if (!inst || !repMesh) return null;", + " var map = inst._repRelByMesh;", + " if (!map) {", + " inst._repRelByMesh = new WeakMap();", + " map = inst._repRelByMesh;", + " }", + " var rel = map.get(repMesh);", + " if (rel) return rel;", + " try { repMesh.updateMatrixWorld(true); } catch (e0) {}", + " var objW = inst._tmpMat4_objWorld;", + " var objWi = inst._tmpMat4_objWorldInv;", + " this.composeObjectWorldFromGDevelop(gdObj, inst, objW);", + " try { objWi.copy(objW).invert(); } catch (e1) { return null; }", + " rel = new THREE.Matrix4();", + " try { rel.multiplyMatrices(objWi, repMesh.matrixWorld); } catch (e2) { return null; }", + " map.set(repMesh, rel);", + " return rel;", + " }", + "", + " /**", + " * @param {THREE.BufferGeometry|null|undefined} geometry", + " * @returns {THREE.Sphere|null}", + " */", + " ensureGeometryBoundingSphere(geometry) {", + " if (!geometry) return null;", + " try {", + " if (!geometry.boundingSphere && typeof geometry.computeBoundingSphere === \"function\") {", + " geometry.computeBoundingSphere();", + " }", + " } catch (eBs) {}", + " var bs = geometry.boundingSphere;", + " if (!bs || !isFinite(bs.radius)) return null;", + " return bs;", + " }", + "", + " /**", + " * @param {THREE.Matrix4|null|undefined} mat", + " * @returns {number}", + " */", + " computeMatrixMaxScale(mat) {", + " if (!mat || !mat.elements) return 1.0;", + " var e = mat.elements;", + " var sx = Math.sqrt(e[0] * e[0] + e[1] * e[1] + e[2] * e[2]);", + " var sy = Math.sqrt(e[4] * e[4] + e[5] * e[5] + e[6] * e[6]);", + " var sz = Math.sqrt(e[8] * e[8] + e[9] * e[9] + e[10] * e[10]);", + " var s = Math.max(sx, sy, sz);", + " return (isFinite(s) && s > 0) ? s : 1.0;", + " }", + "", + " /**", + " * @param {FoliageInstancingGroup|null|undefined} group", + " * @param {number} idx", + " * @param {number} cx", + " * @param {number} cy", + " * @param {number} cz", + " * @param {number} radius", + " * @returns {void}", + " */", + " writeInstanceCullValues(group, idx, cx, cy, cz, radius) {", + " if (!group || idx === undefined || idx === null || idx < 0) return;", + "", + " var slotCount = Math.max(idx + 1, group.matrixCount || 0);", + " var centersRequired = slotCount * 2;", + " if (!group.centersXY || group.centersXY.length < centersRequired) {", + " var centersCurrentSize = group.centersXY ? group.centersXY.length : 0;", + " var centersNewSize = Math.max(centersRequired, Math.max(Math.floor(centersCurrentSize * 1.5), 512));", + " var newCenters = new Float32Array(centersNewSize);", + " if (group.centersXY) newCenters.set(group.centersXY);", + " group.centersXY = newCenters;", + " }", + "", + " if (!group.centersZ || group.centersZ.length < slotCount) {", + " var centersCurrentSizeZ = group.centersZ ? group.centersZ.length : 0;", + " var centersNewSizeZ = Math.max(slotCount, Math.max(Math.floor(centersCurrentSizeZ * 1.5), 256));", + " var newCentersZ = new Float32Array(centersNewSizeZ);", + " if (group.centersZ) newCentersZ.set(group.centersZ);", + " group.centersZ = newCentersZ;", + " }", + "", + " if (!group.cullRadii || group.cullRadii.length < slotCount) {", + " var radiiCurrentSize = group.cullRadii ? group.cullRadii.length : 0;", + " var radiiNewSize = Math.max(slotCount, Math.max(Math.floor(radiiCurrentSize * 1.5), 256));", + " var newRadii = new Float32Array(radiiNewSize);", + " if (group.cullRadii) newRadii.set(group.cullRadii);", + " group.cullRadii = newRadii;", + " }", + "", + " group.centersXY[idx * 2] = isFinite(cx) ? cx : 0;", + " group.centersXY[idx * 2 + 1] = isFinite(cy) ? cy : 0;", + " group.centersZ[idx] = isFinite(cz) ? cz : 0;", + " group.cullRadii[idx] = (isFinite(radius) && radius > 0) ? radius : 0;", + " }", + "", + " /**", + " * @param {THREE.Matrix4|null|undefined} worldMat", + " * @param {THREE.BufferGeometry|null|undefined} geometry", + " * @param {number} fallbackRadius", + " * @param {{ cx: number, cy: number, cz: number, radius: number }=} outSphere", + " * @returns {{ cx: number, cy: number, cz: number, radius: number }}", + " */", + " computeWorldCullSphere(worldMat, geometry, fallbackRadius, outSphere) {", + " var out = outSphere || { cx: 0, cy: 0, cz: 0, radius: 0 };", + " out.cx = 0;", + " out.cy = 0;", + " out.cz = 0;", + " out.radius = (isFinite(fallbackRadius) && fallbackRadius > 0) ? fallbackRadius : 0;", + "", + " if (worldMat && worldMat.elements) {", + " var e = worldMat.elements;", + " out.cx = e[12];", + " out.cy = e[13];", + " out.cz = e[14];", + "", + " var bs = this.ensureGeometryBoundingSphere(geometry);", + " if (bs && bs.center) {", + " var bcx = isFinite(bs.center.x) ? bs.center.x : 0;", + " var bcy = isFinite(bs.center.y) ? bs.center.y : 0;", + " var bcz = isFinite(bs.center.z) ? bs.center.z : 0;", + " out.cx = e[0] * bcx + e[4] * bcy + e[8] * bcz + e[12];", + " out.cy = e[1] * bcx + e[5] * bcy + e[9] * bcz + e[13];", + " out.cz = e[2] * bcx + e[6] * bcy + e[10] * bcz + e[14];", + " out.radius = bs.radius * this.computeMatrixMaxScale(worldMat);", + " }", + " }", + "", + " return out;", + " }", + "", + " /**", + " * @param {{ cx: number, cy: number, cz: number, radius: number }} a", + " * @param {{ cx: number, cy: number, cz: number, radius: number }} b", + " * @param {{ cx: number, cy: number, cz: number, radius: number }=} outSphere", + " * @returns {{ cx: number, cy: number, cz: number, radius: number }}", + " */", + " unionCullSpheres(a, b, outSphere) {", + " var out = outSphere || { cx: 0, cy: 0, cz: 0, radius: 0 };", + " var ar = a && isFinite(a.radius) && a.radius > 0 ? a.radius : 0;", + " var br = b && isFinite(b.radius) && b.radius > 0 ? b.radius : 0;", + " if (ar <= 0 && br <= 0) {", + " out.cx = 0;", + " out.cy = 0;", + " out.cz = 0;", + " out.radius = 0;", + " return out;", + " }", + " if (ar <= 0) {", + " out.cx = b.cx;", + " out.cy = b.cy;", + " out.cz = b.cz;", + " out.radius = br;", + " return out;", + " }", + " if (br <= 0) {", + " out.cx = a.cx;", + " out.cy = a.cy;", + " out.cz = a.cz;", + " out.radius = ar;", + " return out;", + " }", + "", + " var dx = b.cx - a.cx;", + " var dy = b.cy - a.cy;", + " var dz = b.cz - a.cz;", + " var distSq = dx * dx + dy * dy + dz * dz;", + " var dist = distSq > 0 ? Math.sqrt(distSq) : 0;", + "", + " if (dist + Math.min(ar, br) <= Math.max(ar, br)) {", + " if (ar >= br) {", + " out.cx = a.cx;", + " out.cy = a.cy;", + " out.cz = a.cz;", + " out.radius = ar;", + " } else {", + " out.cx = b.cx;", + " out.cy = b.cy;", + " out.cz = b.cz;", + " out.radius = br;", + " }", + " return out;", + " }", + "", + " if (dist <= 1e-6) {", + " out.cx = a.cx;", + " out.cy = a.cy;", + " out.cz = a.cz;", + " out.radius = Math.max(ar, br);", + " return out;", + " }", + "", + " var newRadius = (dist + ar + br) * 0.5;", + " var t = (newRadius - ar) / dist;", + " out.cx = a.cx + dx * t;", + " out.cy = a.cy + dy * t;", + " out.cz = a.cz + dz * t;", + " out.radius = newRadius;", + " return out;", + " }", + "", + " /**", + " * @param {FoliageInstancingGroup|null|undefined} group", + " * @param {number} idx", + " * @param {THREE.Matrix4} worldMat", + " * @param {THREE.BufferGeometry|null|undefined} geometry", + " * @param {number} fallbackRadius", + " * @returns {void}", + " */", + " writeInstanceCullData(group, idx, worldMat, geometry, fallbackRadius) {", + " var state = this.state;", + " var tmpSphere = this.computeWorldCullSphere(", + " worldMat,", + " geometry,", + " fallbackRadius,", + " state._tmpInstanceCullSphere || (state._tmpInstanceCullSphere = { cx: 0, cy: 0, cz: 0, radius: 0 })", + " );", + " this.writeInstanceCullValues(group, idx, tmpSphere.cx, tmpSphere.cy, tmpSphere.cz, tmpSphere.radius);", + " }", + "", + " /**", + " * @param {THREE.BufferGeometry|null|undefined} geometry", + " * @param {number} fallbackRadius", + " * @returns {number}", + " */", + " computeInstanceCullRadius(geometry, fallbackRadius) {", + " if (!geometry) return isFinite(fallbackRadius) ? fallbackRadius : 0;", + " var bs = this.ensureGeometryBoundingSphere(geometry);", + " var r = bs && isFinite(bs.radius) ? bs.radius : (isFinite(fallbackRadius) ? fallbackRadius : 0);", + " return r > 0 ? r : 0;", + " }", + "", + " /**", + " * @param {THREE.Material|null|undefined} mat", + " * @param {number} determinant", + " * @returns {void}", + " */", + " applyInstancedTrunkFaceFix(mat, determinant) {", + " if (!mat || !isFinite(determinant) || determinant === 0) return;", + " if (typeof THREE === \"undefined\") return;", + " var sign = determinant < 0 ? -1 : 1;", + " var ud = mat.userData || (mat.userData = {});", + " var preparedSign = (ud._foliagePreparedDetSign === -1 || ud._foliagePreparedDetSign === 1) ? ud._foliagePreparedDetSign : 0;", + " if (ud._foliageDetGrouped === true && preparedSign === sign) {", + " ud._foliageInstanceDetSign = sign;", + " return;", + " }", + " var prevSign = (ud._foliageInstanceDetSign === -1 || ud._foliageInstanceDetSign === 1) ? ud._foliageInstanceDetSign : 0;", + " if (prevSign && prevSign !== sign) {", + " if (mat.side !== THREE.DoubleSide) {", + " mat.side = THREE.DoubleSide;", + " mat.needsUpdate = true;", + " if (ud._foliageDepthMat) {", + " ud._foliageDepthMat.side = THREE.DoubleSide;", + " ud._foliageDepthMat.needsUpdate = true;", + " }", + " if (ud._foliageDistanceMat) {", + " ud._foliageDistanceMat.side = THREE.DoubleSide;", + " ud._foliageDistanceMat.needsUpdate = true;", + " }", + " }", + " ud._foliageInstanceDetMixed = true;", + " return;", + " }", + " if (!prevSign) {", + " ud._foliageInstanceDetSign = sign;", + " }", + " if (sign >= 0 || ud._foliageMirroredFaceFixApplied) return;", + " if (mat.side === THREE.FrontSide) {", + " mat.side = THREE.BackSide;", + " mat.needsUpdate = true;", + " } else if (mat.side === THREE.BackSide) {", + " mat.side = THREE.FrontSide;", + " mat.needsUpdate = true;", + " } else {", + " ud._foliageMirroredFaceFixApplied = true;", + " return;", + " }", + " if (ud._foliageDepthMat) {", + " if (ud._foliageDepthMat.side === THREE.FrontSide) ud._foliageDepthMat.side = THREE.BackSide;", + " else if (ud._foliageDepthMat.side === THREE.BackSide) ud._foliageDepthMat.side = THREE.FrontSide;", + " ud._foliageDepthMat.needsUpdate = true;", + " }", + " if (ud._foliageDistanceMat) {", + " if (ud._foliageDistanceMat.side === THREE.FrontSide) ud._foliageDistanceMat.side = THREE.BackSide;", + " else if (ud._foliageDistanceMat.side === THREE.BackSide) ud._foliageDistanceMat.side = THREE.FrontSide;", + " ud._foliageDistanceMat.needsUpdate = true;", + " }", + " ud._foliageMirroredFaceFixApplied = true;", + " }", + "", + " /**", + " * Consume queued runtime objects, resolve their final instancing groups, and", + " * write instance data into storage buffers without rebuilding render meshes yet.", + " * @param {SharedFrameContext} ctx", + " */", + " flushPending(ctx) {", + " var state = this.state;", + " var inst = state.instancingState || state.instancing || null;", + " if (!inst) return;", + " inst.ensureReady();", + " state.instancingState = inst;", + " state.instancing = inst;", + " if (!Array.isArray(inst.pending) || inst.pending.length === 0) return;", + "", + " var list = inst.pending;", + " inst.pending = [];", + " var listLen = list.length;", + " if (listLen === 0) return;", + "", + " if (!inst.foliageRoot) {", + " inst.foliageRoot = new THREE.Object3D();", + " inst.foliageRoot.name = \"FoliageRoot\";", + " inst.foliageRoot.matrixAutoUpdate = false;", + " inst.foliageRoot.matrix.identity();", + " var sceneRoot = null;", + " if (list.length > 0 && list[0].threeObj) {", + " sceneRoot = list[0].threeObj;", + " while (sceneRoot && sceneRoot.parent) sceneRoot = sceneRoot.parent;", + " }", + " if (!sceneRoot && inst._cachedSceneRoot) {", + " sceneRoot = inst._cachedSceneRoot;", + " }", + " if (!sceneRoot) {", + " try {", + " var layer = ctx.runtimeScene.getLayer(\"\");", + " if (layer && layer.getRenderer() && layer.getRenderer().getThreeScene) {", + " sceneRoot = layer.getRenderer().getThreeScene();", + " }", + " } catch (eSceneRoot) {}", + " }", + " if (sceneRoot) {", + " sceneRoot.add(inst.foliageRoot);", + " inst._cachedSceneRoot = sceneRoot;", + " }", + " }", + "", + " var rootSet = inst._tmpRootSet;", + " if (!rootSet || typeof rootSet.add !== \"function\" || typeof rootSet.clear !== \"function\") {", + " rootSet = new Set();", + " inst._tmpRootSet = rootSet;", + " }", + " rootSet.clear();", + " for (var ri = 0; ri < listLen; ri++) {", + " var rootItem = list[ri];", + " if (!rootItem || !rootItem.threeObj) continue;", + " var root = rootItem.threeObj;", + " while (root && root.parent) root = root.parent;", + " if (root) rootSet.add(root);", + " }", + " if (rootSet.size === 0 && inst._cachedSceneRoot) {", + " rootSet.add(inst._cachedSceneRoot);", + " }", + " rootSet.forEach(function(r) {", + " try { r.updateMatrixWorld(true); } catch (eRootUpdate) {}", + " });", + "", + " var itemsByParentRepMesh = inst._itemsByParentRepMesh;", + " var repRelMatrixCache = inst._repRelMatrixCache;", + " itemsByParentRepMesh.clear();", + " repRelMatrixCache.clear();", + "", + " var localM = inst._tmpMat4_local;", + " if (!localM) {", + " localM = new THREE.Matrix4();", + " inst._tmpMat4_local = localM;", + " }", + "", + " var foliageRootMWInv = inst._tmpMat4_foliageRootInv;", + " if (!foliageRootMWInv) {", + " foliageRootMWInv = new THREE.Matrix4();", + " inst._tmpMat4_foliageRootInv = foliageRootMWInv;", + " }", + " if (inst.foliageRoot) {", + " try { foliageRootMWInv.copy(inst.foliageRoot.matrixWorld).invert(); } catch (eInv) { foliageRootMWInv.identity(); }", + " } else {", + " foliageRootMWInv.identity();", + " }", + "", + " for (var k = 0; k < listLen; k++) {", + " var item = list[k];", + " if (!item) continue;", + " if (item.queueId !== undefined && inst.cancelledQueueIds && inst.cancelledQueueIds.has(item.queueId)) {", + " continue;", + " }", + " var repMesh = item.repMesh;", + " if (!repMesh) continue;", + " var repMeshId = repMesh.uuid || repMesh.id || \"unknown\";", + " var cacheKey = \"RM:\" + repMeshId;", + " if (!itemsByParentRepMesh.has(cacheKey)) {", + " itemsByParentRepMesh.set(cacheKey, []);", + " }", + " itemsByParentRepMesh.get(cacheKey).push(item);", + " }", + "", + " for (const repMeshKey of itemsByParentRepMesh.keys()) {", + " var items = itemsByParentRepMesh.get(repMeshKey);", + " if (!items || items.length === 0) continue;", + " var firstItem = items[0];", + " var firstRepMesh = firstItem.repMesh;", + " var firstObj = firstItem.gdObj;", + " if (!firstRepMesh || !firstObj) continue;", + " var repRelMatrix = this.getOrComputeRepRelMatrix(firstObj, firstRepMesh);", + " repRelMatrixCache.set(repMeshKey, repRelMatrix);", + " }", + "", + " for (var i = 0; i < listLen; i++) {", + " var item = list[i];", + " if (!item) continue;", + "", + " if (item.queueId !== undefined && inst.cancelledQueueIds && inst.cancelledQueueIds.has(item.queueId)) {", + " inst.cancelledQueueIds.delete(item.queueId);", + " if (item.behavior && item.behavior.__foliageSharedKey) {", + " var cancelledEntry = state.sharedByKey.get(item.behavior.__foliageSharedKey);", + " if (cancelledEntry && cancelledEntry.refCount > 0) {", + " cancelledEntry.refCount--;", + " if (cancelledEntry.refCount <= 0) {", + " state.sharedByKey.delete(item.behavior.__foliageSharedKey);", + " state.unregisterActiveMaterial(cancelledEntry.material);", + " if (cancelledEntry.material) {", + " state.disposeShadowMaterials(cancelledEntry.material);", + " }", + " if (cancelledEntry.material && typeof cancelledEntry.material.dispose === \"function\") {", + " try { cancelledEntry.material.dispose(); } catch (eD) {}", + " }", + " }", + " }", + " }", + " if (item.behavior && item.behavior.__foliageSharedKeyTrunk) {", + " var cancelledEntryTrunk = state.sharedByKey.get(item.behavior.__foliageSharedKeyTrunk);", + " if (cancelledEntryTrunk && cancelledEntryTrunk.refCount > 0) {", + " cancelledEntryTrunk.refCount--;", + " if (cancelledEntryTrunk.refCount <= 0) {", + " state.sharedByKey.delete(item.behavior.__foliageSharedKeyTrunk);", + " state.unregisterActiveMaterial(cancelledEntryTrunk.material);", + " if (cancelledEntryTrunk.material) {", + " state.disposeShadowMaterials(cancelledEntryTrunk.material);", + " }", + " if (cancelledEntryTrunk.material && typeof cancelledEntryTrunk.material.dispose === \"function\") {", + " try { cancelledEntryTrunk.material.dispose(); } catch (eD2) {}", + " }", + " }", + " }", + " }", + " continue;", + " }", + "", + " if (item.parts && Array.isArray(item.parts) && item.parts.length >= 2) {", + " var part0 = item.parts[0];", + " var part1 = item.parts[1];", + " var repMeshP0 = part0.repMesh;", + " var gdObjParts = item.gdObj;", + " var threeObjParts = item.threeObj;", + " var behaviorParts = item.behavior;", + " if (!repMeshP0 || !part0.geometry || !part0.material || !part0.baseGroupKey ||", + " !part1.repMesh || !part1.geometry || !part1.material || !part1.baseGroupKey ||", + " !gdObjParts || !behaviorParts) continue;", + " if (behaviorParts.__foliageInstancingIndex !== undefined) continue;", + "", + " var objW2P = inst._tmpMat4_objWorld;", + " var repW2P = inst._tmpMat4_repWorld;", + " this.composeObjectWorldFromGDevelop(gdObjParts, inst, objW2P);", + " var repRel2P = this.getOrComputeRepRelMatrix(gdObjParts, repMeshP0);", + " if (repRel2P) {", + " repW2P.multiplyMatrices(objW2P, repRel2P);", + " } else {", + " try { repMeshP0.updateMatrixWorld(true); } catch (eF0p) {}", + " repW2P.copy(repMeshP0.matrixWorld);", + " }", + "", + " localM.multiplyMatrices(foliageRootMWInv, repW2P);", + " var detSignParts = this._getDetSignFromMatrix(localM);", + " var detSuffixParts = this._getDetSignSuffix(detSignParts);", + " var groupKeyTrunk = part0.baseGroupKey + detSuffixParts;", + " var groupKeyLeaves = part1.baseGroupKey + detSuffixParts;", + " var materialTrunk = this._getSignedInstancedMaterial(part0.material, detSignParts) || part0.material;", + " var materialLeaves = this._getSignedInstancedMaterial(part1.material, detSignParts) || part1.material;", + " this._logDetSignDiagnostic(\"split\", part0.baseGroupKey, detSignParts, behaviorParts);", + " var gTrunk = inst.ensureGroup(groupKeyTrunk, {", + " key: groupKeyTrunk,", + " geometry: part0.geometry,", + " material: materialTrunk,", + " parent: inst.foliageRoot", + " });", + " gTrunk.instanceDetSign = detSignParts;", + " gTrunk.castShadow = !!gTrunk.castShadow || !!(repMeshP0 && repMeshP0.castShadow);", + " gTrunk.receiveShadow = !!gTrunk.receiveShadow || !!(repMeshP0 && repMeshP0.receiveShadow);", + " if (typeof gTrunk.fadeEnabled !== \"boolean\" && behaviorParts) {", + " this._refreshLiveFadeState(gTrunk, behaviorParts, false);", + " }", + " if (behaviorParts) {", + " gTrunk._fadeBehavior = behaviorParts;", + " }", + "", + " var gLeaves = inst.ensureGroup(groupKeyLeaves, {", + " key: groupKeyLeaves,", + " geometry: part1.geometry,", + " material: materialLeaves,", + " parent: inst.foliageRoot", + " });", + " gLeaves.instanceDetSign = detSignParts;", + " if (!gLeaves.freeIndices || gLeaves.freeIndices !== gTrunk.freeIndices) gLeaves.freeIndices = gTrunk.freeIndices;", + " if (!gLeaves.freeIndexSet || gLeaves.freeIndexSet !== gTrunk.freeIndexSet) gLeaves.freeIndexSet = gTrunk.freeIndexSet;", + " gLeaves.castShadow = !!gLeaves.castShadow || !!(part1.repMesh && part1.repMesh.castShadow);", + " gLeaves.receiveShadow = !!gLeaves.receiveShadow || !!(part1.repMesh && part1.repMesh.receiveShadow);", + " if (typeof gLeaves.fadeEnabled !== \"boolean\" && behaviorParts) {", + " this._refreshLiveFadeState(gLeaves, behaviorParts, false);", + " }", + " if (behaviorParts) {", + " gLeaves._fadeBehavior = behaviorParts;", + " }", + "", + " var idxP;", + " var isReuseP = false;", + " if (gTrunk.freeIndices && gTrunk.freeIndices.length > 0 && gTrunk.freeIndexSet) {", + " while (gTrunk.freeIndices.length > 0) {", + " idxP = gTrunk.freeIndices.pop();", + " if (gTrunk.freeIndexSet.has(idxP)) {", + " gTrunk.freeIndexSet.delete(idxP);", + " isReuseP = true;", + " break;", + " }", + " }", + " }", + " if (!isReuseP) {", + " idxP = gTrunk.matrixCount;", + " gTrunk.matrixCount++;", + " }", + " if (typeof gTrunk.aliveCount !== \"number\") gTrunk.aliveCount = 0;", + " gTrunk.aliveCount++;", + "", + " var requiredSizeP = Math.max(idxP + 1, gTrunk.matrixCount) * 16;", + " if (!gTrunk.matricesBuffer || gTrunk.matricesBuffer.length < requiredSizeP) {", + " var currentSizeP = gTrunk.matricesBuffer ? gTrunk.matricesBuffer.length : 0;", + " var newSizeP = Math.max(requiredSizeP, Math.max(Math.floor(currentSizeP * 1.5), 256 * 16));", + " var newBufferP = new Float32Array(newSizeP);", + " if (gTrunk.matricesBuffer) newBufferP.set(gTrunk.matricesBuffer);", + " gTrunk.matricesBuffer = newBufferP;", + " }", + " var offsetP = idxP * 16;", + " this.applyInstancedTrunkFaceFix(gTrunk.material || null, detSignParts);", + " var srcP = localM.elements;", + " for (var mP = 0; mP < 16; mP++) {", + " gTrunk.matricesBuffer[offsetP + mP] = srcP[mP];", + " }", + " var cullSphereTrunk = this.computeWorldCullSphere(", + " repW2P,", + " part0.geometry,", + " gTrunk._instanceCullRadius,", + " state._tmpInstanceCullSphereA || (state._tmpInstanceCullSphereA = { cx: 0, cy: 0, cz: 0, radius: 0 })", + " );", + " if (gTrunk.instanceFade && idxP < gTrunk.instanceFade.length) gTrunk.instanceFade[idxP] = 1.0;", + " if (gTrunk.instanceFadePrev && idxP < gTrunk.instanceFadePrev.length) gTrunk.instanceFadePrev[idxP] = 1.0;", + "", + " gLeaves.matrixCount = Math.max(gLeaves.matrixCount, gTrunk.matrixCount);", + " gLeaves.aliveCount++;", + " var requiredSizeL = Math.max(idxP + 1, gLeaves.matrixCount) * 16;", + " if (!gLeaves.matricesBuffer || gLeaves.matricesBuffer.length < requiredSizeL) {", + " var currentSizeL = gLeaves.matricesBuffer ? gLeaves.matricesBuffer.length : 0;", + " var newSizeL = Math.max(requiredSizeL, Math.max(Math.floor(currentSizeL * 1.5), 256 * 16));", + " var newBufferL = new Float32Array(newSizeL);", + " if (gLeaves.matricesBuffer) newBufferL.set(gLeaves.matricesBuffer);", + " gLeaves.matricesBuffer = newBufferL;", + " }", + " for (var mL = 0; mL < 16; mL++) {", + " gLeaves.matricesBuffer[offsetP + mL] = srcP[mL];", + " }", + " var cullSphereLeaves = this.computeWorldCullSphere(", + " repW2P,", + " part1.geometry,", + " gLeaves._instanceCullRadius,", + " state._tmpInstanceCullSphereB || (state._tmpInstanceCullSphereB = { cx: 0, cy: 0, cz: 0, radius: 0 })", + " );", + " var cullSphereShared = this.unionCullSpheres(", + " cullSphereTrunk,", + " cullSphereLeaves,", + " state._tmpInstanceCullSphereShared || (state._tmpInstanceCullSphereShared = { cx: 0, cy: 0, cz: 0, radius: 0 })", + " );", + " this.writeInstanceCullValues(gTrunk, idxP, cullSphereShared.cx, cullSphereShared.cy, cullSphereShared.cz, cullSphereShared.radius);", + " this.writeInstanceCullValues(gLeaves, idxP, cullSphereShared.cx, cullSphereShared.cy, cullSphereShared.cz, cullSphereShared.radius);", + " if (gLeaves.instanceFade && idxP < gLeaves.instanceFade.length) gLeaves.instanceFade[idxP] = 1.0;", + " if (gLeaves.instanceFadePrev && idxP < gLeaves.instanceFadePrev.length) gLeaves.instanceFadePrev[idxP] = 1.0;", + "", + " try {", + " behaviorParts.__foliageInstancingGroupKey = groupKeyTrunk;", + " behaviorParts.__foliageInstancingGroupKeyLeaves = groupKeyLeaves;", + " behaviorParts.__foliageInstancingIndex = idxP;", + " delete behaviorParts.__foliageQueued;", + " delete behaviorParts.__foliageQueueId;", + " if (threeObjParts && threeObjParts.userData) {", + " threeObjParts.userData.__foliageInstancingGroupKey = groupKeyTrunk;", + " threeObjParts.userData.__foliageInstancingGroupKeyLeaves = groupKeyLeaves;", + " threeObjParts.userData.__foliageInstancingIndex = idxP;", + " if (threeObjParts.userData.__foliageQueueId !== undefined) delete threeObjParts.userData.__foliageQueueId;", + " }", + " } catch (eIdxP) {}", + "", + " try {", + " if (gdObjParts) {", + " var behP = gdObjParts ? /** @type {FoliageBehavior|null} */ (gdObjParts.getBehavior(\"FoliageSwaying\")) : null;", + " if (behP) behP.__foliageSkipOnDestroy = true;", + " if (gdObjParts.deleteFromScene) {", + " gdObjParts.deleteFromScene();", + " } else if (gdObjParts.hide && !gdObjParts.isHidden()) {", + " gdObjParts.hide();", + " }", + " }", + " } catch (eHideP) {}", + " inst.markDirty();", + " continue;", + " }", + "", + " var gdObj = item.gdObj;", + " var threeObj = item.threeObj;", + " var repMesh = item.repMesh;", + " var baseGroupKey = item.baseGroupKey || item.groupKey;", + " var sharedMat = item.material;", + " var geometry = item.geometry;", + "", + " if (!baseGroupKey || !geometry || !sharedMat) continue;", + " if (!threeObj || !repMesh) continue;", + " if (item.behavior && item.behavior.__foliageInstancingIndex !== undefined) continue;", + "", + " var objW2 = inst._tmpMat4_objWorld;", + " var repW2 = inst._tmpMat4_repWorld;", + " this.composeObjectWorldFromGDevelop(gdObj, inst, objW2);", + "", + " var repMeshId = repMesh.uuid || repMesh.id || \"unknown\";", + " var relKey = \"RM:\" + repMeshId;", + " var repRel2 = repRelMatrixCache.get(relKey);", + " if (!repRel2) {", + " repRel2 = this.getOrComputeRepRelMatrix(gdObj, repMesh);", + " }", + "", + " if (repRel2) {", + " repW2.multiplyMatrices(objW2, repRel2);", + " } else {", + " try { repMesh.updateMatrixWorld(true); } catch (eF0) {}", + " repW2.copy(repMesh.matrixWorld);", + " }", + "", + " localM.multiplyMatrices(foliageRootMWInv, repW2);", + " var groupKey = baseGroupKey;", + " var resolvedSharedMat = sharedMat;", + " var detSignSingle = 1;", + " var isSignedLeavesGroup = false;", + " var itemSwayType = item.swayType || \"\";", + " if (!itemSwayType && item.behavior) {", + " var itemProps = state.getBehaviorProps(item.behavior);", + " if (itemProps && itemProps.swayType) itemSwayType = itemProps.swayType;", + " }", + " if (itemSwayType === \"leavesSway\") {", + " detSignSingle = this._getDetSignFromMatrix(localM);", + " groupKey = baseGroupKey + this._getDetSignSuffix(detSignSingle);", + " resolvedSharedMat = this._getSignedInstancedMaterial(sharedMat, detSignSingle) || sharedMat;", + " isSignedLeavesGroup = true;", + " this._logDetSignDiagnostic(\"single\", baseGroupKey, detSignSingle, item.behavior);", + " }", + " var g = inst.ensureGroup(groupKey, {", + " key: groupKey,", + " geometry: geometry,", + " material: resolvedSharedMat,", + " parent: inst.foliageRoot", + " });", + " if (isSignedLeavesGroup) {", + " g.instanceDetSign = detSignSingle;", + " }", + " g.castShadow = !!g.castShadow || !!(repMesh && repMesh.castShadow);", + " g.receiveShadow = !!g.receiveShadow || !!(repMesh && repMesh.receiveShadow);", + " if (typeof g.fadeEnabled !== \"boolean\" && item.behavior) {", + " this._refreshLiveFadeState(g, item.behavior, false);", + " }", + " if (item.behavior) {", + " g._fadeBehavior = item.behavior;", + " }", + "", + " var idx;", + " var isReuse = false;", + " if (g.freeIndices && g.freeIndices.length > 0 && g.freeIndexSet) {", + " while (g.freeIndices.length > 0) {", + " idx = g.freeIndices.pop();", + " if (g.freeIndexSet.has(idx)) {", + " g.freeIndexSet.delete(idx);", + " isReuse = true;", + " break;", + " }", + " }", + " }", + " if (!isReuse) {", + " idx = g.matrixCount;", + " g.matrixCount++;", + " }", + " if (typeof g.aliveCount !== \"number\") g.aliveCount = 0;", + " g.aliveCount++;", + "", + " var requiredSize = Math.max(idx + 1, g.matrixCount) * 16;", + " if (!g.matricesBuffer || g.matricesBuffer.length < requiredSize) {", + " var currentSize = g.matricesBuffer ? g.matricesBuffer.length : 0;", + " var newSize = Math.max(requiredSize, Math.max(Math.floor(currentSize * 1.5), 256 * 16));", + " var newBuffer = new Float32Array(newSize);", + " if (g.matricesBuffer) newBuffer.set(g.matricesBuffer);", + " g.matricesBuffer = newBuffer;", + " }", + "", + " var offset = idx * 16;", + " var src = localM.elements;", + " for (var m = 0; m < 16; m++) {", + " g.matricesBuffer[offset + m] = src[m];", + " }", + "", + " this.writeInstanceCullData(g, idx, repW2, geometry, g._instanceCullRadius);", + " if (g.instanceFade && idx < g.instanceFade.length) g.instanceFade[idx] = 1.0;", + " if (g.instanceFadePrev && idx < g.instanceFadePrev.length) g.instanceFadePrev[idx] = 1.0;", + "", + " try {", + " if (item.behavior) {", + " item.behavior.__foliageInstancingGroupKey = groupKey;", + " item.behavior.__foliageInstancingIndex = idx;", + " delete item.behavior.__foliageQueued;", + " delete item.behavior.__foliageQueueId;", + " }", + " if (threeObj && threeObj.userData) {", + " threeObj.userData.__foliageInstancingGroupKey = groupKey;", + " threeObj.userData.__foliageInstancingIndex = idx;", + " if (threeObj.userData.__foliageQueueId !== undefined) {", + " delete threeObj.userData.__foliageQueueId;", + " }", + " }", + " } catch (eIdx) {}", + "", + " try {", + " if (gdObj) {", + " var beh = gdObj ? /** @type {FoliageBehavior|null} */ (gdObj.getBehavior(\"FoliageSwaying\")) : null;", + " if (beh) beh.__foliageSkipOnDestroy = true;", + " if (gdObj.deleteFromScene) {", + " gdObj.deleteFromScene(ctx.runtimeScene);", + " } else if (gdObj.hide) {", + " gdObj.hide(true);", + " }", + " } else if (threeObj) {", + " threeObj.visible = false;", + " }", + " } catch (eHide) {}", + " }", + "", + " inst.markDirty();", + " }", + "", + " /**", + " * Rebuild only the instanced groups marked dirty by queue or lifecycle changes.", + " * This is the point where storage buffers become live InstancedMesh attributes.", + " */", + " rebuildDirtyGroups() {", + " var state = this.state;", + " var inst = state.instancingState || state.instancing || null;", + " if (!inst || !inst.dirty || !inst.groups) return;", + " inst.ensureReady();", + " inst.dirty = false;", + "", + " var self = this;", + " inst.groups.forEach(function(group) {", + " var g = group;", + " if (!g || !g.parent || !g.geometry || !g.material || !g.matricesBuffer) return;", + "", + " var aliveCount = g.aliveCount || 0;", + " var totalSlots = g.matrixCount || 0;", + " if (aliveCount <= 0 || totalSlots <= 0) return;", + "", + " g._instanceCullRadius = self.computeInstanceCullRadius(g.geometry, g._instanceCullRadius);", + " var mesh = g.mesh;", + " var cap = (typeof g.capacity === \"number\") ? g.capacity : 0;", + " if (mesh && mesh.instanceMatrix && isFinite(mesh.instanceMatrix.count)) {", + " if (!cap || cap < mesh.instanceMatrix.count) cap = mesh.instanceMatrix.count;", + " }", + "", + " var need = aliveCount;", + " var shouldRecreate = false;", + " var newCap = cap;", + " if (mesh && g._srcGeometryRef && g.geometry !== g._srcGeometryRef) {", + " shouldRecreate = true;", + " }", + "", + " if (!mesh) {", + " newCap = nextPow2(need);", + " var floor = need > 512 ? 4096 : 512;", + " if (newCap < floor) newCap = floor;", + " shouldRecreate = true;", + " } else if (need > cap) {", + " newCap = nextPow2(need);", + " if (cap > 0) newCap = Math.max(newCap, Math.floor(cap * 1.5));", + " var floor2 = need > 512 ? 4096 : 512;", + " if (newCap < floor2) newCap = floor2;", + " shouldRecreate = true;", + " }", + "", + " if (shouldRecreate) {", + " try {", + " if (mesh) {", + " if (mesh.parent) mesh.parent.remove(mesh);", + " if (g._ownedGeometry && typeof g._ownedGeometry.dispose === \"function\") {", + " try { g._ownedGeometry.dispose(); } catch (eGeo) {}", + " }", + " try { if (mesh.dispose) mesh.dispose(); } catch (eMesh) {}", + " }", + " } catch (eDisposeMesh) {}", + "", + " try {", + " var clonedGeometry = g.geometry.clone();", + " mesh = new THREE.InstancedMesh(clonedGeometry, g.material, newCap);", + " mesh.frustumCulled = false;", + " mesh.matrixAutoUpdate = false;", + " mesh.matrix.identity();", + " mesh.name = \"FoliageInstanced::\" + (g.key || \"\");", + " mesh.castShadow = !!g.castShadow;", + " mesh.receiveShadow = !!g.receiveShadow;", + " if (g.material && g.material.userData) {", + " mesh.customDepthMaterial = g.material.userData._foliageDepthMat || null;", + " mesh.customDistanceMaterial = g.material.userData._foliageDistanceMat || null;", + " } else {", + " mesh.customDepthMaterial = null;", + " mesh.customDistanceMaterial = null;", + " }", + "", + " var fadeAttr = new THREE.InstancedBufferAttribute(new Float32Array(newCap), 1);", + " fadeAttr.setUsage(THREE.DynamicDrawUsage);", + " for (var fi = 0; fi < newCap; fi++) fadeAttr.array[fi] = 1.0;", + " mesh.geometry.setAttribute(\"aFade\", fadeAttr);", + "", + " var fadePrevAttr = new THREE.InstancedBufferAttribute(new Float32Array(newCap), 1);", + " fadePrevAttr.setUsage(THREE.DynamicDrawUsage);", + " for (var fpi = 0; fpi < newCap; fpi++) fadePrevAttr.array[fpi] = 1.0;", + " mesh.geometry.setAttribute(\"aFadePrev\", fadePrevAttr);", + "", + " mesh.count = 0;", + " mesh.visible = false;", + " var attachParent = g.parent || inst.foliageRoot;", + " if (attachParent) attachParent.add(mesh);", + " g.mesh = mesh;", + " g._ownedGeometry = clonedGeometry;", + " g.capacity = newCap;", + " g._srcGeometryRef = g.geometry;", + " g._fadeDisabledApplied = false;", + " } catch (eRebuild) {", + " g.mesh = null;", + " g._ownedGeometry = null;", + " g.capacity = 0;", + " return;", + " }", + " } else {", + " if (mesh && !g.mesh) g.mesh = mesh;", + " try {", + " mesh.castShadow = !!g.castShadow;", + " mesh.receiveShadow = !!g.receiveShadow;", + " if (g.material && g.material.userData) {", + " mesh.customDepthMaterial = g.material.userData._foliageDepthMat || null;", + " mesh.customDistanceMaterial = g.material.userData._foliageDistanceMat || null;", + " } else {", + " mesh.customDepthMaterial = null;", + " mesh.customDistanceMaterial = null;", + " }", + " } catch (eShadow) {}", + " if (mesh && mesh.instanceMatrix && isFinite(mesh.instanceMatrix.count)) {", + " g.capacity = mesh.instanceMatrix.count;", + " } else if (typeof g.capacity !== \"number\") {", + " g.capacity = cap || 0;", + " }", + " }", + " });", + " }", + " }", + "", + " /**", + " * Owner of instanced foliage culling decisions and the camera snapshots needed", + " * to decide when distance or frustum work has to run again.", + " */", + " class FoliageCullingCoordinator {", + " /**", + " * @param {FoliageSceneState} state", + " */", + " constructor(state) {", + " /** @type {FoliageSceneState} */", + " this.state = state;", + " /** @type {THREE.Vector3|null} */", + " this._tmpVec3InstancedCull = null;", + " /** @type {THREE.Sphere|null} */", + " this._tmpSphereInstancedCull = null;", + " /** @type {THREE.Vector3|null} */", + " this._tmpVec3InstancedCamDir = null;", + " /** @type {THREE.Vector3|null} */", + " this._tmpVec3FrustumCamDir = null;", + " /** @type {number|undefined} */", + " this._prevCullCamDirX = undefined;", + " /** @type {number|undefined} */", + " this._prevCullCamDirY = undefined;", + " /** @type {number|undefined} */", + " this._prevCullCamDirZ = undefined;", + " /** @type {number|undefined} */", + " this._prevCullCamX = undefined;", + " /** @type {number|undefined} */", + " this._prevCullCamY = undefined;", + " /** @type {number|undefined} */", + " this._prevCullCamZ = undefined;", + " /** @type {number|undefined} */", + " this._prevFrustumPassCamX = undefined;", + " /** @type {number|undefined} */", + " this._prevFrustumPassCamY = undefined;", + " /** @type {number|undefined} */", + " this._prevFrustumPassCamZ = undefined;", + " /** @type {number|undefined} */", + " this._prevFrustumPassCamDirX = undefined;", + " /** @type {number|undefined} */", + " this._prevFrustumPassCamDirY = undefined;", + " /** @type {number|undefined} */", + " this._prevFrustumPassCamDirZ = undefined;", + " this.reset();", + " }", + "", + " /**", + " * @returns {FoliageCullingCoordinator}", + " */", + " reset() {", + " this._tmpVec3InstancedCull = null;", + " this._tmpSphereInstancedCull = null;", + " this._tmpVec3InstancedCamDir = null;", + " this._tmpVec3FrustumCamDir = null;", + " this._prevCullCamDirX = undefined;", + " this._prevCullCamDirY = undefined;", + " this._prevCullCamDirZ = undefined;", + " this._prevCullCamX = undefined;", + " this._prevCullCamY = undefined;", + " this._prevCullCamZ = undefined;", + " this._prevFrustumPassCamX = undefined;", + " this._prevFrustumPassCamY = undefined;", + " this._prevFrustumPassCamZ = undefined;", + " this._prevFrustumPassCamDirX = undefined;", + " this._prevFrustumPassCamDirY = undefined;", + " this._prevFrustumPassCamDirZ = undefined;", + " return this;", + " }", + "", + " /**", + " * @param {SharedFrameContext|null|undefined} frameCam", + " */", + " didFrustumCameraChange(frameCam) {", + " var cam = frameCam && frameCam.cam ? frameCam.cam : null;", + " if (!cam) return false;", + "", + " var camX = isFinite(frameCam.camX) ? frameCam.camX : cam.position.x;", + " var camY = isFinite(frameCam.camY) ? frameCam.camY : cam.position.y;", + " var camZ = isFinite(frameCam.camZ) ? frameCam.camZ : cam.position.z;", + " var prevCamX = isFinite(this._prevFrustumPassCamX) ? this._prevFrustumPassCamX : camX;", + " var prevCamY = isFinite(this._prevFrustumPassCamY) ? this._prevFrustumPassCamY : camY;", + " var prevCamZ = isFinite(this._prevFrustumPassCamZ) ? this._prevFrustumPassCamZ : camZ;", + " var camDelta = Math.abs(camX - prevCamX) + Math.abs(camY - prevCamY) + Math.abs(camZ - prevCamZ);", + " var camMoved = camDelta > 0.5;", + " this._prevFrustumPassCamX = camX;", + " this._prevFrustumPassCamY = camY;", + " this._prevFrustumPassCamZ = camZ;", + "", + " var dirChanged = false;", + " if (typeof cam.getWorldDirection === \"function\" && typeof THREE.Vector3 !== \"undefined\") {", + " if (!this._tmpVec3FrustumCamDir) this._tmpVec3FrustumCamDir = new THREE.Vector3();", + " var camDir = this._tmpVec3FrustumCamDir;", + " try { cam.getWorldDirection(camDir); } catch (eDir) {}", + " var prevDirX = isFinite(this._prevFrustumPassCamDirX) ? this._prevFrustumPassCamDirX : camDir.x;", + " var prevDirY = isFinite(this._prevFrustumPassCamDirY) ? this._prevFrustumPassCamDirY : camDir.y;", + " var prevDirZ = isFinite(this._prevFrustumPassCamDirZ) ? this._prevFrustumPassCamDirZ : camDir.z;", + " var dirDelta = Math.abs(camDir.x - prevDirX) + Math.abs(camDir.y - prevDirY) + Math.abs(camDir.z - prevDirZ);", + " dirChanged = dirDelta > 0.001;", + " this._prevFrustumPassCamDirX = camDir.x;", + " this._prevFrustumPassCamDirY = camDir.y;", + " this._prevFrustumPassCamDirZ = camDir.z;", + " }", + "", + " return camMoved || dirChanged;", + " }", + "", + " /**", + " * Update per-instance fade values and candidate lists from camera distance.", + " * This pass prepares the compact instance set used later by frustum compaction.", + " * @param {SharedFrameContext|null|undefined} ctx", + " * @param {boolean} forceRun", + " * @param {(target: FoliageInstancingGroup|FoliageNonInstancedEntry|null|undefined, behavior: FoliageBehavior|null|undefined, defaultEnabled: boolean) => void} refreshFadeState", + " */", + " performDistanceCullTick(ctx, forceRun, refreshFadeState) {", + " var state = this.state;", + " var inst = state.instancing;", + " if (!inst || !inst.groups) return false;", + "", + " var cam = ctx && ctx.cam ? ctx.cam : null;", + " var camX = ctx && isFinite(ctx.camX) ? ctx.camX : 0;", + " var camY = ctx && isFinite(ctx.camY) ? ctx.camY : 0;", + " var camZ = ctx && isFinite(ctx.camZ) ? ctx.camZ : 0;", + "", + " state.lastCamX = camX;", + " state.lastCamY = camY;", + " state.lastCamZ = camZ;", + "", + " var prevCamX = isFinite(this._prevCullCamX) ? this._prevCullCamX : camX;", + " var prevCamY = isFinite(this._prevCullCamY) ? this._prevCullCamY : camY;", + " var prevCamZ = isFinite(this._prevCullCamZ) ? this._prevCullCamZ : camZ;", + " var camDelta = Math.abs(camX - prevCamX) + Math.abs(camY - prevCamY) + Math.abs(camZ - prevCamZ);", + " var camMoved = camDelta > 0.5;", + " this._prevCullCamX = camX;", + " this._prevCullCamY = camY;", + " this._prevCullCamZ = camZ;", + "", + " var camDirChanged = false;", + " if (cam && typeof cam.getWorldDirection === \"function\" && typeof THREE.Vector3 !== \"undefined\") {", + " if (!this._tmpVec3InstancedCamDir) this._tmpVec3InstancedCamDir = new THREE.Vector3();", + " var camDir = this._tmpVec3InstancedCamDir;", + " try { cam.getWorldDirection(camDir); } catch (eDir) {}", + " var prevDirX = isFinite(this._prevCullCamDirX) ? this._prevCullCamDirX : camDir.x;", + " var prevDirY = isFinite(this._prevCullCamDirY) ? this._prevCullCamDirY : camDir.y;", + " var prevDirZ = isFinite(this._prevCullCamDirZ) ? this._prevCullCamDirZ : camDir.z;", + " var dirDelta = Math.abs(camDir.x - prevDirX) + Math.abs(camDir.y - prevDirY) + Math.abs(camDir.z - prevDirZ);", + " camDirChanged = dirDelta > 0.001;", + " this._prevCullCamDirX = camDir.x;", + " this._prevCullCamDirY = camDir.y;", + " this._prevCullCamDirZ = camDir.z;", + " }", + "", + " var fadeParamsChanged = false;", + " inst.groups.forEach(function(group) {", + " /** @type {FoliageInstancingGroup} */", + " var g = group;", + " if (!g) return;", + " if (typeof refreshFadeState === \"function\") {", + " refreshFadeState(g, g._fadeBehavior || null, false);", + " }", + " var sigEnabled = !!g.fadeEnabled;", + " var sigStart = isFinite(g.fadeStart) ? Math.round(g.fadeStart) : 1200;", + " var sigEnd = isFinite(g.fadeEnd) ? Math.round(g.fadeEnd) : 1600;", + " var newSig = (sigEnabled ? 1 : 0) + \"|\" + sigStart + \"|\" + sigEnd;", + " if (g._fadeSig !== newSig) {", + " g._fadeSig = newSig;", + " fadeParamsChanged = true;", + " }", + " });", + " if (fadeParamsChanged) state._fadeParamsDirty = true;", + "", + " var hasPending = !!(inst.pending && inst.pending.length > 0);", + " if (!forceRun && !camMoved && !camDirChanged && !inst.dirty && !hasPending && !state._fadeParamsDirty) {", + " return false;", + " }", + "", + " inst.groups.forEach(function(group) {", + " /** @type {FoliageInstancingGroup} */", + " var g = group;", + " if (!g || !g.mesh) return;", + "", + " var mesh = g.mesh;", + "", + " if (!g.fadeEnabled) {", + " var fadeEnabledPrev = (typeof g._lastFadeEnabled === \"boolean\") ? g._lastFadeEnabled : false;", + " var justDisabled = fadeEnabledPrev && !g.fadeEnabled;", + " g._lastFadeEnabled = !!g.fadeEnabled;", + " g._distanceCandidateCount = 0;", + " if (justDisabled || !g._fadeDisabledApplied) {", + " var aliveN = g.aliveCount || 0;", + " if (aliveN === 0) {", + " mesh.count = 0;", + " mesh.visible = false;", + " g.visibleCount = 0;", + " g._fadeDisabledApplied = true;", + " return;", + " }", + " var freeSet0 = g.freeIndexSet;", + " var totalSlots0 = g.matrixCount || 0;", + " var storMat0 = g.matricesBuffer;", + " var renderMat0 = mesh.instanceMatrix ? mesh.instanceMatrix.array : null;", + " var renderFade0 = mesh.geometry ? mesh.geometry.getAttribute(\"aFade\") : null;", + " var renderFadePrev0 = mesh.geometry ? mesh.geometry.getAttribute(\"aFadePrev\") : null;", + " if (!renderMat0 || !storMat0) { g._fadeDisabledApplied = true; return; }", + " var vis0 = 0;", + " for (var i0 = 0; i0 < totalSlots0; i0++) {", + " if (freeSet0 && freeSet0.has(i0)) continue;", + " var sOff0 = i0 * 16; var dOff0 = vis0 * 16;", + " renderMat0[dOff0] = storMat0[sOff0];", + " renderMat0[dOff0 + 1] = storMat0[sOff0 + 1];", + " renderMat0[dOff0 + 2] = storMat0[sOff0 + 2];", + " renderMat0[dOff0 + 3] = storMat0[sOff0 + 3];", + " renderMat0[dOff0 + 4] = storMat0[sOff0 + 4];", + " renderMat0[dOff0 + 5] = storMat0[sOff0 + 5];", + " renderMat0[dOff0 + 6] = storMat0[sOff0 + 6];", + " renderMat0[dOff0 + 7] = storMat0[sOff0 + 7];", + " renderMat0[dOff0 + 8] = storMat0[sOff0 + 8];", + " renderMat0[dOff0 + 9] = storMat0[sOff0 + 9];", + " renderMat0[dOff0 + 10] = storMat0[sOff0 + 10];", + " renderMat0[dOff0 + 11] = storMat0[sOff0 + 11];", + " renderMat0[dOff0 + 12] = storMat0[sOff0 + 12];", + " renderMat0[dOff0 + 13] = storMat0[sOff0 + 13];", + " renderMat0[dOff0 + 14] = storMat0[sOff0 + 14];", + " renderMat0[dOff0 + 15] = storMat0[sOff0 + 15];", + " if (renderFade0 && renderFade0.array) renderFade0.array[vis0] = 1.0;", + " if (renderFadePrev0 && renderFadePrev0.array) renderFadePrev0.array[vis0] = 1.0;", + " vis0++;", + " }", + " mesh.count = vis0;", + " mesh.instanceMatrix.needsUpdate = true;", + " if (renderFade0) renderFade0.needsUpdate = true;", + " if (renderFadePrev0) renderFadePrev0.needsUpdate = true;", + " mesh.visible = (vis0 > 0);", + " g.visibleCount = vis0;", + " g._fadeDisabledApplied = true;", + " }", + " return;", + " }", + "", + " g._lastFadeEnabled = true;", + " g._fadeDisabledApplied = false;", + "", + " if (!g.centersXY) {", + " g._distanceCandidateCount = 0;", + " return;", + " }", + "", + " var freeSet = g.freeIndexSet;", + " var totalSlots = g.matrixCount || 0;", + " var startD = g.fadeStart || 1200;", + " var endD = g.fadeEnd || 1600;", + " var range = endD - startD;", + " if (range <= 0) range = 100;", + " var invRange = 1.0 / range;", + " var startDSq = startD * startD;", + " var margin = 256;", + " var endPlusMargin = endD + margin;", + " var endPlusMarginSq = endPlusMargin * endPlusMargin;", + "", + " var oldFade = g.instanceFade;", + " var oldPrev = g.instanceFadePrev;", + " var needResize = !oldFade || !oldPrev || oldFade.length < totalSlots || oldPrev.length < totalSlots;", + " if (needResize) {", + " var newCap = Math.max(", + " totalSlots,", + " 256,", + " oldFade ? oldFade.length : 0,", + " oldPrev ? oldPrev.length : 0", + " );", + " var newFade = new Float32Array(newCap);", + " var newPrev = new Float32Array(newCap);", + " if (oldFade) newFade.set(oldFade);", + " if (oldPrev) newPrev.set(oldPrev);", + " for (var fInit = oldFade ? oldFade.length : 0; fInit < newCap; fInit++) {", + " newFade[fInit] = 1.0;", + " }", + " for (var pInit = oldPrev ? oldPrev.length : 0; pInit < newCap; pInit++) {", + " newPrev[pInit] = 1.0;", + " }", + " g.instanceFade = newFade;", + " g.instanceFadePrev = newPrev;", + " }", + "", + " var tmpSwap = g.instanceFadePrev;", + " g.instanceFadePrev = g.instanceFade;", + " g.instanceFade = tmpSwap;", + "", + " var storCenters = g.centersXY;", + " var storFade = g.instanceFade;", + " if (!storCenters || !storFade) {", + " g._distanceCandidateCount = 0;", + " return;", + " }", + "", + " var candidates = g._distanceCandidateIndices;", + " if (!candidates || candidates.length < totalSlots) {", + " var candCap = Math.max(totalSlots, candidates ? candidates.length : 0, 256);", + " candidates = new Uint32Array(candCap);", + " g._distanceCandidateIndices = candidates;", + " }", + "", + " var candidateCount = 0;", + " for (var i = 0; i < totalSlots; i++) {", + " if (freeSet && freeSet.has(i)) continue;", + "", + " var wx = storCenters[i * 2];", + " var wy = storCenters[i * 2 + 1];", + " var dx = wx - camX;", + " var dy = wy - camY;", + " var distSq = dx * dx + dy * dy;", + "", + " if (distSq > endPlusMarginSq) {", + " storFade[i] = 0.0;", + " continue;", + " }", + "", + " var fade;", + " if (distSq < startDSq) {", + " fade = 1.0;", + " } else {", + " var dist = Math.sqrt(distSq);", + " var t = (dist - startD) * invRange;", + " if (t < 0) t = 0; else if (t > 1) t = 1;", + " fade = 1.0 - (t * t * (3.0 - 2.0 * t));", + " }", + " storFade[i] = fade;", + "", + " if (fade < 0.01) {", + " continue;", + " }", + "", + " candidates[candidateCount++] = i;", + " }", + "", + " g._distanceCandidateCount = candidateCount;", + " });", + "", + " state.lastCullTime = state.time;", + " state._fadeParamsDirty = false;", + " inst.dirty = false;", + " return true;", + " }", + "", + " /**", + " * Compact instanced groups into their visible render buffers after the latest", + " * distance pass, preserving only instances that should stay renderable this frame.", + " * @param {SharedFrameContext|null|undefined} ctx", + " */", + " performFrustumCompactionPass(ctx) {", + " var state = this.state;", + " var inst = state.instancing;", + " if (!inst || !inst.groups) return false;", + "", + " var frustumInstanced = ctx && ctx.frustum ? ctx.frustum : null;", + "", + " var tmpVec3InstancedCull = null;", + " if (typeof THREE.Vector3 !== \"undefined\") {", + " if (!this._tmpVec3InstancedCull) this._tmpVec3InstancedCull = new THREE.Vector3();", + " tmpVec3InstancedCull = this._tmpVec3InstancedCull;", + " }", + " var tmpSphereInstancedCull = null;", + " if (typeof THREE.Sphere !== \"undefined\") {", + " if (!this._tmpSphereInstancedCull) this._tmpSphereInstancedCull = new THREE.Sphere();", + " tmpSphereInstancedCull = this._tmpSphereInstancedCull;", + " }", + " var frustumRadiusScale = 1.10;", + " var frustumRadiusMargin = 32.0;", + "", + " var anyProcessed = false;", + " inst.groups.forEach(function(group) {", + " /** @type {FoliageInstancingGroup} */", + " var g = group;", + " if (!g || !g.mesh) return;", + "", + " var mesh = g.mesh;", + " anyProcessed = true;", + "", + " var fadeEnabled = !!g.fadeEnabled;", + " var candidates = g._distanceCandidateIndices;", + " var candidateCount = (typeof g._distanceCandidateCount === \"number\") ? g._distanceCandidateCount : 0;", + " var totalSlots = g.matrixCount || 0;", + " var freeSet = g.freeIndexSet;", + " if (fadeEnabled && (!candidates || candidateCount <= 0)) {", + " mesh.count = 0;", + " mesh.visible = false;", + " g.visibleCount = 0;", + " return;", + " }", + " if (!fadeEnabled && totalSlots <= 0) {", + " mesh.count = 0;", + " mesh.visible = false;", + " g.visibleCount = 0;", + " return;", + " }", + "", + " var storMat = g.matricesBuffer;", + " var renderMat = mesh.instanceMatrix ? mesh.instanceMatrix.array : null;", + " var renderFadeAttr = mesh.geometry ? mesh.geometry.getAttribute(\"aFade\") : null;", + " var renderFadePrevAttr = mesh.geometry ? mesh.geometry.getAttribute(\"aFadePrev\") : null;", + " var storCenters = g.centersXY;", + " var storCentersZ = g.centersZ;", + " var storRadii = g.cullRadii;", + " var storFade = fadeEnabled ? g.instanceFade : null;", + " var storFadePrev = fadeEnabled ? g.instanceFadePrev : null;", + " var fallbackCullRadius = (isFinite(g._instanceCullRadius) && g._instanceCullRadius > 0) ? g._instanceCullRadius : 0;", + " if (!renderMat || !storMat || !storCenters) return;", + " if (fadeEnabled && (!storFade || !storFadePrev)) return;", + "", + " // Fade-enabled shadow casters stay on distance-only filtering so off-screen casters can still project visible shadows.", + " var useDistanceOnly = !!g.castShadow && !!fadeEnabled;", + " var visibleCount = 0;", + " var iterCount = fadeEnabled ? candidateCount : totalSlots;", + " for (var ci = 0; ci < iterCount; ci++) {", + " var i = fadeEnabled ? candidates[ci] : ci;", + " if (!fadeEnabled && freeSet && freeSet.has(i)) continue;", + "", + " var wx = storCenters[i * 2];", + " var wy = storCenters[i * 2 + 1];", + " var wz = 0;", + " if (storCentersZ && i < storCentersZ.length) {", + " wz = storCentersZ[i];", + " } else {", + " var zOffDbg = i * 16 + 14;", + " if (zOffDbg < storMat.length) wz = storMat[zOffDbg];", + " }", + " if (!isFinite(wz)) wz = 0;", + "", + " var inFrustum = true;", + " if (!useDistanceOnly && frustumInstanced) {", + " var instanceCullRadius = (storRadii && i < storRadii.length && isFinite(storRadii[i]) && storRadii[i] > 0)", + " ? storRadii[i]", + " : fallbackCullRadius;", + " if (instanceCullRadius > 0 && tmpSphereInstancedCull && typeof frustumInstanced.intersectsSphere === \"function\") {", + " tmpSphereInstancedCull.center.set(wx, wy, wz);", + " tmpSphereInstancedCull.radius = instanceCullRadius * frustumRadiusScale + frustumRadiusMargin;", + " inFrustum = frustumInstanced.intersectsSphere(tmpSphereInstancedCull);", + " } else if (tmpVec3InstancedCull && typeof frustumInstanced.containsPoint === \"function\") {", + " tmpVec3InstancedCull.set(wx, wy, wz);", + " inFrustum = frustumInstanced.containsPoint(tmpVec3InstancedCull);", + " }", + " }", + "", + " if (!inFrustum) {", + " continue;", + " }", + "", + " var sOff = i * 16; var dOff = visibleCount * 16;", + " renderMat[dOff] = storMat[sOff];", + " renderMat[dOff + 1] = storMat[sOff + 1];", + " renderMat[dOff + 2] = storMat[sOff + 2];", + " renderMat[dOff + 3] = storMat[sOff + 3];", + " renderMat[dOff + 4] = storMat[sOff + 4];", + " renderMat[dOff + 5] = storMat[sOff + 5];", + " renderMat[dOff + 6] = storMat[sOff + 6];", + " renderMat[dOff + 7] = storMat[sOff + 7];", + " renderMat[dOff + 8] = storMat[sOff + 8];", + " renderMat[dOff + 9] = storMat[sOff + 9];", + " renderMat[dOff + 10] = storMat[sOff + 10];", + " renderMat[dOff + 11] = storMat[sOff + 11];", + " renderMat[dOff + 12] = storMat[sOff + 12];", + " renderMat[dOff + 13] = storMat[sOff + 13];", + " renderMat[dOff + 14] = storMat[sOff + 14];", + " renderMat[dOff + 15] = storMat[sOff + 15];", + "", + " if (renderFadeAttr && renderFadeAttr.array) renderFadeAttr.array[visibleCount] = fadeEnabled ? storFade[i] : 1.0;", + " if (renderFadePrevAttr && renderFadePrevAttr.array) renderFadePrevAttr.array[visibleCount] = fadeEnabled ? storFadePrev[i] : 1.0;", + " visibleCount++;", + " }", + "", + " mesh.count = visibleCount;", + " mesh.instanceMatrix.needsUpdate = true;", + " if (renderFadeAttr) renderFadeAttr.needsUpdate = true;", + " if (renderFadePrevAttr) renderFadePrevAttr.needsUpdate = true;", + " mesh.visible = (visibleCount > 0);", + " g.visibleCount = visibleCount;", + " });", + "", + " return anyProcessed;", + " }", + " }", + "", + " /**", + " * Sync helper for foliage shadow materials.", + " * It mirrors the host foliage material state onto depth/distance shadow variants", + " * so shadow passes follow sway, gust, and fade values consistently.", + " */", + " class FoliageShadowUniformSync {", + " /**", + " * @param {FoliageSceneState} state", + " */", + " constructor(state) {", + " /** @type {FoliageSceneState} */", + " this.state = state;", + " }", + "", + " /**", + " * Push the current frame's foliage runtime values into a single shadow material.", + " * The host material remains the source of truth for fade and foliage config state.", + " * @param {THREE.Material|null|undefined} hostMat", + " * @param {THREE.Material|null|undefined} shadowMat", + " * @param {SharedFrameContext} ctx", + " * @param {number} fadeInterpT", + " */", + " syncMaterial(hostMat, shadowMat, ctx, fadeInterpT) {", + " var state = this.state;", + " if (!shadowMat || !shadowMat.userData) return;", + " var su = shadowMat.userData.foliageUniforms;", + " if (!su) return;", + " var cfgShadow = shadowMat.userData.foliageConfig || (hostMat && hostMat.userData ? hostMat.userData.foliageConfig : null);", + " var pendingFade = 1.0;", + " if (hostMat && hostMat.userData && typeof hostMat.userData._pendingFade === \"number\") {", + " pendingFade = hostMat.userData._pendingFade;", + " } else if (typeof shadowMat.userData._pendingFade === \"number\") {", + " pendingFade = shadowMat.userData._pendingFade;", + " }", + "", + " if (su.uTime) su.uTime.value = ctx.time;", + " if (su.uWindStrength) su.uWindStrength.value = ctx.windStrength;", + " if (su.uWindSpeed) su.uWindSpeed.value = ctx.windSpeed;", + " if (su.uWindDir) su.uWindDir.value.set(ctx.wx, ctx.wy);", + " if (su.uLocalWindDir) su.uLocalWindDir.value.set(ctx.wx, ctx.wy);", + " if (su.uLocalWindPerp) su.uLocalWindPerp.value.set(-ctx.wy, ctx.wx);", + " if (su.uFade) su.uFade.value = pendingFade;", + " if (su.uFadeInterpT) su.uFadeInterpT.value = fadeInterpT;", + "", + " if (cfgShadow) {", + " if (su.uPhase) su.uPhase.value = isFinite(cfgShadow.phase) ? cfgShadow.phase : 0.0;", + " if (su.uPolyScale) su.uPolyScale.value = isFinite(cfgShadow.polyScale) ? cfgShadow.polyScale : 1.0;", + " if (su.uGradStart) su.uGradStart.value = isFinite(cfgShadow.gradStart) ? cfgShadow.gradStart : 0.0;", + " if (su.uGradEnd) su.uGradEnd.value = isFinite(cfgShadow.gradEnd) ? cfgShadow.gradEnd : 1.0;", + " if (su.uIgnoreUV) su.uIgnoreUV.value = cfgShadow.ignoreUV ? 1.0 : 0.0;", + " if (su.uGradLocalZMin) su.uGradLocalZMin.value = isFinite(cfgShadow.gradLocalZMin) ? cfgShadow.gradLocalZMin : -0.5;", + " if (su.uGradLocalZMax) su.uGradLocalZMax.value = isFinite(cfgShadow.gradLocalZMax) ? cfgShadow.gradLocalZMax : 0.5;", + " if (su.uBendMultiplier) {", + " var bm = 1.0;", + " if (cfgShadow.swayType === \"bushSway\") bm = 0.1;", + " else if (cfgShadow.swayType === \"leavesSway\") bm = 0.05;", + " su.uBendMultiplier.value = bm;", + " }", + " if (su.uFlutterStrength) {", + " var p = isFinite(cfgShadow.polyScale) ? cfgShadow.polyScale : 1.0;", + " su.uFlutterStrength.value = cfgShadow.swayType === \"leavesSway\" && p >= 0.6 ? 0.4 * p : 0.0;", + " }", + " }", + "", + " var appliedShadow = shadowMat.userData.__gustVersionApplied;", + " if (appliedShadow !== state.gustVersion) {", + " shadowMat.userData.__gustVersionApplied = state.gustVersion;", + " if (su.uGustTex) su.uGustTex.value = state.gustTexture || state.gustFallbackTex;", + " if (su.uGustEnabled) su.uGustEnabled.value = state.gustEnabled ? 1.0 : 0.0;", + " if (su.uGustStrength) su.uGustStrength.value = state.gustEnabled ? state.gustStrength : 0.0;", + " if (su.uGustScale) su.uGustScale.value = state.gustScale;", + " if (su.uGustSpeed) su.uGustSpeed.value = state.gustSpeed;", + " if (su.uGustThreshold) su.uGustThreshold.value = state.gustThreshold;", + " if (su.uGustContrast) su.uGustContrast.value = state.gustContrast;", + " }", + " }", + "", + " /**", + " * Walk the shadow-host bucket, drop inactive hosts, and sync both depth and", + " * distance shadow variants for every still-active foliage material.", + " * @param {Set|null|undefined} bucket", + " * @param {Set|null|undefined} activeMaterials", + " * @param {(mat: THREE.Material|null|undefined) => void} unregisterActiveMaterial", + " * @param {SharedFrameContext} ctx", + " * @param {number} fadeInterpT", + " */", + " syncShadowHostBucket(bucket, activeMaterials, unregisterActiveMaterial, ctx, fadeInterpT) {", + " if (!bucket || typeof bucket[Symbol.iterator] !== \"function\") return;", + " for (const matShadow of bucket) {", + " if (!matShadow || !activeMaterials || !activeMaterials.has(matShadow)) {", + " unregisterActiveMaterial(matShadow);", + " continue;", + " }", + " var udShadow = matShadow.userData;", + " if (!udShadow) {", + " unregisterActiveMaterial(matShadow);", + " continue;", + " }", + " this.syncMaterial(matShadow, udShadow._foliageDepthMat, ctx, fadeInterpT);", + " this.syncMaterial(matShadow, udShadow._foliageDistanceMat, ctx, fadeInterpT);", + " }", + " }", + " }", + " ", + " /**", + " * Ensure a fallback gust texture exists even when no external gust resource is assigned.", + " * @returns {THREE.Texture|null}", + " */", + " function ensureGustFallbackTex() {", + " var state = /** @type {FoliageSceneState} */ (this);", + " try {", + " if (state.gustFallbackTex && state._gustFallbackIsStripe === true) {", + " return state.gustFallbackTex;", + " }", + " ", + " var oldFallback = state.gustFallbackTex;", + " var width = 256;", + " var data = new Uint8Array(width * 4);", + " for (var i = 0; i < width; i++) {", + " var x = i / (width - 1);", + " var d = Math.abs(x - 0.5) * 2.0;", + " var m = 1.0 - d;", + " if (m < 0) m = 0;", + " m = m * m * (3.0 - 2.0 * m);", + " var r = Math.max(0, Math.min(255, Math.round(m * 255)));", + " var off = i * 4;", + " data[off] = r;", + " data[off + 1] = 0;", + " data[off + 2] = 0;", + " data[off + 3] = 255;", + " }", + " ", + " var tex = new THREE.DataTexture(data, width, 1, THREE.RGBAFormat);", + " tex.needsUpdate = true;", + " tex.wrapS = THREE.RepeatWrapping;", + " tex.wrapT = THREE.RepeatWrapping;", + " tex.flipY = false;", + " state.gustFallbackTex = tex;", + " state._gustFallbackIsStripe = true;", + " ", + " if (oldFallback && oldFallback !== tex && oldFallback !== state.gustTexture && typeof oldFallback.dispose === \"function\") {", + " try { oldFallback.dispose(); } catch (e) {}", + " }", + " return tex;", + " } catch (e) {", + " state.gustFallbackTex = null;", + " state._gustFallbackIsStripe = false;", + " return null;", + " }", + " }", + " ", + " /**", + " * Dispose only foliage-owned runtime textures while preserving shared external resources.", + " * @param {THREE.Texture|null|undefined} tex", + " * @returns {void}", + " */", + " function disposeNonFallbackTex(tex) {", + " var state = /** @type {FoliageSceneState} */ (this);", + " if (!tex || tex === state.gustFallbackTex || typeof tex.dispose !== \"function\") return;", + " if (!tex.userData || tex.userData.__foliageOwnedTexture !== true) return;", + " try { tex.dispose(); } catch (e) {}", + " }", + " ", + " /**", + " * Reset scene-scoped foliage runtime state when the active scene changes.", + " * Shared helper code stays loaded on gdjs, while mutable scene data is rebuilt here.", + " */", + " function cleanupForSceneChange() {", + " var cacheObj = /** @type {FoliageSceneState} */ (this);", + " if (cacheObj._frameCtx) {", + " cacheObj._frameCtx.runtimeScene = null;", + " cacheObj._frameCtx.cam = null;", + " cacheObj._frameCtx.frustum = null;", + " cacheObj._frameCtx._frustumBuilt = false;", + " cacheObj._frameCtx.camX = 0;", + " cacheObj._frameCtx.camY = 0;", + " cacheObj._frameCtx.camZ = 0;", + " cacheObj._frameCtx.dt = 0;", + " cacheObj._frameCtx.time = 0;", + " }", + " cacheObj.time = 0;", + " ", + " var seen = new Set();", + " cacheObj.sharedByKey.forEach(function(entry) {", + " if (!entry || entry._ownedByFoliage === false || !entry.material) return;", + " var mat = entry.material;", + " if (seen.has(mat)) return;", + " seen.add(mat);", + " cacheObj.disposeShadowMaterials(mat);", + " if (typeof mat.dispose === \"function\") {", + " try { mat.dispose(); } catch (eDispose) {}", + " }", + " });", + " cacheObj.sharedByKey.clear();", + " ", + " cacheObj.activeMaterials.forEach(function(mat) {", + " if (!mat || seen.has(mat)) return;", + " seen.add(mat);", + " cacheObj.disposeShadowMaterials(mat);", + " if (typeof mat.dispose === \"function\") {", + " try { mat.dispose(); } catch (eDispose) {}", + " }", + " });", + " cacheObj.activeMaterials.clear();", + " ", + " cacheObj.resetMaterialBuckets();", + " if (cacheObj.nonInstancedRegistry && typeof cacheObj.nonInstancedRegistry.reset === \"function\") {", + " cacheObj.nonInstancedRegistry.reset();", + " if (typeof cacheObj._syncNonInstancedAliases === \"function\") cacheObj._syncNonInstancedAliases();", + " } else {", + " cacheObj.nonInstancedFadeObjects = [];", + " cacheObj.nonInstancedStaticObjects = [];", + " cacheObj._nonInstancedStaticCheckAccum = 0;", + " }", + " cacheObj.gustTexture = null;", + " cacheObj.gustTextureKey = \"\";", + " cacheObj.gustTextureResourceName = \"\";", + " ", + " var defaults = cacheObj.GUST_DEFAULTS;", + " cacheObj.gustEnabled = false;", + " cacheObj.gustStrength = defaults.strength;", + " cacheObj.gustScale = defaults.scale;", + " cacheObj.gustSpeed = defaults.speed;", + " cacheObj.gustThreshold = defaults.threshold;", + " cacheObj.gustContrast = defaults.contrast;", + " cacheObj.gustVersion = 0;", + " cacheObj._gustDefineDirty = false;", + " ", + " var instancingState = cacheObj.instancingState || cacheObj.instancing || null;", + " var sceneTag = instancingState && instancingState.sceneTag !== undefined ? instancingState.sceneTag : null;", + " if (instancingState && typeof instancingState.reset === \"function\") {", + " instancingState.reset(sceneTag);", + " cacheObj.instancingState = instancingState;", + " if (typeof cacheObj._syncInstancingAliases === \"function\") cacheObj._syncInstancingAliases();", + " } else {", + " cacheObj.instancingState = new FoliageInstancingState(sceneTag);", + " cacheObj.instancing = cacheObj.instancingState;", + " }", + " cacheObj._frustumShared = null;", + " cacheObj._projViewMatrixShared = null;", + " if (cacheObj.cullingCoordinator && typeof cacheObj.cullingCoordinator.reset === \"function\") {", + " cacheObj.cullingCoordinator.reset();", + " } else {", + " cacheObj.cullingCoordinator = new FoliageCullingCoordinator(cacheObj);", + " }", + " cacheObj.patchedMaterials = new WeakSet();", + " ", + " if (cacheObj.objectTypeCacheRegistry && typeof cacheObj.objectTypeCacheRegistry.reset === \"function\") {", + " cacheObj.objectTypeCacheRegistry.reset();", + " if (typeof cacheObj._syncObjectTypeCacheAliases === \"function\") {", + " cacheObj._syncObjectTypeCacheAliases();", + " }", + " } else {", + " cacheObj.objectTypeCache.forEach(function(entry) {", + " if (!entry) return;", + " if (entry.records) {", + " for (var ri = 0; ri < entry.records.length; ri++) {", + " entry.records[ri].materialRef = null;", + " }", + " }", + " entry._cachedGeometry = null;", + " entry._gpuGeometry = null;", + " });", + " cacheObj.objectTypeCache.clear();", + " cacheObj.autoPolyScaleCache.clear();", + " cacheObj.debugPrinted.clear();", + " cacheObj.geometryBBoxCache = new WeakMap();", + " cacheObj._objectTypeCacheState = { tick: 0, setCount: 0, meta: new Map() };", + " }", + " cacheObj._tmpVec3NonInstancedFade = null;", + " }", + " ", + " /**", + " * Build the per-frame runtime snapshot used by update code and shadow/material sync.", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @param {FoliageEventsFunctionContext} eventsFunctionContext", + " * @returns {SharedFrameContext}", + " */", + " function buildFrameContext(runtimeScene, eventsFunctionContext) {", + " var cache = /** @type {FoliageSceneState} */ (this);", + " cache._behaviorPropsCache = new WeakMap();", + " var ctx = cache._frameCtx;", + " if (!ctx) {", + " ctx = cache._frameCtx = {", + " runtimeScene: null,", + " dt: 0,", + " time: 0,", + " windStrength: 6,", + " windSpeed: 4,", + " wx: 1,", + " wy: 0,", + " cam: null,", + " camX: 0,", + " camY: 0,", + " camZ: 0,", + " frustum: null,", + " _frustumBuilt: false", + " };", + " }", + " ctx.runtimeScene = runtimeScene;", + " ctx.frustum = null;", + " ctx._frustumBuilt = false;", + " ", + " var dt = runtimeScene.getTimeManager().getElapsedTime() / 1000.0;", + " if (!isFinite(dt) || dt < 0) dt = 0;", + " cache.time += dt;", + " ctx.dt = dt;", + " ctx.time = cache.time;", + " ", + " var ws = cache._readNumArg(eventsFunctionContext, \"windStrength\", 6.0);", + " var wsp = cache._readNumArg(eventsFunctionContext, \"windSpeed\", 4.0);", + " var wdx = cache._readNumArg(eventsFunctionContext, \"windDirectionX\", 1.0);", + " var wdy = cache._readNumArg(eventsFunctionContext, \"windDirectionY\", 1.0);", + " var len = Math.sqrt(wdx * wdx + wdy * wdy);", + " ctx.windStrength = ws;", + " ctx.windSpeed = wsp;", + " ctx.wx = len > 0 ? wdx / len : 1.0;", + " ctx.wy = len > 0 ? wdy / len : 0.0;", + " ", + " ctx.cam = null;", + " ctx.camX = 0;", + " ctx.camY = 0;", + " ctx.camZ = 0;", + " try {", + " var layer = runtimeScene.getLayer(\"\");", + " if (layer && layer.getRenderer && layer.getRenderer().getThreeCamera) {", + " var cam = layer.getRenderer().getThreeCamera();", + " if (cam) {", + " ctx.cam = cam;", + " ctx.camX = cam.position.x;", + " ctx.camY = cam.position.y;", + " ctx.camZ = cam.position.z;", + " }", + " }", + " } catch (eCam) {}", + " ", + " return ctx;", + " }", + " ", + " /**", + " * Lazily build the camera frustum once per frame context.", + " * @param {SharedFrameContext} ctx", + " * @returns {THREE.Frustum|null}", + " */", + " function ensureFrustum(ctx) {", + " var cache = /** @type {FoliageSceneState} */ (this);", + " if (ctx._frustumBuilt) return ctx.frustum;", + " ctx._frustumBuilt = true;", + " if (!ctx.cam) return null;", + " if (!cache._frustumShared) cache._frustumShared = new THREE.Frustum();", + " if (!cache._projViewMatrixShared) cache._projViewMatrixShared = new THREE.Matrix4();", + " try {", + " ctx.cam.updateMatrixWorld(true);", + " cache._projViewMatrixShared.multiplyMatrices(ctx.cam.projectionMatrix, ctx.cam.matrixWorldInverse);", + " var frustumBridge = /** @type {{ setFromProjectionMatrix?: (matrix: THREE.Matrix4) => void, setFromMatrix?: (matrix: THREE.Matrix4) => void }} */ (cache._frustumShared);", + " if (typeof frustumBridge.setFromProjectionMatrix === \"function\") {", + " frustumBridge.setFromProjectionMatrix(cache._projViewMatrixShared);", + " } else if (typeof frustumBridge.setFromMatrix === \"function\") {", + " frustumBridge.setFromMatrix(cache._projViewMatrixShared);", + " }", + " ctx.frustum = cache._frustumShared;", + " } catch (eFrustum) {", + " ctx.frustum = null;", + " }", + " return ctx.frustum;", + " }", + " ", + " /**", + " * Scene-scoped root object for all mutable foliage runtime state.", + " * Events fetch this once and then work through its owner registries and coordinators.", + " */", + " class FoliageSceneState {", + " constructor(runtimeScene) {", + " var sceneTag = runtimeScene && typeof runtimeScene.getName === \"function\" ? runtimeScene.getName() : null;", + " this.patchedMaterials = new WeakSet();", + " this.sharedByKey = new Map();", + " this.activeMaterials = new Set();", + " this.time = 0;", + " this.gustVersion = 0;", + " this.gustEnabled = false;", + " this.gustStrength = 3.0;", + " this.gustScale = 0.0005;", + " this.gustSpeed = 0.25;", + " this.gustThreshold = 0.28;", + " this.gustContrast = 1.25;", + " /** @type {THREE.Texture|null} */", + " this.gustTexture = null;", + " this.gustTextureKey = \"\";", + " this.gustTextureResourceName = \"\";", + " /** @type {THREE.Texture|null} */", + " this.gustFallbackTex = null;", + " this._gustFallbackIsStripe = false;", + " this._gustDefineDirty = false;", + " this.GUST_DEFAULTS = {", + " strength: 3.0,", + " scale: 0.0005,", + " speed: 0.25,", + " threshold: 0.28,", + " contrast: 1.25", + " };", + " this._materialBuckets = new FoliageMaterialBuckets();", + " this.materialRegistry = new FoliageSharedMaterialRegistry(this);", + " this.objectTypeCacheRegistry = new FoliageObjectTypeCache();", + " this.nonInstancedRegistry = new FoliageNonInstancedRegistry(this);", + " this.instancingState = new FoliageInstancingState(sceneTag);", + " this.instancingCoordinator = new FoliageInstancingCoordinator(this);", + " this.cullingCoordinator = new FoliageCullingCoordinator(this);", + " this.shadowUniformSync = new FoliageShadowUniformSync(this);", + " this._syncObjectTypeCacheAliases();", + " this._syncNonInstancedAliases();", + " this._syncInstancingAliases();", + " /** @type {SharedFrameContext|null} */", + " this._frameCtx = null;", + " /** @type {THREE.Frustum|null} */", + " this._frustumShared = null;", + " /** @type {THREE.Matrix4|null} */", + " this._projViewMatrixShared = null;", + " this._behaviorPropsCache = new WeakMap();", + " /** @type {{ cx: number, cy: number, cz: number, radius: number }|undefined} */", + " this._tmpInstanceCullSphere = undefined;", + " /** @type {{ cx: number, cy: number, cz: number, radius: number }|undefined} */", + " this._tmpInstanceCullSphereA = undefined;", + " /** @type {{ cx: number, cy: number, cz: number, radius: number }|undefined} */", + " this._tmpInstanceCullSphereB = undefined;", + " /** @type {{ cx: number, cy: number, cz: number, radius: number }|undefined} */", + " this._tmpInstanceCullSphereShared = undefined;", + " /** @type {number|undefined} */", + " this.fadeUpdateHz = undefined;", + " /** @type {number|undefined} */", + " this.cullAccum = undefined;", + " /** @type {number|undefined} */", + " this.cullInterval = undefined;", + " /** @type {number|undefined} */", + " this._lastLoggedFadeHz = undefined;", + " /** @type {number|undefined} */", + " this.lastCullTime = undefined;", + " /** @type {number|undefined} */", + " this._lastFadeUpdateHz = undefined;", + " /** @type {number|undefined} */", + " this.lastCamX = undefined;", + " /** @type {number|undefined} */", + " this.lastCamY = undefined;", + " /** @type {number|undefined} */", + " this.lastCamZ = undefined;", + " /** @type {boolean|undefined} */", + " this._fadeParamsDirty = undefined;", + " /** @type {THREE.Vector3|null} */", + " this._tmpVec3NonInstancedFade = null;", + " this.getBehaviorProps = getBehaviorProps;", + " this._readBoolArg = readBoolArg;", + " this._readNumArg = readNumArg;", + " this._readStringArg = readStringArg;", + " this._libReady = true;", + " this.ensureGustFallbackTex();", + " }", + "", + " resetMaterialBuckets() { return this.materialRegistry.resetBuckets(); }", + " registerActiveMaterial(mat) { return this.materialRegistry.registerActiveMaterial(mat); }", + " unregisterActiveMaterial(mat) { return this.materialRegistry.unregisterActiveMaterial(mat); }", + " disposeShadowMaterials(mat) { return this.materialRegistry.disposeShadowMaterials(mat); }", + " markGustForRecompile() { return this.materialRegistry.markGustForRecompile(); }", + " _syncObjectTypeCacheAliases() {", + " var registry = this.objectTypeCacheRegistry;", + " this.objectTypeCache = registry ? registry.entries : new Map();", + " this._objectTypeCacheState = registry ? registry.state : { tick: 0, setCount: 0, meta: new Map() };", + " this.geometryBBoxCache = registry ? registry.geometryBBoxCache : new WeakMap();", + " this.autoPolyScaleCache = registry ? registry.autoPolyScaleCache : new Map();", + " this.debugPrinted = registry ? registry.debugPrinted : new Set();", + " return this;", + " }", + " _syncNonInstancedAliases() {", + " var registry = this.nonInstancedRegistry;", + " if (registry && typeof registry.syncAliases === \"function\") return registry.syncAliases();", + " this.nonInstancedFadeObjects = [];", + " this.nonInstancedStaticObjects = [];", + " this._nonInstancedStaticCheckAccum = 0;", + " return this;", + " }", + " _syncInstancingAliases() {", + " if (!this.instancingState) this.instancingState = new FoliageInstancingState(null);", + " if (this.instancingState && typeof this.instancingState.ensureReady === \"function\") this.instancingState.ensureReady();", + " this.instancing = this.instancingState;", + " return this;", + " }", + " ensureGustFallbackTex() { return ensureGustFallbackTex.call(this); }", + " disposeNonFallbackTex(tex) { return disposeNonFallbackTex.call(this, tex); }", + " cleanupForSceneChange() { return cleanupForSceneChange.call(this); }", + " buildFrameContext(runtimeScene, eventsFunctionContext) { return buildFrameContext.call(this, runtimeScene, eventsFunctionContext); }", + " _ensureFrustum(ctx) { return ensureFrustum.call(this, ctx); }", + " }", + " ", + " /**", + " * Create fresh scene-owned runtime state for one runtimeScene instance.", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {FoliageSceneState}", + " */", + " function createSceneState(runtimeScene) {", + " return new FoliageSceneState(runtimeScene);", + " }", + " ", + " /**", + " * Lazily attach foliage runtime state to the scene and reuse it across event calls.", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {FoliageSceneState}", + " */", + " function getSceneState(runtimeScene) {", + " var scene = /** @type {FoliageRuntimeScene} */ (runtimeScene);", + " if (!scene._natureElementsFoliageSway) {", + " scene._natureElementsFoliageSway = createSceneState(runtimeScene);", + " }", + " return scene._natureElementsFoliageSway;", + " }", + " ", + " /**", + " * Recreate the scene-owned foliage state from scratch.", + " * @param {gdjs.RuntimeScene} runtimeScene", + " * @returns {FoliageSceneState}", + " */", + " function resetSceneState(runtimeScene) {", + " var scene = /** @type {FoliageRuntimeScene} */ (runtimeScene);", + " scene._natureElementsFoliageSway = createSceneState(runtimeScene);", + " return scene._natureElementsFoliageSway;", + " }", + " ", + " /**", + " * Expose only the shared helpers that runtime snippets actually consume.", + " * @returns {FoliageExtensionApi}", + " */", + " function createExtensionApi() {", + " /** @type {FoliageExtensionApi} */", + " var api = {", + " _libReady: true,", + " normalizeCullingMode: normalizeCullingMode,", + " resolveRenderSide: resolveRenderSide,", + " buildSideSuffix: buildSideSuffix,", + " buildPbrSuffix: buildPbrSuffix,", + " buildGpuFastCacheKey: buildGpuFastCacheKey,", + " parseColor: parseColor,", + " isAlphaLikely: isAlphaLikely,", + " scoreMaterial: scoreMaterial,", + " sanitizeKeyPart: sanitizeKeyPart,", + " meshMatchesSelection: meshMatchesSelection,", + " findFirstMatchingMesh: findFirstMatchingMesh,", + " findSplitTreeMeshes: findSplitTreeMeshes,", + " getSceneState: getSceneState,", + " };", + " return api;", + " }", + " ", + " var gdjsFoliage = /** @type {FoliageGdjs} */ (gdjs);", + " var foliageExt = gdjsFoliage._natureElementsFoliageSway;", + " if (!foliageExt) {", + " foliageExt = createExtensionApi();", + " gdjsFoliage._natureElementsFoliageSway = foliageExt;", + " }", + " foliageExt.getSceneState(/** @type {FoliageRuntimeScene} */ (runtimeScene));", + " ", + " ", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [], + "objectGroups": [] + }, + { + "description": "Makes wind sway the foliage.", + "fullName": "Update foliage swaying", + "functionType": "Action", + "name": "UpdateFoliageSwaying", + "sentence": "Update foliage swaying on layer _PARAM1_ with wind strength _PARAM2_ wind speed _PARAM3_ wind direction X _PARAM4_ and Y _PARAM5_ at _PARAM6_ times per second", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @file updateFoliageSwaying — Per-frame update: uniforms, instancing flush, distance/frustum culling. */", + " /** @typedef {{ __foliageSkipOnDestroy?: boolean, __foliageNonInstancedRegistered?: boolean, __foliageSharedKey?: string, __foliageSharedKeyTrunk?: string, __foliageInstancingIndex?: number, __foliageInstancingGroupKey?: string, __foliageInstancingGroupKeyLeaves?: string, __foliageQueued?: boolean, __foliageQueueId?: number }} FoliageBehaviorPrivateFieldsBridge */", + " /** @typedef {gdjs.RuntimeBehavior & FoliageBehaviorPrivateFieldsBridge} FoliageBehaviorBridge */", + " /** @typedef {string|number|boolean|null|undefined} FoliagePrimitiveValueBridge */", + " /** @typedef {FoliagePrimitiveValueBridge|gdjs.Variable} FoliageArgumentValueBridge */", + " /** @typedef {{ getArgument: (name: string) => FoliageArgumentValueBridge }} FoliageEventsFunctionContextBridge */", + " /** @typedef {{ distanceFadeEnabled: boolean, fadeStart: number, fadeEnd: number }} FoliageBehaviorFadePropsBridge */", + " /** @typedef {{ repMesh?: THREE.Object3D|null, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, baseGroupKey?: string }} FoliagePendingPartBridge */", + " /** @typedef {{ gdObj?: (gdjs.RuntimeObject3D & { getX?: () => number, getY?: () => number, getZ?: () => number, getScaleX?: () => number, getScaleY?: () => number, getScaleZ?: () => number, getRotationX?: () => number, getRotationY?: () => number, getRotationZ?: () => number, getAngle?: () => number, deleteFromScene?: (runtimeScene?: gdjs.RuntimeScene) => void, hide?: (enable?: boolean) => void, isHidden?: () => boolean })|null, threeObj?: THREE.Object3D|null, repMesh?: THREE.Object3D|null, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, parent?: THREE.Object3D|null, baseGroupKey?: string, groupKey?: string, swayType?: string, behavior?: FoliageBehaviorBridge|null, queueId?: number, parts?: FoliagePendingPartBridge[] }} FoliagePendingItemBridge */", + " /** @typedef {{ fadeEnabled?: boolean, fadeStart?: number, fadeEnd?: number }} FoliageFadeStateTargetBridge */", + " /** @typedef {{ gdObj?: (gdjs.RuntimeObject3D & { getX?: () => number, getY?: () => number, getZ?: () => number, getScaleX?: () => number, getScaleY?: () => number, getScaleZ?: () => number, getRotationX?: () => number, getRotationY?: () => number, getRotationZ?: () => number, getAngle?: () => number, deleteFromScene?: (runtimeScene?: gdjs.RuntimeScene) => void, hide?: (enable?: boolean) => void, isHidden?: () => boolean })|null, threeObj?: THREE.Object3D|null, material?: THREE.Material|null, trunkMaterial?: THREE.Material|null, fadeStart?: number, fadeEnd?: number, fadeEnabled?: boolean, fadeBehavior?: FoliageBehaviorBridge|null, _wasHidden?: boolean, _parkedNoFade?: boolean, _firstHideWarmupDone?: boolean }} FoliageNonInstancedEntryBridge */", + " /** @typedef {{ material: THREE.Material, refCount: number }} FoliageSharedMaterialEntryBridge */", + " /** @typedef {{ polyScale: number, polyScaleRaw: number, sizeFactor: number, complexityFactor: number, vertexFactor: number, triFactor: number, structureFactor: number, planeFactor: number, responseGain: number, planeLikeRatio: number, baseByType: number, boundsMode: string }} FoliageAutoPolyScaleEntryBridge */", + " /** @typedef {{ key?: string, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, parent?: THREE.Object3D|null, matricesBuffer?: Float32Array|null, centersXY?: Float32Array|null, centersZ?: Float32Array|null, cullRadii?: Float32Array|null, matrixCount?: number, aliveCount?: number, freeIndices?: number[], freeIndexSet?: Set, capacity?: number, mesh?: THREE.InstancedMesh|null, castShadow?: boolean, receiveShadow?: boolean, fadeEnabled?: boolean, fadeStart?: number, fadeEnd?: number, _fadeBehavior?: FoliageBehaviorBridge|null, _fadeSig?: string, _instanceCullRadius?: number, instanceFade?: Float32Array|null, instanceFadePrev?: Float32Array|null, _distanceCandidateIndices?: Uint32Array|null, _distanceCandidateCount?: number, _fadeDisabledApplied?: boolean, _lastFadeEnabled?: boolean, visibleCount?: number, _srcGeometryRef?: THREE.BufferGeometry|null, _ownedGeometry?: THREE.BufferGeometry|null }} FoliageInstancingGroupBridge */", + " /** @typedef {{ groups: Map, dirty: boolean, sceneTag: string|null, pending: FoliagePendingItemBridge[], queueIdCounter: number, cancelledQueueIds: Set, foliageRoot?: THREE.Object3D|null, _cachedSceneRoot?: THREE.Object3D|null, _tmpRootSet?: Set|null, _tmpMat4?: THREE.Matrix4, _tmpObj3D?: THREE.Object3D, _tmpVec3_pos?: THREE.Vector3, _tmpVec3_scale?: THREE.Vector3, _tmpEuler?: THREE.Euler, _tmpQuat?: THREE.Quaternion, _tmpMat4_objWorld?: THREE.Matrix4, _tmpMat4_objWorldInv?: THREE.Matrix4, _tmpMat4_repWorld?: THREE.Matrix4, _repRelByMesh?: WeakMap, _itemsByParentRepMesh?: Map, _repRelMatrixCache?: Map, _tmpMat4_local?: THREE.Matrix4, _tmpMat4_foliageRootInv?: THREE.Matrix4, ensureReady: () => FoliageInstancingStateBridge, markDirty: () => FoliageInstancingStateBridge }} FoliageInstancingStateBridge */", + " /** @typedef {{ fadeEntries: FoliageNonInstancedEntryBridge[], staticEntries: FoliageNonInstancedEntryBridge[], staticCheckAccum: number, removeInvalidEntry: (entry: FoliageNonInstancedEntryBridge|null|undefined, fromList: \"fade\"|\"static\") => boolean, parkEntry: (entry: FoliageNonInstancedEntryBridge|null|undefined) => FoliageNonInstancedEntryBridge|null, reactivateEntry: (entry: FoliageNonInstancedEntryBridge|null|undefined) => FoliageNonInstancedEntryBridge|null }} FoliageNonInstancedRegistryBridge */", + " /** @typedef {{ timeWind: Set, fadeInterp: Set, gust: Set, shadowHost: Set, unknown: Set, rebuildNeeded: boolean }} FoliageMaterialBucketsBridge */", + " /** @typedef {{ flushPending: (ctx: SharedFrameContextBridge) => void, rebuildDirtyGroups: () => void }} FoliageInstancingCoordinatorBridge */", + " /** @typedef {{ reset: () => FoliageCullingCoordinatorBridge, didFrustumCameraChange: (frameCam: SharedFrameContextBridge|null|undefined) => boolean, performDistanceCullTick: (ctx: SharedFrameContextBridge|null|undefined, forceRun: boolean, refreshFadeState: (target: FoliageFadeStateTargetBridge|null|undefined, behavior: FoliageBehaviorBridge|null|undefined, defaultEnabled: boolean) => void) => boolean, performFrustumCompactionPass: (ctx: SharedFrameContextBridge|null|undefined) => boolean }} FoliageCullingCoordinatorBridge */", + " /** @typedef {{ syncMaterial: (hostMat: THREE.Material|null|undefined, shadowMat: THREE.Material|null|undefined, ctx: SharedFrameContextBridge, fadeInterpT: number) => void, syncShadowHostBucket: (bucket: Set|null|undefined, activeMaterials: Set|null|undefined, unregisterActiveMaterial: (mat: THREE.Material|null|undefined) => void, ctx: SharedFrameContextBridge, fadeInterpT: number) => void }} FoliageShadowUniformSyncBridge */", + " /** @typedef {{ runtimeScene: gdjs.RuntimeScene|null, dt: number, time: number, windStrength: number, windSpeed: number, wx: number, wy: number, cam: THREE.Camera|null, camX: number, camY: number, camZ: number, frustum: THREE.Frustum|null, _frustumBuilt: boolean }} SharedFrameContextBridge */", + " /** @typedef {{ _libReady?: boolean, time: number, gustVersion: number, fadeUpdateHz?: number, cullAccum?: number, cullInterval?: number, _lastLoggedFadeHz?: number, lastCullTime?: number, _lastFadeUpdateHz?: number, lastCamX?: number, lastCamY?: number, lastCamZ?: number, _fadeParamsDirty?: boolean, nonInstancedFadeObjects: FoliageNonInstancedEntryBridge[], nonInstancedStaticObjects: FoliageNonInstancedEntryBridge[], nonInstancedRegistry: FoliageNonInstancedRegistryBridge, autoPolyScaleCache: Map, _nonInstancedStaticCheckAccum: number, _readBoolArg: (efc: FoliageEventsFunctionContextBridge, name: string, fallback: boolean) => boolean, _readNumArg: (efc: FoliageEventsFunctionContextBridge, name: string, fallback: number) => number, _readStringArg: (efc: FoliageEventsFunctionContextBridge, name: string, fallback: string) => string, _gustDefineDirty?: boolean, markGustForRecompile: () => void, instancing: FoliageInstancingStateBridge, instancingState: FoliageInstancingStateBridge, instancingCoordinator: FoliageInstancingCoordinatorBridge, cullingCoordinator: FoliageCullingCoordinatorBridge, shadowUniformSync: FoliageShadowUniformSyncBridge, cleanupForSceneChange: () => void, buildFrameContext: (runtimeScene: gdjs.RuntimeScene, eventsFunctionContext: FoliageEventsFunctionContextBridge) => SharedFrameContextBridge, activeMaterials: Set, _materialBuckets: FoliageMaterialBucketsBridge, resetMaterialBuckets: () => void, unregisterActiveMaterial: (mat: THREE.Material|null|undefined) => void, disposeShadowMaterials: (mat: THREE.Material|null|undefined) => void, sharedByKey: Map, _ensureFrustum: (ctx: SharedFrameContextBridge) => THREE.Frustum|null, gustEnabled: boolean, gustStrength: number, gustScale: number, gustSpeed: number, gustThreshold: number, gustContrast: number, gustTexture: THREE.Texture|null, gustFallbackTex: THREE.Texture|null, _tmpVec3NonInstancedFade?: THREE.Vector3|null, getBehaviorProps: (behavior: FoliageBehaviorBridge|null|undefined) => FoliageBehaviorFadePropsBridge|null }} FoliageSceneStateBridge */", + " /** @typedef {{ _libReady?: boolean, getSceneState: (runtimeScene: gdjs.RuntimeScene) => FoliageSceneStateBridge }} FoliageExtensionBridge */", + " /** @typedef {typeof gdjs & { _natureElementsFoliageSway?: FoliageExtensionBridge }} FoliageGdjsBridge */", + " var gdjsFoliage = /** @type {FoliageGdjsBridge} */ (gdjs);", + " var foliageExt = gdjsFoliage._natureElementsFoliageSway;", + " if (!foliageExt || !foliageExt._libReady) return;", + " /** @type {FoliageSceneStateBridge} */", + " var cache = foliageExt.getSceneState(runtimeScene);", + " ", + " if (!isFinite(cache.time)) cache.time = 0;", + " if (!isFinite(cache.gustVersion)) cache.gustVersion = 0;", + " var DEFAULT_FADE_UPDATE_HZ = 10;", + " ", + " // Runtime fade tick override from update argument (optional).", + " // This controls how often distance-fade / candidate data is recomputed; the", + " // per-frame pass still interpolates between those lower-frequency snapshots.", + " /** @type {FoliageEventsFunctionContextBridge} */", + " var efc = eventsFunctionContext;", + " var fadeHzArg = cache._readNumArg(efc, \"fadeUpdateHz\", NaN);", + " if (isFinite(fadeHzArg)) {", + " if (fadeHzArg < 1) fadeHzArg = 1;", + " if (fadeHzArg > 120) fadeHzArg = 120;", + " cache.fadeUpdateHz = fadeHzArg;", + " }", + " ", + " // Distance fade cullTick timing.", + " // cache.cullAccum + cache.cullInterval throttle the heavier distance pass so", + " // instanced fading/culling work does not rerun every frame by default.", + " if (!isFinite(cache.cullAccum)) cache.cullAccum = 0;", + " if (!isFinite(cache.fadeUpdateHz) || cache.fadeUpdateHz < 1) cache.fadeUpdateHz = DEFAULT_FADE_UPDATE_HZ;", + " if (!isFinite(cache._lastLoggedFadeHz) || cache._lastLoggedFadeHz !== cache.fadeUpdateHz) {", + " cache._lastLoggedFadeHz = cache.fadeUpdateHz;", + " }", + " if (!isFinite(cache.lastCullTime)) cache.lastCullTime = 0;", + " // Cache cull interval and recompute only when fadeUpdateHz changes.", + " if (!isFinite(cache.cullInterval) || cache._lastFadeUpdateHz !== cache.fadeUpdateHz) {", + " cache.cullInterval = 1.0 / (cache.fadeUpdateHz || DEFAULT_FADE_UPDATE_HZ);", + " cache._lastFadeUpdateHz = cache.fadeUpdateHz;", + " }", + " // Camera position tracking", + " if (!isFinite(cache.lastCamX)) cache.lastCamX = Infinity;", + " if (!isFinite(cache.lastCamY)) cache.lastCamY = Infinity;", + " if (typeof cache._fadeParamsDirty !== \"boolean\") cache._fadeParamsDirty = false;", + " /** @type {FoliageNonInstancedRegistryBridge|null|undefined} */", + " var nonInstanced = cache.nonInstancedRegistry;", + " if (!nonInstanced) return;", + " if (!cache.nonInstancedFadeObjects || !Array.isArray(cache.nonInstancedFadeObjects)) cache.nonInstancedFadeObjects = nonInstanced.fadeEntries;", + " if (!cache.nonInstancedStaticObjects || !Array.isArray(cache.nonInstancedStaticObjects)) cache.nonInstancedStaticObjects = nonInstanced.staticEntries;", + " if (!cache.autoPolyScaleCache || typeof cache.autoPolyScaleCache.get !== \"function\") cache.autoPolyScaleCache = new Map();", + " if (!isFinite(nonInstanced.staticCheckAccum)) nonInstanced.staticCheckAccum = 0;", + " cache._nonInstancedStaticCheckAccum = nonInstanced.staticCheckAccum;", + " ", + " /**", + " * Refresh fade state from the live behavior getters.", + " * @param {FoliageFadeStateTargetBridge|null|undefined} target", + " * @param {FoliageBehaviorBridge|null|undefined} behavior", + " * @param {boolean} defaultEnabled", + " */", + " function refreshLiveFadeState(target, behavior, defaultEnabled) {", + " if (!target) return;", + " var enabled = (target.fadeEnabled === undefined) ? defaultEnabled : !!target.fadeEnabled;", + " var start = target.fadeStart != null ? target.fadeStart : 1200;", + " var end = target.fadeEnd != null ? target.fadeEnd : 1600;", + " if (behavior) {", + " /** @type {FoliageBehaviorFadePropsBridge|null} */", + " var fadePropsLive = /** @type {FoliageBehaviorFadePropsBridge|null} */ (cache.getBehaviorProps(behavior));", + " if (fadePropsLive) {", + " enabled = !!fadePropsLive.distanceFadeEnabled;", + " start = fadePropsLive.fadeStart;", + " end = fadePropsLive.fadeEnd;", + " }", + " }", + " if (end <= start) end = start + 100;", + " target.fadeEnabled = !!enabled;", + " target.fadeStart = start;", + " target.fadeEnd = end;", + " }", + "", + " /**", + " * Sync current fade value to a non-instanced material and its foliage fade uniform.", + " * @param {THREE.Material|null|undefined} mat", + " * @param {number} fadeValue", + " */", + " function applyFadeToMaterial(mat, fadeValue) {", + " if (!mat || !mat.userData) return;", + " mat.userData._pendingFade = fadeValue;", + " var u = mat.userData.foliageUniforms;", + " if (u && u.uFade) u.uFade.value = fadeValue;", + " }", + "", + " /**", + " * Clear a material bucket set in place without reallocating the Set.", + " * @param {Set|null|undefined} setObj", + " */", + " function resetBucketSet(setObj) {", + " if (setObj && typeof setObj.clear === \"function\") setObj.clear();", + " }", + "", + " if (cache._gustDefineDirty) {", + " cache.markGustForRecompile();", + " cache._gustDefineDirty = false;", + " }", + " ", + " /** @type {FoliageInstancingStateBridge|null|undefined} */", + " var instancing = cache.instancingState;", + " if (!instancing) return;", + " instancing.ensureReady();", + " cache.instancing = instancing;", + " ", + " // Scene change detection", + " var currentSceneTag = (runtimeScene && typeof runtimeScene.getName === \"function\") ? runtimeScene.getName() : null;", + " if (instancing.sceneTag !== currentSceneTag) {", + " cache.cleanupForSceneChange();", + " instancing = cache.instancingState;", + " if (!instancing) return;", + " instancing.ensureReady();", + " instancing.sceneTag = currentSceneTag;", + " cache.instancing = instancing;", + " }", + "", + " /** @type {FoliageInstancingCoordinatorBridge|null|undefined} */", + " var instancingOps = cache.instancingCoordinator;", + " if (!instancingOps) return;", + " /** @type {FoliageCullingCoordinatorBridge|null|undefined} */", + " var cullingOps = cache.cullingCoordinator;", + " if (!cullingOps) return;", + " /** @type {FoliageShadowUniformSyncBridge|null|undefined} */", + " var shadowSync = cache.shadowUniformSync;", + " if (!shadowSync) return;", + " ", + " var ctx = cache.buildFrameContext(runtimeScene, eventsFunctionContext);", + " var windStrength = ctx.windStrength;", + " var windSpeed = ctx.windSpeed;", + " var wx = ctx.wx;", + " var wy = ctx.wy;", + " var dt = ctx.dt;", + " ", + " // Frame orchestration starts by materializing queued instancing work into", + " // live groups, then rebuilding only the groups dirtied by those changes.", + " instancingOps.flushPending(ctx);", + " var hadDirty = cache.instancing && cache.instancing.dirty;", + " instancingOps.rebuildDirtyGroups();", + " ", + " var arrNi = cache.nonInstancedFadeObjects;", + " var arrNiLen = (arrNi && Array.isArray(arrNi)) ? arrNi.length : 0;", + " var arrNiStatic = cache.nonInstancedStaticObjects;", + " if (!arrNiStatic || !Array.isArray(arrNiStatic)) {", + " arrNiStatic = [];", + " cache.nonInstancedStaticObjects = arrNiStatic;", + " }", + " var arrNiStaticLen = arrNiStatic.length;", + " ", + " // Run the distance tick at fadeUpdateHz, or immediately after a rebuild so new groups get initialized.", + " var cullInterval = cache.cullInterval || (1.0 / (cache.fadeUpdateHz || DEFAULT_FADE_UPDATE_HZ));", + " cache.cullAccum += dt;", + " var shouldCull = hadDirty || cache.cullAccum >= cullInterval;", + " if (shouldCull) {", + " cache.cullAccum = 0;", + " }", + " ", + " // Early exit when there are no groups, non-instanced objects, or active materials.", + " var noGroups = !cache.instancing || !cache.instancing.groups || (typeof cache.instancing.groups.size === \"number\" && cache.instancing.groups.size === 0);", + " var hasNonInstanced = (arrNiLen > 0 || arrNiStaticLen > 0);", + " var noActiveMats = !cache.activeMaterials || (typeof cache.activeMaterials.size === \"number\" && cache.activeMaterials.size === 0);", + " if (noGroups && !hasNonInstanced && noActiveMats) return;", + " ", + " // Capture camera once per frame; build the frustum only when a cull/frustum pass needs it.", + " // frameCam is just a nullable alias for ctx.", + " var needFrameCam = shouldCull || arrNiLen > 0 || !noGroups;", + " var frameCam = needFrameCam ? ctx : null;", + " ", + " // Culling runs in two stages: distance first to update fade/candidate data,", + " // then frustum compaction to pack only the currently renderable instances.", + " var camChangedForFrustum = cullingOps.didFrustumCameraChange(frameCam);", + " var distanceTickRan = false;", + " if (shouldCull) {", + " distanceTickRan = cullingOps.performDistanceCullTick(ctx, !!hadDirty, refreshLiveFadeState);", + " }", + " ", + " var shouldRunFrustumPass = distanceTickRan || camChangedForFrustum;", + " if (shouldRunFrustumPass) {", + " cache._ensureFrustum(ctx);", + " }", + " ", + " if (shouldRunFrustumPass) {", + " cullingOps.performFrustumCompactionPass(ctx);", + " }", + " ", + " // Parked non-instanced entries (fade disabled): periodically check if fade was re-enabled.", + " if (arrNiStaticLen > 0) {", + " nonInstanced.staticCheckAccum += dt;", + " cache._nonInstancedStaticCheckAccum = nonInstanced.staticCheckAccum;", + " var staticCheckInterval = Math.max(cullInterval, 0.05);", + " var shouldScanStatic = shouldCull || nonInstanced.staticCheckAccum >= staticCheckInterval;", + " if (shouldScanStatic) {", + " nonInstanced.staticCheckAccum = 0;", + " cache._nonInstancedStaticCheckAccum = 0;", + " for (var si = arrNiStatic.length - 1; si >= 0; si--) {", + " var entryStatic = arrNiStatic[si];", + " var invalidStatic = !entryStatic || !entryStatic.gdObj || !entryStatic.threeObj || !entryStatic.material;", + " if (invalidStatic) {", + " nonInstanced.removeInvalidEntry(entryStatic, \"static\");", + " continue;", + " }", + " ", + " var enabledStatic = (entryStatic.fadeEnabled === undefined) ? false : !!entryStatic.fadeEnabled;", + " refreshLiveFadeState(entryStatic, entryStatic.fadeBehavior || null, false);", + " enabledStatic = !!entryStatic.fadeEnabled;", + " if (!enabledStatic) {", + " applyFadeToMaterial(entryStatic.material, 1.0);", + " applyFadeToMaterial(entryStatic.trunkMaterial, 1.0);", + " if (entryStatic._wasHidden !== false) {", + " try {", + " if (entryStatic.gdObj.hide) entryStatic.gdObj.hide(false);", + " } catch (eStaticHide) {}", + " entryStatic._wasHidden = false;", + " }", + " continue;", + " }", + " ", + " entryStatic._parkedNoFade = false;", + " nonInstanced.reactivateEntry(entryStatic);", + " }", + " }", + " }", + " ", + " // Static scan can reactivate entries; ensure we have camera data before active loop.", + " if (!frameCam && arrNi && arrNi.length > 0) {", + " frameCam = ctx;", + " }", + " ", + " // Non-instanced fade stays frame-driven so first visibility and fade state", + " // are correct even between the lower-frequency instanced cull ticks.", + " arrNiLen = (arrNi && Array.isArray(arrNi)) ? arrNi.length : 0;", + " var camX = frameCam && isFinite(frameCam.camX) ? frameCam.camX : 0;", + " var camY = frameCam && isFinite(frameCam.camY) ? frameCam.camY : 0;", + " var tmpVecNi = (cache.instancing && cache.instancing._tmpVec3_pos) ? cache.instancing._tmpVec3_pos : (cache._tmpVec3NonInstancedFade || (cache._tmpVec3NonInstancedFade = new THREE.Vector3()));", + " var marginNi = 256.0;", + " // Backward loop: swap-remove invalid entries in-place (O(1) per removal, order not guaranteed)", + " for (var ni = arrNiLen - 1; ni >= 0; ni--) {", + " var entryNi = arrNi[ni];", + " var isInvalid = !entryNi || !entryNi.gdObj || !entryNi.threeObj || !entryNi.material;", + " // Swap-remove invalid entries: swap with last element, then pop (O(1), no splice)", + " if (isInvalid) {", + " nonInstanced.removeInvalidEntry(entryNi, \"fade\");", + " continue;", + " }", + " // Runtime refresh from live behavior getters: allows toggling distanceFadeEnabled.", + " var enabledNi = (entryNi.fadeEnabled === undefined) ? true : !!entryNi.fadeEnabled;", + " refreshLiveFadeState(entryNi, entryNi.fadeBehavior || null, true);", + " enabledNi = !!entryNi.fadeEnabled;", + " // Optional perf path: when fade disabled, skip all distance/frustum work immediately and park entry.", + " if (!enabledNi) {", + " applyFadeToMaterial(entryNi.material, 1.0);", + " applyFadeToMaterial(entryNi.trunkMaterial, 1.0);", + " if (entryNi._wasHidden !== false) {", + " try {", + " if (entryNi.gdObj.hide) entryNi.gdObj.hide(false);", + " } catch (eHideOff) {}", + " entryNi._wasHidden = false;", + " }", + " nonInstanced.parkEntry(entryNi);", + " continue;", + " }", + " var objX = 0, objY = 0;", + " var posOk = false;", + " try {", + " entryNi.threeObj.getWorldPosition(tmpVecNi);", + " objX = tmpVecNi.x;", + " objY = tmpVecNi.y;", + " posOk = true;", + " } catch (ePos) {", + " try {", + " objX = entryNi.gdObj.getX ? entryNi.gdObj.getX() : 0;", + " objY = entryNi.gdObj.getY ? entryNi.gdObj.getY() : 0;", + " tmpVecNi.set(objX, objY, 0);", + " posOk = true;", + " } catch (eGd) {}", + " }", + " if (!posOk) {", + " nonInstanced.removeInvalidEntry(entryNi, \"fade\");", + " continue;", + " }", + " // Valid entry: compute fade", + " var startNi = entryNi.fadeStart != null ? entryNi.fadeStart : 1200;", + " var endNi = entryNi.fadeEnd != null ? entryNi.fadeEnd : 1600;", + " if (endNi <= startNi) endNi = startNi + 100;", + " var dxNi = objX - camX;", + " var dyNi = objY - camY;", + " var distSqNi = dxNi * dxNi + dyNi * dyNi;", + " var startNiSq = startNi * startNi;", + " var endNiWithMargin = endNi + marginNi;", + " var endNiWithMarginSq = endNiWithMargin * endNiWithMargin;", + " var rangeNi = endNi - startNi;", + " if (rangeNi <= 0) rangeNi = 100;", + " var invRangeNi = 1.0 / rangeNi;", + " var fadeNi;", + " if (distSqNi > endNiWithMarginSq) {", + " fadeNi = 0.0;", + " } else if (distSqNi < startNiSq) {", + " fadeNi = 1.0;", + " } else {", + " var distNi = Math.sqrt(distSqNi);", + " var tNi = (distNi - startNi) * invRangeNi;", + " if (tNi < 0) tNi = 0;", + " else if (tNi > 1) tNi = 1;", + " fadeNi = 1.0 - (tNi * tNi * (3.0 - 2.0 * tNi));", + " }", + " // Non-instanced frustum culling is handled natively by Three.js/GDevelop.", + " var shouldHideNi = fadeNi < 0.01;", + " // Let each non-instanced entry survive one real draw at near-zero fade before the first hard hide.", + " if (shouldHideNi && entryNi._firstHideWarmupDone !== true) {", + " entryNi._firstHideWarmupDone = true;", + " shouldHideNi = false;", + " }", + " applyFadeToMaterial(entryNi.material, fadeNi);", + " applyFadeToMaterial(entryNi.trunkMaterial, fadeNi);", + " if (entryNi._wasHidden !== shouldHideNi) {", + " try {", + " if (entryNi.gdObj.hide) entryNi.gdObj.hide(shouldHideNi);", + " } catch (eHide) {}", + " entryNi._wasHidden = shouldHideNi;", + " }", + " }", + " ", + " // GPU fade smoothing: update uFadeInterpT uniform every frame", + " // interpT = smoothstep(clamp((time - lastCullTime) / cullInterval, 0, 1))", + " var fadeInterpT = 1.0;", + " if (isFinite(cache.lastCullTime) && cache.lastCullTime > 0) {", + " var timeSinceCull = cache.time - cache.lastCullTime;", + " var cullIntervalForInterp = cache.cullInterval || (1.0 / (cache.fadeUpdateHz || DEFAULT_FADE_UPDATE_HZ));", + " var rawT = timeSinceCull / cullIntervalForInterp;", + " if (rawT < 0) rawT = 0; else if (rawT > 1) rawT = 1;", + " fadeInterpT = rawT * rawT * (3.0 - 2.0 * rawT); // smoothstep", + " }", + " ", + " var buckets = cache._materialBuckets;", + " if (!buckets || typeof buckets !== \"object\") {", + " cache.resetMaterialBuckets();", + " buckets = cache._materialBuckets;", + " }", + " var bucketCount = (buckets.timeWind ? buckets.timeWind.size : 0) +", + " (buckets.fadeInterp ? buckets.fadeInterp.size : 0) +", + " (buckets.gust ? buckets.gust.size : 0) +", + " (buckets.shadowHost ? buckets.shadowHost.size : 0) +", + " (buckets.unknown ? buckets.unknown.size : 0);", + " // Rebuild the bucket classification only when needed; bucket membership changes", + " // far less often than uniform values, so this keeps the steady-state frame cheaper.", + " if (buckets.rebuildNeeded || (bucketCount === 0 && cache.activeMaterials && cache.activeMaterials.size > 0)) {", + " resetBucketSet(buckets.timeWind);", + " resetBucketSet(buckets.fadeInterp);", + " resetBucketSet(buckets.gust);", + " resetBucketSet(buckets.shadowHost);", + " resetBucketSet(buckets.unknown);", + " if (cache.activeMaterials && typeof cache.activeMaterials[Symbol.iterator] === \"function\") {", + " for (const m0 of cache.activeMaterials) {", + " if (m0) buckets.unknown.add(m0);", + " }", + " }", + " buckets.rebuildNeeded = false;", + " }", + " ", + " // Resolve newly seen materials into the smallest set of update buckets they need.", + " // After that, steady-state updates can touch only relevant uniforms instead of", + " // rescanning every active material every frame.", + " for (const matUnknown of buckets.unknown) {", + " if (!matUnknown) {", + " buckets.unknown.delete(matUnknown);", + " continue;", + " }", + " if (!cache.activeMaterials || !cache.activeMaterials.has(matUnknown)) {", + " cache.unregisterActiveMaterial(matUnknown);", + " continue;", + " }", + " var udUnknown = matUnknown.userData;", + " if (!udUnknown) {", + " cache.unregisterActiveMaterial(matUnknown);", + " continue;", + " }", + " if (udUnknown._foliageDepthMat || udUnknown._foliageDistanceMat) {", + " buckets.shadowHost.add(matUnknown);", + " }", + " var uUnknown = udUnknown.foliageUniforms;", + " if (!uUnknown) continue;", + " if (uUnknown.uTime || uUnknown.uWindStrength || uUnknown.uWindSpeed || uUnknown.uWindDir || uUnknown.uLocalWindDir || uUnknown.uLocalWindPerp) {", + " buckets.timeWind.add(matUnknown);", + " }", + " if (uUnknown.uFadeInterpT) buckets.fadeInterp.add(matUnknown);", + " if (uUnknown.uGustTex || uUnknown.uGustEnabled || uUnknown.uGustStrength || uUnknown.uGustScale || uUnknown.uGustSpeed || uUnknown.uGustThreshold || uUnknown.uGustContrast) {", + " buckets.gust.add(matUnknown);", + " }", + " buckets.unknown.delete(matUnknown);", + " }", + " ", + " // Time/Wind bucket", + " for (const matTW of buckets.timeWind) {", + " if (!matTW || !cache.activeMaterials || !cache.activeMaterials.has(matTW)) {", + " cache.unregisterActiveMaterial(matTW);", + " continue;", + " }", + " var udTW = matTW.userData;", + " if (!udTW || !udTW.foliageUniforms) {", + " cache.unregisterActiveMaterial(matTW);", + " continue;", + " }", + " var uTW = udTW.foliageUniforms;", + " var last = udTW._lastUniforms;", + " if (!last) {", + " last = udTW._lastUniforms = {", + " time: undefined,", + " windStrength: undefined,", + " windSpeed: undefined,", + " windDirX: undefined,", + " windDirY: undefined,", + " localWindDirX: undefined,", + " localWindDirY: undefined,", + " localWindPerpX: undefined,", + " localWindPerpY: undefined", + " };", + " }", + " if (uTW.uTime && last.time !== cache.time) {", + " uTW.uTime.value = cache.time;", + " last.time = cache.time;", + " }", + " if (uTW.uWindStrength && last.windStrength !== windStrength) {", + " uTW.uWindStrength.value = windStrength;", + " last.windStrength = windStrength;", + " }", + " if (uTW.uWindSpeed && last.windSpeed !== windSpeed) {", + " uTW.uWindSpeed.value = windSpeed;", + " last.windSpeed = windSpeed;", + " }", + " if (uTW.uWindDir && (last.windDirX !== wx || last.windDirY !== wy)) {", + " uTW.uWindDir.value.set(wx, wy);", + " last.windDirX = wx;", + " last.windDirY = wy;", + " }", + " if (uTW.uLocalWindDir && (last.localWindDirX !== wx || last.localWindDirY !== wy)) {", + " uTW.uLocalWindDir.value.set(wx, wy);", + " last.localWindDirX = wx;", + " last.localWindDirY = wy;", + " }", + " if (uTW.uLocalWindPerp && (last.localWindPerpX !== -wy || last.localWindPerpY !== wx)) {", + " uTW.uLocalWindPerp.value.set(-wy, wx);", + " last.localWindPerpX = -wy;", + " last.localWindPerpY = wx;", + " }", + " }", + " ", + " // Fade interpolation bucket", + " for (const matFade of buckets.fadeInterp) {", + " if (!matFade || !cache.activeMaterials || !cache.activeMaterials.has(matFade)) {", + " cache.unregisterActiveMaterial(matFade);", + " continue;", + " }", + " var udFade = matFade.userData;", + " var uFade = udFade && udFade.foliageUniforms;", + " if (!uFade) {", + " cache.unregisterActiveMaterial(matFade);", + " continue;", + " }", + " if (uFade.uFadeInterpT) {", + " uFade.uFadeInterpT.value = fadeInterpT;", + " }", + " }", + " ", + " // Gust bucket", + " for (const matGust of buckets.gust) {", + " if (!matGust || !cache.activeMaterials || !cache.activeMaterials.has(matGust)) {", + " cache.unregisterActiveMaterial(matGust);", + " continue;", + " }", + " var udGust = matGust.userData;", + " var uGust = udGust && udGust.foliageUniforms;", + " if (!uGust) {", + " cache.unregisterActiveMaterial(matGust);", + " continue;", + " }", + " var applied = udGust.__gustVersionApplied;", + " if (applied !== cache.gustVersion) {", + " udGust.__gustVersionApplied = cache.gustVersion;", + " if (uGust.uGustTex) uGust.uGustTex.value = cache.gustTexture || cache.gustFallbackTex;", + " if (uGust.uGustEnabled) uGust.uGustEnabled.value = cache.gustEnabled ? 1.0 : 0.0;", + " if (uGust.uGustStrength) uGust.uGustStrength.value = cache.gustEnabled ? cache.gustStrength : 0.0;", + " if (uGust.uGustScale) uGust.uGustScale.value = cache.gustScale;", + " if (uGust.uGustSpeed) uGust.uGustSpeed.value = cache.gustSpeed;", + " if (uGust.uGustThreshold) uGust.uGustThreshold.value = cache.gustThreshold;", + " if (uGust.uGustContrast) uGust.uGustContrast.value = cache.gustContrast;", + " }", + " }", + " ", + " // Shadow variants are updated after foliage materials so depth/distance", + " // shadow passes inherit the latest sway, gust, and fade values.", + " shadowSync.syncShadowHostBucket(", + " buckets.shadowHost,", + " cache.activeMaterials,", + " function(mat) { cache.unregisterActiveMaterial(mat); },", + " ctx,", + " fadeInterpT", + " );", + " ", + " ", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Layer name", + "name": "layerName", + "type": "layer" + }, + { + "description": "Wind strength", + "longDescription": "Controls how strong the sway is. (default: 6)", + "name": "windStrength", + "type": "expression" + }, + { + "description": "Wind speed", + "longDescription": "Controls sway animation speed. (default: 4)", + "name": "windSpeed", + "type": "expression" + }, + { + "description": "Wind direction (X)", + "longDescription": "X component of wind direction vector. (default: 1)", + "name": "windDirectionX", + "type": "expression" + }, + { + "description": "Wind direction (Y)", + "longDescription": "Y component of wind direction vector. (default: 1)", + "name": "windDirectionY", + "type": "expression" + }, + { + "description": "Frameskip", + "longDescription": "How often distance-fade culling updates. (times per second; 1-120; default: 10)", + "name": "fadeUpdateHz", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Sets gust of wind accross foliage.", + "fullName": "Set wind gust", + "functionType": "Action", + "name": "SetWindGust", + "sentence": "Set wind gust _PARAM1_ with strength _PARAM2_ scale _PARAM3_ speed _PARAM4_ threshold _PARAM5_ contrast _PARAM6_ and texture _PARAM7_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @file SetWindGust — Configures wind gust parameters and resolves gust textures from GDevelop image resources. */", + " /** @typedef {{ strength: number, scale: number, speed: number, threshold: number, contrast: number }} GustDefaultsBridge */", + " /** @typedef {string|number|boolean|null|undefined} FoliagePrimitiveValueBridge */", + " /** @typedef {FoliagePrimitiveValueBridge|gdjs.Variable} FoliageArgumentValueBridge */", + " /** @typedef {{ getArgument: (name: string) => FoliageArgumentValueBridge }} FoliageEventsFunctionContextBridge */", + " /** @typedef {{ _libReady?: boolean, GUST_DEFAULTS: GustDefaultsBridge, gustEnabled: boolean, gustStrength: number, gustScale: number, gustSpeed: number, gustThreshold: number, gustContrast: number, gustVersion: number, gustTexture: THREE.Texture|null, gustTextureKey: string, gustTextureResourceName: string, gustFallbackTex: THREE.Texture|null, _gustDefineDirty: boolean, _readBoolArg: (efc: FoliageEventsFunctionContextBridge, name: string, fallback: boolean) => boolean, _readNumArg: (efc: FoliageEventsFunctionContextBridge, name: string, fallback: number) => number, _readStringArg: (efc: FoliageEventsFunctionContextBridge, name: string, fallback: string) => string, ensureGustFallbackTex: () => THREE.Texture|null, disposeNonFallbackTex: (tex: THREE.Texture|null|undefined) => void }} FoliageSceneStateBridge */", + " /** @typedef {{ getImageManager?: () => FoliageImageManagerBridge|null }} FoliageGameBridge */", + " /** @typedef {{ getThreeTexture?: (resourceName: string) => THREE.Texture|null|undefined }} FoliageImageManagerBridge */", + " /** @typedef {{ _libReady?: boolean, getSceneState: (runtimeScene: gdjs.RuntimeScene) => FoliageSceneStateBridge }} FoliageExtensionBridge */", + " /** @typedef {typeof gdjs & { _natureElementsFoliageSway?: FoliageExtensionBridge }} FoliageGdjsBridge */", + " var gdjsFoliage = /** @type {FoliageGdjsBridge} */ (gdjs);", + " var foliageExt = gdjsFoliage._natureElementsFoliageSway;", + " if (!foliageExt || !foliageExt._libReady) return;", + " /** @type {FoliageSceneStateBridge} */", + " var cache = foliageExt.getSceneState(runtimeScene);", + " if (!cache || !cache._libReady) return;", + " ", + " var GD = cache.GUST_DEFAULTS;", + " ", + " cache.ensureGustFallbackTex();", + " /** @type {FoliageEventsFunctionContextBridge} */", + " var efc = eventsFunctionContext;", + " ", + " var enabled = cache._readBoolArg(efc, \"enabled\", false);", + " ", + " var strength = cache._readNumArg(efc, \"strength\", GD.strength);", + " var scale = cache._readNumArg(efc, \"scale\", GD.scale);", + " var speed = cache._readNumArg(efc, \"speed\", GD.speed);", + " var threshold = cache._readNumArg(efc, \"threshold\", GD.threshold);", + " var contrast = cache._readNumArg(efc, \"contrast\", GD.contrast);", + " ", + " strength = Math.max(0.0, strength);", + " scale = Math.max(0.000001, scale);", + " speed = Math.max(0.0, speed);", + " threshold = Math.max(0.0, Math.min(1.0, threshold));", + " contrast = Math.max(0.000001, contrast);", + " ", + " var textureKey = cache._readStringArg(efc, \"textureKey\", \"\").trim();", + " // `textureUrl` is kept for backward compatibility, but at runtime it is treated as an image resource name.", + " var textureResourceName = cache._readStringArg(efc, \"textureUrl\", \"\").trim();", + " ", + " // Store previous values for change detection", + " var prevEnabled = !!cache.gustEnabled;", + " var prevStrength = cache.gustStrength;", + " var prevScale = cache.gustScale;", + " var prevSpeed = cache.gustSpeed;", + " var prevThreshold = cache.gustThreshold;", + " var prevContrast = cache.gustContrast;", + " ", + " cache.gustEnabled = !!enabled;", + " cache.gustStrength = strength;", + " cache.gustScale = scale;", + " cache.gustSpeed = speed;", + " cache.gustThreshold = threshold;", + " cache.gustContrast = contrast;", + " ", + " var changed =", + " prevEnabled !== cache.gustEnabled ||", + " prevStrength !== cache.gustStrength ||", + " prevScale !== cache.gustScale ||", + " prevSpeed !== cache.gustSpeed ||", + " prevThreshold !== cache.gustThreshold ||", + " prevContrast !== cache.gustContrast;", + " ", + " if (changed) {", + " cache.gustVersion = (cache.gustVersion || 0) + 1;", + " }", + " if (prevEnabled !== cache.gustEnabled) {", + " cache._gustDefineDirty = true;", + " }", + " ", + " if (!textureResourceName) {", + " var oldTexNoUrl = cache.gustTexture;", + " var hadTextureBinding = !!oldTexNoUrl || !!cache.gustTextureKey || !!cache.gustTextureResourceName;", + " cache.gustTextureKey = \"\";", + " cache.gustTextureResourceName = \"\";", + " cache.gustTexture = null; // Fallback is consumed via cache.gustTexture || cache.gustFallbackTex", + " if (hadTextureBinding) {", + " cache.disposeNonFallbackTex(oldTexNoUrl);", + " cache.gustVersion = (cache.gustVersion || 0) + 1;", + " }", + " } else {", + " var key = textureKey || textureResourceName;", + " var prevTexture = cache.gustTexture;", + " var prevTextureKey = cache.gustTextureKey || \"\";", + " var prevResourceName = cache.gustTextureResourceName || \"\";", + " /** @type {THREE.Texture|null} */", + " var nextTexture = null;", + "", + " /** @type {FoliageGameBridge|null} */", + " var game = runtimeScene && typeof runtimeScene.getGame === \"function\" ? runtimeScene.getGame() : null;", + " /** @type {FoliageImageManagerBridge|null} */", + " var imageManager = game && typeof game.getImageManager === \"function\" ? game.getImageManager() : null;", + " if (imageManager && typeof imageManager.getThreeTexture === \"function\") {", + " nextTexture = imageManager.getThreeTexture(textureResourceName) || null;", + " }", + " ", + " var sameBinding =", + " prevTexture === nextTexture &&", + " prevTextureKey === key &&", + " prevResourceName === textureResourceName;", + " ", + " if (!sameBinding) {", + " cache.gustTextureKey = key;", + " cache.gustTextureResourceName = textureResourceName;", + " cache.gustTexture = nextTexture;", + " cache.disposeNonFallbackTex(prevTexture);", + " cache.gustVersion = (cache.gustVersion || 0) + 1;", + " }", + " }", + " ", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Enable wind gust", + "name": "enabled", + "type": "trueorfalse" + }, + { + "description": "Wind gust strength", + "longDescription": "Gust intensity multiplier. (default: 3)", + "name": "strength", + "type": "expression" + }, + { + "description": "Wind gust scale", + "longDescription": "Gust noise/wave spatial scale (pattern size; default: 0.0005)", + "name": "scale", + "type": "expression" + }, + { + "description": "Wind gust speed", + "longDescription": "Gust animation speed over time. (default: 0.25)", + "name": "speed", + "type": "expression" + }, + { + "description": "Gust travel speed", + "longDescription": "Cutoff for where gust texture starts affecting motion. (default: 0.28)", + "name": "threshold", + "type": "expression" + }, + { + "description": "Gust edge sharpness", + "longDescription": "Sharpness/strength of gust mask transition. default: 1.6)", + "name": "contrast", + "type": "expression" + }, + { + "description": "Gust texture", + "longDescription": "URL/path of the gust map texture. (optional; red channel drives gust mask)", + "name": "textureUrl", + "type": "imageResource" + }, + { + "description": "Gust texture key", + "longDescription": "Cache key to avoid reloading same texture. (optional)", + "name": "textureKey", + "type": "string" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "UpdateFoliageSwaying" + }, + { + "functionName": "SetWindGust" + } + ] + }, + "eventsBasedBehaviors": [ + { + "description": "Adds wind-based swaying to 3D foliage objects — grass, bushes, trees.", + "fullName": "Foliage swaying", + "helpPath": "", + "iconUrl": "", + "name": "FoliageSwaying", + "objectType": "Scene3D::Model3DObject", + "previewIconUrl": "", + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NatureElements::DefineHelperClasses" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "/** @file onCreated — Analyzes 3D objects, patches materials with sway shaders, queues for GPU instancing. */", + " /** @typedef {{ __foliageSkipOnDestroy?: boolean, __foliageNonInstancedRegistered?: boolean, __foliageSharedKey?: string, __foliageSharedKeyTrunk?: string, __foliageInstancingGroupKey?: string, __foliageInstancingGroupKeyLeaves?: string, __foliageInstancingIndex?: number, __foliageQueued?: boolean, __foliageQueueId?: number }} FoliageBehaviorPrivateFieldsBridge */", + " /** @typedef {gdjs.RuntimeBehavior & FoliageBehaviorPrivateFieldsBridge} FoliageBehaviorBridge */", + " /** @typedef {string|number|boolean|null|undefined} FoliagePrimitiveValueBridge */", + " /** @typedef {{ materialName: string, swayType: string, uniformSway: boolean, customLit: boolean, metallic: number, roughness: number, specular: number, normalStrength: number, aoStrength: number, envStrength: number, cullingMode: string, twoSidedLighting: boolean, gpuInstancing: boolean, distanceFadeEnabled: boolean, fadeStart: number, fadeEnd: number, polyScale: number, ignoreUV: boolean, gradStart: number, gradEnd: number, gradHeight: number, useColorGrading: boolean, colorTop: string, colorBottom: string, uContrast: number, uSat: number, debugOutput: boolean }} FoliageBehaviorPropsBridge */", + " /** @typedef {{ meshName: string, materialIndex: number, materialRef: THREE.Material|null, materialName: string, alphaLikely: boolean, score: number }} FoliageMaterialRecordBridge */", + " /** @typedef {{ zMin: number, zMax: number, relZMin: number, relZMax: number, hasGeom: boolean, totalVerts: number, totalTris: number, meshCount: number, slotCount: number, planeLikeCount: number, sizeLocalMin: THREE.Vector3, sizeLocalMax: THREE.Vector3, sizeWorldMin: THREE.Vector3, sizeWorldMax: THREE.Vector3, _meshIds?: Set }} FoliageMaterialStatsBridge */", + " /** @typedef {{ srcRef: THREE.Material|null, pickedId: string, matchMode: \"name\"|\"auto\", matchName: string }} FoliageMaterialSelectionBridge */", + " /** @typedef {{ material: THREE.Material, refCount: number, _ownedByFoliage?: boolean }} FoliageSharedMaterialEntryBridge */", + " /** @typedef {THREE.Material & { alphaMap?: THREE.Texture|null, map?: THREE.Texture|null, alphaTest?: number, transparent?: boolean, opacity?: number, name?: string }} FoliageInspectableMaterialBridge */", + " /** @typedef {{ polyScale: number, polyScaleRaw: number, sizeFactor: number, complexityFactor: number, vertexFactor: number, triFactor: number, structureFactor: number, planeFactor: number, responseGain: number, planeLikeRatio: number, baseByType: number, boundsMode: string }} FoliageAutoPolyScaleEntryBridge */", + " /** @typedef {{ _cachedGeometry?: THREE.BufferGeometry|null, _gpuGeometry?: THREE.BufferGeometry|null, _gpuGroupKey?: string, _sharedMaterialKey?: string, _resolvedSide?: THREE.Side, _gpuFastMode?: \"single\"|\"splitLeavesTree\", _gpuLeavesGeometry?: THREE.BufferGeometry|null, _gpuTrunkGeometry?: THREE.BufferGeometry|null, _gpuLeavesBaseGroupKey?: string, _gpuTrunkBaseGroupKey?: string, _gpuLeavesSharedMaterialKey?: string, _gpuTrunkSharedMaterialKey?: string, _gpuLeavesResolvedSide?: THREE.Side, _gpuTrunkResolvedSide?: THREE.Side, records?: FoliageMaterialRecordBridge[], statsByRef?: Map, statsByName?: Map, selection?: FoliageMaterialSelectionBridge|null }} FoliageObjectTypeCacheEntryBridge */", + " /** @typedef {{ repMesh?: THREE.Object3D|null, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, baseGroupKey?: string }} FoliagePendingPartBridge */", + " /** @typedef {{ gdObj?: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string })|null, threeObj?: THREE.Object3D|null, repMesh?: THREE.Object3D|null, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, parent?: THREE.Object3D|null, baseGroupKey?: string, groupKey?: string, swayType?: string, behavior?: FoliageBehaviorBridge|null, queueId?: number, parts?: FoliagePendingPartBridge[] }} FoliagePendingItemBridge */", + " /** @typedef {{ key?: string, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, parent?: THREE.Object3D|null, matricesBuffer?: Float32Array|null, centersXY?: Float32Array|null, matrixCount?: number, aliveCount?: number, freeIndices?: number[], freeIndexSet?: Set, capacity?: number, mesh?: THREE.InstancedMesh|null, centersZ?: Float32Array|null, cullRadii?: Float32Array|null, fadeEnabled?: boolean, fadeStart?: number, fadeEnd?: number, instanceFade?: Float32Array|null, instanceFadePrev?: Float32Array|null }} FoliageInstancingGroupBridge */", + " /** @typedef {{ groups: Map, dirty: boolean, sceneTag: string|null, pending: FoliagePendingItemBridge[], queueIdCounter: number, cancelledQueueIds: Set, foliageRoot?: THREE.Object3D|null, _cachedSceneRoot?: THREE.Object3D|null, ensureReady: () => FoliageInstancingStateBridge, nextQueueId: () => number, enqueue: (item: FoliagePendingItemBridge) => FoliagePendingItemBridge, ensureGroup: (groupKey: string, seedData?: FoliageInstancingGroupBridge) => FoliageInstancingGroupBridge, markDirty: () => FoliageInstancingStateBridge }} FoliageInstancingStateBridge */", + " /** @typedef {{ gdObj?: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string })|null, threeObj?: THREE.Object3D|null, material?: THREE.Material|null, trunkMaterial?: THREE.Material|null, fadeStart?: number, fadeEnd?: number, fadeEnabled?: boolean, fadeBehavior?: FoliageBehaviorBridge|null, _wasHidden?: boolean, _parkedNoFade?: boolean, _firstHideWarmupDone?: boolean }} FoliageNonInstancedEntryBridge */", + " /** @typedef {{ upsertFadeEntry: (entryData: FoliageNonInstancedEntryBridge) => FoliageNonInstancedEntryBridge|null }} FoliageNonInstancedRegistryBridge */", + " /** @typedef {{ get: (key: string) => FoliageObjectTypeCacheEntryBridge|undefined, set: (key: string, value: FoliageObjectTypeCacheEntryBridge, kindHint?: string) => void, getCachedBBox: (geometry: THREE.BufferGeometry) => THREE.Box3|null|undefined, setCachedBBox: (geometry: THREE.BufferGeometry, bbox: THREE.Box3) => void, getAutoPolyScale: (key: string) => FoliageAutoPolyScaleEntryBridge|null|undefined, setAutoPolyScale: (key: string, value: FoliageAutoPolyScaleEntryBridge) => void, hasDebugKey: (key: string) => boolean, markDebugKey: (key: string) => void, resolveOrPick: (root: THREE.Object3D, wantedName: string, sharedKeyForCache: string, helpers: { isAlphaLikely: (mat: FoliageInspectableMaterialBridge|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterialBridge|null|undefined) => number }) => FoliageObjectTypeCacheEntryBridge, debugPrintFromRecords: (objName: string, records: FoliageMaterialRecordBridge[]) => void }} FoliageObjectTypeCacheRegistryBridge */", + " /** @typedef {THREE.Object3D & { isMesh?: boolean, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|THREE.Material[]|null, castShadow?: boolean, receiveShadow?: boolean, id?: number, name?: string }} FoliageMeshLike */", + " /** @typedef {{ _libReady?: boolean, getSceneState: (runtimeScene: gdjs.RuntimeScene) => FoliageSceneStateBridge, normalizeCullingMode: (v: FoliagePrimitiveValueBridge) => { name: string, code: number }, resolveRenderSide: (sourceSide: THREE.Side|undefined|null, cullingModeName: string) => THREE.Side, buildSideSuffix: (cullingModeCode: number, renderSide: THREE.Side|undefined|null) => string, buildPbrSuffix: (customLitValue: boolean, metallicValue: number, roughnessValue: number, specularValue: number, normalStrengthValue: number, aoStrengthValue: number, envStrengthValue: number) => string, buildGpuFastCacheKey: (localSharedKey: string, localMaterialName: string, localSwayType: string, localDistanceFadeEnabled: boolean, localPbrSuffix: string, localSideSuffix: string, localTwoSidedLighting: boolean, localPolyScaleAutoMode: boolean) => string, parseColor: (str: string, fallbackHex: string) => THREE.Color, isAlphaLikely: (mat: FoliageInspectableMaterialBridge|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterialBridge|null|undefined) => number, sanitizeKeyPart: (s: string) => string, meshMatchesSelection: (mesh: FoliageMeshLike, selection: FoliageMaterialSelectionBridge, srcRef: THREE.Material|null, srcName: string) => boolean, findFirstMatchingMesh: (root: THREE.Object3D, selection: FoliageMaterialSelectionBridge, srcRef: THREE.Material|null, srcName: string) => FoliageMeshLike|null, findSplitTreeMeshes: (root: THREE.Object3D, trunkMatcher: (mesh: FoliageMeshLike) => boolean, leavesMatcher: (mesh: FoliageMeshLike) => boolean) => { trunk: FoliageMeshLike|null, leaves: FoliageMeshLike|null } }} FoliageExtensionBridge */", + " /** @typedef {typeof gdjs & { _natureElementsFoliageSway?: FoliageExtensionBridge }} FoliageGdjsBridge */", + " /** @typedef {{ _libReady?: boolean, instancingState: FoliageInstancingStateBridge, nonInstancedRegistry: FoliageNonInstancedRegistryBridge, cleanupForSceneChange: () => void, disposeShadowMaterials: (mat: THREE.Material|null|undefined) => void, ensureGustFallbackTex: () => THREE.Texture|null, getBehaviorProps: (behavior: FoliageBehaviorBridge|null|undefined) => FoliageBehaviorPropsBridge|null, gustContrast: number, gustEnabled: boolean, gustFallbackTex: THREE.Texture|null, gustScale: number, gustSpeed: number, gustStrength: number, gustTexture: THREE.Texture|null, gustThreshold: number, objectTypeCacheRegistry: FoliageObjectTypeCacheRegistryBridge, patchedMaterials: WeakSet, registerActiveMaterial: (mat: THREE.Material|null|undefined) => void, sharedByKey: Map, unregisterActiveMaterial: (mat: THREE.Material|null|undefined) => void, _defaultTopColor?: THREE.Color, _defaultBottomColor?: THREE.Color }} FoliageSceneStateBridge */", + " var gdjsFoliage = /** @type {FoliageGdjsBridge} */ (gdjs);", + " var foliageExt = gdjsFoliage._natureElementsFoliageSway;", + " if (!foliageExt || !foliageExt._libReady) return;", + " /** @type {FoliageSceneStateBridge} */", + " var cache = foliageExt.getSceneState(runtimeScene);", + " /** @type {FoliageNonInstancedRegistryBridge|null|undefined} */", + " var nonInstanced = cache.nonInstancedRegistry;", + " if (!nonInstanced) return;", + " /** @type {FoliageInstancingStateBridge|null|undefined} */", + " var instancing = cache.instancingState;", + " if (!instancing) return;", + " instancing.ensureReady();", + " ", + " // Scene change detection. Scene change invalidates scene-scoped caches and pooled state.", + " var currentSceneTag = (runtimeScene && typeof runtimeScene.getName === \"function\") ? runtimeScene.getName() : null;", + " if (instancing.sceneTag !== currentSceneTag) {", + " cache.cleanupForSceneChange();", + " instancing = cache.instancingState;", + " if (!instancing) return;", + " instancing.ensureReady();", + " instancing.sceneTag = currentSceneTag;", + " }", + " ", + " /** @type {FoliageObjectTypeCacheRegistryBridge|null|undefined} */", + " var objectTypeCache = cache.objectTypeCacheRegistry;", + " if (!objectTypeCache) return;", + " ", + " cache.ensureGustFallbackTex();", + " ", + " /** @type {(gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string })|null|undefined} */", + " var gdObj = /** @type {(gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string })|null|undefined} */ (objects[0]);", + " if (!gdObj) return;", + " ", + " /** @type {FoliageBehaviorBridge|null} */", + " var behavior = /** @type {FoliageBehaviorBridge|null} */ (gdObj.getBehavior(\"FoliageSwaying\"));", + " if (!behavior) return;", + " ", + " if (behavior.__foliageInstancingIndex !== undefined || behavior.__foliageQueued === true) {", + " return;", + " }", + " ", + " /** @type {FoliageBehaviorPropsBridge|null} */", + " var props = /** @type {FoliageBehaviorPropsBridge|null} */ (cache.getBehaviorProps(behavior));", + " if (!props) return;", + " var normalizeCullingMode = foliageExt.normalizeCullingMode;", + " var resolveRenderSide = foliageExt.resolveRenderSide;", + " var buildSideSuffix = foliageExt.buildSideSuffix;", + " var buildPbrSuffix = foliageExt.buildPbrSuffix;", + " var buildGpuFastCacheKey = foliageExt.buildGpuFastCacheKey;", + " var parseColor = foliageExt.parseColor;", + " var isAlphaLikely = foliageExt.isAlphaLikely;", + " var scoreMaterial = foliageExt.scoreMaterial;", + " var sanitizeKeyPart = foliageExt.sanitizeKeyPart;", + " var meshMatchesSelection = foliageExt.meshMatchesSelection;", + " var findFirstMatchingMesh = foliageExt.findFirstMatchingMesh;", + " var findSplitTreeMeshes = foliageExt.findSplitTreeMeshes;", + " if (!normalizeCullingMode || !resolveRenderSide || !buildSideSuffix || !buildPbrSuffix ||", + " !buildGpuFastCacheKey || !parseColor || !isAlphaLikely || !scoreMaterial ||", + " !sanitizeKeyPart || !meshMatchesSelection || !findFirstMatchingMesh || !findSplitTreeMeshes) return;", + " ", + " // ============================================================", + " // Fast path: reuse cached instancing metadata so repeated objects of the", + " // same asset can skip the expensive material/mesh analysis path.", + " // ============================================================", + " var sharedKeyFast = gdObj.getName ? gdObj.getName() : gdObj.name || \"DEFAULT\";", + " var gpuInstancingFast = props.gpuInstancing;", + " var distanceFadeFast = props.distanceFadeEnabled;", + " var customLitFast = props.customLit;", + " var twoSidedLightingFast = props.twoSidedLighting;", + " var metallicFast = props.metallic;", + " var roughnessFast = props.roughness;", + " var specularFast = props.specular;", + " var normalStrengthFast = props.normalStrength;", + " var aoStrengthFast = props.aoStrength;", + " var envStrengthFast = props.envStrength;", + " var polyScaleRawFast = props.polyScale;", + " var polyScaleAutoModeFast = (polyScaleRawFast === 0);", + " var cullingModeFastInfo = normalizeCullingMode(props.cullingMode);", + " var pbrSuffixFast = buildPbrSuffix(customLitFast, metallicFast, roughnessFast, specularFast, normalStrengthFast, aoStrengthFast, envStrengthFast);", + " if (gpuInstancingFast) {", + " var swayTypeFast = (props.swayType && String(props.swayType).trim() !== \"\") ? props.swayType : \"grassSway\";", + " if (swayTypeFast === \"treeTrunkSway\") twoSidedLightingFast = false;", + " ", + " // Fast path for grassSway, bushSway, and treeTrunkSway with GPU instancing.", + " if (swayTypeFast === \"grassSway\" || swayTypeFast === \"bushSway\" || swayTypeFast === \"treeTrunkSway\") {", + " var materialNameFast = String(props.materialName || \"\").trim();", + " /** @type {FoliageObjectTypeCacheEntryBridge|undefined} */", + " var cachedRopFast = objectTypeCache.get(sharedKeyFast + \"::\" + (materialNameFast || \"\"));", + " var sourceSideFast = (cachedRopFast && cachedRopFast.selection && cachedRopFast.selection.srcRef && typeof cachedRopFast.selection.srcRef.side === \"number\")", + " ? cachedRopFast.selection.srcRef.side", + " : THREE.FrontSide;", + " /** @type {THREE.Side} */", + " var resolvedRenderSideFast = resolveRenderSide(sourceSideFast, cullingModeFastInfo.name);", + " var sideSuffixFast = buildSideSuffix(cullingModeFastInfo.code, resolvedRenderSideFast);", + " var fastCacheKey = buildGpuFastCacheKey(sharedKeyFast, materialNameFast, swayTypeFast, distanceFadeFast, pbrSuffixFast, sideSuffixFast, twoSidedLightingFast, polyScaleAutoModeFast);", + " /** @type {FoliageObjectTypeCacheEntryBridge|undefined} */", + " var fastCached = objectTypeCache.get(fastCacheKey);", + " ", + " if (fastCached && fastCached._gpuGroupKey && fastCached._gpuGeometry) {", + " // Fast path hit: required GPU instancing data is cached.", + " var threeObjFast = gdObj.get3DRendererObject ? gdObj.get3DRendererObject() : null;", + " if (threeObjFast) {", + " var inst = instancing;", + " var groupKey = fastCached._gpuGroupKey;", + " var g = inst.groups.get(groupKey);", + " ", + " if (g && !(fastCached._resolvedSide !== undefined && g.material && typeof g.material.side === \"number\" && g.material.side !== fastCached._resolvedSide)) {", + " // Find the mesh using cached geometry (fast lookup).", + " /** @type {FoliageMeshLike|null} */", + " var repMeshFast = null;", + " var geoFast = fastCached._gpuGeometry;", + " threeObjFast.traverse(function(o) {", + " if (repMeshFast) return;", + " var meshFast = /** @type {FoliageMeshLike} */ (o);", + " if (meshFast && meshFast.isMesh && meshFast.geometry === geoFast) {", + " repMeshFast = meshFast;", + " }", + " });", + " ", + " if (repMeshFast) {", + " // Queue the item and return immediately; skip the full setup path.", + " // Keep queueId for queued-destroy cancellation.", + " var queueId = inst.nextQueueId();", + " inst.enqueue({", + " gdObj: gdObj,", + " threeObj: threeObjFast,", + " repMesh: repMeshFast,", + " geometry: geoFast,", + " material: g.material,", + " parent: g.parent,", + " // flushPending may still derive the final runtime key from this base key.", + " baseGroupKey: groupKey,", + " swayType: swayTypeFast,", + " behavior: behavior,", + " queueId: queueId", + " });", + " behavior.__foliageQueued = true; // Prevent duplicate registration before flush.", + " behavior.__foliageQueueId = queueId;", + " delete behavior.__foliageNonInstancedRegistered;", + " // Keep the current base key on behavior/userData until flush resolves the final group key.", + " behavior.__foliageInstancingGroupKey = groupKey;", + " threeObjFast.userData = threeObjFast.userData || {};", + " delete threeObjFast.userData.__foliageNonInstancedRegistered;", + " threeObjFast.userData.__foliageInstancingGroupKey = groupKey;", + " inst.markDirty();", + " ", + " // Increment shared material refCount for later cleanup/release.", + " var sharedMatKey = fastCached._sharedMaterialKey;", + " var entry = cache.sharedByKey.get(sharedMatKey);", + " if (entry) {", + " entry.refCount++;", + " }", + " ", + " // Persist the shared material key so destroy/cleanup can release the right entry.", + " behavior.__foliageSharedKey = sharedMatKey;", + " threeObjFast.userData.__foliageSharedKey = sharedMatKey;", + " ", + " return; // Fast path complete.", + " }", + " }", + " }", + " }", + " }", + "", + " if (swayTypeFast === \"leavesSway\") {", + " var materialNameFastLeaves = String(props.materialName || \"\").trim();", + " var fastCacheKeyLeaves = buildGpuFastCacheKey(sharedKeyFast, materialNameFastLeaves, swayTypeFast, distanceFadeFast, pbrSuffixFast, sideSuffixFast, twoSidedLightingFast, polyScaleAutoModeFast);", + " /** @type {FoliageObjectTypeCacheEntryBridge|undefined} */", + " var fastCachedLeaves = objectTypeCache.get(fastCacheKeyLeaves);", + " if (fastCachedLeaves && fastCachedLeaves._gpuFastMode === \"splitLeavesTree\" &&", + " fastCachedLeaves._gpuTrunkGeometry && fastCachedLeaves._gpuLeavesGeometry &&", + " fastCachedLeaves._gpuTrunkBaseGroupKey && fastCachedLeaves._gpuLeavesBaseGroupKey &&", + " fastCachedLeaves._gpuTrunkSharedMaterialKey && fastCachedLeaves._gpuLeavesSharedMaterialKey) {", + " var threeObjFastLeaves = gdObj.get3DRendererObject ? gdObj.get3DRendererObject() : null;", + " if (threeObjFastLeaves) {", + " var entryFastLeaves = cache.sharedByKey.get(fastCachedLeaves._gpuLeavesSharedMaterialKey);", + " var entryFastTrunk = cache.sharedByKey.get(fastCachedLeaves._gpuTrunkSharedMaterialKey);", + " if (entryFastLeaves && entryFastLeaves.material && entryFastTrunk && entryFastTrunk.material) {", + " var leavesSideOk = !(typeof fastCachedLeaves._gpuLeavesResolvedSide === \"number\" &&", + " typeof entryFastLeaves.material.side === \"number\" &&", + " entryFastLeaves.material.side !== fastCachedLeaves._gpuLeavesResolvedSide);", + " var trunkSideOk = !(typeof fastCachedLeaves._gpuTrunkResolvedSide === \"number\" &&", + " typeof entryFastTrunk.material.side === \"number\" &&", + " entryFastTrunk.material.side !== fastCachedLeaves._gpuTrunkResolvedSide);", + "\t if (leavesSideOk && trunkSideOk) {", + "\t var geoFastTrunk = fastCachedLeaves._gpuTrunkGeometry;", + "\t var geoFastLeaves = fastCachedLeaves._gpuLeavesGeometry;", + "\t var splitFastMeshes = findSplitTreeMeshes(", + "\t threeObjFastLeaves,", + "\t function(mesh) {", + "\t return mesh.geometry === geoFastTrunk;", + "\t },", + "\t function(mesh) {", + "\t return mesh.geometry === geoFastLeaves;", + "\t }", + "\t );", + "\t var repMeshFastTrunk = splitFastMeshes.trunk;", + "\t var repMeshFastLeaves = splitFastMeshes.leaves;", + " if (repMeshFastTrunk && repMeshFastLeaves) {", + " var queueIdLeaves = instancing.nextQueueId();", + " instancing.enqueue({", + " gdObj: gdObj,", + " threeObj: threeObjFastLeaves,", + " parent: null,", + " swayType: \"leavesSway\",", + " behavior: behavior,", + " queueId: queueIdLeaves,", + " parts: [", + " { repMesh: repMeshFastTrunk, geometry: geoFastTrunk, material: entryFastTrunk.material, baseGroupKey: fastCachedLeaves._gpuTrunkBaseGroupKey },", + " { repMesh: repMeshFastLeaves, geometry: geoFastLeaves, material: entryFastLeaves.material, baseGroupKey: fastCachedLeaves._gpuLeavesBaseGroupKey }", + " ]", + " });", + " behavior.__foliageQueued = true;", + " behavior.__foliageQueueId = queueIdLeaves;", + " delete behavior.__foliageNonInstancedRegistered;", + " // Split-tree cache stores base keys only; flushPending adds any determinant-sign suffix.", + " behavior.__foliageSharedKey = fastCachedLeaves._gpuLeavesSharedMaterialKey;", + " behavior.__foliageSharedKeyTrunk = fastCachedLeaves._gpuTrunkSharedMaterialKey;", + " threeObjFastLeaves.userData = threeObjFastLeaves.userData || {};", + " delete threeObjFastLeaves.userData.__foliageNonInstancedRegistered;", + " threeObjFastLeaves.userData.__foliageSharedKey = fastCachedLeaves._gpuLeavesSharedMaterialKey;", + " threeObjFastLeaves.userData.__foliageSharedKeyTrunk = fastCachedLeaves._gpuTrunkSharedMaterialKey;", + " entryFastLeaves.refCount++;", + " entryFastTrunk.refCount++;", + " instancing.markDirty();", + " return;", + " }", + " }", + " }", + " }", + " }", + " }", + " }", + " // ============================================================", + " // Fall back to full path if fast path cannot be used.", + " // ============================================================", + " ", + " var uniformSway = props.uniformSway;", + " ", + " var swayType = (props.swayType && String(props.swayType).trim() !== \"\") ? props.swayType : \"grassSway\";", + " ", + " var useColorGrading = props.useColorGrading;", + " var gpuInstancing = props.gpuInstancing;", + " var debugOutput = props.debugOutput;", + "", + " // Distance fade properties (for dither dissolve culling)", + " var distanceFadeEnabled = props.distanceFadeEnabled;", + " var fadeStart = props.fadeStart;", + " var fadeEnd = props.fadeEnd;", + " // Ensure fadeEnd > fadeStart", + " if (fadeEnd <= fadeStart) fadeEnd = fadeStart + 100;", + " var customLit = props.customLit;", + " var twoSidedLighting = props.twoSidedLighting;", + " if (swayType === \"treeTrunkSway\") twoSidedLighting = false;", + " var metallic = props.metallic;", + " var roughness = props.roughness;", + " var specular = props.specular;", + " var normalStrength = props.normalStrength;", + " var aoStrength = props.aoStrength;", + " var envStrength = props.envStrength;", + " var cullingModeInfo = normalizeCullingMode(props.cullingMode);", + " var cullingMode = cullingModeInfo.name;", + " var cullingModeCode = cullingModeInfo.code;", + " var pbrSuffix = buildPbrSuffix(customLit, metallic, roughness, specular, normalStrength, aoStrength, envStrength);", + " ", + " var materialNameRaw = String(props.materialName || \"\");", + " var materialName = materialNameRaw.trim();", + " ", + " var topColor = useColorGrading", + " ? parseColor(props.colorTop, \"#f9372b\")", + " : (cache._defaultTopColor || (cache._defaultTopColor = parseColor(\"\", \"#f9372b\")));", + " var bottomColor = useColorGrading", + " ? parseColor(props.colorBottom, \"#ede060\")", + " : (cache._defaultBottomColor || (cache._defaultBottomColor = parseColor(\"\", \"#ede060\")));", + " var sat = props.uSat;", + " var contrast = props.uContrast;", + " ", + " // Allow polyScaleRaw up to 200 for complex models (auto-scale can exceed 100)", + " var polyScaleRaw = props.polyScale;", + " var polyScaleAutoMode = (polyScaleRaw === 0);", + " var polyScale = polyScaleRaw / 50.0;", + " ", + " var gradStart = props.gradStart;", + " var gradEnd = props.gradEnd;", + " if (gradEnd < gradStart) {", + " var tmp = gradStart;", + " gradStart = gradEnd;", + " gradEnd = tmp;", + " }", + " if (gradEnd - gradStart < 0.0001) gradEnd = Math.min(1.0, gradStart + 0.0001);", + " ", + " var gradStartKey = Math.round(gradStart * 1000);", + " var gradEndKey = Math.round(gradEnd * 1000);", + " ", + " // Optional: ignore UV gradient and use world-up gradient relative to mesh origin.", + " var ignoreUV = props.ignoreUV;", + " // If color grading is disabled, ignoreUV has no effect (gradient isn't used anyway)", + " if (!useColorGrading) ignoreUV = false;", + " ", + " // Height range for world-space gradient, in world units.", + " // Defines the vertical span mapped to full gradient before gradStart/gradEnd remap.", + " var gradHeight = props.gradHeight;", + " var gradHeightKey = Math.round(gradHeight * 1000);", + " ", + " var threeObj = gdObj.get3DRendererObject ? gdObj.get3DRendererObject() : null;", + " if (!threeObj) return;", + " ", + " var sharedKey = gdObj.getName ? gdObj.getName() : gdObj.name || \"DEFAULT\";", + " var analysisHelpers = {", + " isAlphaLikely: isAlphaLikely,", + " scoreMaterial: scoreMaterial", + " };", + " ", + " // Main analysis pass: resolve the selected material entry for this object", + " // type once, then reuse that cached decision for later objects.", + " var objectTypeCacheKey = sharedKey + \"::\" + (materialName || \"\");", + " /** @type {FoliageObjectTypeCacheEntryBridge|undefined} */", + " var cachedRop = objectTypeCache.get(objectTypeCacheKey);", + " ", + " /** @type {FoliageObjectTypeCacheEntryBridge} */", + " var rop;", + " if (cachedRop) {", + " // Reuse cached result to skip traversal and analysis.", + " rop = cachedRop;", + " } else {", + " // First object of this type/materialName: run full analysis.", + " // collectAndStats is cached inside resolveOrPick by sharedKey.", + " rop = objectTypeCache.resolveOrPick(threeObj, materialName, sharedKey, analysisHelpers);", + " if (rop && rop.selection && rop.selection.srcRef) {", + " objectTypeCache.set(objectTypeCacheKey, rop);", + " }", + " }", + " ", + " if (!rop || !rop.selection || !rop.selection.srcRef) return;", + " ", + " if (debugOutput && !objectTypeCache.hasDebugKey(sharedKey)) {", + " objectTypeCache.markDebugKey(sharedKey);", + " objectTypeCache.debugPrintFromRecords(sharedKey, rop.records);", + " }", + " ", + " var selection = rop.selection;", + " ", + " // Calculate bounding box (min/max Z) ONLY for meshes that use the selected material", + " // This ensures gradient works across all polygons with the same material as if they were one mesh", + " // Uses geometry AABB instead of iterating all vertices - much faster! (O(m) instead of O(n log n))", + " // Stats for selected material/name were precomputed during collectAndStats (single traverse)", + " var gradLocalZMin = Infinity;", + " var gradLocalZMax = -Infinity;", + " var hasGeometry = false;", + " ", + " // For auto polyScale (size + density). Prefer root-local bounds for stability.", + " /** @type {THREE.Vector3|null} */", + " var sizeWorldMin = null;", + " /** @type {THREE.Vector3|null} */", + " var sizeWorldMax = null;", + " /** @type {THREE.Vector3|null} */", + " var sizeLocalMin = null;", + " /** @type {THREE.Vector3|null} */", + " var sizeLocalMax = null;", + " var totalVerts = 0;", + " var totalTris = 0;", + " var meshCountForScale = 0;", + " var slotCountForScale = 0;", + " var planeLikeCountForScale = 0;", + " ", + " var srcRef = selection.srcRef;", + " var srcName = selection.matchName;", + " ", + " /** @type {FoliageMaterialStatsBridge|null} */", + " var st = null;", + " if (selection.matchMode === \"name\") {", + " st = rop.statsByName ? rop.statsByName.get(selection.matchName) : null;", + " } else {", + " st = rop.statsByRef ? rop.statsByRef.get(srcRef) : null;", + " if (!st && srcName && rop.statsByName) st = rop.statsByName.get(srcName);", + " }", + " ", + " if (st) {", + " hasGeometry = !!st.hasGeom;", + " // ignoreUV mode: use relative world Z (world Z minus mesh origin Z)", + " // GDevelop uses Z-up coordinate system (Z is vertical, Y is depth)", + " // This ensures gradient ALWAYS goes up-down regardless of model orientation", + " // UV mode: use local Z (position.z in shader)", + " if (ignoreUV && isFinite(st.relZMin) && isFinite(st.relZMax) && st.relZMax > st.relZMin) {", + " gradLocalZMin = st.relZMin;", + " gradLocalZMax = st.relZMax;", + " } else if (!ignoreUV) {", + " gradLocalZMin = st.zMin;", + " gradLocalZMax = st.zMax;", + " }", + " totalVerts = st.totalVerts || 0;", + " totalTris = st.totalTris || 0;", + " meshCountForScale = st.meshCount || 0;", + " slotCountForScale = st.slotCount || 0;", + " planeLikeCountForScale = st.planeLikeCount || 0;", + " if (st.sizeLocalMin) sizeLocalMin = st.sizeLocalMin;", + " if (st.sizeLocalMax) sizeLocalMax = st.sizeLocalMax;", + " if (st.sizeWorldMin) sizeWorldMin = st.sizeWorldMin;", + " if (st.sizeWorldMax) sizeWorldMax = st.sizeWorldMax;", + " }", + " ", + " var useLocalBoundsForAuto =", + " !!sizeLocalMin && !!sizeLocalMax &&", + " isFinite(sizeLocalMin.x) && isFinite(sizeLocalMin.y) && isFinite(sizeLocalMin.z) &&", + " isFinite(sizeLocalMax.x) && isFinite(sizeLocalMax.y) && isFinite(sizeLocalMax.z) &&", + " sizeLocalMax.x > sizeLocalMin.x &&", + " sizeLocalMax.y > sizeLocalMin.y &&", + " sizeLocalMax.z > sizeLocalMin.z;", + " ", + " var autoBoundsMin = useLocalBoundsForAuto ? sizeLocalMin : sizeWorldMin;", + " var autoBoundsMax = useLocalBoundsForAuto ? sizeLocalMax : sizeWorldMax;", + " ", + " // Fallback if no geometry found or invalid bounds", + " if (!hasGeometry || !isFinite(gradLocalZMin) || !isFinite(gradLocalZMax) || gradLocalZMax <= gradLocalZMin) {", + " // For ignoreUV, try using world height (Z dimension) as fallback", + " if (ignoreUV) {", + " var fallbackHeight = (autoBoundsMin && autoBoundsMax) ? (autoBoundsMax.z - autoBoundsMin.z) : NaN;", + " if (isFinite(fallbackHeight) && fallbackHeight > 0) {", + " // Assume origin is at center", + " gradLocalZMin = -fallbackHeight * 0.5;", + " gradLocalZMax = fallbackHeight * 0.5;", + " } else {", + " gradLocalZMin = -gradHeight * 0.5;", + " gradLocalZMax = gradHeight * 0.5;", + " }", + " } else {", + " gradLocalZMin = -gradHeight * 0.5;", + " gradLocalZMax = gradHeight * 0.5;", + " }", + " }", + " ", + " // Auto polyScale estimates bend response from bounds + mesh complexity.", + " // It exists so very different assets can share one behavior preset while still", + " // landing on a usable sway intensity when the user opts into automatic mode.", + " // Auto polyScale runs only when the behavior sets polyScale = 0.", + " if (polyScaleAutoMode) {", + " var autoPolyKey = sharedKey + \"::AUTO_POLY::\" + (selection.pickedId || \"\") + \"::\" + swayType + \"::C3\";", + " /** @type {FoliageAutoPolyScaleEntryBridge|null|undefined} */", + " var autoCached = objectTypeCache.getAutoPolyScale(autoPolyKey);", + " var autoFromCache = !!(autoCached && isFinite(autoCached.polyScale) && isFinite(autoCached.polyScaleRaw));", + " var sizeX = (autoBoundsMin && autoBoundsMax) ? (autoBoundsMax.x - autoBoundsMin.x) : NaN;", + " var sizeY = (autoBoundsMin && autoBoundsMax) ? (autoBoundsMax.y - autoBoundsMin.y) : NaN;", + " var sizeZ = (autoBoundsMin && autoBoundsMax) ? (autoBoundsMax.z - autoBoundsMin.z) : NaN;", + " var maxSize = Math.max(sizeX, sizeY, sizeZ);", + " if (!isFinite(maxSize) || maxSize <= 0) maxSize = 1.0;", + " ", + " var sizeFactor = 1.0;", + " var complexityFactor = 1.0;", + " var vertexFactor = 1.0;", + " var triFactor = 1.0;", + " var structureFactor = 1.0;", + " var planeFactor = 1.0;", + " var responseGain = 1.0;", + " var baseByType = 0.65;", + " var planeLikeRatio = slotCountForScale > 0 ? (planeLikeCountForScale / slotCountForScale) : 0.0;", + " if (!isFinite(planeLikeRatio) || planeLikeRatio < 0) planeLikeRatio = 0.0;", + " if (planeLikeRatio > 1.0) planeLikeRatio = 1.0;", + " ", + " if (autoFromCache) {", + " polyScale = autoCached.polyScale;", + " polyScaleRaw = autoCached.polyScaleRaw;", + " sizeFactor = isFinite(autoCached.sizeFactor) ? autoCached.sizeFactor : sizeFactor;", + " complexityFactor = isFinite(autoCached.complexityFactor) ? autoCached.complexityFactor : complexityFactor;", + " vertexFactor = isFinite(autoCached.vertexFactor) ? autoCached.vertexFactor : vertexFactor;", + " triFactor = isFinite(autoCached.triFactor) ? autoCached.triFactor : triFactor;", + " structureFactor = isFinite(autoCached.structureFactor) ? autoCached.structureFactor : structureFactor;", + " planeFactor = isFinite(autoCached.planeFactor) ? autoCached.planeFactor : planeFactor;", + " responseGain = isFinite(autoCached.responseGain) ? autoCached.responseGain : responseGain;", + " planeLikeRatio = isFinite(autoCached.planeLikeRatio) ? autoCached.planeLikeRatio : planeLikeRatio;", + " baseByType = isFinite(autoCached.baseByType) ? autoCached.baseByType : baseByType;", + " } else {", + " var sizeRef = 160.0;", + " sizeFactor = Math.log(maxSize + 1.0) / Math.log(sizeRef + 1.0);", + " sizeFactor = Math.min(Math.max(sizeFactor, 0.55), 1.6);", + " ", + " if (totalVerts > 0) {", + " var vertRef = 1200.0;", + " vertexFactor = Math.log(totalVerts + 1.0) / Math.log(vertRef + 1.0);", + " vertexFactor = Math.min(Math.max(vertexFactor, 0.65), 1.8);", + " }", + " ", + " if (totalTris > 0) {", + " var triRef = 800.0;", + " triFactor = Math.log(totalTris + 1.0) / Math.log(triRef + 1.0);", + " triFactor = Math.min(Math.max(triFactor, 0.65), 1.8);", + " }", + " ", + " var structureCount = Math.max(meshCountForScale, slotCountForScale);", + " if (structureCount > 0) {", + " var structureRef = 6.0;", + " structureFactor = Math.log(structureCount + 1.0) / Math.log(structureRef + 1.0);", + " structureFactor = Math.min(Math.max(structureFactor, 0.75), 1.65);", + " }", + " ", + " if (swayType === \"grassSway\" || swayType === \"bushSway\") {", + " // Keep plane-like assets slightly simpler in complexity term; main scaling is in responseGain.", + " planeFactor = 1.0 - Math.min(0.15, planeLikeRatio * 0.15);", + " }", + " ", + " complexityFactor = vertexFactor * 0.50 + triFactor * 0.25 + structureFactor * 0.25;", + " complexityFactor *= planeFactor;", + " complexityFactor = Math.min(Math.max(complexityFactor, 0.70), 2.0);", + " ", + " if (swayType === \"grassSway\") baseByType = 0.82;", + " else if (swayType === \"bushSway\") baseByType = 0.45;", + " else if (swayType === \"leavesSway\") baseByType = 0.70;", + " else if (swayType === \"treeTrunkSway\") baseByType = 0.30;", + " ", + " var autoScale = baseByType * sizeFactor * complexityFactor;", + " var nonPlaneGain = 4.0;", + " var fullPlaneGain = 0.10;", + " if (swayType === \"treeTrunkSway\") {", + " nonPlaneGain = 2.5;", + " fullPlaneGain = 0.35;", + " }", + " responseGain = nonPlaneGain * (1.0 - planeLikeRatio) + fullPlaneGain * planeLikeRatio;", + " autoScale *= responseGain;", + " var minAutoScale = 0.2;", + " if (swayType === \"grassSway\") minAutoScale = 0.32;", + " else if (swayType === \"bushSway\") minAutoScale = 0.28;", + " // Reduce floor for plane-like assets so low-poly planes can stay very light.", + " minAutoScale *= Math.max(0.0, 1.0 - planeLikeRatio);", + " autoScale = Math.min(Math.max(autoScale, minAutoScale), 8.0);", + " polyScale = autoScale;", + " polyScaleRaw = Math.round(autoScale * 50.0);", + " ", + " objectTypeCache.setAutoPolyScale(autoPolyKey, {", + " polyScale: polyScale,", + " polyScaleRaw: polyScaleRaw,", + " sizeFactor: sizeFactor,", + " complexityFactor: complexityFactor,", + " vertexFactor: vertexFactor,", + " triFactor: triFactor,", + " structureFactor: structureFactor,", + " planeFactor: planeFactor,", + " responseGain: responseGain,", + " planeLikeRatio: planeLikeRatio,", + " baseByType: baseByType,", + " boundsMode: useLocalBoundsForAuto ? \"local\" : \"world\"", + " });", + " }", + " ", + " // Debug output for auto polyScale calculation", + " var autoDebugKey = autoPolyKey + \"::debug\";", + " if (debugOutput && !objectTypeCache.hasDebugKey(autoDebugKey)) {", + " objectTypeCache.markDebugKey(autoDebugKey);", + " console.log(\"[\" + sharedKey + \"] Auto polyScale (\" + (autoFromCache ? \"cache\" : \"computed\") + \"): \" + polyScale.toFixed(3) + \" (raw: \" + polyScaleRaw + \")\");", + " // console.log(\" Sway Type: \" + swayType);", + " // console.log(\" Vertices: \" + totalVerts);", + " // console.log(\" Triangles: \" + totalTris);", + " // console.log(\" Meshes: \" + meshCountForScale + \", Slots: \" + slotCountForScale + \", Plane-like: \" + planeLikeCountForScale + \" (\" + (planeLikeRatio * 100.0).toFixed(1) + \"%)\");", + " // console.log(\" Bounds: \" + (useLocalBoundsForAuto ? \"local\" : \"world\"));", + " // console.log(\" Size: \" + maxSize.toFixed(2) + \" (X:\" + sizeX.toFixed(2) + \" Y:\" + sizeY.toFixed(2) + \" Z:\" + sizeZ.toFixed(2) + \")\");", + " // console.log(\" Factors: base=\" + baseByType.toFixed(2) + \", size=\" + sizeFactor.toFixed(2) + \", complexity=\" + complexityFactor.toFixed(2) + \" (v=\" + vertexFactor.toFixed(2) + \", t=\" + triFactor.toFixed(2) + \", s=\" + structureFactor.toFixed(2) + \", p=\" + planeFactor.toFixed(2) + \"), gain=\" + responseGain.toFixed(2));", + " // console.log(\" Calculated polyScale: \" + polyScale.toFixed(3) + \" (raw: \" + polyScaleRaw + \")\");", + " }", + " }", + " ", + " var gradLocalZMinKey = Math.round(gradLocalZMin * 1000);", + " var gradLocalZMaxKey = Math.round(gradLocalZMax * 1000);", + " var sourceRenderSide = (selection && selection.srcRef && typeof selection.srcRef.side === \"number\")", + " ? selection.srcRef.side", + " : THREE.FrontSide;", + " /** @type {THREE.Side} */", + " var resolvedRenderSide = resolveRenderSide(sourceRenderSide, cullingMode);", + " var sideSuffix = buildSideSuffix(cullingModeCode, resolvedRenderSide);", + " ", + " // pipelineKey: ONLY shader-code and GL-state variants, NOT uniform-only params", + " // Removed: PS (polyScale), GS/GE (gradStart/End), GH (gradHeight), ZMIN/ZMAX (gradLocalZ)", + " var modeKey =", + " (uniformSway ? \"U\" : \"C\") +", + " \"_\" + swayType +", + " \"_\" + (useColorGrading ? \"GRAD\" : \"TEX\") +", + " \"_IU\" + (ignoreUV ? \"1\" : \"0\") +", + " \"_PA\" + (polyScaleAutoMode ? \"1\" : \"0\") +", + " \"_FD\" + (distanceFadeEnabled ? \"1\" : \"0\") +", + " \"_TSL\" + (twoSidedLighting ? \"1\" : \"0\") +", + " pbrSuffix +", + " sideSuffix;", + " ", + " var key = sanitizeKeyPart(sharedKey) + \"::\" + sanitizeKeyPart(selection.pickedId) + \"::\" + modeKey;", + " ", + " /**", + " * Build the beauty-pass vertex transform body for the current foliage mode.", + " * @param {FoliageBehaviorPropsBridge} cfg", + " * @returns {string}", + " */", + " function buildVertexBody(cfg) {", + " var header = `", + " vec3 transformed = vec3(position);", + " ", + " // Respect instancing: per-instance transform must be included.", + " mat4 m = modelMatrix;", + " #ifdef USE_INSTANCING", + " m = modelMatrix * instanceMatrix;", + " #endif", + " ", + " vec3 worldPos = (m * vec4(transformed, 1.0)).xyz;", + " // Use the XY wind-basis uniforms so the common sway path avoids inverse(m).", + " ", + " // UV-based gradient path.", + " float uvYClamped = clamp(uv.y, 0.0, 1.0);", + " float gRawUV = clamp(1.0 - uvYClamped, 0.0, 1.0);", + " ", + " // World-space Z gradient (relative to instance origin)", + " // GDevelop uses Z-up coordinate system (Z is vertical, Y is depth)", + " // This ensures gradient ALWAYS goes up-down regardless of model orientation", + " // Calculate instance origin (world position of mesh's local origin point)", + " // Optimization: use translation directly from matrix column 3 instead of matrix multiplication", + " vec3 instanceOrigin = m[3].xyz;", + " ", + " // Relative height: how far is this vertex above/below the instance origin in world Z", + " float relativeWorldZ = worldPos.z - instanceOrigin.z;", + " ", + " // For UV mode, use local Z (mesh local space)", + " // uGradLocalZMin/Max contain: for ignoreUV - relative Z range, for UV mode - local Z range", + " float h = (uIgnoreUV > 0.5) ? relativeWorldZ : position.z;", + " float localN = (h - uGradLocalZMin) / max(uGradLocalZMax - uGradLocalZMin, 0.0001);", + " localN = clamp(localN, 0.0, 1.0);", + " float gRawW = (uIgnoreUV > 0.5) ? localN : (1.0 - localN);", + " ", + " // Select source for COLOR gradient only", + " float gRaw = (uIgnoreUV > 0.5) ? gRawW : gRawUV;", + " ", + " // grad remap [gradStart..gradEnd]", + " // Both UV and world-space now use the same direction (0.0=bottom, 1.0=top)", + " float g = smoothstep(uGradStart, uGradEnd, gRaw);", + " ", + " vGrad = g;", + " // Keep sway tip mask UV-based even when color gradient uses world-space mode.", + " // This ensures sway follows the material/UV mapping, not world-space position", + " float tip = pow(gRawUV, 3.0);", + " ", + " float phase = worldPos.x * 0.18 + worldPos.y * 0.18;", + " `;", + " ", + " var nodePhaseBlock = `", + " float nodePhase = 0.0;", + " `;", + " if (!cfg.uniformSway) {", + " nodePhaseBlock = `", + " vec3 t = vec3(m[3].x, m[3].y, m[3].z);", + " float nodeSeed = fract(sin(dot(t, vec3(12.9898, 78.233, 37.719))) * 43758.5453);", + " float nodePhase = nodeSeed * 6.2831853;", + " `;", + " }", + " ", + " var dirSetup = `", + " // World XY wind basis for gust sampling.", + " vec2 dir = normalize(uWindDir);", + " vec2 perp = vec2(-dir.y, dir.x);", + " // Secondary XY wind basis reused for direct vertex offsets.", + " vec2 localDir = normalize(uLocalWindDir);", + " vec2 localPerp = normalize(uLocalWindPerp);", + " `;", + " ", + " var gustBlock = `", + " // Compile-time gust branch strips gust code when disabled.", + " #ifdef USE_GUST", + " float gustMask = 0.0;", + " float gustPush = 0.0;", + " float gustMul = 1.0;", + " float gustAmp = 0.0;", + " ", + " if (uGustEnabled > 0.5 && uGustStrength > 0.0) {", + " vec2 p = worldPos.xy;", + " ", + " // Gust wave travels along wind direction", + " // gustSamplePos = distance along wind direction (U coordinate)", + " float gustSamplePos = dot(p, dir);", + " ", + " // Wave phase: position along wind minus time (wave moves with wind)", + " float wavePhase = gustSamplePos * uGustScale - uTime * uGustSpeed;", + " ", + " // Perpendicular position for V coordinate (to see wavy line variation)", + " float gustPerpPos = dot(p, perp);", + " ", + " // Use both U and V coordinates to sample the wavy texture", + " vec2 gustUV = vec2(fract(wavePhase), fract(gustPerpPos * uGustScale));", + " ", + " float n = texture2D(uGustTex, gustUV).r;", + " float m = smoothstep(uGustThreshold, 1.0, n);", + " m = pow(m, max(uGustContrast, 0.0001));", + " ", + " gustMask = m;", + " gustAmp = m * uGustStrength;", + " ", + " // Mild amplification of existing sway (uniform, no polyScale)", + " gustMul = 1.0 + gustAmp * 0.35;", + " ", + " // Small additional push in wind direction (uniform, no polyScale)", + " gustPush = gustAmp * uWindStrength * pow(tip, 1.3) * 0.6;", + " }", + " #else", + " // Default values when gust is disabled.", + " float gustMask = 0.0;", + " float gustPush = 0.0;", + " float gustMul = 1.0;", + " float gustAmp = 0.0;", + " #endif", + " `;", + " ", + " var baseSway = `", + " float wave = sin(uTime * uWindSpeed + phase + uPhase + nodePhase);", + " float flutterBase = sin(uTime * (uWindSpeed * 2.6) + phase * 4.1 + uPhase + nodePhase) * 0.20;", + " float offset = (wave + flutterBase) * uWindStrength * tip;", + " // Gust gently scales base sway when enabled.", + " #ifdef USE_GUST", + " offset *= gustMul;", + " #endif", + " ", + " float side = sin(uTime * (uWindSpeed * 1.7) + phase * 2.2 + uPhase + nodePhase) * 0.30;", + " ", + " // Apply movement in local space directly.", + " vec2 localMove = (localDir * offset + localPerp * offset * side) * (uPolyScale);", + " transformed.x += localMove.x;", + " transformed.y += localMove.y;", + " ", + " // Transform only the extra gust push from world wind into instance space.", + " #ifdef USE_GUST", + " // Use inverse(m) only for gust push so the base sway path stays cheap.", + " vec3 worldGustMove = vec3(dir.x * gustPush, dir.y * gustPush, 0.0);", + " mat4 invM = inverse(m);", + " vec3 localGustMove = (invM * vec4(worldGustMove, 0.0)).xyz;", + " transformed.x += localGustMove.x;", + " transformed.y += localGustMove.y;", + " #endif", + " ", + " // Height scaling for bending effect (base + tip-based for future-proof behavior)", + " // Complex models (higher polyScale) bend more, simple planes bend less", + " // Add extra vertical bend during gust peaks.", + " #ifdef USE_GUST", + " float bendFactor = clamp(uPolyScale * 0.4, 0.2, 1.2);", + " // Base bend - minimum for all vertices (ensures middle of grass also bends)", + " // uBendMultiplier reduces vertical bend: bushes (0.1), leaves (0.05), grass (1.0)", + " float baseBend = gustAmp * bendFactor * 0.15 * uBendMultiplier;", + " // Tip-based bend - additional effect at tips (less aggressive than before)", + " float tipBend = pow(tip, 1.5);", + " float tipBendAmount = gustAmp * bendFactor * tipBend * 0.35 * uBendMultiplier;", + " // Combination: base bend ensures all vertices bend, tip adds extra at top", + " float bend = clamp(baseBend + tipBendAmount, 0.0, 0.5);", + " transformed.z *= (1.0 - bend);", + " #endif", + " `;", + " ", + " var leavesFlutter = `", + " if (uFlutterStrength > 0.0) {", + " float leafMask = pow(vGrad, 2.0);", + " float fPhase = (worldPos.x + worldPos.y) * 0.35;", + " // Flutter speed is now tied to wind speed (flutter is faster than base sway)", + " float flutterSpeed = uWindSpeed * 4.0;", + " float f1 = sin(uTime * flutterSpeed + fPhase + uPhase * 1.7 + nodePhase);", + " float f2 = sin(uTime * (flutterSpeed * 1.9) + fPhase * 2.3 + uPhase * 0.9 + nodePhase);", + " float flutter = (f1 * 0.65 + f2 * 0.35) * uFlutterStrength * leafMask;", + " ", + " // Reuse the same XY wind basis in the gust-enabled branch.", + " #ifdef USE_GUST", + " vec2 localFlutterMove = localPerp * flutter * gustMul;", + " #else", + " vec2 localFlutterMove = localPerp * flutter;", + " #endif", + " transformed.x += localFlutterMove.x;", + " transformed.y += localFlutterMove.y;", + " }", + " `;", + " ", + " var bushSway = `", + " float radial = length(position.xz);", + " float edgeMask = smoothstep(0.15, 0.65, radial);", + " float bushMask = edgeMask * tip;", + " ", + " float bwave = sin(uTime * (uWindSpeed * 0.8) + phase * 0.6 + uPhase + nodePhase);", + " // Scale bush sway by gust multiplier when enabled.", + " #ifdef USE_GUST", + " float boff = bwave * (uWindStrength * 0.6) * bushMask * (uPolyScale * gustMul);", + " #else", + " float boff = bwave * (uWindStrength * 0.6) * bushMask * uPolyScale;", + " #endif", + " ", + " // Apply the XY wind basis directly in vertex space.", + " vec2 localBushMove = localDir * boff;", + " transformed.x += localBushMove.x;", + " transformed.y += localBushMove.y;", + " `;", + " ", + " var treeTrunkSway = `", + " float h2 = tip;", + " ", + " // Use only world-space phase for tree trunk (no uPhase/nodePhase variation)", + " // This makes all meshes of the same tree move together as one unit", + " float trunkPhase = worldPos.x * 0.18 + worldPos.y * 0.18;", + " ", + " float t1 = sin(uTime * (uWindSpeed * 0.40) + trunkPhase * 0.30);", + " float t2 = sin(uTime * (uWindSpeed * 0.60) + trunkPhase * 0.45) * 0.30;", + " ", + " // Scale trunk bend and twist by gust multiplier when enabled.", + " #ifdef USE_GUST", + " float bend = (t1 + t2) * (uWindStrength * 0.40) * h2 * uPolyScale * gustMul;", + " float twist = sin(uTime * (uWindSpeed * 0.50) + trunkPhase * 0.25) * (uWindStrength * 0.18) * h2 * uPolyScale * gustMul;", + " // Add gust push for stronger gusts (smaller than grass - trunk is stiffer)", + " // Gust push is defined in world wind direction, then transformed to instance-local space.", + " vec3 worldGustTrunkPush = vec3(dir.x * gustPush * 0.3, dir.y * gustPush * 0.3, 0.0);", + " mat4 invM = inverse(m);", + " vec3 localGustTrunkPush = (invM * vec4(worldGustTrunkPush, 0.0)).xyz;", + " // Apply the XY wind basis directly in vertex space.", + " vec2 localTrunkMove = localDir * bend + localPerp * (bend * 0.25 + twist) + vec2(localGustTrunkPush.x, localGustTrunkPush.y);", + " #else", + " float bend = (t1 + t2) * (uWindStrength * 0.40) * h2 * uPolyScale;", + " float twist = sin(uTime * (uWindSpeed * 0.50) + trunkPhase * 0.25) * (uWindStrength * 0.18) * h2 * uPolyScale;", + " // Apply the XY wind basis directly in vertex space.", + " vec2 localTrunkMove = localDir * bend + localPerp * (bend * 0.25 + twist);", + " #endif", + " transformed.x += localTrunkMove.x;", + " transformed.y += localTrunkMove.y;", + " `;", + " ", + " var body = header + nodePhaseBlock + dirSetup + gustBlock;", + " ", + " if (cfg.swayType === \"bushSway\") {", + " body += baseSway + bushSway;", + " } else if (cfg.swayType === \"leavesSway\") {", + " body += baseSway + leavesFlutter;", + " } else if (cfg.swayType === \"treeTrunkSway\") {", + " body += treeTrunkSway;", + " } else {", + " body += baseSway;", + " }", + " ", + " // Distance fade: GPU smoothing via mix(prev, current, interpT)", + " body += `", + " #ifdef USE_INSTANCING", + " vFade = mix(aFadePrev, aFade, uFadeInterpT);", + " #else", + " vFade = 1.0;", + " #endif", + " `;", + " ", + " return body;", + " }", + " ", + " function buildFadeOnlyFragment() {", + " return `", + " #include ", + " // Distance fade (dither dissolve) with the same pattern as the main foliage pass.", + " if (vFade < 0.01) discard;", + " vec2 screenPos = gl_FragCoord.xy;", + " float noise = fract(52.9829189 * fract(dot(screenPos, vec2(0.06711056, 0.00583715))));", + " if (vFade < 0.999 && noise > vFade) discard;", + " `;", + " }", + " ", + " function buildFadeOnlyFragmentNoFade() {", + " return `", + " #include ", + " `;", + " }", + "", + " // Shadow pass uses the same IGN dither so caster fade tracks the beauty fade pattern.", + " function buildShadowFadeFragment() {", + " return `", + " #include ", + " if (vShadowFade < 0.01) discard;", + " vec2 screenPos = gl_FragCoord.xy;", + " float noise = fract(52.9829189 * fract(dot(screenPos, vec2(0.06711056, 0.00583715))));", + " if (vShadowFade < 0.999 && noise > vShadowFade) discard;", + " `;", + " }", + " ", + " function buildColorFragment() {", + " return `", + " #include ", + " ", + " // Distance fade: Dither dissolve (screen-door effect) - check early to save color processing", + " // vFade: 1.0 = fully visible, 0.0 = fully invisible", + " ", + " // Early-out: if completely invisible, discard immediately (saves all color processing)", + " if (vFade < 0.01) discard;", + " ", + " // Interleaved Gradient Noise (IGN) - blue noise-like quality without texture", + " // Used in AAA games (Activision, etc.) - much smoother than white noise hash", + " vec2 screenPos = gl_FragCoord.xy;", + " float noise = fract(52.9829189 * fract(dot(screenPos, vec2(0.06711056, 0.00583715))));", + " ", + " // Dither threshold: discard pixel if noise > fade", + " // This creates gradual dissolve without alpha blending/sorting issues", + " if (vFade < 0.999 && noise > vFade) discard;", + " ", + " // Color processing (only executed if pixel passed dither check)", + " vec3 baseCol = diffuseColor.rgb;", + " if (uUseGrad > 0.5) {", + " baseCol = mix(uBottomColor, uTopColor, clamp(vGrad, 0.0, 1.0));", + " }", + " ", + " float luma = dot(baseCol, vec3(0.2126, 0.7152, 0.0722));", + " baseCol = mix(vec3(luma), baseCol, uSat);", + " baseCol = (baseCol - 0.5) * uContrast + 0.5;", + " baseCol = clamp(baseCol, 0.0, 1.0);", + " ", + " diffuseColor.rgb = baseCol;", + " `;", + " }", + " ", + " function buildColorFragmentNoFade() {", + " return `", + " #include ", + " ", + " vec3 baseCol = diffuseColor.rgb;", + " if (uUseGrad > 0.5) {", + " baseCol = mix(uBottomColor, uTopColor, clamp(vGrad, 0.0, 1.0));", + " }", + " ", + " float luma = dot(baseCol, vec3(0.2126, 0.7152, 0.0722));", + " baseCol = mix(vec3(luma), baseCol, uSat);", + " baseCol = (baseCol - 0.5) * uContrast + 0.5;", + " baseCol = clamp(baseCol, 0.0, 1.0);", + " ", + " diffuseColor.rgb = baseCol;", + " `;", + " }", + " ", + " /**", + " * Build the shadow-pass vertex transform body for the current foliage mode.", + " * @param {FoliageBehaviorPropsBridge} cfg", + " * @returns {string}", + " */", + " function buildShadowVertexBody(cfg) {", + " var header = `", + " vec3 transformed = vec3(position);", + " ", + " mat4 m = modelMatrix;", + " #ifdef USE_INSTANCING", + " m = modelMatrix * instanceMatrix;", + " #endif", + " ", + " vec3 worldPos = (m * vec4(transformed, 1.0)).xyz;", + " ", + " float uvYClamped = clamp(uv.y, 0.0, 1.0);", + " float gRawUV = clamp(1.0 - uvYClamped, 0.0, 1.0);", + " ", + " vec3 instanceOrigin = m[3].xyz;", + " float relativeWorldZ = worldPos.z - instanceOrigin.z;", + " float h = (uIgnoreUV > 0.5) ? relativeWorldZ : position.z;", + " float localN = (h - uGradLocalZMin) / max(uGradLocalZMax - uGradLocalZMin, 0.0001);", + " localN = clamp(localN, 0.0, 1.0);", + " float gRawW = (uIgnoreUV > 0.5) ? localN : (1.0 - localN);", + " float gRaw = (uIgnoreUV > 0.5) ? gRawW : gRawUV;", + " float swayGrad = smoothstep(uGradStart, uGradEnd, gRaw);", + " float tip = pow(gRawUV, 3.0);", + " ", + " float phase = worldPos.x * 0.18 + worldPos.y * 0.18;", + " `;", + " ", + " var nodePhaseBlock = `", + " float nodePhase = 0.0;", + " `;", + " if (!cfg.uniformSway) {", + " nodePhaseBlock = `", + " vec3 t = vec3(m[3].x, m[3].y, m[3].z);", + " float nodeSeed = fract(sin(dot(t, vec3(12.9898, 78.233, 37.719))) * 43758.5453);", + " float nodePhase = nodeSeed * 6.2831853;", + " `;", + " }", + " ", + " var dirSetup = `", + " vec2 dir = normalize(uWindDir);", + " vec2 perp = vec2(-dir.y, dir.x);", + " vec2 localDir = normalize(uLocalWindDir);", + " vec2 localPerp = normalize(uLocalWindPerp);", + " `;", + " ", + " var gustBlock = `", + " #ifdef USE_GUST", + " float gustMask = 0.0;", + " float gustPush = 0.0;", + " float gustMul = 1.0;", + " float gustAmp = 0.0;", + " ", + " if (uGustEnabled > 0.5 && uGustStrength > 0.0) {", + " vec2 p = worldPos.xy;", + " float gustSamplePos = dot(p, dir);", + " float wavePhase = gustSamplePos * uGustScale - uTime * uGustSpeed;", + " float gustPerpPos = dot(p, perp);", + " vec2 gustUV = vec2(fract(wavePhase), fract(gustPerpPos * uGustScale));", + " float n = texture2D(uGustTex, gustUV).r;", + " float msk = smoothstep(uGustThreshold, 1.0, n);", + " msk = pow(msk, max(uGustContrast, 0.0001));", + " gustMask = msk;", + " gustAmp = msk * uGustStrength;", + " gustMul = 1.0 + gustAmp * 0.35;", + " gustPush = gustAmp * uWindStrength * pow(tip, 1.3) * 0.6;", + " }", + " #else", + " float gustMask = 0.0;", + " float gustPush = 0.0;", + " float gustMul = 1.0;", + " float gustAmp = 0.0;", + " #endif", + " `;", + " ", + " var baseSway = `", + " float wave = sin(uTime * uWindSpeed + phase + uPhase + nodePhase);", + " float flutterBase = sin(uTime * (uWindSpeed * 2.6) + phase * 4.1 + uPhase + nodePhase) * 0.20;", + " float offset = (wave + flutterBase) * uWindStrength * tip;", + " #ifdef USE_GUST", + " offset *= gustMul;", + " #endif", + " ", + " float side = sin(uTime * (uWindSpeed * 1.7) + phase * 2.2 + uPhase + nodePhase) * 0.30;", + " vec2 localMove = (localDir * offset + localPerp * offset * side) * (uPolyScale);", + " transformed.x += localMove.x;", + " transformed.y += localMove.y;", + " ", + " #ifdef USE_GUST", + " vec3 worldGustMove = vec3(dir.x * gustPush, dir.y * gustPush, 0.0);", + " mat4 invM = inverse(m);", + " vec3 localGustMove = (invM * vec4(worldGustMove, 0.0)).xyz;", + " transformed.x += localGustMove.x;", + " transformed.y += localGustMove.y;", + " #endif", + " ", + " #ifdef USE_GUST", + " float bendFactor = clamp(uPolyScale * 0.4, 0.2, 1.2);", + " float baseBend = gustAmp * bendFactor * 0.15 * uBendMultiplier;", + " float tipBend = pow(tip, 1.5);", + " float tipBendAmount = gustAmp * bendFactor * tipBend * 0.35 * uBendMultiplier;", + " float bend = clamp(baseBend + tipBendAmount, 0.0, 0.5);", + " transformed.z *= (1.0 - bend);", + " #endif", + " `;", + " ", + " var leavesFlutter = `", + " if (uFlutterStrength > 0.0) {", + " float leafMask = pow(swayGrad, 2.0);", + " float fPhase = (worldPos.x + worldPos.y) * 0.35;", + " float flutterSpeed = uWindSpeed * 4.0;", + " float f1 = sin(uTime * flutterSpeed + fPhase + uPhase * 1.7 + nodePhase);", + " float f2 = sin(uTime * (flutterSpeed * 1.9) + fPhase * 2.3 + uPhase * 0.9 + nodePhase);", + " float flutter = (f1 * 0.65 + f2 * 0.35) * uFlutterStrength * leafMask;", + " #ifdef USE_GUST", + " vec2 localFlutterMove = localPerp * flutter * gustMul;", + " #else", + " vec2 localFlutterMove = localPerp * flutter;", + " #endif", + " transformed.x += localFlutterMove.x;", + " transformed.y += localFlutterMove.y;", + " }", + " `;", + " ", + " var bushSway = `", + " float radial = length(position.xz);", + " float edgeMask = smoothstep(0.15, 0.65, radial);", + " float bushMask = edgeMask * tip;", + " float bwave = sin(uTime * (uWindSpeed * 0.8) + phase * 0.6 + uPhase + nodePhase);", + " #ifdef USE_GUST", + " float boff = bwave * (uWindStrength * 0.6) * bushMask * (uPolyScale * gustMul);", + " #else", + " float boff = bwave * (uWindStrength * 0.6) * bushMask * uPolyScale;", + " #endif", + " vec2 localBushMove = localDir * boff;", + " transformed.x += localBushMove.x;", + " transformed.y += localBushMove.y;", + " `;", + " ", + " var treeTrunkSway = `", + " float h2 = tip;", + " float trunkPhase = worldPos.x * 0.18 + worldPos.y * 0.18;", + " float t1 = sin(uTime * (uWindSpeed * 0.40) + trunkPhase * 0.30);", + " float t2 = sin(uTime * (uWindSpeed * 0.60) + trunkPhase * 0.45) * 0.30;", + " #ifdef USE_GUST", + " float bend = (t1 + t2) * (uWindStrength * 0.40) * h2 * uPolyScale * gustMul;", + " float twist = sin(uTime * (uWindSpeed * 0.50) + trunkPhase * 0.25) * (uWindStrength * 0.18) * h2 * uPolyScale * gustMul;", + " vec3 worldGustTrunkPush = vec3(dir.x * gustPush * 0.3, dir.y * gustPush * 0.3, 0.0);", + " mat4 invM = inverse(m);", + " vec3 localGustTrunkPush = (invM * vec4(worldGustTrunkPush, 0.0)).xyz;", + " vec2 localTrunkMove = localDir * bend + localPerp * (bend * 0.25 + twist) + vec2(localGustTrunkPush.x, localGustTrunkPush.y);", + " #else", + " float bend = (t1 + t2) * (uWindStrength * 0.40) * h2 * uPolyScale;", + " float twist = sin(uTime * (uWindSpeed * 0.50) + trunkPhase * 0.25) * (uWindStrength * 0.18) * h2 * uPolyScale;", + " vec2 localTrunkMove = localDir * bend + localPerp * (bend * 0.25 + twist);", + " #endif", + " transformed.x += localTrunkMove.x;", + " transformed.y += localTrunkMove.y;", + " `;", + " ", + " var body = header + nodePhaseBlock + dirSetup + gustBlock;", + " if (cfg.swayType === \"bushSway\") {", + " body += baseSway + bushSway;", + " } else if (cfg.swayType === \"leavesSway\") {", + " body += baseSway + leavesFlutter;", + " } else if (cfg.swayType === \"treeTrunkSway\") {", + " body += treeTrunkSway;", + " } else {", + " body += baseSway;", + " }", + " return body;", + " }", + " ", + " /**", + " * Patch a shadow material so it mirrors the foliage sway/gust/fade deformation path.", + " * @param {THREE.Material|null|undefined} shadowMat", + " * @param {FoliageBehaviorPropsBridge} cfgSource", + " * @returns {void}", + " */", + " function patchShadowMaterialIfNeeded(shadowMat, cfgSource) {", + " if (!shadowMat) return;", + " shadowMat.userData = shadowMat.userData || {};", + " shadowMat.userData.foliageConfig = cfgSource;", + " if (shadowMat.userData.__foliageShadowPatched) return;", + " ", + " shadowMat.userData.__foliageShadowPatched = true;", + " var originalOnBeforeCompile = shadowMat.onBeforeCompile;", + " shadowMat.onBeforeCompile = function(shader) {", + " if (originalOnBeforeCompile) originalOnBeforeCompile.call(this, shader);", + "", + " var cfgShadow = this.userData && this.userData.foliageConfig ? this.userData.foliageConfig : cfgSource;", + " if (!cfgShadow) return;", + " if (!cfgShadow.swayType) {", + " this.userData.foliageUniforms = shader.uniforms;", + " return;", + " }", + " var useShadowFade = !!cfgShadow.distanceFadeEnabled;", + " if (!shader.defines) shader.defines = {};", + " if (cache.gustEnabled) shader.defines.USE_GUST = \"\";", + " else delete shader.defines.USE_GUST;", + "", + " var pendingShadowFade = (this.userData && typeof this.userData._pendingFade === \"number\")", + " ? this.userData._pendingFade", + " : 1.0;", + " shader.uniforms.uTime = { value: 0.0 };", + " shader.uniforms.uWindStrength = { value: 0.5 };", + " shader.uniforms.uWindSpeed = { value: 1.0 };", + " shader.uniforms.uWindDir = { value: new THREE.Vector2(1, 0) };", + " shader.uniforms.uLocalWindDir = { value: new THREE.Vector2(1, 0) };", + " shader.uniforms.uLocalWindPerp = { value: new THREE.Vector2(0, 1) };", + " shader.uniforms.uPhase = { value: cfgShadow.phase || 0.0 };", + " shader.uniforms.uPolyScale = { value: cfgShadow.polyScale || 1.0 };", + " shader.uniforms.uFlutterStrength = { value: cfgShadow.swayType === \"leavesSway\" && cfgShadow.polyScale >= 0.6 ? 0.4 * cfgShadow.polyScale : 0.0 };", + " var bendMultiplierShadow = 1.0;", + " if (cfgShadow.swayType === \"bushSway\") bendMultiplierShadow = 0.1;", + " else if (cfgShadow.swayType === \"leavesSway\") bendMultiplierShadow = 0.05;", + " shader.uniforms.uBendMultiplier = { value: bendMultiplierShadow };", + " shader.uniforms.uGradStart = { value: isFinite(cfgShadow.gradStart) ? cfgShadow.gradStart : 0.0 };", + " shader.uniforms.uGradEnd = { value: isFinite(cfgShadow.gradEnd) ? cfgShadow.gradEnd : 1.0 };", + " shader.uniforms.uIgnoreUV = { value: cfgShadow.ignoreUV ? 1.0 : 0.0 };", + " shader.uniforms.uGradLocalZMin = { value: isFinite(cfgShadow.gradLocalZMin) ? cfgShadow.gradLocalZMin : -0.5 };", + " shader.uniforms.uGradLocalZMax = { value: isFinite(cfgShadow.gradLocalZMax) ? cfgShadow.gradLocalZMax : 0.5 };", + " if (useShadowFade) {", + " shader.uniforms.uFade = { value: pendingShadowFade };", + " shader.uniforms.uFadeInterpT = { value: 1.0 };", + " }", + "", + " if (cache.gustEnabled) {", + " shader.uniforms.uGustTex = { value: cache.gustTexture || cache.gustFallbackTex };", + " shader.uniforms.uGustEnabled = { value: 1.0 };", + " shader.uniforms.uGustStrength = { value: cache.gustStrength };", + " shader.uniforms.uGustScale = { value: cache.gustScale };", + " shader.uniforms.uGustSpeed = { value: cache.gustSpeed };", + " shader.uniforms.uGustThreshold = { value: cache.gustThreshold };", + " shader.uniforms.uGustContrast = { value: cache.gustContrast };", + " }", + " ", + " this.userData.foliageUniforms = shader.uniforms;", + " ", + " var shadowFadeVertexDecl = useShadowFade ? `", + " #ifdef USE_INSTANCING", + " attribute float aFade;", + " attribute float aFadePrev;", + " #endif", + " uniform float uFade;", + " uniform float uFadeInterpT;", + " varying float vShadowFade;", + " ` : ``;", + " var commonShadowVertex = `", + " #include ", + " uniform float uTime;", + " uniform float uWindStrength;", + " uniform float uWindSpeed;", + " uniform vec2 uWindDir;", + " uniform vec2 uLocalWindDir;", + " uniform vec2 uLocalWindPerp;", + " uniform float uPhase;", + " uniform float uPolyScale;", + " uniform float uFlutterStrength;", + " uniform float uBendMultiplier;", + " uniform float uGradStart;", + " uniform float uGradEnd;", + " uniform float uIgnoreUV;", + " uniform float uGradLocalZMin;", + " uniform float uGradLocalZMax;", + " ${shadowFadeVertexDecl}", + " #ifdef USE_GUST", + " uniform sampler2D uGustTex;", + " uniform float uGustEnabled;", + " uniform float uGustStrength;", + " uniform float uGustScale;", + " uniform float uGustSpeed;", + " uniform float uGustThreshold;", + " uniform float uGustContrast;", + " #endif", + " `;", + " shader.vertexShader = shader.vertexShader.replace(\"#include \", commonShadowVertex);", + " var shadowVertexBody = buildShadowVertexBody(cfgShadow);", + " if (useShadowFade) {", + " shadowVertexBody += `", + " #ifdef USE_INSTANCING", + " vShadowFade = mix(aFadePrev, aFade, uFadeInterpT);", + " #else", + " vShadowFade = uFade;", + " #endif", + " `;", + " }", + " shader.vertexShader = shader.vertexShader.replace(\"#include \", shadowVertexBody);", + " if (useShadowFade) {", + " shader.fragmentShader = shader.fragmentShader.replace(", + " \"#include \",", + " \"#include \\nvarying float vShadowFade;\"", + " );", + " shader.fragmentShader = shader.fragmentShader.replace(", + " \"#include \",", + " buildShadowFadeFragment()", + " );", + " }", + " };", + "", + " shadowMat.needsUpdate = true;", + " }", + "", + " function applyPendingFadeToMaterial(mat, fadeValue) {", + " if (!mat || !isFinite(fadeValue)) return;", + " mat.userData = mat.userData || {};", + " mat.userData._pendingFade = fadeValue;", + " var uniforms = mat.userData.foliageUniforms;", + " if (uniforms && uniforms.uFade) uniforms.uFade.value = fadeValue;", + " }", + "", + " function propagatePendingFadeToShadowMaterials(mainMat) {", + " if (!mainMat || !mainMat.userData) return;", + " var pendingFade = (typeof mainMat.userData._pendingFade === \"number\")", + " ? mainMat.userData._pendingFade", + " : NaN;", + " if (!isFinite(pendingFade)) return;", + "", + " var shadowDepth = mainMat.userData._foliageDepthMat;", + " if (shadowDepth) {", + " shadowDepth.userData = shadowDepth.userData || {};", + " shadowDepth.userData._pendingFade = pendingFade;", + " var depthUniforms = shadowDepth.userData.foliageUniforms;", + " if (depthUniforms && depthUniforms.uFade) depthUniforms.uFade.value = pendingFade;", + " }", + "", + " var shadowDistance = mainMat.userData._foliageDistanceMat;", + " if (shadowDistance) {", + " shadowDistance.userData = shadowDistance.userData || {};", + " shadowDistance.userData._pendingFade = pendingFade;", + " var distanceUniforms = shadowDistance.userData.foliageUniforms;", + " if (distanceUniforms && distanceUniforms.uFade) distanceUniforms.uFade.value = pendingFade;", + " }", + " }", + "", + " /**", + " * Compute the first non-instanced fade value before update has had a chance to run.", + " * @param {(gdjs.RuntimeObject3D & { getX?: () => number, getY?: () => number })|null|undefined} gdObject", + " * @param {THREE.Object3D|null|undefined} threeObject", + " * @param {number} startDistance", + " * @param {number} endDistance", + " * @returns {number}", + " */", + " function computeInitialNonInstancedFade(gdObject, threeObject, startDistance, endDistance) {", + " if (!runtimeScene || !distanceFadeEnabled) return 1.0;", + "", + " var camXInit = 0;", + " var camYInit = 0;", + " var hasCamera = false;", + " try {", + " var layerInit = runtimeScene.getLayer(\"\");", + " if (layerInit && layerInit.getRenderer && layerInit.getRenderer().getThreeCamera) {", + " var camInit = layerInit.getRenderer().getThreeCamera();", + " if (camInit) {", + " camXInit = camInit.position.x;", + " camYInit = camInit.position.y;", + " hasCamera = true;", + " }", + " }", + " } catch (eInitCam) {}", + " if (!hasCamera) return 1.0;", + "", + " var objXInit = 0;", + " var objYInit = 0;", + " var gotPos = false;", + " if (threeObject && typeof threeObject.getWorldPosition === \"function\" && typeof THREE.Vector3 !== \"undefined\") {", + " try {", + " var tmpPosInit = new THREE.Vector3();", + " threeObject.getWorldPosition(tmpPosInit);", + " objXInit = tmpPosInit.x;", + " objYInit = tmpPosInit.y;", + " gotPos = true;", + " } catch (eInitPos3D) {}", + " }", + " if (!gotPos && gdObject) {", + " try {", + " objXInit = gdObject.getX ? gdObject.getX() : 0;", + " objYInit = gdObject.getY ? gdObject.getY() : 0;", + " gotPos = true;", + " } catch (eInitPos2D) {}", + " }", + " if (!gotPos) return 1.0;", + "", + " var safeStart = isFinite(startDistance) ? startDistance : 1200;", + " var safeEnd = isFinite(endDistance) ? endDistance : 1600;", + " if (safeEnd <= safeStart) safeEnd = safeStart + 100;", + "", + " var dxInit = objXInit - camXInit;", + " var dyInit = objYInit - camYInit;", + " var distSqInit = dxInit * dxInit + dyInit * dyInit;", + " var startSqInit = safeStart * safeStart;", + " var marginInit = 256.0;", + " var endWithMarginInit = safeEnd + marginInit;", + " var endWithMarginSqInit = endWithMarginInit * endWithMarginInit;", + " if (distSqInit > endWithMarginSqInit) return 0.0;", + " if (distSqInit < startSqInit) return 1.0;", + "", + " var rangeInit = safeEnd - safeStart;", + " if (rangeInit <= 0) rangeInit = 100;", + " var distInit = Math.sqrt(distSqInit);", + " var tInit = (distInit - safeStart) / rangeInit;", + " if (tInit < 0) tInit = 0;", + " else if (tInit > 1) tInit = 1;", + " return 1.0 - (tInit * tInit * (3.0 - 2.0 * tInit));", + " }", + "", + " /**", + " * Create or refresh the sway-aware shadow materials that mirror the main foliage material.", + " * @param {FoliageInspectableMaterialBridge|null|undefined} mainMat", + " * @returns {void}", + " */", + " function ensureFoliageShadowMaterials(mainMat) {", + " if (!mainMat) return;", + " mainMat.userData = mainMat.userData || {};", + " var cfgShadow = mainMat.userData.foliageConfig || null;", + " if (!cfgShadow) return;", + " ", + " var shadowDepth = mainMat.userData._foliageDepthMat;", + " if (!shadowDepth) {", + " shadowDepth = new THREE.MeshDepthMaterial({", + " depthPacking: THREE.RGBADepthPacking,", + " map: mainMat.map || null,", + " alphaMap: mainMat.alphaMap || null,", + " alphaTest: isFinite(mainMat.alphaTest) ? mainMat.alphaTest : 0,", + " side: mainMat.side", + " });", + " shadowDepth.name = (mainMat.name || \"Foliage\") + \"::ShadowDepth\";", + " mainMat.userData._foliageDepthMat = shadowDepth;", + " mainMat.userData._foliageShadowOwned = true;", + " } else {", + " shadowDepth.map = mainMat.map || null;", + " shadowDepth.alphaMap = mainMat.alphaMap || null;", + " shadowDepth.alphaTest = isFinite(mainMat.alphaTest) ? mainMat.alphaTest : 0;", + " shadowDepth.side = mainMat.side;", + " }", + " ", + " var shadowDistance = mainMat.userData._foliageDistanceMat;", + " if (!shadowDistance) {", + " shadowDistance = new THREE.MeshDistanceMaterial({", + " map: mainMat.map || null,", + " alphaMap: mainMat.alphaMap || null,", + " alphaTest: isFinite(mainMat.alphaTest) ? mainMat.alphaTest : 0", + " });", + " shadowDistance.side = mainMat.side;", + " shadowDistance.name = (mainMat.name || \"Foliage\") + \"::ShadowDistance\";", + " mainMat.userData._foliageDistanceMat = shadowDistance;", + " mainMat.userData._foliageShadowOwned = true;", + " } else {", + " shadowDistance.map = mainMat.map || null;", + " shadowDistance.alphaMap = mainMat.alphaMap || null;", + " shadowDistance.alphaTest = isFinite(mainMat.alphaTest) ? mainMat.alphaTest : 0;", + " shadowDistance.side = mainMat.side;", + " }", + "", + " patchShadowMaterialIfNeeded(shadowDepth, cfgShadow);", + " patchShadowMaterialIfNeeded(shadowDistance, cfgShadow);", + " propagatePendingFadeToShadowMaterials(mainMat);", + " }", + "", + " /**", + " * Patch a shadow material with fade-only support for non-sway paths such as bark/trunk fade meshes.", + " * @param {THREE.Material|null|undefined} shadowMat", + " * @returns {void}", + " */", + " function patchFadeOnlyShadowMaterialIfNeeded(shadowMat) {", + " if (!shadowMat) return;", + " shadowMat.userData = shadowMat.userData || {};", + " if (shadowMat.userData.__foliageFadeOnlyShadowPatched) return;", + "", + " shadowMat.userData.__foliageFadeOnlyShadowPatched = true;", + " var originalOnBeforeCompile = shadowMat.onBeforeCompile;", + " shadowMat.onBeforeCompile = function(shader) {", + " if (originalOnBeforeCompile) originalOnBeforeCompile.call(this, shader);", + "", + " var pendingFade = (this.userData && typeof this.userData._pendingFade === \"number\")", + " ? this.userData._pendingFade", + " : 1.0;", + " shader.uniforms.uFade = { value: pendingFade };", + " shader.uniforms.uFadeInterpT = { value: 1.0 };", + "", + " if (shader.vertexShader.indexOf(\"varying float vShadowFade;\") === -1) {", + " shader.vertexShader = shader.vertexShader.replace(", + " \"#include \",", + " \"#include \\n#ifdef USE_INSTANCING\\nattribute float aFade;\\nattribute float aFadePrev;\\n#endif\\nuniform float uFade;\\nuniform float uFadeInterpT;\\nvarying float vShadowFade;\"", + " );", + " } else if (shader.vertexShader.indexOf(\"uniform float uFade;\") === -1 || shader.vertexShader.indexOf(\"uniform float uFadeInterpT;\") === -1) {", + " shader.vertexShader = shader.vertexShader.replace(", + " \"varying float vShadowFade;\",", + " \"#ifdef USE_INSTANCING\\nattribute float aFade;\\nattribute float aFadePrev;\\n#endif\\nuniform float uFade;\\nuniform float uFadeInterpT;\\nvarying float vShadowFade;\"", + " );", + " }", + "", + " if (shader.vertexShader.indexOf(\"vShadowFade = mix(aFadePrev, aFade, uFadeInterpT);\") === -1 &&", + " shader.vertexShader.indexOf(\"vShadowFade = uFade;\") === -1) {", + " shader.vertexShader = shader.vertexShader.replace(", + " \"#include \",", + " \"#include \\n#ifdef USE_INSTANCING\\nvShadowFade = mix(aFadePrev, aFade, uFadeInterpT);\\n#else\\nvShadowFade = uFade;\\n#endif\"", + " );", + " }", + "", + " if (shader.fragmentShader.indexOf(\"varying float vShadowFade;\") === -1) {", + " shader.fragmentShader = shader.fragmentShader.replace(", + " \"#include \",", + " \"#include \\nvarying float vShadowFade;\"", + " );", + " }", + "", + " if (shader.fragmentShader.indexOf(\"if (vShadowFade < 0.01) discard\") === -1 &&", + " shader.fragmentShader.indexOf(\"#include \") !== -1) {", + " shader.fragmentShader = shader.fragmentShader.replace(", + " \"#include \",", + " buildShadowFadeFragment()", + " );", + " }", + "", + " this.userData.foliageUniforms = shader.uniforms;", + " };", + "", + " shadowMat.needsUpdate = true;", + " }", + "", + " /**", + " * Ensure simple fade-only shadow materials exist for materials that do not need full sway deformation.", + " * @param {FoliageInspectableMaterialBridge|null|undefined} mainMat", + " * @returns {void}", + " */", + " function ensureFadeOnlyShadowMaterials(mainMat) {", + " if (!mainMat) return;", + " mainMat.userData = mainMat.userData || {};", + "", + " var shadowDepth = mainMat.userData._foliageDepthMat;", + " if (!shadowDepth) {", + " shadowDepth = new THREE.MeshDepthMaterial({", + " depthPacking: THREE.RGBADepthPacking,", + " map: mainMat.map || null,", + " alphaMap: mainMat.alphaMap || null,", + " alphaTest: isFinite(mainMat.alphaTest) ? mainMat.alphaTest : 0,", + " side: mainMat.side", + " });", + " shadowDepth.name = (mainMat.name || \"Foliage\") + \"::FadeOnlyShadowDepth\";", + " mainMat.userData._foliageDepthMat = shadowDepth;", + " mainMat.userData._foliageShadowOwned = true;", + " } else {", + " shadowDepth.map = mainMat.map || null;", + " shadowDepth.alphaMap = mainMat.alphaMap || null;", + " shadowDepth.alphaTest = isFinite(mainMat.alphaTest) ? mainMat.alphaTest : 0;", + " shadowDepth.side = mainMat.side;", + " }", + "", + " var shadowDistance = mainMat.userData._foliageDistanceMat;", + " if (!shadowDistance) {", + " shadowDistance = new THREE.MeshDistanceMaterial({", + " map: mainMat.map || null,", + " alphaMap: mainMat.alphaMap || null,", + " alphaTest: isFinite(mainMat.alphaTest) ? mainMat.alphaTest : 0", + " });", + " shadowDistance.side = mainMat.side;", + " shadowDistance.name = (mainMat.name || \"Foliage\") + \"::FadeOnlyShadowDistance\";", + " mainMat.userData._foliageDistanceMat = shadowDistance;", + " mainMat.userData._foliageShadowOwned = true;", + " } else {", + " shadowDistance.map = mainMat.map || null;", + " shadowDistance.alphaMap = mainMat.alphaMap || null;", + " shadowDistance.alphaTest = isFinite(mainMat.alphaTest) ? mainMat.alphaTest : 0;", + " shadowDistance.side = mainMat.side;", + " }", + "", + " patchFadeOnlyShadowMaterialIfNeeded(shadowDepth);", + " patchFadeOnlyShadowMaterialIfNeeded(shadowDistance);", + " propagatePendingFadeToShadowMaterials(mainMat);", + " }", + " ", + " /**", + " * Attach the prepared custom depth/distance materials to the render mesh.", + " * @param {THREE.Object3D|null|undefined} mesh", + " * @param {THREE.Material|null|undefined} matForMesh", + " * @returns {void}", + " */", + " function assignShadowMaterialsToMesh(mesh, matForMesh) {", + " if (!mesh || !matForMesh || !matForMesh.userData) return;", + " if (matForMesh.userData._foliageDepthMat) mesh.customDepthMaterial = matForMesh.userData._foliageDepthMat;", + " if (matForMesh.userData._foliageDistanceMat) mesh.customDistanceMaterial = matForMesh.userData._foliageDistanceMat;", + " }", + " ", + " /**", + " * Apply the behavior's optional PBR overrides only on materials that expose those fields.", + " * @returns {void}", + " */", + " function applyCustomPBRIfSupported(mat, customLitValue, metallicValue, roughnessValue, specularValue, normalStrengthValue, aoStrengthValue, envStrengthValue) {", + " if (!mat || !customLitValue) return;", + " if (\"metalness\" in mat) mat.metalness = metallicValue;", + " if (\"roughness\" in mat) mat.roughness = roughnessValue;", + " if (typeof mat.specularIntensity !== \"undefined\") mat.specularIntensity = specularValue;", + " if (typeof mat.aoMapIntensity !== \"undefined\") mat.aoMapIntensity = aoStrengthValue;", + " if (typeof mat.envMapIntensity !== \"undefined\") mat.envMapIntensity = envStrengthValue;", + " if (mat.normalScale && typeof mat.normalScale.set === \"function\") {", + " mat.normalScale.set(normalStrengthValue, normalStrengthValue);", + " }", + " }", + " ", + " /**", + " * Patch the main render material with foliage sway, gust, grading, fade, and instancing hooks.", + " * This is the main beauty-pass shader patch for foliage rendering.", + " * @param {THREE.Material} mat", + " * @returns {void}", + " */", + " function patchMaterialIfNeeded(mat) {", + " if (cache.patchedMaterials.has(mat)) return;", + " cache.patchedMaterials.add(mat);", + " ", + " var cfg = mat.userData && mat.userData.foliageConfig ? mat.userData.foliageConfig : null;", + " ", + " var applyLeafCardState = !!cfg && (cfg.swayType !== \"treeTrunkSway\" || !!cfg.alphaLikely);", + " if (applyLeafCardState) {", + " mat.transparent = false;", + " mat.alphaTest = 0.10;", + " mat.depthWrite = true;", + " }", + " if (cfg && typeof cfg.renderSide === \"number\" && mat.side !== cfg.renderSide) {", + " mat.side = cfg.renderSide;", + " }", + " ", + " var originalOnBeforeCompile = mat.onBeforeCompile;", + " ", + " mat.onBeforeCompile = function (shader) {", + " if (originalOnBeforeCompile) originalOnBeforeCompile.call(this, shader);", + " ", + " var cfg2 = this.userData.foliageConfig;", + " var gustTex = cache.gustTexture || cache.gustFallbackTex;", + " ", + " // Some materials may not set defines before onBeforeCompile", + " if (!shader.defines) shader.defines = {};", + " ", + " // Compile-time gust branch strips gust code when disabled.", + " if (cache.gustEnabled) {", + " shader.defines.USE_GUST = \"\";", + " } else {", + " delete shader.defines.USE_GUST;", + " }", + " ", + " shader.uniforms.uTime = { value: 0.0 };", + " shader.uniforms.uWindStrength = { value: 0.5 };", + " shader.uniforms.uWindSpeed = { value: 1.0 };", + " shader.uniforms.uWindDir = { value: new THREE.Vector2(1, 0) };", + " // Keep a secondary XY wind basis uniform so the common sway path avoids", + " // per-vertex inverse(m). It currently mirrors the world XY wind direction.", + " var localWindDir = new THREE.Vector2(1, 0);", + " var localWindPerp = new THREE.Vector2(0, 1);", + " shader.uniforms.uLocalWindDir = { value: localWindDir };", + " shader.uniforms.uLocalWindPerp = { value: localWindPerp };", + " shader.uniforms.uPhase = { value: cfg2.phase };", + " ", + " shader.uniforms.uTopColor = { value: cfg2.topColor.clone() };", + " shader.uniforms.uBottomColor = { value: cfg2.bottomColor.clone() };", + " ", + " shader.uniforms.uSat = { value: cfg2.sat };", + " shader.uniforms.uContrast = { value: cfg2.contrast };", + " shader.uniforms.uUseGrad = { value: cfg2.useGrad ? 1.0 : 0.0 };", + " shader.uniforms.uTwoSidedLighting = { value: cfg2.twoSidedLighting ? 1.0 : 0.0 };", + " shader.uniforms.uInstanceDetSign = { value: cfg2.instanceDetSign < 0 ? -1.0 : 1.0 };", + " ", + " shader.uniforms.uGradStart = { value: cfg2.gradStart };", + " shader.uniforms.uGradEnd = { value: cfg2.gradEnd };", + " ", + " // World-gradient controls", + " shader.uniforms.uIgnoreUV = { value: cfg2.ignoreUV ? 1.0 : 0.0 };", + " shader.uniforms.uGradLocalZMin = { value: cfg2.gradLocalZMin };", + " shader.uniforms.uGradLocalZMax = { value: cfg2.gradLocalZMax };", + " ", + " shader.uniforms.uPolyScale = { value: cfg2.polyScale };", + " ", + " // Bend multiplier: bushes (0.1), leaves (0.05), grass/trunk (1.0)", + " var bendMultiplier = 1.0;", + " if (cfg2.swayType === \"bushSway\") bendMultiplier = 0.1;", + " else if (cfg2.swayType === \"leavesSway\") bendMultiplier = 0.05;", + " shader.uniforms.uBendMultiplier = { value: bendMultiplier };", + " ", + " var flutterAllowed = cfg2.swayType === \"leavesSway\" && cfg2.polyScale >= 0.6;", + " shader.uniforms.uFlutterStrength = { value: flutterAllowed ? 0.4 * cfg2.polyScale : 0.0 };", + " ", + " // Set gust uniforms only when gust is enabled.", + " if (cache.gustEnabled) {", + " shader.uniforms.uGustTex = { value: gustTex };", + " shader.uniforms.uGustEnabled = { value: 1.0 };", + " shader.uniforms.uGustStrength = { value: cache.gustStrength };", + " shader.uniforms.uGustScale = { value: cache.gustScale };", + " shader.uniforms.uGustSpeed = { value: cache.gustSpeed };", + " shader.uniforms.uGustThreshold = { value: cache.gustThreshold };", + " shader.uniforms.uGustContrast = { value: cache.gustContrast };", + " }", + " ", + " // GPU fade smoothing: interpolation factor updated every frame", + " shader.uniforms.uFadeInterpT = { value: 1.0 };", + " ", + " this.userData.foliageUniforms = shader.uniforms;", + " ", + " var commonVertex = `", + " #include ", + " uniform float uTime;", + " uniform float uWindStrength;", + " uniform float uWindSpeed;", + " uniform vec2 uWindDir;", + " // Secondary XY wind basis used for direct vertex offsets.", + " uniform vec2 uLocalWindDir;", + " uniform vec2 uLocalWindPerp;", + " uniform float uPhase;", + " ", + " uniform float uGradStart;", + " uniform float uGradEnd;", + " ", + " uniform float uIgnoreUV;", + " uniform float uGradLocalZMin;", + " uniform float uGradLocalZMax;", + " ", + " uniform float uPolyScale;", + " uniform float uFlutterStrength;", + " uniform float uBendMultiplier;", + " ", + " // Declare gust uniforms only when USE_GUST is defined.", + " #ifdef USE_GUST", + " uniform sampler2D uGustTex;", + " uniform float uGustEnabled;", + " uniform float uGustStrength;", + " uniform float uGustScale;", + " uniform float uGustSpeed;", + " uniform float uGustThreshold;", + " uniform float uGustContrast;", + " #endif", + " ", + " varying float vGrad;", + " ", + " // Distance fade (dither dissolve) + GPU smoothing", + " #ifdef USE_INSTANCING", + " attribute float aFade;", + " attribute float aFadePrev;", + " #endif", + " uniform float uFadeInterpT;", + " varying float vFade;", + " `;", + " shader.vertexShader = shader.vertexShader.replace(\"#include \", commonVertex);", + " ", + " var beginVertex = buildVertexBody(cfg2);", + " shader.vertexShader = shader.vertexShader.replace(\"#include \", beginVertex);", + " ", + " var commonFragment = `", + " #include ", + " uniform vec3 uBottomColor;", + " uniform vec3 uTopColor;", + " uniform float uSat;", + " uniform float uContrast;", + " uniform float uUseGrad;", + " uniform float uTwoSidedLighting;", + " uniform float uInstanceDetSign;", + " varying float vGrad;", + " varying float vFade;", + " `;", + " var useDistanceFadeShader = cfg2 ? !!cfg2.distanceFadeEnabled : true;", + " var colorFragment = useDistanceFadeShader ? buildColorFragment() : buildColorFragmentNoFade();", + " shader.fragmentShader = shader.fragmentShader", + " .replace(", + " \"#include \",", + " commonFragment", + " )", + " .replace(\"#include \", colorFragment);", + " ", + " if (cfg2) {", + " var normalLightPatch = \"normal *= uInstanceDetSign;\";", + " if (cfg2.swayType !== \"treeTrunkSway\") {", + " normalLightPatch +=", + " \"\\n#ifdef DOUBLE_SIDED\\n\" +", + " \" if (uTwoSidedLighting > 0.5) {\\n\" +", + " \" normal *= faceDirection;\\n\" +", + " \" }\\n\" +", + " \"#endif\";", + " }", + " if (shader.fragmentShader.indexOf(\"#include \") !== -1) {", + " shader.fragmentShader = shader.fragmentShader.replace(", + " \"#include \",", + " \"#include \\n\" + normalLightPatch", + " );", + " } else if (shader.fragmentShader.indexOf(\"#include \") !== -1) {", + " shader.fragmentShader = shader.fragmentShader.replace(", + " \"#include \",", + " \"#include \\n\" + normalLightPatch", + " );", + " }", + " }", + " };", + " ", + " mat.needsUpdate = true;", + " }", + " ", + " function patchMaterialFadeOnly(mat) {", + " if (cache.patchedMaterials.has(mat)) return;", + " cache.patchedMaterials.add(mat);", + " var cfgFadeOnly = mat.userData && mat.userData.foliageConfig ? mat.userData.foliageConfig : null;", + " if (cfgFadeOnly && typeof cfgFadeOnly.renderSide === \"number\" && mat.side !== cfgFadeOnly.renderSide) {", + " mat.side = cfgFadeOnly.renderSide;", + " }", + " var originalOnBeforeCompile = mat.onBeforeCompile;", + " mat.onBeforeCompile = function (shader) {", + " if (originalOnBeforeCompile) originalOnBeforeCompile.call(mat, shader);", + " var cfgFadeOnly2 = this.userData && this.userData.foliageConfig ? this.userData.foliageConfig : cfgFadeOnly;", + " if (!shader.defines) shader.defines = {};", + " // GPU fade smoothing: aFadePrev + uFadeInterpT", + " shader.uniforms.uFadeInterpT = { value: 1.0 };", + " shader.uniforms.uInstanceDetSign = { value: cfgFadeOnly2 && cfgFadeOnly2.instanceDetSign < 0 ? -1.0 : 1.0 };", + " shader.vertexShader = shader.vertexShader.replace(", + " \"#include \",", + " \"#include \\n#ifdef USE_INSTANCING\\n attribute float aFade;\\n attribute float aFadePrev;\\n#endif\\nuniform float uFadeInterpT;\\nvarying float vFade;\"", + " );", + " shader.vertexShader = shader.vertexShader.replace(", + " \"#include \",", + " \"#include \\n#ifdef USE_INSTANCING\\n vFade = mix(aFadePrev, aFade, uFadeInterpT);\\n#else\\n vFade = 1.0;\\n#endif\"", + " );", + " shader.fragmentShader = shader.fragmentShader.replace(", + " \"#include \",", + " \"#include \\nuniform float uInstanceDetSign;\\nvarying float vFade;\"", + " );", + " if (shader.fragmentShader.indexOf(\"#include \") !== -1) {", + " shader.fragmentShader = shader.fragmentShader.replace(", + " \"#include \",", + " \"#include \\nnormal *= uInstanceDetSign;\"", + " );", + " } else if (shader.fragmentShader.indexOf(\"#include \") !== -1) {", + " shader.fragmentShader = shader.fragmentShader.replace(", + " \"#include \",", + " \"#include \\nnormal *= uInstanceDetSign;\"", + " );", + " }", + " var useDistanceFadeShader = cfgFadeOnly2 ? !!cfgFadeOnly2.distanceFadeEnabled : true;", + " shader.fragmentShader = shader.fragmentShader.replace(", + " \"#include \",", + " useDistanceFadeShader ? buildFadeOnlyFragment() : buildFadeOnlyFragmentNoFade()", + " );", + " // Store uniforms for per-frame uFadeInterpT update", + " this.userData.foliageUniforms = shader.uniforms;", + " };", + " mat.needsUpdate = true;", + " }", + " ", + " var entry = cache.sharedByKey.get(key);", + " ", + " if (!entry) {", + " var src = selection.srcRef;", + " if (!src) return;", + " ", + " var shared = src.clone();", + " shared.name = src.name;", + " ", + " shared.userData = {", + " foliageConfig: {", + " topColor: topColor.clone(),", + " bottomColor: bottomColor.clone(),", + " sat: sat,", + " contrast: contrast,", + " useGrad: !!useColorGrading,", + " swayType: swayType,", + " uniformSway: !!uniformSway,", + " phase: Math.random() * Math.PI * 2,", + " alphaLikely: isAlphaLikely(src),", + " distanceFadeEnabled: !!distanceFadeEnabled,", + " customLit: !!customLit,", + " twoSidedLighting: !!twoSidedLighting,", + " metallic: metallic,", + " roughness: roughness,", + " specular: specular,", + " normalStrength: normalStrength,", + " aoStrength: aoStrength,", + " envStrength: envStrength,", + " cullingMode: cullingMode,", + " renderSide: resolvedRenderSide,", + " instanceDetSign: 1.0,", + " polyScale: polyScale,", + " gradStart: gradStart,", + " gradEnd: gradEnd,", + " ", + " // World-gradient config.", + " ignoreUV: !!ignoreUV,", + " gradHeight: gradHeight,", + " gradLocalZMin: gradLocalZMin,", + " gradLocalZMax: gradLocalZMax,", + " receiveShadow: false", + " }", + " };", + " shared.side = resolvedRenderSide;", + " ", + " applyCustomPBRIfSupported(shared, customLit, metallic, roughness, specular, normalStrength, aoStrength, envStrength);", + " ", + " patchMaterialIfNeeded(shared);", + " ensureFoliageShadowMaterials(shared);", + " ", + " entry = { material: shared, refCount: 0, _ownedByFoliage: true };", + " cache.sharedByKey.set(key, entry);", + " cache.registerActiveMaterial(shared);", + " } else {", + " var cfg3 = entry.material.userData.foliageConfig;", + " cfg3.topColor.copy(topColor);", + " cfg3.bottomColor.copy(bottomColor);", + " cfg3.sat = sat;", + " cfg3.contrast = contrast;", + " cfg3.useGrad = !!useColorGrading;", + " cfg3.swayType = swayType;", + " cfg3.uniformSway = !!uniformSway;", + " var prevDistanceFadeEnabled = !!cfg3.distanceFadeEnabled;", + " cfg3.distanceFadeEnabled = !!distanceFadeEnabled;", + " cfg3.customLit = !!customLit;", + " cfg3.twoSidedLighting = !!twoSidedLighting;", + " cfg3.metallic = metallic;", + " cfg3.roughness = roughness;", + " cfg3.specular = specular;", + " cfg3.normalStrength = normalStrength;", + " cfg3.aoStrength = aoStrength;", + " cfg3.envStrength = envStrength;", + " cfg3.cullingMode = cullingMode;", + " cfg3.renderSide = resolvedRenderSide;", + " cfg3.instanceDetSign = 1.0;", + " if (typeof cfg3.renderSide === \"number\" && entry.material.side !== cfg3.renderSide) {", + " entry.material.side = cfg3.renderSide;", + " entry.material.needsUpdate = true;", + " }", + " cfg3.polyScale = polyScale;", + " cfg3.gradStart = gradStart;", + " cfg3.gradEnd = gradEnd;", + " ", + " // Keep world-gradient config in sync for shared material.", + " cfg3.ignoreUV = !!ignoreUV;", + " cfg3.gradHeight = gradHeight;", + " cfg3.gradLocalZMin = gradLocalZMin;", + " cfg3.gradLocalZMax = gradLocalZMax;", + " applyCustomPBRIfSupported(entry.material, cfg3.customLit, cfg3.metallic, cfg3.roughness, cfg3.specular, cfg3.normalStrength, cfg3.aoStrength, cfg3.envStrength);", + " ", + " var u = entry.material.userData.foliageUniforms;", + " if (u) {", + " if (u.uSat) u.uSat.value = sat;", + " if (u.uContrast) u.uContrast.value = contrast;", + " if (u.uUseGrad) u.uUseGrad.value = useColorGrading ? 1.0 : 0.0;", + " if (u.uTwoSidedLighting) u.uTwoSidedLighting.value = cfg3.twoSidedLighting ? 1.0 : 0.0;", + " if (u.uInstanceDetSign) u.uInstanceDetSign.value = cfg3.instanceDetSign < 0 ? -1.0 : 1.0;", + " if (u.uTopColor) u.uTopColor.value.copy(cfg3.topColor);", + " if (u.uBottomColor) u.uBottomColor.value.copy(cfg3.bottomColor);", + " ", + " if (u.uGradStart) u.uGradStart.value = cfg3.gradStart;", + " if (u.uGradEnd) u.uGradEnd.value = cfg3.gradEnd;", + " ", + " // Sync world-gradient uniforms.", + " if (u.uIgnoreUV) u.uIgnoreUV.value = cfg3.ignoreUV ? 1.0 : 0.0;", + " if (u.uGradLocalZMin) u.uGradLocalZMin.value = cfg3.gradLocalZMin;", + " if (u.uGradLocalZMax) u.uGradLocalZMax.value = cfg3.gradLocalZMax;", + " ", + " if (u.uPolyScale) u.uPolyScale.value = cfg3.polyScale;", + " ", + " // Update bend multiplier: bushes (0.1), leaves (0.05), grass/trunk (1.0)", + " var bendMultiplier2 = 1.0;", + " if (swayType === \"bushSway\") bendMultiplier2 = 0.1;", + " else if (swayType === \"leavesSway\") bendMultiplier2 = 0.05;", + " if (u.uBendMultiplier) u.uBendMultiplier.value = bendMultiplier2;", + " ", + " var flutterAllowed2 = swayType === \"leavesSway\" && cfg3.polyScale >= 0.6;", + " if (u.uFlutterStrength) u.uFlutterStrength.value = flutterAllowed2 ? 0.4 * cfg3.polyScale : 0.0;", + " ", + " if (u.uGustTex) u.uGustTex.value = cache.gustTexture || cache.gustFallbackTex;", + " if (u.uGustEnabled) u.uGustEnabled.value = cache.gustEnabled ? 1.0 : 0.0;", + " if (u.uGustStrength) u.uGustStrength.value = cache.gustEnabled ? cache.gustStrength : 0.0;", + " if (u.uGustScale) u.uGustScale.value = cache.gustScale;", + " if (u.uGustSpeed) u.uGustSpeed.value = cache.gustSpeed;", + " if (u.uGustThreshold) u.uGustThreshold.value = cache.gustThreshold;", + " if (u.uGustContrast) u.uGustContrast.value = cache.gustContrast;", + " }", + " if (prevDistanceFadeEnabled !== cfg3.distanceFadeEnabled) {", + " entry.material.needsUpdate = true;", + " }", + " ensureFoliageShadowMaterials(entry.material);", + " }", + " ", + " entry.refCount++;", + " ", + " var sharedMat = entry.material;", + " var srcRef = selection.srcRef;", + " var srcName = selection.matchName;", + " ", + " // GPU instancing path: build queue items only; the coordinator resolves", + " // final transforms and group assignment one frame later during update.", + " if (gpuInstancing && (swayType === \"grassSway\" || swayType === \"bushSway\" || swayType === \"treeTrunkSway\" || swayType === \"leavesSway\")) {", + " try {", + " // Make sure world matrices are up to date so we can capture correct transforms", + " threeObj.updateMatrixWorld(true);", + " } catch (eMW) {}", + " ", + " // leavesSway two-part mode: with exactly 2 materials, the other material becomes the trunk group and leaves keep the materialName selection.", + " var useTwoPartTree = swayType === \"leavesSway\" && rop && rop.records && rop.records.length === 2;", + " if (useTwoPartTree) {", + " var leavesMatName = (selection && (selection.pickedId || selection.matchName)) ? String(selection.pickedId || selection.matchName).trim() : \"\";", + " var trunkMatName = null;", + " for (var ri = 0; ri < rop.records.length; ri++) {", + " var rName = (rop.records[ri].materialName || \"\").trim();", + " if (rName && rName !== leavesMatName) {", + " trunkMatName = rName;", + " break;", + " }", + " }", + " if (!trunkMatName) useTwoPartTree = false;", + " }", + " if (useTwoPartTree) {", + " var objectTypeCacheKeyTrunk = sharedKey + \"::\" + trunkMatName;", + " var cachedRopTrunk = objectTypeCache.get(objectTypeCacheKeyTrunk);", + " var ropTrunk = cachedRopTrunk;", + " if (!ropTrunk) {", + " ropTrunk = objectTypeCache.resolveOrPick(threeObj, trunkMatName, sharedKey, analysisHelpers);", + " if (ropTrunk && ropTrunk.selection && ropTrunk.selection.srcRef) {", + " objectTypeCache.set(objectTypeCacheKeyTrunk, ropTrunk);", + " }", + " }", + " /** @type {FoliageMaterialSelectionBridge|null} */", + " var selectionTrunk = ropTrunk && ropTrunk.selection ? ropTrunk.selection : null;", + " if (!selectionTrunk || !selectionTrunk.srcRef) {", + " useTwoPartTree = false;", + " } else {", + " /** @type {THREE.Material} */", + " var srcTrunk = selectionTrunk.srcRef;", + " var sourceRenderSideTrunk = (typeof srcTrunk.side === \"number\") ? srcTrunk.side : THREE.FrontSide;", + " /** @type {THREE.Side} */", + " var resolvedRenderSideTrunk = resolveRenderSide(sourceRenderSideTrunk, cullingMode);", + " var sideSuffixTrunk = buildSideSuffix(cullingModeCode, resolvedRenderSideTrunk);", + " // pipelineKey for trunk: treeTrunkSway, same uniform/grading flags as leaves", + " var keyTrunk = sanitizeKeyPart(sharedKey) + \"::\" + sanitizeKeyPart(selectionTrunk.pickedId) + \"::\" + (uniformSway ? \"U\" : \"C\") + \"_treeTrunkSway_\" + (useColorGrading ? \"GRAD\" : \"TEX\") + \"_IU\" + (ignoreUV ? \"1\" : \"0\") + \"_FD\" + (distanceFadeEnabled ? \"1\" : \"0\") + pbrSuffix + sideSuffixTrunk;", + " /** @type {FoliageSharedMaterialEntryBridge|undefined} */", + " var entryTrunk = cache.sharedByKey.get(keyTrunk);", + " if (!entryTrunk) {", + " var sharedTrunk = srcTrunk.clone();", + " sharedTrunk.name = srcTrunk.name;", + " sharedTrunk.userData = sharedTrunk.userData || {};", + " sharedTrunk.userData.foliageConfig = {", + " distanceFadeEnabled: !!distanceFadeEnabled,", + " customLit: !!customLit,", + " twoSidedLighting: false,", + " metallic: metallic,", + " roughness: roughness,", + " specular: specular,", + " normalStrength: normalStrength,", + " aoStrength: aoStrength,", + " envStrength: envStrength,", + " cullingMode: cullingMode,", + " renderSide: resolvedRenderSideTrunk,", + " instanceDetSign: 1.0", + " };", + " sharedTrunk.side = resolvedRenderSideTrunk;", + " applyCustomPBRIfSupported(sharedTrunk, customLit, metallic, roughness, specular, normalStrength, aoStrength, envStrength);", + " patchMaterialFadeOnly(sharedTrunk);", + " ensureFadeOnlyShadowMaterials(sharedTrunk);", + " entryTrunk = { material: sharedTrunk, refCount: 0, _ownedByFoliage: true };", + " cache.sharedByKey.set(keyTrunk, entryTrunk);", + " cache.registerActiveMaterial(sharedTrunk);", + " } else {", + " var trunkCfg = entryTrunk.material && entryTrunk.material.userData ? entryTrunk.material.userData.foliageConfig : null;", + " if (trunkCfg) {", + " trunkCfg.distanceFadeEnabled = !!distanceFadeEnabled;", + " trunkCfg.customLit = !!customLit;", + " trunkCfg.twoSidedLighting = false;", + " trunkCfg.metallic = metallic;", + " trunkCfg.roughness = roughness;", + " trunkCfg.specular = specular;", + " trunkCfg.normalStrength = normalStrength;", + " trunkCfg.aoStrength = aoStrength;", + " trunkCfg.envStrength = envStrength;", + " trunkCfg.cullingMode = cullingMode;", + " trunkCfg.renderSide = resolvedRenderSideTrunk;", + " trunkCfg.instanceDetSign = 1.0;", + " }", + " if (typeof resolvedRenderSideTrunk === \"number\" && entryTrunk.material.side !== resolvedRenderSideTrunk) {", + " entryTrunk.material.side = resolvedRenderSideTrunk;", + " entryTrunk.material.needsUpdate = true;", + " }", + " var trunkUniforms = entryTrunk.material && entryTrunk.material.userData ? entryTrunk.material.userData.foliageUniforms : null;", + " if (trunkUniforms && trunkUniforms.uInstanceDetSign) {", + " trunkUniforms.uInstanceDetSign.value = trunkCfg && trunkCfg.instanceDetSign < 0 ? -1.0 : 1.0;", + " }", + " applyCustomPBRIfSupported(entryTrunk.material, !!customLit, metallic, roughness, specular, normalStrength, aoStrength, envStrength);", + " ensureFadeOnlyShadowMaterials(entryTrunk.material);", + " }", + "\t var sharedMatTrunk = entryTrunk.material;", + "\t var splitMeshes = findSplitTreeMeshes(", + "\t threeObj,", + "\t function(mesh) {", + "\t return meshMatchesSelection(mesh, selectionTrunk, selectionTrunk.srcRef, selectionTrunk.matchName);", + "\t },", + "\t function(mesh) {", + "\t return meshMatchesSelection(mesh, selection, srcRef, srcName);", + "\t }", + "\t );", + "\t var repMeshTrunk = splitMeshes.trunk;", + "\t var repMeshLeaves = splitMeshes.leaves;", + "\t if (!repMeshTrunk || !repMeshTrunk.geometry || !repMeshLeaves || !repMeshLeaves.geometry) {", + "\t useTwoPartTree = false;", + "\t }", + " }", + " if (useTwoPartTree) {", + " // Global instancing: geoKey stays a stable asset+mesh identity without runtime-instance suffixes.", + " var trunkPickedId = selectionTrunk && selectionTrunk.pickedId ? String(selectionTrunk.pickedId) : \"\";", + " var leavesPickedId = selection && selection.pickedId ? String(selection.pickedId) : \"\";", + " var geoKeyTrunk = trunkPickedId", + " ? (sanitizeKeyPart(sharedKey) + \"::\" + sanitizeKeyPart(repMeshTrunk.name || \"(unnamed)\") + \"::\" + sanitizeKeyPart(trunkPickedId))", + " : (repMeshTrunk.geometry.id !== undefined && repMeshTrunk.geometry.id !== null ? String(repMeshTrunk.geometry.id) : (repMeshTrunk.geometry.uuid || \"\"));", + "\t var geoKeyLeaves = leavesPickedId", + "\t ? (sanitizeKeyPart(sharedKey) + \"::\" + sanitizeKeyPart(repMeshLeaves.name || \"(unnamed)\") + \"::\" + sanitizeKeyPart(leavesPickedId))", + "\t : (repMeshLeaves.geometry.id !== undefined && repMeshLeaves.geometry.id !== null ? String(repMeshLeaves.geometry.id) : (repMeshLeaves.geometry.uuid || \"\"));", + "\t var baseGroupKeyTrunk = keyTrunk + \"::GEO::\" + geoKeyTrunk;", + "\t var baseGroupKeyLeaves = key + \"::GEO::\" + geoKeyLeaves;", + "\t var fastCacheKeySplit = buildGpuFastCacheKey(sharedKey, materialName || \"\", swayType, distanceFadeEnabled, pbrSuffix, sideSuffix, twoSidedLighting, polyScaleAutoMode);", + "\t /** @type {FoliageObjectTypeCacheEntryBridge} */", + "\t var splitFastCacheEntry = objectTypeCache.get(fastCacheKeySplit) || {};", + "\t splitFastCacheEntry._gpuFastMode = \"splitLeavesTree\";", + "\t splitFastCacheEntry._gpuTrunkGeometry = repMeshTrunk.geometry;", + "\t splitFastCacheEntry._gpuLeavesGeometry = repMeshLeaves.geometry;", + "\t splitFastCacheEntry._gpuTrunkBaseGroupKey = baseGroupKeyTrunk;", + "\t splitFastCacheEntry._gpuLeavesBaseGroupKey = baseGroupKeyLeaves;", + "\t splitFastCacheEntry._gpuTrunkSharedMaterialKey = keyTrunk;", + "\t splitFastCacheEntry._gpuLeavesSharedMaterialKey = key;", + "\t splitFastCacheEntry._gpuTrunkResolvedSide = resolvedRenderSideTrunk;", + "\t splitFastCacheEntry._gpuLeavesResolvedSide = resolvedRenderSide;", + "\t objectTypeCache.set(fastCacheKeySplit, splitFastCacheEntry);", + "\t var instTree = instancing;", + "\t var queueIdTree = instTree.nextQueueId();", + "\t instTree.enqueue({", + " gdObj: gdObj,", + " threeObj: threeObj,", + " parent: null, // FoliageRoot will be attached during flushPending.", + " swayType: \"leavesSway\",", + " behavior: behavior,", + " queueId: queueIdTree,", + " parts: [", + " { repMesh: repMeshTrunk, geometry: repMeshTrunk.geometry, material: sharedMatTrunk, baseGroupKey: baseGroupKeyTrunk },", + " { repMesh: repMeshLeaves, geometry: repMeshLeaves.geometry, material: sharedMat, baseGroupKey: baseGroupKeyLeaves }", + " ]", + " });", + " behavior.__foliageQueued = true;", + " behavior.__foliageQueueId = queueIdTree;", + " delete behavior.__foliageNonInstancedRegistered;", + " behavior.__foliageSharedKey = key;", + " behavior.__foliageSharedKeyTrunk = keyTrunk;", + " threeObj.userData = threeObj.userData || {};", + " delete threeObj.userData.__foliageNonInstancedRegistered;", + " threeObj.userData.__foliageSharedKey = key;", + " threeObj.userData.__foliageSharedKeyTrunk = keyTrunk;", + " entry.refCount++;", + " entryTrunk.refCount++;", + " instTree.markDirty();", + " }", + " }", + " ", + " // Single-item path for grass, bush, trunk, or leavesSway with one material.", + " if (!useTwoPartTree) {", + " // Optimization: use cached geometry when available.", + " var geo = null;", + " var repMesh = null;", + " ", + " // Check whether cached geometry exists for this object type.", + " if (cachedRop && cachedRop._cachedGeometry) {", + " // Cached geometry found; locate a matching mesh using it (faster).", + " geo = cachedRop._cachedGeometry;", + " threeObj.traverse(function(o) {", + " if (repMesh) return; // Early exit", + " var mesh = /** @type {FoliageMeshLike} */ (o);", + " if (mesh && mesh.isMesh && mesh.geometry === geo) {", + " repMesh = mesh;", + " }", + " });", + " }", + " ", + " // Fallback: full search if cache is missing or no match is found.", + " if (!repMesh) {", + " repMesh = findFirstMatchingMesh(threeObj, selection, srcRef, srcName);", + " // Cache geometry for future objects.", + " if (repMesh && repMesh.geometry && cachedRop) {", + " cachedRop._cachedGeometry = repMesh.geometry;", + " geo = repMesh.geometry;", + " }", + " }", + " ", + " if (!repMesh || !repMesh.geometry) {", + " // Fallback to non-instanced behavior if we cannot resolve geometry", + " gpuInstancing = false;", + " } else {", + " if (!geo) geo = repMesh.geometry;", + " {", + " // Global instancing: geoKey stays a stable asset+mesh identity without runtime-instance suffixes.", + " var materialPickedId = selection && selection.pickedId ? String(selection.pickedId) : \"\";", + " var geoKey = materialPickedId", + " ? (sanitizeKeyPart(sharedKey) + \"::\" + sanitizeKeyPart(repMesh.name || \"(unnamed)\") + \"::\" + sanitizeKeyPart(materialPickedId))", + " : (geo.id !== undefined && geo.id !== null ? String(geo.id) : (geo.uuid || \"\"));", + " var geoGroupKey = key + \"::GEO::\" + geoKey;", + " ", + " var inst = instancing;", + " var g = inst.ensureGroup(geoGroupKey, {", + " key: geoGroupKey,", + " geometry: geo,", + " material: sharedMat,", + " parent: null, // FoliageRoot will be attached during flushPending.", + " fadeEnabled: distanceFadeEnabled,", + " fadeStart: fadeStart,", + " fadeEnd: fadeEnd", + " });", + " ", + " // Fast cache entry for subsequent objects of same type", + "\t var fastCacheKey = buildGpuFastCacheKey(sharedKey, materialName || \"\", swayType, distanceFadeEnabled, pbrSuffix, sideSuffix, twoSidedLighting, polyScaleAutoMode);", + "\t if (!objectTypeCache.get(fastCacheKey)) {", + "\t objectTypeCache.set(fastCacheKey, {", + "\t _gpuFastMode: \"single\",", + "\t _gpuGroupKey: geoGroupKey,", + "\t _gpuGeometry: geo,", + "\t _sharedMaterialKey: key,", + " _resolvedSide: resolvedRenderSide", + " });", + " }", + " ", + " // Defer capture by one frame (GDevelop applies transform slightly later)", + " var queueId = inst.nextQueueId();", + " inst.enqueue({", + " gdObj: gdObj,", + " threeObj: threeObj,", + " repMesh: repMesh,", + " geometry: geo,", + " material: sharedMat,", + " parent: null, // FoliageRoot will be attached during flushPending.", + " baseGroupKey: geoGroupKey,", + " swayType: swayType,", + " behavior: behavior,", + " queueId: queueId", + " });", + " behavior.__foliageQueued = true;", + " behavior.__foliageQueueId = queueId;", + " delete behavior.__foliageNonInstancedRegistered;", + " if (threeObj && threeObj.userData) delete threeObj.userData.__foliageNonInstancedRegistered;", + " ", + " inst.markDirty();", + " }", + " }", + " }", + " }", + " ", + " // Non-instanced path: keep the original runtime object alive and assign", + " // per-object materials directly, with optional split trunk/leaves fade handling.", + " if (!gpuInstancing || (swayType !== \"grassSway\" && swayType !== \"bushSway\" && swayType !== \"treeTrunkSway\" && swayType !== \"leavesSway\")) {", + " var matToAssign = sharedMat;", + " var useNonInstancedFade = distanceFadeEnabled && sharedMat.userData && sharedMat.userData.foliageConfig;", + " var useSplitLeavesFade = !!(useNonInstancedFade && swayType === \"leavesSway\");", + " var initialNonInstancedFade = useNonInstancedFade", + " ? computeInitialNonInstancedFade(gdObj, threeObj, fadeStart, fadeEnd)", + " : 1.0;", + " /** @type {THREE.Material|null} */", + " var entryTrunkMaterial = null;", + " /** @type {THREE.Material|null} */", + " var trunkMatFound = null;", + " /** @type {FoliageMeshLike[]} */", + " var trunkMeshes = [];", + " if (useNonInstancedFade) {", + " var fadeMat = sharedMat.clone();", + " fadeMat.userData = { foliageConfig: sharedMat.userData.foliageConfig };", + " if (typeof sharedMat.side === \"number\") fadeMat.side = sharedMat.side;", + " applyPendingFadeToMaterial(fadeMat, initialNonInstancedFade);", + " // Explicitly chain sharedMat's onBeforeCompile (full patch) then add uFade — clone() may not copy onBeforeCompile", + " var sharedOnBeforeCompile = sharedMat.onBeforeCompile;", + " fadeMat.onBeforeCompile = function (shader) {", + " if (sharedOnBeforeCompile) sharedOnBeforeCompile.call(fadeMat, shader);", + " var pendingFade = (fadeMat.userData && typeof fadeMat.userData._pendingFade === \"number\") ? fadeMat.userData._pendingFade : 1.0;", + " shader.uniforms.uFade = { value: pendingFade };", + " var hasUFadeUniform = /uniform\\s+float\\s+uFade\\s*;/.test(shader.vertexShader);", + " if (!hasUFadeUniform) {", + " if (shader.vertexShader.indexOf(\"varying float vFade;\") !== -1) {", + " shader.vertexShader = shader.vertexShader.replace(\"varying float vFade;\", \"uniform float uFade;\\nvarying float vFade;\");", + " } else {", + " shader.vertexShader = shader.vertexShader.replace(\"#include \", \"#include \\nuniform float uFade;\\nvarying float vFade;\");", + " }", + " }", + " if (shader.vertexShader.indexOf(\"vFade = uFade\") === -1) {", + " shader.vertexShader = shader.vertexShader.replace(/vFade\\s*=\\s*1\\.0\\s*;/, \"vFade = uFade;\");", + " if (shader.vertexShader.indexOf(\"vFade = uFade\") === -1 && shader.vertexShader.indexOf(\"#include \") !== -1) {", + " shader.vertexShader = shader.vertexShader.replace(\"#include \", \"#include \\nvFade = uFade;\");", + " }", + " }", + " if (shader.fragmentShader.indexOf(\"if (vFade < 0.01) discard\") === -1 && shader.fragmentShader.indexOf(\"#include \") !== -1) {", + " if (shader.fragmentShader.indexOf(\"varying float vFade;\") === -1) {", + " shader.fragmentShader = shader.fragmentShader.replace(\"#include \", \"#include \\nvarying float vFade;\");", + " }", + " shader.fragmentShader = shader.fragmentShader.replace(\"#include \", buildColorFragment());", + " }", + " fadeMat.userData.foliageUniforms = shader.uniforms;", + " };", + " fadeMat.needsUpdate = true;", + " ensureFoliageShadowMaterials(fadeMat);", + " matToAssign = fadeMat;", + " }", + "", + " function matchesSelectedMaterial(localMat) {", + " if (!localMat) return false;", + " if (selection.matchMode === \"name\") {", + " return (localMat.name || \"\") === selection.matchName;", + " }", + " if (localMat === srcRef) return true;", + " return !!(srcName && (localMat.name || \"\") === srcName && isAlphaLikely(localMat));", + " }", + "", + " threeObj.traverse(function (o) {", + " var mesh = /** @type {FoliageMeshLike} */ (o);", + " if (!mesh || !mesh.isMesh || !mesh.material) return;", + " ", + " // Single material: no allocations", + " if (!Array.isArray(mesh.material)) {", + " var mSingle2 = mesh.material;", + " if (!mSingle2) return;", + "", + " var matchSingle2 = matchesSelectedMaterial(mSingle2);", + " if (useSplitLeavesFade && !matchSingle2) {", + " if (trunkMatFound === null) trunkMatFound = mSingle2;", + " if (trunkMatFound === mSingle2) trunkMeshes.push(mesh);", + " }", + "", + " if (matchSingle2) {", + " mesh.material = matToAssign;", + " assignShadowMaterialsToMesh(mesh, matToAssign);", + " }", + " return;", + " }", + " ", + " // Material array: first scan without allocating", + " var mats2 = mesh.material;", + " var needsChange2 = false;", + " for (var i2 = 0; i2 < mats2.length; i2++) {", + " var m2 = mats2[i2];", + " if (!m2) continue;", + "", + " var matchA = matchesSelectedMaterial(m2);", + " if (matchA) {", + " needsChange2 = true;", + " break;", + " }", + " }", + " ", + " if (!needsChange2) return;", + " ", + " // Allocate only when needed", + " var newMats2 = mats2.slice();", + " for (var j2 = 0; j2 < newMats2.length; j2++) {", + " var mm2 = newMats2[j2];", + " if (!mm2) continue;", + "", + " var matchB = matchesSelectedMaterial(mm2);", + " if (matchB) newMats2[j2] = matToAssign;", + " }", + " ", + " mesh.material = newMats2;", + " });", + " if (useNonInstancedFade) {", + " // leavesSway two-part: trunk must fade with leaves (same uFade)", + " if (useSplitLeavesFade && trunkMatFound) {", + " /** @type {THREE.Material} */", + " var trunkFadeMat = trunkMatFound.clone();", + " trunkFadeMat.userData = trunkFadeMat.userData || {};", + " trunkFadeMat.side = /** @type {THREE.Side} */ (resolveRenderSide(trunkMatFound.side, cullingMode));", + " applyCustomPBRIfSupported(trunkFadeMat, customLit, metallic, roughness, specular, normalStrength, aoStrength, envStrength);", + " applyPendingFadeToMaterial(trunkFadeMat, initialNonInstancedFade);", + " var trunkOriginal = trunkMatFound.onBeforeCompile;", + " trunkFadeMat.onBeforeCompile = function (shader) {", + " if (trunkOriginal) trunkOriginal.call(trunkFadeMat, shader);", + " var pendingTrunk = (trunkFadeMat.userData && typeof trunkFadeMat.userData._pendingFade === \"number\") ? trunkFadeMat.userData._pendingFade : 1.0;", + " shader.uniforms.uFade = { value: pendingTrunk };", + " var hasUFadeUniform = /uniform\\s+float\\s+uFade\\s*;/.test(shader.vertexShader);", + " if (!hasUFadeUniform) {", + " if (shader.vertexShader.indexOf(\"varying float vFade;\") !== -1) {", + " shader.vertexShader = shader.vertexShader.replace(\"varying float vFade;\", \"uniform float uFade;\\nvarying float vFade;\");", + " } else {", + " shader.vertexShader = shader.vertexShader.replace(\"#include \", \"#include \\nuniform float uFade;\\nvarying float vFade;\");", + " }", + " }", + " if (shader.vertexShader.indexOf(\"vFade = uFade\") === -1) {", + " shader.vertexShader = shader.vertexShader.replace(/vFade\\s*=\\s*1\\.0\\s*;/, \"vFade = uFade;\");", + " if (shader.vertexShader.indexOf(\"vFade = uFade\") === -1 && shader.vertexShader.indexOf(\"#include \") !== -1) {", + " shader.vertexShader = shader.vertexShader.replace(\"#include \", \"#include \\nvFade = uFade;\");", + " }", + " }", + " if (shader.fragmentShader.indexOf(\"if (vFade < 0.01) discard\") === -1 && shader.fragmentShader.indexOf(\"#include \") !== -1) {", + " shader.fragmentShader = shader.fragmentShader.replace(\"#include \", \"#include \\nvarying float vFade;\");", + " shader.fragmentShader = shader.fragmentShader.replace(\"#include \", buildFadeOnlyFragment());", + " }", + " trunkFadeMat.userData.foliageUniforms = shader.uniforms;", + " };", + " trunkFadeMat.needsUpdate = true;", + " ensureFadeOnlyShadowMaterials(trunkFadeMat);", + " for (var trunkMeshIndex = 0; trunkMeshIndex < trunkMeshes.length; trunkMeshIndex++) {", + " var trunkMesh = trunkMeshes[trunkMeshIndex];", + " if (!trunkMesh || !trunkMesh.material || Array.isArray(trunkMesh.material)) continue;", + " if (trunkMesh.material === trunkMatFound) {", + " trunkMesh.material = trunkFadeMat;", + " assignShadowMaterialsToMesh(trunkMesh, trunkFadeMat);", + " }", + " }", + " entryTrunkMaterial = trunkFadeMat;", + " }", + " nonInstanced.upsertFadeEntry({", + " gdObj: gdObj,", + " threeObj: threeObj,", + " material: fadeMat,", + " trunkMaterial: entryTrunkMaterial,", + " fadeStart: fadeStart,", + " fadeEnd: fadeEnd,", + " fadeEnabled: !!distanceFadeEnabled,", + " fadeBehavior: behavior || null,", + " _parkedNoFade: false,", + " _firstHideWarmupDone: false", + " });", + " if (behavior) behavior.__foliageNonInstancedRegistered = true;", + " if (threeObj && threeObj.userData) threeObj.userData.__foliageNonInstancedRegistered = true;", + " }", + " }", + " ", + " behavior.__foliageSharedKey = key;", + " threeObj.userData = threeObj.userData || {};", + " threeObj.userData.__foliageSharedKey = key;", + " ", + " // For instancing cleanup (persist even if we hide this render tree)", + " if (behavior.__foliageInstancingGroupKey) {", + " threeObj.userData.__foliageInstancingGroupKey = behavior.__foliageInstancingGroupKey;", + " if (isFinite(behavior.__foliageInstancingIndex)) threeObj.userData.__foliageInstancingIndex = behavior.__foliageInstancingIndex;", + " else delete threeObj.userData.__foliageInstancingIndex;", + " } else {", + " delete threeObj.userData.__foliageInstancingGroupKey;", + " delete threeObj.userData.__foliageInstancingIndex;", + " }", + " ", + " ", + "" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Scene3D::Model3DObject", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NatureElements::FoliageSwaying", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "onDestroy", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + " /** @file onDestroy — Cleans up shared materials, instancing slots, and non-instanced entries. */", + " /** @typedef {{ __foliageSkipOnDestroy?: boolean, __foliageNonInstancedRegistered?: boolean, __foliageSharedKey?: string, __foliageSharedKeyTrunk?: string, __foliageInstancingGroupKey?: string, __foliageInstancingGroupKeyLeaves?: string, __foliageInstancingIndex?: number, __foliageQueued?: boolean, __foliageQueueId?: number }} FoliageBehaviorPrivateFieldsBridge */", + " /** @typedef {gdjs.RuntimeBehavior & FoliageBehaviorPrivateFieldsBridge} FoliageBehaviorBridge */", + " /** @typedef {{ material: THREE.Material, refCount: number }} FoliageSharedMaterialEntryBridge */", + " /** @typedef {{ key?: string, matrixCount?: number, aliveCount?: number, freeIndices?: number[], freeIndexSet?: Set, mesh?: THREE.InstancedMesh|null, _ownedGeometry?: THREE.BufferGeometry|null, matricesBuffer?: Float32Array|null, centersXY?: Float32Array|null, centersZ?: Float32Array|null, instanceFade?: Float32Array|null, instanceFadePrev?: Float32Array|null, _srcGeometryRef?: THREE.BufferGeometry|null, _instanceCullRadius?: number }} FoliageInstancingGroupBridge */", + " /** @typedef {{ groups: Map, dirty: boolean, queueIdCounter: number, cancelledQueueIds: Set, cancelQueueId: (queueId: number) => boolean, freeIndex: (groupKey: string, instanceIndex: number, leavesGroupKey?: string|null) => boolean }} FoliageInstancingStateBridge */", + " /** @typedef {{ removeForObject: (gdObj: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null })|null, threeObj: THREE.Object3D|null) => number }} FoliageNonInstancedRegistryBridge */", + " /** @typedef {{ _libReady?: boolean, sharedByKey: Map, instancingState: FoliageInstancingStateBridge, nonInstancedRegistry: FoliageNonInstancedRegistryBridge, unregisterActiveMaterial: (mat: THREE.Material|null|undefined) => void, disposeShadowMaterials: (mat: THREE.Material|null|undefined) => void }} FoliageSceneStateBridge */", + " /** @typedef {{ _libReady?: boolean, getSceneState: (runtimeScene: gdjs.RuntimeScene) => FoliageSceneStateBridge }} FoliageExtensionBridge */", + " /** @typedef {typeof gdjs & { _natureElementsFoliageSway?: FoliageExtensionBridge }} FoliageGdjsBridge */", + " var gdjsFoliage = /** @type {FoliageGdjsBridge} */ (gdjs);", + " var foliageExt = gdjsFoliage._natureElementsFoliageSway;", + " if (!foliageExt || !foliageExt._libReady) return;", + " /** @type {FoliageSceneStateBridge} */", + " var cache = foliageExt.getSceneState(runtimeScene);", + " if (!cache || !cache.sharedByKey) return;", + " ", + " /** @type {(gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null })|null|undefined} */", + " var gdObj = /** @type {(gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null })|null|undefined} */ (objects[0]);", + " if (!gdObj) return;", + " ", + " /** @type {FoliageBehaviorBridge|null} */", + " var behavior = /** @type {FoliageBehaviorBridge|null} */ (gdObj.getBehavior(\"FoliageSwaying\"));", + " var threeObj = gdObj.get3DRendererObject ? gdObj.get3DRendererObject() : null;", + " ", + " // If this object was auto-deleted right after being converted to a GPU instance,", + " // skip all instancing/material cleanup. The visual instance lives only in the", + " // InstancedMesh and is no longer tied to this GDevelop object.", + " if (behavior && behavior.__foliageSkipOnDestroy) {", + " delete behavior.__foliageSkipOnDestroy;", + " delete behavior.__foliageNonInstancedRegistered;", + " if (threeObj && threeObj.userData) delete threeObj.userData.__foliageNonInstancedRegistered;", + " return;", + " }", + " ", + " var key = null;", + " if (behavior && behavior.__foliageSharedKey) key = behavior.__foliageSharedKey;", + " if (!key && threeObj && threeObj.userData) key = threeObj.userData.__foliageSharedKey;", + " ", + " // Non-instanced entries can be in active fade list or parked static list.", + " cache.nonInstancedRegistry.removeForObject(gdObj, threeObj);", + " ", + " // GPU instancing cleanup (if this object was converted into an InstancedMesh instance)", + " var instKey = null;", + " var instKeyLeaves = null;", + " var instIndex = null;", + " ", + " if (behavior && behavior.__foliageInstancingGroupKey) instKey = behavior.__foliageInstancingGroupKey;", + " if (behavior && behavior.__foliageInstancingGroupKeyLeaves) instKeyLeaves = behavior.__foliageInstancingGroupKeyLeaves;", + " if (behavior && isFinite(behavior.__foliageInstancingIndex)) instIndex = behavior.__foliageInstancingIndex;", + " ", + " if ((!instKey || instIndex === null) && threeObj && threeObj.userData) {", + " if (threeObj.userData.__foliageInstancingGroupKey) instKey = threeObj.userData.__foliageInstancingGroupKey;", + " if (threeObj.userData.__foliageInstancingGroupKeyLeaves) instKeyLeaves = threeObj.userData.__foliageInstancingGroupKeyLeaves;", + " if (isFinite(threeObj.userData.__foliageInstancingIndex)) instIndex = threeObj.userData.__foliageInstancingIndex;", + " }", + " ", + " // Edge case: object destroyed while still queued for the next instancing flush.", + " // Mark queueId as cancelled so flushPending can skip the item and roll back refCounts.", + " // Return here to avoid double decrement; the pending flush path owns that rollback.", + " if (instIndex === null && behavior && behavior.__foliageQueued === true) {", + " var queueId = behavior.__foliageQueueId;", + " if (queueId !== undefined && isFinite(queueId)) {", + " cache.instancingState.cancelQueueId(queueId);", + " }", + " // Clean up queued flags and related keys (two-part path also clears the leaves key).", + " delete behavior.__foliageQueued;", + " delete behavior.__foliageQueueId;", + " delete behavior.__foliageNonInstancedRegistered;", + " if (behavior.__foliageInstancingGroupKey) delete behavior.__foliageInstancingGroupKey;", + " if (behavior.__foliageInstancingGroupKeyLeaves) delete behavior.__foliageInstancingGroupKeyLeaves;", + " if (threeObj && threeObj.userData) {", + " if (threeObj.userData.__foliageInstancingGroupKey) delete threeObj.userData.__foliageInstancingGroupKey;", + " if (threeObj.userData.__foliageInstancingGroupKeyLeaves) delete threeObj.userData.__foliageInstancingGroupKeyLeaves;", + " delete threeObj.userData.__foliageNonInstancedRegistered;", + " if (threeObj.userData.__foliageQueueId !== undefined) delete threeObj.userData.__foliageQueueId;", + " }", + " // Return early; the pending flush path will handle refCount rollback to avoid double decrement.", + " return;", + " }", + " ", + " if (instKey && instIndex !== null) {", + " try {", + " cache.instancingState.freeIndex(instKey, instIndex, instKeyLeaves || null);", + " } catch (eInst) {}", + " }", + " ", + " // Main shared key owns the primary foliage material. Split-tree setups may also", + " // keep a second shared key for the trunk material, which is released separately below.", + " if (key) {", + " var entry = cache.sharedByKey.get(key);", + " if (entry) {", + " entry.refCount--;", + " if (entry.refCount <= 0) {", + " cache.sharedByKey.delete(key);", + " ", + " cache.unregisterActiveMaterial(entry.material);", + " cache.disposeShadowMaterials(entry.material);", + " // Release GPU material resources when shared material refCount reaches zero.", + " if (entry.material && typeof entry.material.dispose === \"function\") {", + " try { entry.material.dispose(); } catch (eDispose) {}", + " }", + " }", + " }", + " }", + " ", + " var keyTrunk = null;", + " if (behavior && behavior.__foliageSharedKeyTrunk) keyTrunk = behavior.__foliageSharedKeyTrunk;", + " if (!keyTrunk && threeObj && threeObj.userData) keyTrunk = threeObj.userData.__foliageSharedKeyTrunk;", + " // Split leavesSway trees can use a second shared material entry for trunk-only fade/shadow handling.", + " if (keyTrunk) {", + " var entryTrunk = cache.sharedByKey.get(keyTrunk);", + " if (entryTrunk) {", + " entryTrunk.refCount--;", + " if (entryTrunk.refCount <= 0) {", + " cache.sharedByKey.delete(keyTrunk);", + " cache.unregisterActiveMaterial(entryTrunk.material);", + " cache.disposeShadowMaterials(entryTrunk.material);", + " if (entryTrunk.material && typeof entryTrunk.material.dispose === \"function\") {", + " try { entryTrunk.material.dispose(); } catch (eDisposeTrunk) {}", + " }", + " }", + " }", + " }", + " ", + " if (behavior) {", + " delete behavior.__foliageSharedKey;", + " delete behavior.__foliageNonInstancedRegistered;", + " delete behavior.__foliageInstancingGroupKey;", + " delete behavior.__foliageInstancingIndex;", + " delete behavior.__foliageQueued;", + " delete behavior.__foliageQueueId;", + " delete behavior.__foliageSharedKeyTrunk;", + " delete behavior.__foliageInstancingGroupKeyLeaves;", + " }", + " if (threeObj && threeObj.userData) {", + " delete threeObj.userData.__foliageSharedKey;", + " delete threeObj.userData.__foliageNonInstancedRegistered;", + " delete threeObj.userData.__foliageInstancingGroupKey;", + " delete threeObj.userData.__foliageInstancingIndex;", + " delete threeObj.userData.__foliageQueueId;", + " delete threeObj.userData.__foliageSharedKeyTrunk;", + " delete threeObj.userData.__foliageInstancingGroupKeyLeaves;", + " }", + " ", + " ", + "" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Scene3D::Model3DObject", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NatureElements::FoliageSwaying", + "type": "behavior" + } + ], + "objectGroups": [] + } + ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "onDestroy" + } + ] + }, + "propertyDescriptors": [ + { + "value": "", + "type": "Boolean", + "label": "Color grading", + "description": "Enables top/bottom gradient tint and color grading (default: off)", + "group": "01 / Color grading", + "name": "_useColorGrading" + }, + { + "value": "", + "type": "Color", + "label": "Bottom color", + "description": "Bottom gradient color", + "group": "01 / Color grading", + "name": "colorBottom" + }, + { + "value": "", + "type": "Color", + "label": "Top color", + "description": "Top gradient color", + "group": "01 / Color grading", + "name": "colorTop" + }, + { + "value": "", + "type": "Number", + "label": "Gradient start", + "description": "Gradient start point (0-1; default: 0)", + "group": "01 / Color grading", + "name": "gradStart" + }, + { + "value": "", + "type": "Number", + "label": "Gradient end", + "description": "Gradient end point (0-1; default: 1)", + "group": "01 / Color grading", + "name": "gradEnd" + }, + { + "value": "1.05", + "type": "Number", + "label": "Color contrast", + "description": "Color contrast multiplier (0-3; default: 1.05)", + "group": "01 / Color grading", + "name": "uContrast" + }, + { + "value": "1.2", + "type": "Number", + "label": "Color saturation", + "description": "Color saturation multiplier (0-3; default: 1.2)", + "group": "01 / Color grading", + "name": "uSat" + }, + { + "value": "", + "type": "String", + "label": "Material to sway", + "description": "Material name to target. (empty = auto-pick)", + "group": "02 / Sway settings", + "name": "materialName" + }, + { + "value": "true", + "type": "Boolean", + "label": "Enable uniform sway", + "description": "If enabled, all instances sway similarly. If off, each gets random phase variation.", + "group": "02 / Sway settings", + "name": "uniformSway" + }, + { + "value": "grassSway", + "type": "Choice", + "label": "Sway type", + "description": "Wind animation model.", + "group": "02 / Sway settings", + "choices": [ + { + "label": "Grass sway", + "value": "grassSway" + }, + { + "label": "Leaves sway", + "value": "leavesSway" + }, + { + "label": "Bush sway", + "value": "bushSway" + }, + { + "label": "Tree trunk sway (dead trees only)", + "value": "treeTrunkSway" + } + ], + "name": "swayType" + }, + { + "value": "false", + "type": "Boolean", + "label": "Debug output", + "description": "Prints material/mesh and auto-scale debug info to console. (requires object to be in the scene; default: off)", + "group": "04 / Object settings", + "name": "debugOutput" + }, + { + "value": "0", + "type": "Number", + "label": "Poly scale", + "description": "Sorts unexpected visual behavior. For low-poly models use smaller number. (0-200; 0 = auto)", + "group": "04 / Object settings", + "name": "polyScale" + }, + { + "value": "", + "type": "Boolean", + "label": "Ignore UV map", + "description": "Uses world-height gradient instead of UV gradient (only when color grading is on; default: off).", + "group": "01 / Color grading", + "name": "ignoreUV" + }, + { + "value": "", + "type": "Boolean", + "label": "GPU instancing", + "description": "Uses GPU instancing for supported sway types. (better performance; default: off).", + "group": "04 / Object settings", + "name": "gpuInstancing" + }, + { + "value": "", + "type": "Boolean", + "label": "Distance culling", + "description": "Enables distance-based dither fade/culling. (default: off)", + "group": "03 / Cull settings", + "name": "distanceFadeEnabled" + }, + { + "value": "1200", + "type": "Number", + "label": "Fade distance start", + "description": "Distance where fade-out starts. (0-100000; default: 1200)", + "group": "03 / Cull settings", + "name": "fadeStart" + }, + { + "value": "1600", + "type": "Number", + "label": "Fade distance end", + "description": "Distance where fade reaches zero. (0-100000; default: 1600)", + "group": "03 / Cull settings", + "name": "fadeEnd" + }, + { + "value": "", + "type": "Boolean", + "label": "Custom PBR settings", + "description": "Enables custom PBR tuning controls. (default: off)", + "group": "05 / Custom PBR (experimental)", + "advanced": true, + "name": "_customLit" + }, + { + "value": "0", + "type": "Number", + "label": "Metallic factor", + "description": "PBR metallic value. (0-1; default: 0)", + "group": "05 / Custom PBR (experimental)", + "advanced": true, + "name": "metallic" + }, + { + "value": "1", + "type": "Number", + "label": "Roughness factor", + "description": "PBR roughness value. (0-1; default: 1)", + "group": "05 / Custom PBR (experimental)", + "advanced": true, + "name": "roughness" + }, + { + "value": "0.1", + "type": "Number", + "label": "Specular factor", + "description": "Specular intensity. (where supported; 0-1; default: 0.1)", + "group": "05 / Custom PBR (experimental)", + "advanced": true, + "name": "specular" + }, + { + "value": "1", + "type": "Number", + "label": "Normal strength", + "description": "Normal map strength. (0-1; default: 1)", + "group": "05 / Custom PBR (experimental)", + "advanced": true, + "name": "normalStrength" + }, + { + "value": "1", + "type": "Number", + "label": "Ambient Occlusion strength", + "description": "Ambient occlusion intensity. (0-1; default: 1)", + "group": "05 / Custom PBR (experimental)", + "advanced": true, + "name": "aoStrength" + }, + { + "value": "1", + "type": "Number", + "label": "Environment strength", + "description": "Environment reflection intensity. (0-1; default: 1)", + "group": "05 / Custom PBR (experimental)", + "advanced": true, + "name": "envStrength" + }, + { + "value": "useSource", + "type": "Choice", + "label": "Culling mode", + "description": "3D model face culling mode.", + "group": "03 / Cull settings", + "choices": [ + { + "label": "Use object defaults", + "value": "useSource" + }, + { + "label": "Backface culling on", + "value": "backfaceCullingOn" + }, + { + "label": "Backface culling off", + "value": "backfaceCullingOff" + } + ], + "name": "cullingMode" + }, + { + "value": "true", + "type": "Boolean", + "label": "", + "description": "If enabled, both sides are lit, mainly for planes/cards (default: on)", + "group": "01 / Color grading", + "name": "twoSidedLighting" + } + ], + "propertiesFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "folderName": "01 / Color grading", + "children": [ + { + "propertyName": "twoSidedLighting" + }, + { + "propertyName": "_useColorGrading" + }, + { + "propertyName": "colorBottom" + }, + { + "propertyName": "colorTop" + }, + { + "propertyName": "gradStart" + }, + { + "propertyName": "gradEnd" + }, + { + "propertyName": "uContrast" + }, + { + "propertyName": "uSat" + }, + { + "propertyName": "ignoreUV" + } + ] + }, + { + "folderName": "02 / Sway settings", + "children": [ + { + "propertyName": "swayType" + }, + { + "propertyName": "materialName" + }, + { + "propertyName": "uniformSway" + } + ] + }, + { + "folderName": "03 / Cull settings", + "children": [ + { + "propertyName": "cullingMode" + }, + { + "propertyName": "distanceFadeEnabled" + }, + { + "propertyName": "fadeStart" + }, + { + "propertyName": "fadeEnd" + } + ] + }, + { + "folderName": "04 / Object settings", + "children": [ + { + "propertyName": "debugOutput" + }, + { + "propertyName": "polyScale" + }, + { + "propertyName": "gpuInstancing" + } + ] + }, + { + "folderName": "05 / Custom PBR (experimental)", + "children": [ + { + "propertyName": "_customLit" + }, + { + "propertyName": "metallic" + }, + { + "propertyName": "roughness" + }, + { + "propertyName": "specular" + }, + { + "propertyName": "normalStrength" + }, + { + "propertyName": "aoStrength" + }, + { + "propertyName": "envStrength" + } + ] + } + ] + } + } + ], + "eventsBasedObjects": [] +}