From 4fcf1e346d5bf1c55fa199a2b205837c8d4acf60 Mon Sep 17 00:00:00 2001 From: baldieandbaldie-commits Date: Fri, 6 Mar 2026 20:02:56 +0000 Subject: [PATCH] Automated Extension submission for issue #2058 --- extensions/community/NatureElements.json | 6396 ++++++++++++++++++++++ 1 file changed, 6396 insertions(+) create mode 100644 extensions/community/NatureElements.json diff --git a/extensions/community/NatureElements.json b/extensions/community/NatureElements.json new file mode 100644 index 000000000..3929b7dac --- /dev/null +++ b/extensions/community/NatureElements.json @@ -0,0 +1,6396 @@ +{ + "author": "Your Name", + "category": "3D", + "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.0", + "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 tileable 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.", + "" + ], + "tags": [ + "3D", + "shader", + "foliage", + "wind", + "sway", + "animation", + "tree", + "grass", + "vegetation", + "nature", + "cozy" + ], + "authorIds": [ + "VNp7UkcF59OvG8pYLklh6IE3tBX2" + ], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "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": [ + "(function(runtimeScene, eventsFunctionContext) {", + " var cache = window.__FOLIAGE_SWAY__;", + " if (!cache) return;", + " ", + " var recreatedActiveSet = false;", + " if (!cache.activeMaterials || typeof cache.activeMaterials[Symbol.iterator] !== \"function\") {", + " cache.activeMaterials = new Set();", + " recreatedActiveSet = true;", + " }", + " 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).", + " var fadeHzArg = Number(eventsFunctionContext.getArgument(\"fadeUpdateHz\"));", + " if (isFinite(fadeHzArg)) {", + " if (fadeHzArg < 1) fadeHzArg = 1;", + " if (fadeHzArg > 120) fadeHzArg = 120;", + " cache.fadeUpdateHz = fadeHzArg;", + " }", + "", + " // Distance fade cullTick timing", + " 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) {", + " // var fadeIntervalMsDbg = 1000.0 / cache.fadeUpdateHz;", + " // var frameSkip60Dbg = Math.max(0, Math.round(60.0 / cache.fadeUpdateHz) - 1);", + " // console.log(\"[FoliageSway][Update] Distance fade tick: \" + cache.fadeUpdateHz + \" Hz (\" + fadeIntervalMsDbg.toFixed(1) + \" ms), frameSkip@60fps≈\" + frameSkip60Dbg);", + " cache._lastLoggedFadeHz = cache.fadeUpdateHz;", + " }", + " if (!isFinite(cache.lastCullTime)) cache.lastCullTime = 0;", + " // Cache cull interval and recompute only when fadeUpdateHz changes.", + " // Recompute interval 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;", + " if (!cache.nonInstancedFadeObjects || !Array.isArray(cache.nonInstancedFadeObjects)) cache.nonInstancedFadeObjects = [];", + " if (!cache.nonInstancedStaticObjects || !Array.isArray(cache.nonInstancedStaticObjects)) cache.nonInstancedStaticObjects = [];", + " if (!cache.autoPolyScaleCache || typeof cache.autoPolyScaleCache.get !== \"function\") cache.autoPolyScaleCache = new Map();", + " if (!isFinite(cache._nonInstancedStaticCheckAccum)) cache._nonInstancedStaticCheckAccum = 0;", + "", + " if (typeof cache._parseBoolRuntime !== \"function\") {", + " cache._parseBoolRuntime = function(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;", + " };", + " }", + " if (typeof cache._readClampedRuntime !== \"function\") {", + " cache._readClampedRuntime = function(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;", + " };", + " }", + " if (typeof cache._clearNonInstancedRegistration !== \"function\") {", + " cache._clearNonInstancedRegistration = function(entryNi) {", + " if (!entryNi) return;", + " var bNi = null;", + " try {", + " if (entryNi.gdObj && entryNi.gdObj.getBehavior) {", + " bNi = entryNi.gdObj.getBehavior(\"FoliageSwaying\");", + " }", + " } catch (eB0) {}", + " if (!bNi) {", + " try {", + " if (entryNi.gdObj && entryNi.gdObj.getBehavior) {", + " bNi = entryNi.gdObj.getBehavior(\"NatureElements::FoliageSwaying\");", + " }", + " } catch (eB1) {}", + " }", + " if (bNi && bNi.__foliageNonInstancedRegistered) delete bNi.__foliageNonInstancedRegistered;", + " if (entryNi.threeObj && entryNi.threeObj.userData) delete entryNi.threeObj.userData.__foliageNonInstancedRegistered;", + " };", + " }", + " var parseBoolRuntime = cache._parseBoolRuntime;", + " var readClampedRuntime = cache._readClampedRuntime;", + " var clearNonInstancedRegistration = cache._clearNonInstancedRegistration;", + "", + " if (!cache._materialBuckets || typeof cache._materialBuckets !== \"object\") {", + " cache._materialBuckets = {", + " timeWind: new Set(),", + " fadeInterp: new Set(),", + " gust: new Set(),", + " shadowHost: new Set(),", + " unknown: new Set(),", + " rebuildNeeded: true", + " };", + " }", + " if (typeof cache._resetMaterialBuckets !== \"function\") {", + " cache._resetMaterialBuckets = function() {", + " cache._materialBuckets = {", + " timeWind: new Set(),", + " fadeInterp: new Set(),", + " gust: new Set(),", + " shadowHost: new Set(),", + " unknown: new Set(),", + " rebuildNeeded: true", + " };", + " };", + " }", + " if (typeof cache._registerActiveMaterial !== \"function\") {", + " cache._registerActiveMaterial = function(mat) {", + " if (!mat) return;", + " if (!cache.activeMaterials || typeof cache.activeMaterials.add !== \"function\") {", + " cache.activeMaterials = new Set();", + " }", + " cache.activeMaterials.add(mat);", + " var b = cache._materialBuckets;", + " if (b && b.unknown && typeof b.unknown.add === \"function\") b.unknown.add(mat);", + " };", + " }", + " if (typeof cache._unregisterActiveMaterial !== \"function\") {", + " cache._unregisterActiveMaterial = function(mat) {", + " if (!mat) return;", + " if (cache.activeMaterials && typeof cache.activeMaterials.delete === \"function\") {", + " cache.activeMaterials.delete(mat);", + " }", + " var b = cache._materialBuckets;", + " if (!b) return;", + " if (b.timeWind) b.timeWind.delete(mat);", + " if (b.fadeInterp) b.fadeInterp.delete(mat);", + " if (b.gust) b.gust.delete(mat);", + " if (b.shadowHost) b.shadowHost.delete(mat);", + " if (b.unknown) b.unknown.delete(mat);", + " };", + " }", + " if (recreatedActiveSet && typeof cache._resetMaterialBuckets === \"function\") {", + " cache._resetMaterialBuckets();", + " }", + " function unregisterActiveMaterial(mat) {", + " if (!mat) return;", + " if (typeof cache._unregisterActiveMaterial === \"function\") cache._unregisterActiveMaterial(mat);", + " else if (cache.activeMaterials && typeof cache.activeMaterials.delete === \"function\") cache.activeMaterials.delete(mat);", + " }", + " if (typeof cache._markGustMaterialsForRecompile !== \"function\") {", + " cache._markGustMaterialsForRecompile = function() {", + " if (!cache.activeMaterials || typeof cache.activeMaterials.forEach !== \"function\") return;", + " cache.activeMaterials.forEach(function(mat) {", + " if (!mat) return;", + " mat.needsUpdate = true;", + " var ud = mat.userData;", + " if (!ud) return;", + " var depthMat = ud._foliageDepthMat;", + " var distanceMat = ud._foliageDistanceMat;", + " if (depthMat) depthMat.needsUpdate = true;", + " if (distanceMat) distanceMat.needsUpdate = true;", + " });", + " };", + " }", + " if (cache._gustDefineDirty) {", + " cache._markGustMaterialsForRecompile();", + " cache._gustDefineDirty = false;", + " }", + "", + " // Ensure instancing manager exists (used by GPU instancing path)", + " if (!cache.instancing || typeof cache.instancing !== \"object\") {", + " cache.instancing = {", + " groups: new Map(),", + " dirty: false,", + " sceneTag: null,", + " pending: [],", + " queueIdCounter: 0,", + " cancelledQueueIds: new Set(),", + " _tmpMat4: new THREE.Matrix4(),", + " _tmpObj3D: new THREE.Object3D(),", + " _tmpVec3_pos: new THREE.Vector3(),", + " _tmpVec3_scale: new THREE.Vector3(1,1,1),", + " _tmpEuler: new THREE.Euler(0,0,0,'XYZ'),", + " _tmpQuat: new THREE.Quaternion(),", + " _tmpMat4_objWorld: new THREE.Matrix4(),", + " _tmpMat4_objWorldInv: new THREE.Matrix4(),", + " _tmpMat4_repWorld: new THREE.Matrix4(),", + " _repRelByMesh: new WeakMap(),", + " _itemsByParentRepMesh: new Map(),", + " _repRelMatrixCache: new Map(),", + " foliageRoot: null,", + " _cachedSceneRoot: null", + " };", + " } else {", + " if (!cache.instancing.groups || typeof cache.instancing.groups.get !== \"function\") {", + " cache.instancing.groups = new Map();", + " }", + " if (typeof cache.instancing.dirty !== \"boolean\") cache.instancing.dirty = false;", + " if (cache.instancing.sceneTag === undefined) cache.instancing.sceneTag = null;", + " if (!cache.instancing._tmpMat4) cache.instancing._tmpMat4 = new THREE.Matrix4();", + " if (!Array.isArray(cache.instancing.pending)) cache.instancing.pending = [];", + " if (!cache.instancing._tmpObj3D) cache.instancing._tmpObj3D = new THREE.Object3D();", + " if (!cache.instancing._tmpVec3_pos) cache.instancing._tmpVec3_pos = new THREE.Vector3();", + " if (!cache.instancing._tmpVec3_scale) cache.instancing._tmpVec3_scale = new THREE.Vector3(1,1,1);", + " if (!cache.instancing._tmpEuler) cache.instancing._tmpEuler = new THREE.Euler(0,0,0,'XYZ');", + " if (!cache.instancing._tmpQuat) cache.instancing._tmpQuat = new THREE.Quaternion();", + " if (!cache.instancing._tmpMat4_objWorld) cache.instancing._tmpMat4_objWorld = new THREE.Matrix4();", + " if (!cache.instancing._tmpMat4_objWorldInv) cache.instancing._tmpMat4_objWorldInv = new THREE.Matrix4();", + " if (!cache.instancing._tmpMat4_repWorld) cache.instancing._tmpMat4_repWorld = new THREE.Matrix4();", + " if (!cache.instancing._repRelByMesh) cache.instancing._repRelByMesh = new WeakMap();", + " if (!cache.instancing._itemsByParentRepMesh || typeof cache.instancing._itemsByParentRepMesh.clear !== \"function\") {", + " cache.instancing._itemsByParentRepMesh = new Map();", + " }", + " if (!cache.instancing._repRelMatrixCache || typeof cache.instancing._repRelMatrixCache.clear !== \"function\") {", + " cache.instancing._repRelMatrixCache = new Map();", + " }", + " if (typeof cache.instancing.queueIdCounter !== \"number\") cache.instancing.queueIdCounter = 0;", + " if (!cache.instancing.cancelledQueueIds || typeof cache.instancing.cancelledQueueIds.has !== \"function\") {", + " cache.instancing.cancelledQueueIds = new Set();", + " }", + " }", + "", + " if (typeof cache._disposeFoliageShadowMaterials !== \"function\") {", + " cache._disposeFoliageShadowMaterials = function(mat) {", + " if (!mat || !mat.userData) return;", + " var ud = mat.userData;", + " var depthMat = ud._foliageDepthMat;", + " var distanceMat = ud._foliageDistanceMat;", + " if (depthMat && depthMat !== mat && typeof depthMat.dispose === \"function\") {", + " try { depthMat.dispose(); } catch (eDepthDispose) {}", + " }", + " if (distanceMat && distanceMat !== mat && distanceMat !== depthMat && typeof distanceMat.dispose === \"function\") {", + " try { distanceMat.dispose(); } catch (eDistanceDispose) {}", + " }", + " delete ud._foliageDepthMat;", + " delete ud._foliageDistanceMat;", + " delete ud._foliageShadowOwned;", + " };", + " }", + "", + " // Shared cleanup helper for scene changes. Attached to cache so onCreated/update (separate IIFEs)", + " // can call identical logic and avoid divergence.", + " if (typeof cache._cleanupForSceneChange !== \"function\") {", + " cache._cleanupForSceneChange = function(cacheObj) {", + " if (!cacheObj || !cacheObj.instancing) return;", + " var instCleanup = cacheObj.instancing;", + "", + " // Dispose all groups (owned resources only)", + " if (instCleanup.groups && typeof instCleanup.groups.forEach === \"function\") {", + " instCleanup.groups.forEach(function(g) {", + " if (!g) return;", + " try {", + " if (g.mesh) {", + " if (g.mesh.parent) g.mesh.parent.remove(g.mesh);", + " // Dispose CLONED geometry (owned), NOT source geometry", + " 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 (eRm) {}", + " g.mesh = null;", + " g._ownedGeometry = null;", + " g.matricesBuffer = null;", + " g.centersXY = null;", + " g.centersZ = null;", + " g.instanceFade = null;", + " g.instanceFadePrev = null;", + " // Backward-compat cleanup for old fade buffers", + " g.fadeBuffer = null;", + " g.prevFadeBuffer = null;", + " });", + " instCleanup.groups.clear();", + " }", + "", + " // Remove foliageRoot from scene", + " if (instCleanup.foliageRoot) {", + " try {", + " if (instCleanup.foliageRoot.parent) instCleanup.foliageRoot.parent.remove(instCleanup.foliageRoot);", + " } catch (eRoot) {}", + " instCleanup.foliageRoot = null;", + " }", + "", + " // Dispose foliage-owned materials; dedupe in case same ref exists in both maps/sets.", + " var seenMaterials = new Set();", + " if (cacheObj.sharedByKey && typeof cacheObj.sharedByKey.forEach === \"function\") {", + " cacheObj.sharedByKey.forEach(function(entry) {", + " if (!entry || entry._ownedByFoliage === false || !entry.material) return;", + " var mat = entry.material;", + " if (seenMaterials.has(mat)) return;", + " seenMaterials.add(mat);", + " if (typeof cacheObj._disposeFoliageShadowMaterials === \"function\") {", + " cacheObj._disposeFoliageShadowMaterials(mat);", + " }", + " if (typeof mat.dispose === \"function\") {", + " try { mat.dispose(); } catch (eMat) {}", + " }", + " });", + " if (typeof cacheObj.sharedByKey.clear === \"function\") cacheObj.sharedByKey.clear();", + " }", + " if (cacheObj.activeMaterials && typeof cacheObj.activeMaterials.forEach === \"function\") {", + " cacheObj.activeMaterials.forEach(function(mat) {", + " if (!mat || seenMaterials.has(mat)) return;", + " seenMaterials.add(mat);", + " if (typeof cacheObj._disposeFoliageShadowMaterials === \"function\") {", + " cacheObj._disposeFoliageShadowMaterials(mat);", + " }", + " if (typeof mat.dispose === \"function\") {", + " try { mat.dispose(); } catch (eMat2) {}", + " }", + " });", + " if (typeof cacheObj.activeMaterials.clear === \"function\") cacheObj.activeMaterials.clear();", + " }", + " if (typeof cacheObj._resetMaterialBuckets === \"function\") cacheObj._resetMaterialBuckets();", + " cacheObj.nonInstancedFadeObjects = [];", + " cacheObj.nonInstancedStaticObjects = [];", + " cacheObj._nonInstancedStaticCheckAccum = 0;", + "", + " // Dispose current gust texture if it's not the shared fallback.", + " if (cacheObj.gustTexture && cacheObj.gustTexture !== cacheObj.gustFallbackTex) {", + " try { cacheObj.gustTexture.dispose(); } catch (eGust) {}", + " cacheObj.gustTexture = null;", + " }", + " cacheObj.gustTextureKey = \"\";", + "", + " // Reset queue state and transient relation caches.", + " instCleanup.cancelledQueueIds = new Set();", + " instCleanup.queueIdCounter = 0;", + " instCleanup.pending = [];", + " instCleanup._repRelByMesh = new WeakMap();", + " instCleanup._repRelMatrixCache = new Map();", + " instCleanup._itemsByParentRepMesh = new Map();", + " instCleanup._cachedSceneRoot = null;", + " instCleanup._tmpRootSet = null;", + " instCleanup.dirty = false;", + " cacheObj._frustumShared = null;", + " cacheObj._projViewMatrixShared = null;", + " cacheObj._frameCameraSnapshot = null;", + " cacheObj._tmpVec3InstancedCull = null;", + " cacheObj._tmpSphereInstancedCull = null;", + " cacheObj._tmpVec3InstancedCamDir = null;", + " cacheObj._tmpVec3FrustumCamDir = null;", + " cacheObj._prevCullCamDirX = undefined;", + " cacheObj._prevCullCamDirY = undefined;", + " cacheObj._prevCullCamDirZ = undefined;", + " cacheObj._prevCullCamZ = undefined;", + " cacheObj._prevFrustumPassCamX = undefined;", + " cacheObj._prevFrustumPassCamY = undefined;", + " cacheObj._prevFrustumPassCamZ = undefined;", + " cacheObj._prevFrustumPassCamDirX = undefined;", + " cacheObj._prevFrustumPassCamDirY = undefined;", + " cacheObj._prevFrustumPassCamDirZ = undefined;", + "", + " // WeakSet can't be cleared; replace to drop stale references.", + " cacheObj.patchedMaterials = new WeakSet();", + "", + " // Clear existing caches", + " if (cacheObj.objectTypeCache && typeof cacheObj.objectTypeCache.clear === \"function\") cacheObj.objectTypeCache.clear();", + " if (cacheObj.autoPolyScaleCache && typeof cacheObj.autoPolyScaleCache.clear === \"function\") cacheObj.autoPolyScaleCache.clear();", + " if (cacheObj.debugPrinted && typeof cacheObj.debugPrinted.clear === \"function\") cacheObj.debugPrinted.clear();", + " cacheObj.geometryBBoxCache = new WeakMap();", + " cacheObj._objectTypeCacheState = { tick: 0, setCount: 0, meta: new Map() };", + " cacheObj._tmpVec3NonInstancedFade = null;", + " };", + " }", + "", + " // Scene change: full cleanup of foliageRoot, InstancedMeshes, storage arrays, caches", + " var currentSceneTag = (runtimeScene && typeof runtimeScene.getName === \"function\") ? runtimeScene.getName() : null;", + " if (cache.instancing && cache.instancing.sceneTag !== currentSceneTag) {", + " if (typeof cache._cleanupForSceneChange === \"function\") {", + " cache._cleanupForSceneChange(cache);", + " }", + " if (cache.instancing) cache.instancing.sceneTag = currentSceneTag;", + " }", + "", + " function flushPendingInstancing() {", + " var inst = cache.instancing;", + " if (!inst || !Array.isArray(inst.pending) || inst.pending.length === 0) return;", + "", + " var list = inst.pending;", + " inst.pending = [];", + " ", + " var listLen = list.length;", + " if (listLen === 0) return;", + "", + " // Create FoliageRoot if needed (shared parent for all InstancedMeshes)", + " 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 = runtimeScene.getLayer(\"\");", + " if (layer && layer.getRenderer() && layer.getRenderer().getThreeScene) {", + " sceneRoot = layer.getRenderer().getThreeScene();", + " }", + " } catch(e) {}", + " }", + " if (sceneRoot) {", + " sceneRoot.add(inst.foliageRoot);", + " inst._cachedSceneRoot = sceneRoot;", + " }", + " }", + "", + " // Update matrixWorld only for unique scene roots referenced by pending items.", + " // This keeps correctness (upward chain freshness) without forcing a full-scene update per flush.", + " 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 rItem = list[ri];", + " if (!rItem || !rItem.threeObj) continue;", + " var root = rItem.threeObj;", + " while (root && root.parent) root = root.parent;", + " if (root) rootSet.add(root);", + " }", + " // Fallback for edge cases where pending items have no threeObj root.", + " if (rootSet.size === 0 && inst._cachedSceneRoot) {", + " rootSet.add(inst._cachedSceneRoot);", + " }", + " rootSet.forEach(function(r) {", + " try { r.updateMatrixWorld(true); } catch (e) {}", + " });", + "", + " // Reuse maps between flushes to reduce GC churn.", + " var itemsByParentRepMesh = inst._itemsByParentRepMesh;", + " var repRelMatrixCache = inst._repRelMatrixCache;", + " itemsByParentRepMesh.clear();", + " repRelMatrixCache.clear();", + " ", + " // Optimization: reuse Matrix4 objects instead of allocating per item.", + " var localM = inst._tmpMat4_local;", + " if (!localM) {", + " localM = new THREE.Matrix4();", + " inst._tmpMat4_local = localM;", + " }", + "", + " // FoliageRoot inverse: convert world matrices to parent-local so InstancedMesh renders correctly.", + " // Scene root may have non-identity transform (e.g. Y-flip for GDevelop Y-down → Three.js Y-up).", + " 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();", + " }", + "", + " // Optimization: avoid updateMatrixWorld per object.", + " // Instead, build world matrix from GDevelop transform and use cached repMesh relation.", + " var DEG2RAD = Math.PI / 180;", + "", + " function 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;", + " // Prefer 3D rotations if available", + " 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) {}", + " // Fallback to 2D angle if 3D rotation is not provided", + " 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 * DEG2RAD, ry * DEG2RAD, rz * DEG2RAD, 'XYZ');", + " q.setFromEuler(eul);", + "", + " outMat.compose(pos, q, scl);", + " return outMat;", + " }", + "", + " function getOrComputeRepRelMatrix(inst, gdObj, repMesh) {", + " 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;", + "", + " // Compute once using real repMesh.matrixWorld (may require a one-time updateMatrixWorld).", + " try { repMesh.updateMatrixWorld(true); } catch (e0) {}", + "", + " var objW = inst._tmpMat4_objWorld;", + " var objWi = inst._tmpMat4_objWorldInv;", + " 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;", + " }", + "", + " // Batch items by representative mesh to avoid repeated relation lookups.", + " for (var k = 0; k < listLen; k++) {", + " var item = list[k];", + " if (!item) continue;", + " ", + " // Skip cancelled items", + " 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 key = \"RM:\" + repMeshId;", + " ", + " if (!itemsByParentRepMesh.has(key)) {", + " itemsByParentRepMesh.set(key, []);", + " }", + " itemsByParentRepMesh.get(key).push(item);", + " }", + "", + " // Precompute one repRelMatrix per batch key.", + " for (var key of itemsByParentRepMesh.keys()) {", + " var items = itemsByParentRepMesh.get(key);", + " if (!items || items.length === 0) continue;", + " ", + " var firstItem = items[0];", + " var repMesh = firstItem.repMesh;", + " var gdObj = firstItem.gdObj;", + " ", + " if (!repMesh || !gdObj) continue;", + " ", + " // Precompute repRelMatrix once per group", + " var repRelMatrix = getOrComputeRepRelMatrix(inst, gdObj, repMesh);", + " repRelMatrixCache.set(key, repRelMatrix);", + " }", + "", + " for (var k = 0; k < listLen; k++) {", + " var item = list[k];", + " if (!item) continue;", + "", + " // Skip cancelled items (object destroyed before flush)", + " if (item.queueId !== undefined && inst.cancelledQueueIds && inst.cancelledQueueIds.has(item.queueId)) {", + " inst.cancelledQueueIds.delete(item.queueId); // Clean up set to prevent growth", + " // Rollback refCount if it was incremented in fast path (prevents memory leak)", + " if (item.behavior && item.behavior.__foliageSharedKey) {", + " var cancelledEntry = cache.sharedByKey.get(item.behavior.__foliageSharedKey);", + " if (cancelledEntry && cancelledEntry.refCount > 0) {", + " cancelledEntry.refCount--;", + " // If refCount reached 0, do cleanup (object is already destroyed, onDestroy won't run)", + " if (cancelledEntry.refCount <= 0) {", + " cache.sharedByKey.delete(item.behavior.__foliageSharedKey);", + " unregisterActiveMaterial(cancelledEntry.material);", + " if (typeof cache._disposeFoliageShadowMaterials === \"function\" && cancelledEntry.material) {", + " cache._disposeFoliageShadowMaterials(cancelledEntry.material);", + " }", + " if (cancelledEntry.material && typeof cancelledEntry.material.dispose === \"function\") {", + " try { cancelledEntry.material.dispose(); } catch (eD) {}", + " }", + " }", + " }", + " }", + " // Two-part: rollback trunk refCount", + " if (item.behavior && item.behavior.__foliageSharedKeyTrunk) {", + " var cancelledEntryTrunk = cache.sharedByKey.get(item.behavior.__foliageSharedKeyTrunk);", + " if (cancelledEntryTrunk && cancelledEntryTrunk.refCount > 0) {", + " cancelledEntryTrunk.refCount--;", + " if (cancelledEntryTrunk.refCount <= 0) {", + " cache.sharedByKey.delete(item.behavior.__foliageSharedKeyTrunk);", + " unregisterActiveMaterial(cancelledEntryTrunk.material);", + " if (typeof cache._disposeFoliageShadowMaterials === \"function\" && cancelledEntryTrunk.material) {", + " cache._disposeFoliageShadowMaterials(cancelledEntryTrunk.material);", + " }", + " if (cancelledEntryTrunk.material && typeof cancelledEntryTrunk.material.dispose === \"function\") {", + " try { cancelledEntryTrunk.material.dispose(); } catch (eD2) {}", + " }", + " }", + " }", + " }", + " continue;", + " }", + "", + " // Two-part item (leavesSway trunk + leaves): one world matrix, same idx in trunk and leaves groups", + " 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;", + " composeObjectWorldFromGDevelop(gdObjParts, inst, objW2P);", + " var repRel2P = getOrComputeRepRelMatrix(inst, gdObjParts, repMeshP0);", + " if (repRel2P) {", + " repW2P.multiplyMatrices(objW2P, repRel2P);", + " } else {", + " try { repMeshP0.updateMatrixWorld(true); } catch (eF0p) {}", + " repW2P.copy(repMeshP0.matrixWorld);", + " }", + "", + " var worldXP = repW2P.elements[12];", + " var worldYP = repW2P.elements[13];", + " var worldZP = repW2P.elements[14];", + " // Global instancing: groupKey = baseGroupKey (no ::SC: suffix)", + " var groupKeyTrunk = part0.baseGroupKey;", + " var groupKeyLeaves = part1.baseGroupKey;", + "", + " var gTrunk = inst.groups.get(groupKeyTrunk);", + " if (!gTrunk) {", + " gTrunk = {", + " key: groupKeyTrunk,", + " geometry: part0.geometry,", + " material: part0.material,", + " parent: inst.foliageRoot,", + " matricesBuffer: null,", + " centersXY: null,", + " centersZ: null,", + " matrixCount: 0,", + " aliveCount: 0,", + " freeIndices: [],", + " freeIndexSet: new Set(),", + " capacity: 0,", + " mesh: null", + " };", + " inst.groups.set(groupKeyTrunk, gTrunk);", + " } else {", + " gTrunk.geometry = part0.geometry;", + " gTrunk.material = part0.material;", + " if (!gTrunk.parent) gTrunk.parent = inst.foliageRoot;", + " if (!gTrunk.matricesBuffer) gTrunk.matricesBuffer = null;", + " if (!gTrunk.centersXY) gTrunk.centersXY = null;", + " if (!gTrunk.centersZ) gTrunk.centersZ = null;", + " if (typeof gTrunk.matrixCount !== \"number\") gTrunk.matrixCount = 0;", + " if (typeof gTrunk.aliveCount !== \"number\") gTrunk.aliveCount = 0;", + " if (!gTrunk.freeIndices) gTrunk.freeIndices = [];", + " if (!gTrunk.freeIndexSet || typeof gTrunk.freeIndexSet.has !== \"function\") gTrunk.freeIndexSet = new Set();", + " if (typeof gTrunk.capacity !== \"number\") gTrunk.capacity = 0;", + " }", + "", + " if (gTrunk.castShadow === undefined) gTrunk.castShadow = !!(repMeshP0 && repMeshP0.castShadow);", + " if (gTrunk.receiveShadow === undefined) gTrunk.receiveShadow = !!(repMeshP0 && repMeshP0.receiveShadow);", + " function parseBoolP(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 readClampedNumberP(v, fallback, minV, maxV) {", + " var n = Number(v);", + " if (!isFinite(n)) n = fallback;", + " if (n < minV) n = minV;", + " if (n > maxV) n = maxV;", + " return n;", + " }", + " if (typeof gTrunk.fadeEnabled !== \"boolean\" && behaviorParts) {", + " var dataP = behaviorParts._behaviorData || behaviorParts;", + " gTrunk.fadeEnabled = parseBoolP(dataP.distanceFadeEnabled, false);", + " gTrunk.fadeStart = readClampedNumberP(dataP.fadeStart, 1200, 0, 100000);", + " gTrunk.fadeEnd = readClampedNumberP(dataP.fadeEnd, 1600, 0, 100000);", + " if (gTrunk.fadeEnd <= gTrunk.fadeStart) gTrunk.fadeEnd = gTrunk.fadeStart + 100;", + " gTrunk._fadeBehaviorData = dataP;", + " }", + " if (behaviorParts) gTrunk._fadeBehaviorData = behaviorParts._behaviorData || behaviorParts;", + "", + " var gLeaves = inst.groups.get(groupKeyLeaves);", + " if (!gLeaves) {", + " gLeaves = {", + " key: groupKeyLeaves,", + " geometry: part1.geometry,", + " material: part1.material,", + " parent: inst.foliageRoot,", + " matricesBuffer: null,", + " centersXY: null,", + " centersZ: null,", + " matrixCount: 0,", + " aliveCount: 0,", + " freeIndices: gTrunk.freeIndices,", + " freeIndexSet: gTrunk.freeIndexSet,", + " capacity: 0,", + " mesh: null", + " };", + " inst.groups.set(groupKeyLeaves, gLeaves);", + " } else {", + " gLeaves.geometry = part1.geometry;", + " gLeaves.material = part1.material;", + " if (!gLeaves.parent) gLeaves.parent = inst.foliageRoot;", + " if (!gLeaves.freeIndices || gLeaves.freeIndices !== gTrunk.freeIndices) gLeaves.freeIndices = gTrunk.freeIndices;", + " if (!gLeaves.freeIndexSet || gLeaves.freeIndexSet !== gTrunk.freeIndexSet) gLeaves.freeIndexSet = gTrunk.freeIndexSet;", + " if (!gLeaves.matricesBuffer) gLeaves.matricesBuffer = null;", + " if (!gLeaves.centersXY) gLeaves.centersXY = null;", + " if (!gLeaves.centersZ) gLeaves.centersZ = null;", + " if (typeof gLeaves.matrixCount !== \"number\") gLeaves.matrixCount = 0;", + " if (typeof gLeaves.aliveCount !== \"number\") gLeaves.aliveCount = 0;", + " if (typeof gLeaves.capacity !== \"number\") gLeaves.capacity = 0;", + " }", + "", + " if (gLeaves.castShadow === undefined) gLeaves.castShadow = !!(part1.repMesh && part1.repMesh.castShadow);", + " if (gLeaves.receiveShadow === undefined) gLeaves.receiveShadow = !!(part1.repMesh && part1.repMesh.receiveShadow);", + " if (typeof gLeaves.fadeEnabled !== \"boolean\" && behaviorParts) {", + " var dataP2 = behaviorParts._behaviorData || behaviorParts;", + " gLeaves.fadeEnabled = parseBoolP(dataP2.distanceFadeEnabled, false);", + " gLeaves.fadeStart = readClampedNumberP(dataP2.fadeStart, 1200, 0, 100000);", + " gLeaves.fadeEnd = readClampedNumberP(dataP2.fadeEnd, 1600, 0, 100000);", + " if (gLeaves.fadeEnd <= gLeaves.fadeStart) gLeaves.fadeEnd = gLeaves.fadeStart + 100;", + " gLeaves._fadeBehaviorData = dataP2;", + " }", + " if (behaviorParts) gLeaves._fadeBehaviorData = behaviorParts._behaviorData || behaviorParts;", + "", + " // Store WORLD matrix directly (FoliageRoot is identity at scene root)", + " 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;", + " // Store foliageRoot-local matrix (compensates scene root Y-flip / transform)", + " localM.multiplyMatrices(foliageRootMWInv, repW2P);", + " var srcP = localM.elements;", + " for (var mP = 0; mP < 16; mP++) {", + " gTrunk.matricesBuffer[offsetP + mP] = srcP[mP];", + " }", + " var centersRequiredP = Math.max(idxP + 1, gTrunk.matrixCount) * 2;", + " if (!gTrunk.centersXY || gTrunk.centersXY.length < centersRequiredP) {", + " var centersCurrentP = gTrunk.centersXY ? gTrunk.centersXY.length : 0;", + " var centersNewP = Math.max(centersRequiredP, Math.max(Math.floor(centersCurrentP * 1.5), 512));", + " var newCentersP = new Float32Array(centersNewP);", + " if (gTrunk.centersXY) newCentersP.set(gTrunk.centersXY);", + " gTrunk.centersXY = newCentersP;", + " }", + " gTrunk.centersXY[idxP * 2] = worldXP;", + " gTrunk.centersXY[idxP * 2 + 1] = worldYP;", + " var centersRequiredZP = Math.max(idxP + 1, gTrunk.matrixCount);", + " if (!gTrunk.centersZ || gTrunk.centersZ.length < centersRequiredZP) {", + " var centersCurrentZP = gTrunk.centersZ ? gTrunk.centersZ.length : 0;", + " var centersNewZP = Math.max(centersRequiredZP, Math.max(Math.floor(centersCurrentZP * 1.5), 256));", + " var newCentersZP = new Float32Array(centersNewZP);", + " if (gTrunk.centersZ) newCentersZP.set(gTrunk.centersZ);", + " gTrunk.centersZ = newCentersZP;", + " }", + " gTrunk.centersZ[idxP] = worldZP;", + " // Init instanceFade for new slot", + " 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 centersRequiredL = Math.max(idxP + 1, gLeaves.matrixCount) * 2;", + " if (!gLeaves.centersXY || gLeaves.centersXY.length < centersRequiredL) {", + " var centersCurrentL = gLeaves.centersXY ? gLeaves.centersXY.length : 0;", + " var centersNewL = Math.max(centersRequiredL, Math.max(Math.floor(centersCurrentL * 1.5), 512));", + " var newCentersL = new Float32Array(centersNewL);", + " if (gLeaves.centersXY) newCentersL.set(gLeaves.centersXY);", + " gLeaves.centersXY = newCentersL;", + " }", + " gLeaves.centersXY[idxP * 2] = worldXP;", + " gLeaves.centersXY[idxP * 2 + 1] = worldYP;", + " var centersRequiredZL = Math.max(idxP + 1, gLeaves.matrixCount);", + " if (!gLeaves.centersZ || gLeaves.centersZ.length < centersRequiredZL) {", + " var centersCurrentZL = gLeaves.centersZ ? gLeaves.centersZ.length : 0;", + " var centersNewZL = Math.max(centersRequiredZL, Math.max(Math.floor(centersCurrentZL * 1.5), 256));", + " var newCentersZL = new Float32Array(centersNewZL);", + " if (gLeaves.centersZ) newCentersZL.set(gLeaves.centersZ);", + " gLeaves.centersZ = newCentersZL;", + " }", + " gLeaves.centersZ[idxP] = worldZP;", + " // Init instanceFade for new slot", + " 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 {", + " if (behaviorParts) {", + " 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) {", + " try {", + " var behP = null;", + " try { behP = gdObjParts.getBehavior(\"FoliageSwaying\"); } catch (eB1p) {}", + " if (!behP) {", + " try { behP = gdObjParts.getBehavior(\"NatureElements::FoliageSwaying\"); } catch (eB2p) {}", + " }", + " if (behP) behP.__foliageSkipOnDestroy = true;", + " } catch (eBehP) {}", + " if (gdObjParts.deleteFromScene) {", + " gdObjParts.deleteFromScene();", + " } else if (gdObjParts.hide && !gdObjParts.isHidden) {", + " gdObjParts.hide();", + " }", + " }", + " } catch (eHideP) {}", + " inst.dirty = true;", + " continue;", + " }", + "", + " var gdObj = item.gdObj;", + " var threeObj = item.threeObj;", + " var repMesh = item.repMesh;", + " var baseGroupKey = item.baseGroupKey;", + " // Backward compatibility: if baseGroupKey is missing (older pending items), use groupKey.", + " if (!baseGroupKey) {", + " baseGroupKey = item.groupKey;", + " }", + " var sharedMat = item.material;", + " var geometry = item.geometry;", + "", + " if (!baseGroupKey || !geometry || !sharedMat) continue;", + " if (!threeObj || !repMesh) continue;", + " ", + " // GUARD: Skip if this object already has an instancing slot (prevents duplicate aliveCount)", + " if (item.behavior && item.behavior.__foliageInstancingIndex !== undefined) continue;", + "", + " var objW2 = inst._tmpMat4_objWorld;", + " var repW2 = inst._tmpMat4_repWorld;", + "", + " // World matrix from GDevelop transform", + " composeObjectWorldFromGDevelop(gdObj, inst, objW2);", + "", + " // Cached repMesh relative matrix (constant per type)", + " var repMeshId = repMesh.uuid || repMesh.id || 'unknown';", + " var cacheKey = \"RM:\" + repMeshId;", + " var repRel2 = repRelMatrixCache.get(cacheKey);", + " ", + " // Fallback: if relation is not cached (edge case), compute directly.", + " if (!repRel2) {", + " repRel2 = getOrComputeRepRelMatrix(inst, gdObj, repMesh);", + " }", + "", + " // 3) repMesh world = objWorld * repRel", + " if (repRel2) {", + " repW2.multiplyMatrices(objW2, repRel2);", + " } else {", + " // Fallback: if relation is unavailable, use current repMesh.matrixWorld.", + " try { repMesh.updateMatrixWorld(true); } catch (eF0) {}", + " repW2.copy(repMesh.matrixWorld);", + " }", + "", + " // Global instancing: groupKey = baseGroupKey (no ::SC: suffix)", + " var groupKey = baseGroupKey;", + " var worldX = repW2.elements[12];", + " var worldY = repW2.elements[13];", + " var worldZ = repW2.elements[14];", + " ", + " // Ensure group exists", + " var g = inst.groups.get(groupKey);", + " if (!g) {", + " g = {", + " key: groupKey,", + " geometry: geometry,", + " material: sharedMat,", + " parent: inst.foliageRoot,", + " matricesBuffer: null,", + " centersXY: null,", + " centersZ: null,", + " matrixCount: 0,", + " aliveCount: 0,", + " freeIndices: [],", + " freeIndexSet: new Set(),", + " capacity: 0,", + " mesh: null", + " };", + " inst.groups.set(groupKey, g);", + " } else {", + " g.geometry = geometry;", + " g.material = sharedMat;", + " if (!g.parent) g.parent = inst.foliageRoot;", + " if (!g.matricesBuffer) g.matricesBuffer = null;", + " if (!g.centersXY) g.centersXY = null;", + " if (!g.centersZ) g.centersZ = null;", + " if (typeof g.matrixCount !== \"number\") g.matrixCount = 0;", + " if (typeof g.aliveCount !== \"number\") g.aliveCount = 0;", + " if (!g.freeIndices) g.freeIndices = [];", + " if (!g.freeIndexSet || typeof g.freeIndexSet.has !== \"function\") g.freeIndexSet = new Set();", + " if (typeof g.capacity !== \"number\") g.capacity = 0;", + " }", + "", + " if (g.castShadow === undefined) g.castShadow = !!(repMesh && repMesh.castShadow);", + " if (g.receiveShadow === undefined) g.receiveShadow = !!(repMesh && repMesh.receiveShadow);", + " ", + " if (typeof g.fadeEnabled !== \"boolean\" && item.behavior) {", + " var data = item.behavior._behaviorData || item.behavior;", + " 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 readClampedNumber(v, fallback, minV, maxV) {", + " var n = Number(v);", + " if (!isFinite(n)) n = fallback;", + " if (n < minV) n = minV;", + " if (n > maxV) n = maxV;", + " return n;", + " }", + " g.fadeEnabled = parseBool(data.distanceFadeEnabled, false);", + " g.fadeStart = readClampedNumber(data.fadeStart, 1200, 0, 100000);", + " g.fadeEnd = readClampedNumber(data.fadeEnd, 1600, 0, 100000);", + " if (g.fadeEnd <= g.fadeStart) g.fadeEnd = g.fadeStart + 100;", + " g._fadeBehaviorData = data;", + " }", + " if (item.behavior) g._fadeBehaviorData = item.behavior._behaviorData || item.behavior;", + "", + " // Store foliageRoot-local matrix (compensates scene root Y-flip / transform)", + " 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;", + " localM.multiplyMatrices(foliageRootMWInv, repW2);", + " var src = localM.elements;", + " for (var m = 0; m < 16; m++) {", + " g.matricesBuffer[offset + m] = src[m];", + " }", + "", + " var centersRequired = Math.max(idx + 1, g.matrixCount) * 2;", + " if (!g.centersXY || g.centersXY.length < centersRequired) {", + " var centersCurrentSize = g.centersXY ? g.centersXY.length : 0;", + " var centersNewSize = Math.max(centersRequired, Math.max(Math.floor(centersCurrentSize * 1.5), 512));", + " var newCenters = new Float32Array(centersNewSize);", + " if (g.centersXY) newCenters.set(g.centersXY);", + " g.centersXY = newCenters;", + " }", + " g.centersXY[idx * 2] = worldX;", + " g.centersXY[idx * 2 + 1] = worldY;", + " var centersRequiredZ = Math.max(idx + 1, g.matrixCount);", + " if (!g.centersZ || g.centersZ.length < centersRequiredZ) {", + " var centersCurrentSizeZ = g.centersZ ? g.centersZ.length : 0;", + " var centersNewSizeZ = Math.max(centersRequiredZ, Math.max(Math.floor(centersCurrentSizeZ * 1.5), 256));", + " var newCentersZ = new Float32Array(centersNewSizeZ);", + " if (g.centersZ) newCentersZ.set(g.centersZ);", + " g.centersZ = newCentersZ;", + " }", + " g.centersZ[idx] = worldZ;", + " // Init instanceFade for new slot", + " if (g.instanceFade && idx < g.instanceFade.length) g.instanceFade[idx] = 1.0;", + " if (g.instanceFadePrev && idx < g.instanceFadePrev.length) g.instanceFadePrev[idx] = 1.0;", + "", + " // Persist indices for cleanup", + " 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) {}", + "", + " // Hide or delete original GDevelop object", + " try {", + " if (gdObj) {", + " try {", + " var beh = null;", + " try { beh = gdObj.getBehavior(\"FoliageSwaying\"); } catch (eB1) {}", + " if (!beh) {", + " try { beh = gdObj.getBehavior(\"NatureElements::FoliageSwaying\"); } catch (eB2) {}", + " }", + " if (beh) beh.__foliageSkipOnDestroy = true;", + " } catch (eBeh) {}", + " if (gdObj.deleteFromScene) {", + " gdObj.deleteFromScene(runtimeScene);", + " } else if (gdObj.hide) {", + " gdObj.hide(true);", + " }", + " } else if (threeObj) {", + " threeObj.visible = false;", + " }", + " } catch (eHide) {}", + " }", + " ", + " inst.dirty = true;", + " }", + "", + " function rebuildInstancedMeshesIfNeeded() {", + " var inst = cache.instancing;", + " if (!inst || !inst.dirty || !inst.groups) return;", + " inst.dirty = false;", + "", + " 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;", + " }", + "", + " function computeInstanceCullRadius(geometry, fallbackRadius) {", + " if (!geometry) return isFinite(fallbackRadius) ? fallbackRadius : 0;", + " try {", + " if (!geometry.boundingSphere && typeof geometry.computeBoundingSphere === \"function\") {", + " geometry.computeBoundingSphere();", + " }", + " } catch (eBs) {}", + " var bs = geometry.boundingSphere;", + " var r = bs && isFinite(bs.radius) ? bs.radius : (isFinite(fallbackRadius) ? fallbackRadius : 0);", + " return r > 0 ? r : 0;", + " }", + "", + " inst.groups.forEach(function (g) {", + " 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 = computeInstanceCullRadius(g.geometry, g._instanceCullRadius);", + "", + " var mesh = g.mesh;", + "", + " // Track capacity separately from draw count.", + " // Capacity is the allocated instanceMatrix size; draw count is mesh.count.", + " var cap = (typeof g.capacity === \"number\") ? g.capacity : 0;", + " if (mesh && mesh.instanceMatrix && isFinite(mesh.instanceMatrix.count)) {", + " // If capacity was not tracked (older state), derive it from the mesh.", + " if (!cap || cap < mesh.instanceMatrix.count) cap = mesh.instanceMatrix.count;", + " }", + "", + " // Create or grow only when needed; do NOT recreate on every aliveCount change.", + " var need = aliveCount;", + " var shouldRecreate = false;", + " var newCap = cap;", + "", + " // Check if source geometry changed (e.g., different source mesh in same groupKey)", + " // If geometry reference changed, force recreate to avoid rendering stale cloned geometry", + " var geometryChanged = false;", + " if (mesh && g._srcGeometryRef && g.geometry !== g._srcGeometryRef) {", + " geometryChanged = true;", + " shouldRecreate = true;", + " }", + "", + " if (!mesh) {", + " newCap = nextPow2(need);", + " // Conditional capacity floor: small groups use 512, large use 4096", + " 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);", + " // Dispose CLONED geometry (owned), NOT source", + " if (g._ownedGeometry && typeof g._ownedGeometry.dispose === \"function\") {", + " try { g._ownedGeometry.dispose(); } catch (eGeo) {}", + " }", + " try { if (mesh.dispose) mesh.dispose(); } catch (eMesh) {}", + " }", + " } catch (e) {}", + "", + " 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;", + " }", + " ", + " // aFade attribute (current fade target)", + " 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);", + " ", + " // aFadePrev attribute (previous fade for GPU smoothing)", + " 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);", + " ", + " // Invalidation: mesh.count = 0 until next cull tick populates render arrays", + " mesh.count = 0;", + " mesh.visible = false;", + " ", + " // Attach to FoliageRoot (or fallback parent)", + " var attachParent = g.parent || inst.foliageRoot;", + " if (attachParent) attachParent.add(mesh);", + " g.mesh = mesh;", + " g._ownedGeometry = clonedGeometry; // Track owned clone for safe disposal", + " g.capacity = newCap;", + " g._srcGeometryRef = g.geometry;", + " // Force re-compaction on next distance/frustum pass (new mesh has count=0)", + " g._fadeDisabledApplied = false;", + " } catch (e2) {", + " 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;", + " }", + " }", + "", + " // NOTE: Do NOT set mesh.count or copy matrices here.", + " // Distance/frustum passes handle compaction: storage -> render arrays, mesh.count = visibleCount.", + " });", + " }", + "", + " function getFrameCameraSnapshot(runtimeScene, needFrustum) {", + " if (!cache._frameCameraSnapshot || typeof cache._frameCameraSnapshot !== \"object\") {", + " cache._frameCameraSnapshot = {", + " cam: null,", + " camX: 0,", + " camY: 0,", + " camZ: 0,", + " frustum: null", + " };", + " }", + " var snap = cache._frameCameraSnapshot;", + " snap.cam = null;", + " snap.camX = 0;", + " snap.camY = 0;", + " snap.camZ = 0;", + " snap.frustum = null;", + "", + " try {", + " var layer = runtimeScene.getLayer(\"\");", + " if (layer && layer.getRenderer && layer.getRenderer().getThreeCamera) {", + " var cam = layer.getRenderer().getThreeCamera();", + " if (cam) {", + " snap.cam = cam;", + " snap.camX = cam.position.x;", + " snap.camY = cam.position.y;", + " snap.camZ = cam.position.z;", + " }", + " }", + " } catch (eCam) {}", + "", + " if (needFrustum && snap.cam && typeof THREE.Frustum !== \"undefined\" && typeof THREE.Matrix4 !== \"undefined\") {", + " if (!cache._frustumShared) cache._frustumShared = new THREE.Frustum();", + " if (!cache._projViewMatrixShared) cache._projViewMatrixShared = new THREE.Matrix4();", + " try {", + " snap.cam.updateMatrixWorld(true);", + " cache._projViewMatrixShared.multiplyMatrices(snap.cam.projectionMatrix, snap.cam.matrixWorldInverse);", + " if (typeof cache._frustumShared.setFromProjectionMatrix === \"function\") {", + " cache._frustumShared.setFromProjectionMatrix(cache._projViewMatrixShared);", + " } else if (typeof cache._frustumShared.setFromMatrix === \"function\") {", + " // Backward compatibility with older Three.js builds.", + " cache._frustumShared.setFromMatrix(cache._projViewMatrixShared);", + " } else {", + " throw new Error(\"THREE.Frustum has no supported setter\");", + " }", + " snap.frustum = cache._frustumShared;", + " } catch (eFr) {", + " snap.frustum = null;", + " }", + " }", + "", + " return snap;", + " }", + "", + " function 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(cache._prevFrustumPassCamX) ? cache._prevFrustumPassCamX : camX;", + " var prevCamY = isFinite(cache._prevFrustumPassCamY) ? cache._prevFrustumPassCamY : camY;", + " var prevCamZ = isFinite(cache._prevFrustumPassCamZ) ? cache._prevFrustumPassCamZ : camZ;", + " var camDelta = Math.abs(camX - prevCamX) + Math.abs(camY - prevCamY) + Math.abs(camZ - prevCamZ);", + " var camMoved = camDelta > 0.5;", + " cache._prevFrustumPassCamX = camX;", + " cache._prevFrustumPassCamY = camY;", + " cache._prevFrustumPassCamZ = camZ;", + "", + " var dirChanged = false;", + " if (typeof cam.getWorldDirection === \"function\" && typeof THREE.Vector3 !== \"undefined\") {", + " if (!cache._tmpVec3FrustumCamDir) cache._tmpVec3FrustumCamDir = new THREE.Vector3();", + " var camDir = cache._tmpVec3FrustumCamDir;", + " try { cam.getWorldDirection(camDir); } catch (eDir) {}", + " var prevDirX = isFinite(cache._prevFrustumPassCamDirX) ? cache._prevFrustumPassCamDirX : camDir.x;", + " var prevDirY = isFinite(cache._prevFrustumPassCamDirY) ? cache._prevFrustumPassCamDirY : camDir.y;", + " var prevDirZ = isFinite(cache._prevFrustumPassCamDirZ) ? cache._prevFrustumPassCamDirZ : camDir.z;", + " var dirDelta = Math.abs(camDir.x - prevDirX) + Math.abs(camDir.y - prevDirY) + Math.abs(camDir.z - prevDirZ);", + " dirChanged = dirDelta > 0.001;", + " cache._prevFrustumPassCamDirX = camDir.x;", + " cache._prevFrustumPassCamDirY = camDir.y;", + " cache._prevFrustumPassCamDirZ = camDir.z;", + " }", + "", + " return camMoved || dirChanged;", + " }", + "", + " // Distance tick only: computes fade and candidate slots (fade > 0.01).", + " function performDistanceCullTick(runtimeScene, frameCam, forceRun) {", + " var inst = cache.instancing;", + " if (!inst || !inst.groups) return false;", + "", + " var cam = frameCam && frameCam.cam ? frameCam.cam : null;", + " var camX = frameCam && isFinite(frameCam.camX) ? frameCam.camX : 0;", + " var camY = frameCam && isFinite(frameCam.camY) ? frameCam.camY : 0;", + " var camZ = frameCam && isFinite(frameCam.camZ) ? frameCam.camZ : 0;", + "", + " cache.lastCamX = camX;", + " cache.lastCamY = camY;", + " cache.lastCamZ = camZ;", + "", + " // Camera movement tracking for distance refresh early-out.", + " var prevCamX = isFinite(cache._prevCullCamX) ? cache._prevCullCamX : camX;", + " var prevCamY = isFinite(cache._prevCullCamY) ? cache._prevCullCamY : camY;", + " var prevCamZ = isFinite(cache._prevCullCamZ) ? cache._prevCullCamZ : camZ;", + " var camDelta = Math.abs(camX - prevCamX) + Math.abs(camY - prevCamY) + Math.abs(camZ - prevCamZ);", + " var camMoved = camDelta > 0.5;", + " cache._prevCullCamX = camX;", + " cache._prevCullCamY = camY;", + " cache._prevCullCamZ = camZ;", + "", + " // Track camera direction changes so distance refresh can react to orientation-based effects.", + " var camDirChanged = false;", + " if (cam && typeof cam.getWorldDirection === \"function\" && typeof THREE.Vector3 !== \"undefined\") {", + " if (!cache._tmpVec3InstancedCamDir) cache._tmpVec3InstancedCamDir = new THREE.Vector3();", + " var camDir = cache._tmpVec3InstancedCamDir;", + " try { cam.getWorldDirection(camDir); } catch (eDir) {}", + " var prevDirX = isFinite(cache._prevCullCamDirX) ? cache._prevCullCamDirX : camDir.x;", + " var prevDirY = isFinite(cache._prevCullCamDirY) ? cache._prevCullCamDirY : camDir.y;", + " var prevDirZ = isFinite(cache._prevCullCamDirZ) ? cache._prevCullCamDirZ : camDir.z;", + " var dirDelta = Math.abs(camDir.x - prevDirX) + Math.abs(camDir.y - prevDirY) + Math.abs(camDir.z - prevDirZ);", + " camDirChanged = dirDelta > 0.001;", + " cache._prevCullCamDirX = camDir.x;", + " cache._prevCullCamDirY = camDir.y;", + " cache._prevCullCamDirZ = camDir.z;", + " }", + "", + " // First pass: refresh fade params and detect changes BEFORE early-out.", + " var fadeParamsChanged = false;", + " inst.groups.forEach(function(g) {", + " if (!g) return;", + " var dataFade = g._fadeBehaviorData;", + " if (dataFade) {", + " var newEnabled = parseBoolRuntime(dataFade.distanceFadeEnabled, false);", + " var newStart = readClampedRuntime(dataFade.fadeStart, 1200, 0, 100000);", + " var newEnd = readClampedRuntime(dataFade.fadeEnd, 1600, 0, 100000);", + " if (newEnd <= newStart) newEnd = newStart + 100;", + " g.fadeEnabled = newEnabled;", + " g.fadeStart = newStart;", + " g.fadeEnd = newEnd;", + " }", + " 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) cache._fadeParamsDirty = true;", + "", + " var hasPending = !!(inst.pending && inst.pending.length > 0);", + " if (!forceRun && !camMoved && !camDirChanged && !inst.dirty && !hasPending && !cache._fadeParamsDirty) {", + " return false;", + " }", + "", + " inst.groups.forEach(function(g) {", + " if (!g || !g.mesh) return;", + "", + " var mesh = g.mesh;", + "", + " // Fade-disabled groups: compact all alive, aFade=1, aFadePrev=1.", + " 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;", + "", + " // Ensure both storage fade arrays exist and have identical capacity.", + " 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;", + " }", + "", + " // Pointer swap only on distance tick.", + " 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;", + "", + " // EARLY REJECT: beyond fadeEnd + margin (no sqrt).", + " 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;", + " });", + "", + " cache.lastCullTime = cache.time;", + " cache._fadeParamsDirty = false;", + " inst.dirty = false;", + " return true;", + " }", + "", + " // Per-frame pass: frustum + compaction only for distance-approved candidates.", + " function performFrustumCompactionPass(frameCam) {", + " var inst = cache.instancing;", + " if (!inst || !inst.groups) return false;", + "", + " var frustumInstanced = frameCam && frameCam.frustum ? frameCam.frustum : null;", + "", + " var tmpVec3InstancedCull = null;", + " if (typeof THREE.Vector3 !== \"undefined\") {", + " if (!cache._tmpVec3InstancedCull) cache._tmpVec3InstancedCull = new THREE.Vector3();", + " tmpVec3InstancedCull = cache._tmpVec3InstancedCull;", + " }", + " var tmpSphereInstancedCull = null;", + " if (typeof THREE.Sphere !== \"undefined\") {", + " if (!cache._tmpSphereInstancedCull) cache._tmpSphereInstancedCull = new THREE.Sphere();", + " tmpSphereInstancedCull = cache._tmpSphereInstancedCull;", + " }", + "", + " var anyProcessed = false;", + " inst.groups.forEach(function(g) {", + " if (!g || !g.mesh) return;", + " if (!g.fadeEnabled) return;", + "", + " var mesh = g.mesh;", + " anyProcessed = true;", + "", + " var candidates = g._distanceCandidateIndices;", + " var candidateCount = (typeof g._distanceCandidateCount === \"number\") ? g._distanceCandidateCount : 0;", + " if (!candidates || candidateCount <= 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 storFade = g.instanceFade;", + " var storFadePrev = g.instanceFadePrev;", + " var instanceCullRadius = (isFinite(g._instanceCullRadius) && g._instanceCullRadius > 0) ? g._instanceCullRadius : 0;", + " if (!renderMat || !storMat || !storCenters || !storFade || !storFadePrev) return;", + "", + " var visibleCount = 0;", + " for (var ci = 0; ci < candidateCount; ci++) {", + " var i = candidates[ci];", + "", + " 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 (frustumInstanced) {", + " if (instanceCullRadius > 0 && tmpSphereInstancedCull && typeof frustumInstanced.intersectsSphere === \"function\") {", + " tmpSphereInstancedCull.center.set(wx, wy, wz);", + " tmpSphereInstancedCull.radius = instanceCullRadius;", + " 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] = storFade[i];", + " if (renderFadePrevAttr && renderFadePrevAttr.array) renderFadePrevAttr.array[visibleCount] = storFadePrev[i];", + " 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;", + " }", + "", + " function readNumArg(name, fallback) {", + " var n = Number(eventsFunctionContext.getArgument(name));", + " return isFinite(n) ? n : fallback;", + " }", + " ", + " var windStrength = readNumArg(\"windStrength\", 6.0);", + " var windSpeed = readNumArg(\"windSpeed\", 4.0);", + " var windDirectionX = readNumArg(\"windDirectionX\", 1.0);", + " var windDirectionY = readNumArg(\"windDirectionY\", 1.0);", + " ", + " var dt = runtimeScene.getTimeManager().getElapsedTime() / 1000.0;", + " if (!isFinite(dt) || dt < 0) dt = 0;", + " ", + " cache.time += dt;", + "", + " var len = Math.sqrt(windDirectionX * windDirectionX + windDirectionY * windDirectionY);", + " var wx = len > 0 ? windDirectionX / len : 1.0;", + " var wy = len > 0 ? windDirectionY / len : 0.0;", + " ", + " // Capture pending instancing transforms (1 frame delayed), then rebuild meshes", + " flushPendingInstancing();", + " var hadDirty = cache.instancing && cache.instancing.dirty;", + " rebuildInstancedMeshesIfNeeded();", + "", + " 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;", + "", + " // Cull tick at fadeUpdateHz, OR immediately after rebuild to populate new meshes", + " 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 frustum only when needed.", + " var needFrameCam = shouldCull || arrNiLen > 0 || !noGroups;", + " var frameCam = needFrameCam ? getFrameCameraSnapshot(runtimeScene, false) : null;", + "", + " var camChangedForFrustum = didFrustumCameraChange(frameCam);", + " var distanceTickRan = false;", + " if (shouldCull) {", + " distanceTickRan = performDistanceCullTick(runtimeScene, frameCam, !!hadDirty);", + " }", + "", + " var shouldRunFrustumPass = distanceTickRan || camChangedForFrustum;", + " if (shouldRunFrustumPass && (!frameCam || !frameCam.frustum)) {", + " frameCam = getFrameCameraSnapshot(runtimeScene, true);", + " }", + "", + " if (shouldRunFrustumPass) {", + " performFrustumCompactionPass(frameCam);", + " }", + "", + " // Parked non-instanced entries (fade disabled): periodically check if fade was re-enabled.", + " if (arrNiStaticLen > 0) {", + " cache._nonInstancedStaticCheckAccum += dt;", + " var staticCheckInterval = Math.max(cullInterval, 0.05);", + " var shouldScanStatic = shouldCull || cache._nonInstancedStaticCheckAccum >= staticCheckInterval;", + " if (shouldScanStatic) {", + " 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) {", + " unregisterActiveMaterial(entryStatic && entryStatic.material);", + " unregisterActiveMaterial(entryStatic && entryStatic.trunkMaterial);", + " if (entryStatic && entryStatic.material) {", + " if (typeof cache._disposeFoliageShadowMaterials === \"function\") cache._disposeFoliageShadowMaterials(entryStatic.material);", + " if (typeof entryStatic.material.dispose === \"function\") {", + " try { entryStatic.material.dispose(); } catch (eNiStaticDisp1) {}", + " }", + " }", + " if (entryStatic && entryStatic.trunkMaterial) {", + " if (typeof cache._disposeFoliageShadowMaterials === \"function\") cache._disposeFoliageShadowMaterials(entryStatic.trunkMaterial);", + " if (typeof entryStatic.trunkMaterial.dispose === \"function\") {", + " try { entryStatic.trunkMaterial.dispose(); } catch (eNiStaticDisp2) {}", + " }", + " }", + " clearNonInstancedRegistration(entryStatic);", + " arrNiStatic.splice(si, 1);", + " continue;", + " }", + "", + " var enabledStatic = (entryStatic.fadeEnabled === undefined) ? false : !!entryStatic.fadeEnabled;", + " var dataStatic = entryStatic.fadeBehaviorData;", + " if (dataStatic) {", + " enabledStatic = parseBoolRuntime(dataStatic.distanceFadeEnabled, enabledStatic);", + " var staticStart = readClampedRuntime(dataStatic.fadeStart, entryStatic.fadeStart != null ? entryStatic.fadeStart : 1200, 0, 100000);", + " var staticEnd = readClampedRuntime(dataStatic.fadeEnd, entryStatic.fadeEnd != null ? entryStatic.fadeEnd : 1600, 0, 100000);", + " if (staticEnd <= staticStart) staticEnd = staticStart + 100;", + " entryStatic.fadeStart = staticStart;", + " entryStatic.fadeEnd = staticEnd;", + " }", + " entryStatic.fadeEnabled = !!enabledStatic;", + " if (!enabledStatic) {", + " if (entryStatic.material && entryStatic.material.userData) {", + " entryStatic.material.userData._pendingFade = 1.0;", + " if (entryStatic.material.userData.foliageUniforms && entryStatic.material.userData.foliageUniforms.uFade) {", + " entryStatic.material.userData.foliageUniforms.uFade.value = 1.0;", + " }", + " }", + " if (entryStatic.trunkMaterial && entryStatic.trunkMaterial.userData) {", + " entryStatic.trunkMaterial.userData._pendingFade = 1.0;", + " if (entryStatic.trunkMaterial.userData.foliageUniforms && entryStatic.trunkMaterial.userData.foliageUniforms.uFade) {", + " entryStatic.trunkMaterial.userData.foliageUniforms.uFade.value = 1.0;", + " }", + " }", + " if (entryStatic._wasHidden !== false) {", + " try {", + " if (entryStatic.gdObj.hide) entryStatic.gdObj.hide(false);", + " } catch (eStaticHide) {}", + " entryStatic._wasHidden = false;", + " }", + " continue;", + " }", + "", + " entryStatic._parkedNoFade = false;", + " var existsActive = false;", + " for (var ai = arrNi.length - 1; ai >= 0; ai--) {", + " if (arrNi[ai] === entryStatic) {", + " existsActive = true;", + " break;", + " }", + " }", + " if (!existsActive) arrNi.push(entryStatic);", + " arrNiStatic.splice(si, 1);", + " }", + " }", + " }", + "", + " // Static scan can reactivate entries; ensure we have fresh camera snapshot before active loop.", + " if ((!frameCam || !frameCam.cam) && arrNi && arrNi.length > 0) {", + " frameCam = getFrameCameraSnapshot(runtimeScene, false);", + " }", + "", + " // Non-instanced distance fade: run every frame so initial state is correct (not only on cull tick)", + " // In-place backward loop avoids per-frame allocations.", + " 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) {", + " unregisterActiveMaterial(entryNi && entryNi.material);", + " unregisterActiveMaterial(entryNi && entryNi.trunkMaterial);", + " if (entryNi && entryNi.material) {", + " if (typeof cache._disposeFoliageShadowMaterials === \"function\") cache._disposeFoliageShadowMaterials(entryNi.material);", + " if (typeof entryNi.material.dispose === \"function\") {", + " try { entryNi.material.dispose(); } catch (eNiDisp1) {}", + " }", + " }", + " if (entryNi && entryNi.trunkMaterial) {", + " if (typeof cache._disposeFoliageShadowMaterials === \"function\") cache._disposeFoliageShadowMaterials(entryNi.trunkMaterial);", + " if (typeof entryNi.trunkMaterial.dispose === \"function\") {", + " try { entryNi.trunkMaterial.dispose(); } catch (eNiDisp2) {}", + " }", + " }", + " clearNonInstancedRegistration(entryNi);", + " var lastIdx = arrNi.length - 1;", + " if (ni < lastIdx) arrNi[ni] = arrNi[lastIdx];", + " arrNi.pop();", + " continue;", + " }", + " // Runtime refresh from behavior data: allows live toggling of distanceFadeEnabled.", + " var enabledNi = (entryNi.fadeEnabled === undefined) ? true : !!entryNi.fadeEnabled;", + " var dataNi = entryNi.fadeBehaviorData;", + " if (dataNi) {", + " enabledNi = parseBoolRuntime(dataNi.distanceFadeEnabled, enabledNi);", + " var newStartNi = readClampedRuntime(dataNi.fadeStart, entryNi.fadeStart != null ? entryNi.fadeStart : 1200, 0, 100000);", + " var newEndNi = readClampedRuntime(dataNi.fadeEnd, entryNi.fadeEnd != null ? entryNi.fadeEnd : 1600, 0, 100000);", + " if (newEndNi <= newStartNi) newEndNi = newStartNi + 100;", + " entryNi.fadeStart = newStartNi;", + " entryNi.fadeEnd = newEndNi;", + " }", + " entryNi.fadeEnabled = !!enabledNi;", + " // Optional perf path: when fade disabled, skip all distance/frustum work immediately and park entry.", + " if (!enabledNi) {", + " if (entryNi.material.userData) {", + " entryNi.material.userData._pendingFade = 1.0;", + " if (entryNi.material.userData.foliageUniforms && entryNi.material.userData.foliageUniforms.uFade) {", + " entryNi.material.userData.foliageUniforms.uFade.value = 1.0;", + " }", + " }", + " if (entryNi.trunkMaterial && entryNi.trunkMaterial.userData) {", + " entryNi.trunkMaterial.userData._pendingFade = 1.0;", + " if (entryNi.trunkMaterial.userData.foliageUniforms && entryNi.trunkMaterial.userData.foliageUniforms.uFade) {", + " entryNi.trunkMaterial.userData.foliageUniforms.uFade.value = 1.0;", + " }", + " }", + " if (entryNi._wasHidden !== false) {", + " try {", + " if (entryNi.gdObj.hide) entryNi.gdObj.hide(false);", + " } catch (eHideOff) {}", + " entryNi._wasHidden = false;", + " }", + " entryNi._parkedNoFade = true;", + " var existsInStatic = false;", + " for (var sChk = arrNiStatic.length - 1; sChk >= 0; sChk--) {", + " if (arrNiStatic[sChk] === entryNi) {", + " existsInStatic = true;", + " break;", + " }", + " }", + " if (!existsInStatic) arrNiStatic.push(entryNi);", + " var lastIdxDisabled = arrNi.length - 1;", + " if (ni < lastIdxDisabled) arrNi[ni] = arrNi[lastIdxDisabled];", + " arrNi.pop();", + " 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) {", + " unregisterActiveMaterial(entryNi.material);", + " unregisterActiveMaterial(entryNi.trunkMaterial);", + " if (entryNi.material) {", + " if (typeof cache._disposeFoliageShadowMaterials === \"function\") cache._disposeFoliageShadowMaterials(entryNi.material);", + " if (typeof entryNi.material.dispose === \"function\") {", + " try { entryNi.material.dispose(); } catch (eNiDisp3) {}", + " }", + " }", + " if (entryNi.trunkMaterial) {", + " if (typeof cache._disposeFoliageShadowMaterials === \"function\") cache._disposeFoliageShadowMaterials(entryNi.trunkMaterial);", + " if (typeof entryNi.trunkMaterial.dispose === \"function\") {", + " try { entryNi.trunkMaterial.dispose(); } catch (eNiDisp4) {}", + " }", + " }", + " clearNonInstancedRegistration(entryNi);", + " var lastIdx2 = arrNi.length - 1;", + " if (ni < lastIdx2) arrNi[ni] = arrNi[lastIdx2];", + " arrNi.pop();", + " 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;", + " if (entryNi.material.userData) {", + " entryNi.material.userData._pendingFade = fadeNi;", + " if (entryNi.material.userData.foliageUniforms && entryNi.material.userData.foliageUniforms.uFade) {", + " entryNi.material.userData.foliageUniforms.uFade.value = fadeNi;", + " }", + " if (entryNi.trunkMaterial && entryNi.trunkMaterial.userData) {", + " entryNi.trunkMaterial.userData._pendingFade = fadeNi;", + " if (entryNi.trunkMaterial.userData.foliageUniforms && entryNi.trunkMaterial.userData.foliageUniforms.uFade) {", + " entryNi.trunkMaterial.userData.foliageUniforms.uFade.value = 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", + " }", + "", + " function updateShadowUniforms(hostMat, shadowMat) {", + " 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);", + "", + " if (su.uTime) su.uTime.value = cache.time;", + " if (su.uWindStrength) su.uWindStrength.value = windStrength;", + " if (su.uWindSpeed) su.uWindSpeed.value = windSpeed;", + " if (su.uWindDir) su.uWindDir.value.set(wx, wy);", + " if (su.uLocalWindDir) su.uLocalWindDir.value.set(wx, wy);", + " if (su.uLocalWindPerp) su.uLocalWindPerp.value.set(-wy, wx);", + "", + " 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 !== cache.gustVersion) {", + " shadowMat.userData.__gustVersionApplied = cache.gustVersion;", + " if (su.uGustTex) su.uGustTex.value = cache.gustTexture || cache.gustFallbackTex;", + " if (su.uGustEnabled) su.uGustEnabled.value = cache.gustEnabled ? 1.0 : 0.0;", + " if (su.uGustStrength) su.uGustStrength.value = cache.gustEnabled ? cache.gustStrength : 0.0;", + " if (su.uGustScale) su.uGustScale.value = cache.gustScale;", + " if (su.uGustSpeed) su.uGustSpeed.value = cache.gustSpeed;", + " if (su.uGustThreshold) su.uGustThreshold.value = cache.gustThreshold;", + " if (su.uGustContrast) su.uGustContrast.value = cache.gustContrast;", + " }", + " }", + "", + " var buckets = cache._materialBuckets;", + " if (!buckets || typeof buckets !== \"object\") {", + " cache._resetMaterialBuckets();", + " buckets = cache._materialBuckets;", + " }", + " function resetBucketSet(setObj) {", + " if (setObj && typeof setObj.clear === \"function\") setObj.clear();", + " }", + " 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);", + " 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 created materials from unknown set into specific update buckets.", + " for (const matUnknown of buckets.unknown) {", + " if (!matUnknown) {", + " buckets.unknown.delete(matUnknown);", + " continue;", + " }", + " if (!cache.activeMaterials || !cache.activeMaterials.has(matUnknown)) {", + " unregisterActiveMaterial(matUnknown);", + " continue;", + " }", + " var udUnknown = matUnknown.userData;", + " if (!udUnknown) {", + " 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)) {", + " unregisterActiveMaterial(matTW);", + " continue;", + " }", + " var udTW = matTW.userData;", + " if (!udTW || !udTW.foliageUniforms) {", + " 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)) {", + " unregisterActiveMaterial(matFade);", + " continue;", + " }", + " var udFade = matFade.userData;", + " var uFade = udFade && udFade.foliageUniforms;", + " if (!uFade) {", + " 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)) {", + " unregisterActiveMaterial(matGust);", + " continue;", + " }", + " var udGust = matGust.userData;", + " var uGust = udGust && udGust.foliageUniforms;", + " if (!uGust) {", + " 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-host bucket: update custom depth/distance uniforms only for materials that own them.", + " for (const matShadow of buckets.shadowHost) {", + " if (!matShadow || !cache.activeMaterials || !cache.activeMaterials.has(matShadow)) {", + " unregisterActiveMaterial(matShadow);", + " continue;", + " }", + " var udShadow = matShadow.userData;", + " if (!udShadow) {", + " unregisterActiveMaterial(matShadow);", + " continue;", + " }", + " updateShadowUniforms(matShadow, udShadow._foliageDepthMat);", + " updateShadowUniforms(matShadow, udShadow._foliageDistanceMat);", + " }", + " })(runtimeScene, eventsFunctionContext);", + "" + ], + "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": [ + "(function (runtimeScene, eventsFunctionContext) {", + " var GUST_DEFAULTS = {", + " strength: 3.0,", + " scale: 0.0005,", + " speed: 0.25,", + " threshold: 0.28,", + " contrast: 1.25", + " };", + "", + " function ensureGustFallbackHelper(cacheObj) {", + " if (!cacheObj) return;", + " if (typeof cacheObj._ensureGustFallbackTex !== \"function\") {", + " cacheObj._ensureGustFallbackTex = function() {", + " try {", + " if (cacheObj.gustFallbackTex && cacheObj._gustFallbackIsStripe === true) {", + " return cacheObj.gustFallbackTex;", + " }", + "", + " var oldFallback = cacheObj.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); // smoothstep-like falloff", + " 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;", + "", + " cacheObj.gustFallbackTex = tex;", + " cacheObj._gustFallbackIsStripe = true;", + "", + " if (oldFallback && oldFallback !== tex && oldFallback !== cacheObj.gustTexture && typeof oldFallback.dispose === \"function\") {", + " try { oldFallback.dispose(); } catch (eOldFallbackDispose) {}", + " }", + "", + " return tex;", + " } catch (eCreateFallback) {", + " cacheObj.gustFallbackTex = null;", + " cacheObj._gustFallbackIsStripe = false;", + " return null;", + " }", + " };", + " }", + " }", + "", + " function disposeNonFallbackTexture(cacheObj, tex) {", + " if (!tex || tex === cacheObj.gustFallbackTex || typeof tex.dispose !== \"function\") return;", + " try { tex.dispose(); } catch (eTexDispose) {}", + " }", + "", + " if (!window.__FOLIAGE_SWAY__) {", + " // Initialize cache if it doesn't exist (same as onCreated does)", + " window.__FOLIAGE_SWAY__ = {", + " patchedMaterials: new WeakSet(),", + " sharedByKey: new Map(),", + " activeMaterials: new Set(),", + " time: 0,", + " gustVersion: 0,", + " debugPrinted: new Set(),", + "", + " // gust state (global)", + " gustEnabled: false,", + " gustStrength: GUST_DEFAULTS.strength,", + " gustScale: GUST_DEFAULTS.scale,", + " gustSpeed: GUST_DEFAULTS.speed,", + " gustThreshold: GUST_DEFAULTS.threshold,", + " gustContrast: GUST_DEFAULTS.contrast,", + " gustTexture: null,", + " gustTextureKey: \"\",", + " _gustTextureLoadState: \"idle\",", + " _gustTextureLoadId: 0,", + " _gustTextureLoadUrl: \"\",", + " _gustDefineDirty: false,", + "", + " // fallback texture (generated stripe)", + " gustFallbackTex: null", + " };", + "", + " var cache = window.__FOLIAGE_SWAY__;", + " ensureGustFallbackHelper(cache);", + " cache._ensureGustFallbackTex();", + " } else {", + " // Ensure cache has all gust properties (for backward compatibility)", + " if (window.__FOLIAGE_SWAY__.gustEnabled === undefined) window.__FOLIAGE_SWAY__.gustEnabled = false;", + " if (!isFinite(window.__FOLIAGE_SWAY__.gustStrength)) window.__FOLIAGE_SWAY__.gustStrength = GUST_DEFAULTS.strength;", + " if (!isFinite(window.__FOLIAGE_SWAY__.gustScale)) window.__FOLIAGE_SWAY__.gustScale = GUST_DEFAULTS.scale;", + " if (!isFinite(window.__FOLIAGE_SWAY__.gustSpeed)) window.__FOLIAGE_SWAY__.gustSpeed = GUST_DEFAULTS.speed;", + " if (!isFinite(window.__FOLIAGE_SWAY__.gustThreshold)) window.__FOLIAGE_SWAY__.gustThreshold = GUST_DEFAULTS.threshold;", + " if (!isFinite(window.__FOLIAGE_SWAY__.gustContrast)) window.__FOLIAGE_SWAY__.gustContrast = GUST_DEFAULTS.contrast;", + " if (window.__FOLIAGE_SWAY__.gustTextureKey === undefined) window.__FOLIAGE_SWAY__.gustTextureKey = \"\";", + " if (!isFinite(window.__FOLIAGE_SWAY__.gustVersion)) window.__FOLIAGE_SWAY__.gustVersion = 0;", + " if (typeof window.__FOLIAGE_SWAY__._gustTextureLoadState !== \"string\") window.__FOLIAGE_SWAY__._gustTextureLoadState = \"idle\";", + " if (typeof window.__FOLIAGE_SWAY__._gustTextureLoadId !== \"number\") window.__FOLIAGE_SWAY__._gustTextureLoadId = 0;", + " if (typeof window.__FOLIAGE_SWAY__._gustTextureLoadUrl !== \"string\") window.__FOLIAGE_SWAY__._gustTextureLoadUrl = \"\";", + " if (typeof window.__FOLIAGE_SWAY__._gustDefineDirty !== \"boolean\") window.__FOLIAGE_SWAY__._gustDefineDirty = false;", + " }", + "", + " var cache = window.__FOLIAGE_SWAY__;", + " ensureGustFallbackHelper(cache);", + " cache._ensureGustFallbackTex();", + "", + " function readNumArg(name, fallback) {", + " var n = Number(eventsFunctionContext.getArgument(name));", + " return isFinite(n) ? n : fallback;", + " }", + "", + " 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;", + " }", + "", + " var enabled = parseBool(eventsFunctionContext.getArgument(\"enabled\"), false);", + "", + " var strength = readNumArg(\"strength\", GUST_DEFAULTS.strength);", + " var scale = readNumArg(\"scale\", GUST_DEFAULTS.scale);", + " var speed = readNumArg(\"speed\", GUST_DEFAULTS.speed);", + " var threshold = readNumArg(\"threshold\", GUST_DEFAULTS.threshold);", + " var contrast = readNumArg(\"contrast\", GUST_DEFAULTS.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 = String(eventsFunctionContext.getArgument(\"textureKey\") || \"\").trim();", + " var textureUrl = String(eventsFunctionContext.getArgument(\"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;", + " }", + "", + " // textureUrl can be a direct URL or a path that TextureLoader can resolve.", + " // textureKey avoids redundant loads; same key can retry after error.", + " if (!textureUrl) {", + " var oldTexNoUrl = cache.gustTexture;", + " var hadCustomTex = !!oldTexNoUrl && oldTexNoUrl !== cache.gustFallbackTex;", + " cache._gustTextureLoadId = (cache._gustTextureLoadId || 0) + 1; // Invalidate pending async callbacks.", + " cache._gustTextureLoadState = \"idle\";", + " cache._gustTextureLoadUrl = \"\";", + " cache.gustTextureKey = \"\";", + " cache.gustTexture = null; // Fallback is consumed via `cache.gustTexture || cache.gustFallbackTex`.", + " if (hadCustomTex) {", + " disposeNonFallbackTexture(cache, oldTexNoUrl);", + " cache.gustVersion = (cache.gustVersion || 0) + 1;", + " }", + " } else {", + " var key = textureKey || textureUrl;", + " var sameKey = cache.gustTextureKey === key;", + " var sameUrl = cache._gustTextureLoadUrl === textureUrl;", + " var state = cache._gustTextureLoadState || \"idle\";", + " var skipLoad = (sameKey && sameUrl && (state === \"loading\" || (state === \"ready\" && !!cache.gustTexture)));", + "", + " if (!skipLoad) {", + " cache.gustTextureKey = key;", + " cache._gustTextureLoadUrl = textureUrl;", + " cache._gustTextureLoadState = \"loading\";", + " var requestId = (cache._gustTextureLoadId || 0) + 1;", + " cache._gustTextureLoadId = requestId;", + " cache.gustVersion = (cache.gustVersion || 0) + 1;", + " try {", + " if (!cache._gustTextureLoader || typeof cache._gustTextureLoader.load !== \"function\") {", + " cache._gustTextureLoader = new THREE.TextureLoader();", + " }", + " cache._gustTextureLoader.load(", + " textureUrl,", + " function (tex) {", + " if (requestId !== cache._gustTextureLoadId || cache.gustTextureKey !== key) {", + " disposeNonFallbackTexture(cache, tex);", + " return;", + " }", + " try {", + " tex.wrapS = THREE.RepeatWrapping;", + " tex.wrapT = THREE.RepeatWrapping;", + " tex.flipY = false;", + " tex.needsUpdate = true;", + " } catch (eSetupTex) {}", + "", + " var oldTex = cache.gustTexture;", + " cache.gustTexture = tex;", + " cache._gustTextureLoadState = \"ready\";", + " disposeNonFallbackTexture(cache, oldTex);", + " cache.gustVersion = (cache.gustVersion || 0) + 1;", + " },", + " undefined,", + " function () {", + " if (requestId !== cache._gustTextureLoadId || cache.gustTextureKey !== key) return;", + " cache._gustTextureLoadState = \"error\";", + " var oldTex = cache.gustTexture;", + " cache.gustTexture = null; // Force fallback when load fails.", + " if (oldTex) disposeNonFallbackTexture(cache, oldTex);", + " cache.gustVersion = (cache.gustVersion || 0) + 1;", + " }", + " );", + " } catch (eLoadStart) {", + " if (requestId === cache._gustTextureLoadId && cache.gustTextureKey === key) {", + " cache._gustTextureLoadState = \"error\";", + " var oldTexSyncErr = cache.gustTexture;", + " cache.gustTexture = null;", + " if (oldTexSyncErr) disposeNonFallbackTexture(cache, oldTexSyncErr);", + " cache.gustVersion = (cache.gustVersion || 0) + 1;", + " }", + " }", + " }", + " }", + "", + "})(runtimeScene, eventsFunctionContext);", + "" + ], + "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": [] + } + ], + "eventsBasedBehaviors": [ + { + "description": "Adds wind-based swaying to 3D foliage objects — grass, bushes, trees.", + "fullName": "Foliage swaying", + "name": "FoliageSwaying", + "objectType": "Scene3D::Model3DObject", + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "(function (runtimeScene, eventsFunctionContext) {", + " var recreatedActiveSet = false;", + " var GUST_DEFAULTS = {", + " strength: 3.0,", + " scale: 0.0005,", + " speed: 0.25,", + " threshold: 0.28,", + " contrast: 1.25", + " };", + " if (!window.__FOLIAGE_SWAY__) {", + " window.__FOLIAGE_SWAY__ = {", + " patchedMaterials: new WeakSet(),", + " sharedByKey: new Map(),", + " activeMaterials: new Set(),", + " time: 0,", + " gustVersion: 0,", + " debugPrinted: new Set(),", + " ", + " // gust state (global)", + " gustEnabled: false,", + " gustStrength: GUST_DEFAULTS.strength,", + " gustScale: GUST_DEFAULTS.scale,", + " gustSpeed: GUST_DEFAULTS.speed,", + " gustThreshold: GUST_DEFAULTS.threshold,", + " gustContrast: GUST_DEFAULTS.contrast,", + " gustTexture: null,", + " gustTextureKey: \"\",", + " _gustTextureLoadState: \"idle\",", + " _gustTextureLoadId: 0,", + " _gustTextureLoadUrl: \"\",", + " _gustDefineDirty: false,", + " ", + " // fallback texture (generated stripe)", + " gustFallbackTex: null,", + "", + " // Cache object-type analysis results to avoid repeated traversals.", + " objectTypeCache: new Map(),", + " _objectTypeCacheState: { tick: 0, setCount: 0, meta: new Map() },", + " // Cache bounding boxes per geometry to avoid recomputing them.", + " geometryBBoxCache: new WeakMap(),", + " // Stable auto polyScale cache by object/material/type.", + " autoPolyScaleCache: new Map(),", + " // Non-instanced distance fade: list of { gdObj, threeObj, material, fadeStart, fadeEnd, _wasHidden? }", + " nonInstancedFadeObjects: [],", + " // Parked non-instanced entries with fade disabled at runtime.", + " nonInstancedStaticObjects: [],", + " _nonInstancedStaticCheckAccum: 0", + " };", + " } else {", + " if (", + " !window.__FOLIAGE_SWAY__.activeMaterials ||", + " typeof window.__FOLIAGE_SWAY__.activeMaterials[Symbol.iterator] !== \"function\"", + " ) {", + " window.__FOLIAGE_SWAY__.activeMaterials = new Set();", + " recreatedActiveSet = true;", + " }", + " if (!isFinite(window.__FOLIAGE_SWAY__.time)) window.__FOLIAGE_SWAY__.time = 0;", + " if (", + " !window.__FOLIAGE_SWAY__.debugPrinted ||", + " typeof window.__FOLIAGE_SWAY__.debugPrinted.has !== \"function\"", + " ) {", + " window.__FOLIAGE_SWAY__.debugPrinted = new Set();", + " }", + " ", + " if (!window.__FOLIAGE_SWAY__.nonInstancedFadeObjects || !Array.isArray(window.__FOLIAGE_SWAY__.nonInstancedFadeObjects)) {", + " window.__FOLIAGE_SWAY__.nonInstancedFadeObjects = [];", + " }", + " if (!window.__FOLIAGE_SWAY__.nonInstancedStaticObjects || !Array.isArray(window.__FOLIAGE_SWAY__.nonInstancedStaticObjects)) {", + " window.__FOLIAGE_SWAY__.nonInstancedStaticObjects = [];", + " }", + " if (!isFinite(window.__FOLIAGE_SWAY__._nonInstancedStaticCheckAccum)) {", + " window.__FOLIAGE_SWAY__._nonInstancedStaticCheckAccum = 0;", + " }", + " if (window.__FOLIAGE_SWAY__.gustEnabled === undefined) window.__FOLIAGE_SWAY__.gustEnabled = false;", + " if (!isFinite(window.__FOLIAGE_SWAY__.gustStrength)) window.__FOLIAGE_SWAY__.gustStrength = GUST_DEFAULTS.strength;", + " if (!isFinite(window.__FOLIAGE_SWAY__.gustScale)) window.__FOLIAGE_SWAY__.gustScale = GUST_DEFAULTS.scale;", + " if (!isFinite(window.__FOLIAGE_SWAY__.gustSpeed)) window.__FOLIAGE_SWAY__.gustSpeed = GUST_DEFAULTS.speed;", + " if (!isFinite(window.__FOLIAGE_SWAY__.gustThreshold)) window.__FOLIAGE_SWAY__.gustThreshold = GUST_DEFAULTS.threshold;", + " if (!isFinite(window.__FOLIAGE_SWAY__.gustContrast)) window.__FOLIAGE_SWAY__.gustContrast = GUST_DEFAULTS.contrast;", + " if (window.__FOLIAGE_SWAY__.gustTextureKey === undefined) window.__FOLIAGE_SWAY__.gustTextureKey = \"\";", + " if (!isFinite(window.__FOLIAGE_SWAY__.gustVersion)) window.__FOLIAGE_SWAY__.gustVersion = 0;", + " if (typeof window.__FOLIAGE_SWAY__._gustTextureLoadState !== \"string\") window.__FOLIAGE_SWAY__._gustTextureLoadState = \"idle\";", + " if (typeof window.__FOLIAGE_SWAY__._gustTextureLoadId !== \"number\") window.__FOLIAGE_SWAY__._gustTextureLoadId = 0;", + " if (typeof window.__FOLIAGE_SWAY__._gustTextureLoadUrl !== \"string\") window.__FOLIAGE_SWAY__._gustTextureLoadUrl = \"\";", + " if (typeof window.__FOLIAGE_SWAY__._gustDefineDirty !== \"boolean\") window.__FOLIAGE_SWAY__._gustDefineDirty = false;", + " // Backward compatibility: ensure objectTypeCache exists.", + " if (!window.__FOLIAGE_SWAY__.objectTypeCache || typeof window.__FOLIAGE_SWAY__.objectTypeCache.get !== \"function\") {", + " window.__FOLIAGE_SWAY__.objectTypeCache = new Map();", + " }", + " if (!window.__FOLIAGE_SWAY__._objectTypeCacheState || typeof window.__FOLIAGE_SWAY__._objectTypeCacheState !== \"object\") {", + " window.__FOLIAGE_SWAY__._objectTypeCacheState = { tick: 0, setCount: 0, meta: new Map() };", + " }", + " // Backward compatibility: ensure geometryBBoxCache exists.", + " if (!window.__FOLIAGE_SWAY__.geometryBBoxCache || typeof window.__FOLIAGE_SWAY__.geometryBBoxCache.get !== \"function\") {", + " window.__FOLIAGE_SWAY__.geometryBBoxCache = new WeakMap();", + " }", + " if (!window.__FOLIAGE_SWAY__.autoPolyScaleCache || typeof window.__FOLIAGE_SWAY__.autoPolyScaleCache.get !== \"function\") {", + " window.__FOLIAGE_SWAY__.autoPolyScaleCache = new Map();", + " }", + " }", + " ", + "var cache = window.__FOLIAGE_SWAY__;", + "", + " function ensureGustFallbackHelper(cacheObj) {", + " if (!cacheObj) return;", + " if (typeof cacheObj._ensureGustFallbackTex !== \"function\") {", + " cacheObj._ensureGustFallbackTex = function() {", + " try {", + " if (cacheObj.gustFallbackTex && cacheObj._gustFallbackIsStripe === true) {", + " return cacheObj.gustFallbackTex;", + " }", + "", + " var oldFallback = cacheObj.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;", + " cacheObj.gustFallbackTex = tex;", + " cacheObj._gustFallbackIsStripe = true;", + "", + " if (oldFallback && oldFallback !== tex && oldFallback !== cacheObj.gustTexture && typeof oldFallback.dispose === \"function\") {", + " try { oldFallback.dispose(); } catch (eOldFallbackDispose) {}", + " }", + " return tex;", + " } catch (eCreateFallback) {", + " cacheObj.gustFallbackTex = null;", + " cacheObj._gustFallbackIsStripe = false;", + " return null;", + " }", + " };", + " }", + " }", + "", + " ensureGustFallbackHelper(cache);", + "", + " function ensureMaterialTrackingHelpers(cacheObj) {", + " if (!cacheObj) return;", + " function ensureBuckets() {", + " if (!cacheObj._materialBuckets || typeof cacheObj._materialBuckets !== \"object\") {", + " cacheObj._materialBuckets = {", + " timeWind: new Set(),", + " fadeInterp: new Set(),", + " gust: new Set(),", + " shadowHost: new Set(),", + " unknown: new Set(),", + " rebuildNeeded: true", + " };", + " }", + " return cacheObj._materialBuckets;", + " }", + " if (typeof cacheObj._resetMaterialBuckets !== \"function\") {", + " cacheObj._resetMaterialBuckets = function() {", + " cacheObj._materialBuckets = {", + " timeWind: new Set(),", + " fadeInterp: new Set(),", + " gust: new Set(),", + " shadowHost: new Set(),", + " unknown: new Set(),", + " rebuildNeeded: true", + " };", + " };", + " }", + " if (typeof cacheObj._registerActiveMaterial !== \"function\") {", + " cacheObj._registerActiveMaterial = function(mat) {", + " if (!mat) return;", + " if (!cacheObj.activeMaterials || typeof cacheObj.activeMaterials.add !== \"function\") {", + " cacheObj.activeMaterials = new Set();", + " }", + " cacheObj.activeMaterials.add(mat);", + " var b = ensureBuckets();", + " b.unknown.add(mat);", + " };", + " }", + " if (typeof cacheObj._unregisterActiveMaterial !== \"function\") {", + " cacheObj._unregisterActiveMaterial = function(mat) {", + " if (!mat) return;", + " if (cacheObj.activeMaterials && typeof cacheObj.activeMaterials.delete === \"function\") {", + " cacheObj.activeMaterials.delete(mat);", + " }", + " var b = ensureBuckets();", + " b.timeWind.delete(mat);", + " b.fadeInterp.delete(mat);", + " b.gust.delete(mat);", + " b.shadowHost.delete(mat);", + " b.unknown.delete(mat);", + " };", + " }", + " ensureBuckets();", + " }", + "", + " ensureMaterialTrackingHelpers(cache);", + " if (recreatedActiveSet && typeof cache._resetMaterialBuckets === \"function\") {", + " cache._resetMaterialBuckets();", + " }", + " function registerActiveMaterial(mat) {", + " if (!mat) return;", + " if (typeof cache._registerActiveMaterial === \"function\") cache._registerActiveMaterial(mat);", + " else if (cache.activeMaterials && typeof cache.activeMaterials.add === \"function\") cache.activeMaterials.add(mat);", + " }", + " function unregisterActiveMaterial(mat) {", + " if (!mat) return;", + " if (typeof cache._unregisterActiveMaterial === \"function\") cache._unregisterActiveMaterial(mat);", + " else if (cache.activeMaterials && typeof cache.activeMaterials.delete === \"function\") cache.activeMaterials.delete(mat);", + " }", + "", + " if (typeof cache._disposeFoliageShadowMaterials !== \"function\") {", + " cache._disposeFoliageShadowMaterials = function(mat) {", + " if (!mat || !mat.userData) return;", + " var ud = mat.userData;", + " var depthMat = ud._foliageDepthMat;", + " var distanceMat = ud._foliageDistanceMat;", + " if (depthMat && depthMat !== mat && typeof depthMat.dispose === \"function\") {", + " try { depthMat.dispose(); } catch (eDepthDispose) {}", + " }", + " if (distanceMat && distanceMat !== mat && distanceMat !== depthMat && typeof distanceMat.dispose === \"function\") {", + " try { distanceMat.dispose(); } catch (eDistanceDispose) {}", + " }", + " delete ud._foliageDepthMat;", + " delete ud._foliageDistanceMat;", + " delete ud._foliageShadowOwned;", + " };", + " }", + "", + " // Ensure instancing manager exists (used by GPU instancing path)", + " if (!cache.instancing || typeof cache.instancing !== \"object\") {", + " cache.instancing = {", + " groups: new Map(),", + " dirty: false,", + " sceneTag: null,", + " pending: [],", + " queueIdCounter: 0,", + " cancelledQueueIds: new Set()", + " };", + " } else {", + " if (!cache.instancing.groups || typeof cache.instancing.groups.get !== \"function\") {", + " cache.instancing.groups = new Map();", + " }", + " if (typeof cache.instancing.dirty !== \"boolean\") cache.instancing.dirty = false;", + " if (cache.instancing.sceneTag === undefined) cache.instancing.sceneTag = null;", + " // Ensure queue cancellation tracking exists for consistency", + " if (!Array.isArray(cache.instancing.pending)) cache.instancing.pending = [];", + " if (typeof cache.instancing.queueIdCounter !== \"number\") cache.instancing.queueIdCounter = 0;", + " if (!cache.instancing.cancelledQueueIds || typeof cache.instancing.cancelledQueueIds.has !== \"function\") {", + " cache.instancing.cancelledQueueIds = new Set();", + " }", + " }", + "", + " function fallbackSceneCleanup(cacheObj) {", + " if (!cacheObj) return;", + " var inst = cacheObj.instancing;", + " if (inst && inst.groups && typeof inst.groups.forEach === \"function\") {", + " inst.groups.forEach(function(g) {", + " if (!g) return;", + " 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 (eOwned) {}", + " }", + " if (typeof g.mesh.dispose === \"function\") {", + " try { g.mesh.dispose(); } catch (eMesh) {}", + " }", + " }", + " } catch (eRm) {}", + " });", + " inst.groups.clear();", + " }", + " if (inst && inst.foliageRoot) {", + " try {", + " if (inst.foliageRoot.parent) inst.foliageRoot.parent.remove(inst.foliageRoot);", + " } catch (eRoot) {}", + " inst.foliageRoot = null;", + " }", + "", + " var seenMaterials = new Set();", + " if (cacheObj.sharedByKey && typeof cacheObj.sharedByKey.forEach === \"function\") {", + " cacheObj.sharedByKey.forEach(function(entry) {", + " if (!entry || entry._ownedByFoliage === false || !entry.material) return;", + " var mat = entry.material;", + " if (seenMaterials.has(mat)) return;", + " seenMaterials.add(mat);", + " if (typeof cacheObj._disposeFoliageShadowMaterials === \"function\") {", + " cacheObj._disposeFoliageShadowMaterials(mat);", + " }", + " if (typeof mat.dispose === \"function\") {", + " try { mat.dispose(); } catch (eMat) {}", + " }", + " });", + " if (typeof cacheObj.sharedByKey.clear === \"function\") cacheObj.sharedByKey.clear();", + " }", + " if (cacheObj.activeMaterials && typeof cacheObj.activeMaterials.forEach === \"function\") {", + " cacheObj.activeMaterials.forEach(function(mat) {", + " if (!mat || seenMaterials.has(mat)) return;", + " seenMaterials.add(mat);", + " if (typeof cacheObj._disposeFoliageShadowMaterials === \"function\") {", + " cacheObj._disposeFoliageShadowMaterials(mat);", + " }", + " if (typeof mat.dispose === \"function\") {", + " try { mat.dispose(); } catch (eMat2) {}", + " }", + " });", + " if (typeof cacheObj.activeMaterials.clear === \"function\") cacheObj.activeMaterials.clear();", + " }", + " if (typeof cacheObj._resetMaterialBuckets === \"function\") cacheObj._resetMaterialBuckets();", + "", + " if (cacheObj.gustTexture && cacheObj.gustTexture !== cacheObj.gustFallbackTex) {", + " try { cacheObj.gustTexture.dispose(); } catch (eGust) {}", + " cacheObj.gustTexture = null;", + " }", + " cacheObj.gustTextureKey = \"\";", + " cacheObj.nonInstancedFadeObjects = [];", + " cacheObj.nonInstancedStaticObjects = [];", + " cacheObj._nonInstancedStaticCheckAccum = 0;", + " cacheObj.patchedMaterials = new WeakSet();", + " if (cacheObj.objectTypeCache && typeof cacheObj.objectTypeCache.clear === \"function\") cacheObj.objectTypeCache.clear();", + " if (cacheObj.autoPolyScaleCache && typeof cacheObj.autoPolyScaleCache.clear === \"function\") cacheObj.autoPolyScaleCache.clear();", + " if (cacheObj.debugPrinted && typeof cacheObj.debugPrinted.clear === \"function\") cacheObj.debugPrinted.clear();", + " cacheObj.geometryBBoxCache = new WeakMap();", + " cacheObj._objectTypeCacheState = { tick: 0, setCount: 0, meta: new Map() };", + "", + " if (inst) {", + " inst.cancelledQueueIds = new Set();", + " inst.queueIdCounter = 0;", + " inst.pending = [];", + " inst._cachedSceneRoot = null;", + " inst._tmpRootSet = null;", + " inst.dirty = false;", + " }", + " }", + "", + " // Scene change invalidates scene-scoped caches and pooled state.", + " var currentSceneTag = (runtimeScene && typeof runtimeScene.getName === \"function\") ? runtimeScene.getName() : null;", + " if (cache.instancing && cache.instancing.sceneTag !== currentSceneTag) {", + " if (typeof cache._cleanupForSceneChange === \"function\") {", + " cache._cleanupForSceneChange(cache);", + " } else {", + " // Fallback on very first scene frame if update hasn't attached shared cleanup helper yet.", + " fallbackSceneCleanup(cache);", + " }", + " cache.instancing.sceneTag = currentSceneTag;", + " }", + "", + " if (!cache._objectTypeCacheState || typeof cache._objectTypeCacheState !== \"object\" || !cache._objectTypeCacheState.meta || typeof cache._objectTypeCacheState.meta.get !== \"function\") {", + " cache._objectTypeCacheState = { tick: 0, setCount: 0, meta: new Map() };", + " }", + " var objectTypeCacheState = cache._objectTypeCacheState;", + " var OBJECT_TYPE_CACHE_CAPS = { rop: 512, stats: 256, fast: 1024 };", + "", + " function classifyObjectTypeCacheKey(key) {", + " if (!key) return \"rop\";", + " if (key.indexOf(\"::GPU::FD\") !== -1) return \"fast\";", + " if (key.indexOf(\"::collectAndStats\") !== -1) return \"stats\";", + " return \"rop\";", + " }", + "", + " function touchObjectTypeCacheKey(key, kindHint) {", + " var kind = kindHint || classifyObjectTypeCacheKey(key);", + " objectTypeCacheState.tick++;", + " objectTypeCacheState.meta.set(key, { kind: kind, lastUsed: objectTypeCacheState.tick });", + " }", + "", + " function pruneObjectTypeCacheIfNeeded(force) {", + " objectTypeCacheState.setCount++;", + " if (!force && (objectTypeCacheState.setCount % 200) !== 0) return;", + " var oversByKind = { rop: 0, stats: 0, fast: 0 };", + " var counts = { rop: 0, stats: 0, fast: 0 };", + " objectTypeCacheState.meta.forEach(function(meta, key) {", + " if (!cache.objectTypeCache.has(key)) {", + " objectTypeCacheState.meta.delete(key);", + " return;", + " }", + " var kind = (meta && meta.kind) || classifyObjectTypeCacheKey(key);", + " if (counts[kind] === undefined) counts[kind] = 0;", + " counts[kind]++;", + " });", + " oversByKind.rop = Math.max(0, (counts.rop || 0) - OBJECT_TYPE_CACHE_CAPS.rop);", + " oversByKind.stats = Math.max(0, (counts.stats || 0) - OBJECT_TYPE_CACHE_CAPS.stats);", + " oversByKind.fast = Math.max(0, (counts.fast || 0) - OBJECT_TYPE_CACHE_CAPS.fast);", + " if (!oversByKind.rop && !oversByKind.stats && !oversByKind.fast) return;", + "", + " var candidates = [];", + " objectTypeCacheState.meta.forEach(function(meta, key) {", + " candidates.push({", + " key: key,", + " kind: (meta && meta.kind) || classifyObjectTypeCacheKey(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;", + " cache.objectTypeCache.delete(c.key);", + " objectTypeCacheState.meta.delete(c.key);", + " oversByKind[c.kind]--;", + " if (!oversByKind.rop && !oversByKind.stats && !oversByKind.fast) break;", + " }", + " }", + "", + " function objectTypeCacheGet(key) {", + " if (!cache.objectTypeCache) return undefined;", + " var value = cache.objectTypeCache.get(key);", + " if (value !== undefined) touchObjectTypeCacheKey(key);", + " return value;", + " }", + "", + " function objectTypeCacheSet(key, value) {", + " if (!cache.objectTypeCache) return;", + " cache.objectTypeCache.set(key, value);", + " touchObjectTypeCacheKey(key);", + " pruneObjectTypeCacheIfNeeded(false);", + " }", + " ", + " cache._ensureGustFallbackTex();", + " ", + " var objs = eventsFunctionContext.getObjects(\"Object\") || [];", + " var gdObj = objs[0];", + " if (!gdObj) return;", + " ", + " var behavior = null;", + " try {", + " behavior = gdObj.getBehavior(\"FoliageSwaying\");", + " } catch (e) {}", + " if (!behavior) {", + " try {", + " behavior = gdObj.getBehavior(\"NatureElements::FoliageSwaying\");", + " } catch (e2) {}", + " }", + " if (!behavior) return;", + " ", + " // Safety guard: Prevent duplicate queuing if object is already queued or has index", + " if (behavior.__foliageInstancingIndex !== undefined || behavior.__foliageQueued === true) {", + " return;", + " }", + " ", + " var data = behavior._behaviorData || behavior;", + "", + " 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 readClampedNumber(v, fallback, minV, maxV) {", + " var n = Number(v);", + " if (!isFinite(n)) n = fallback;", + " if (n < minV) n = minV;", + " if (n > maxV) n = maxV;", + " return n;", + " }", + "", + " 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 };", + " }", + "", + " function resolveRenderSide(sourceSide, cullingModeName) {", + " if (cullingModeName === \"backfaceCullingOn\") return THREE.FrontSide;", + " if (cullingModeName === \"backfaceCullingOff\") return THREE.DoubleSide;", + " return (typeof sourceSide === \"number\") ? sourceSide : THREE.FrontSide;", + " }", + "", + " function buildSideSuffix(cullingModeCode, renderSide) {", + " var sd = (typeof renderSide === \"number\") ? renderSide : THREE.FrontSide;", + " return \"_CM\" + cullingModeCode + \"_SD\" + sd;", + " }", + "", + " 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);", + " }", + "", + " // ============================================================", + " // Fast path: if instancing metadata is cached, skip full setup.", + " // If so, queue transform capture and return early.", + " // ============================================================", + " var sharedKeyFast = gdObj.getName ? gdObj.getName() : gdObj.name || \"DEFAULT\";", + " var gpuInstancingFast = data.gpuInstancing;", + " var distanceFadeFast = parseBool(data.distanceFadeEnabled, false);", + " var customLitFast = parseBool(data.customLit, false);", + " var twoSidedLightingFast = parseBool(data.twoSidedLighting, true);", + " var metallicFast = readClampedNumber(data.metallic, 0, 0, 1);", + " var roughnessFast = readClampedNumber(data.roughness, 1, 0, 1);", + " var specularFast = readClampedNumber(data.specular, 0.1, 0, 1);", + " var normalStrengthFast = readClampedNumber(data.normalStrength, 1, 0, 1);", + " var aoStrengthFast = readClampedNumber(data.aoStrength, 1, 0, 1);", + " var envStrengthFast = readClampedNumber(data.envStrength, 1, 0, 1);", + " var polyScaleRawFast = readClampedNumber(data.polyScale, 0, 0, 200);", + " var polyScaleAutoModeFast = (polyScaleRawFast === 0);", + " var cullingModeFastInfo = normalizeCullingMode(data.cullingMode);", + " var pbrSuffixFast = buildPbrSuffix(customLitFast, metallicFast, roughnessFast, specularFast, normalStrengthFast, aoStrengthFast, envStrengthFast);", + " if (gpuInstancingFast === true || gpuInstancingFast === \"true\" || gpuInstancingFast === \"1\") {", + " var swayTypeFast = data.swayType !== undefined && data.swayType !== null && String(data.swayType).trim() !== \"\" ", + " ? String(data.swayType) ", + " : \"grassSway\";", + " if (swayTypeFast === \"treeTrunkSway\") twoSidedLightingFast = false;", + " ", + " // Fast path za grassSway, bushSway i treeTrunkSway s GPU instancing", + " if (swayTypeFast === \"grassSway\" || swayTypeFast === \"bushSway\" || swayTypeFast === \"treeTrunkSway\") {", + " var materialNameFast = data.materialName !== undefined && data.materialName !== null ", + " ? String(data.materialName).trim() ", + " : \"\";", + " var cachedRopFast = objectTypeCacheGet(sharedKeyFast + \"::\" + (materialNameFast || \"\"));", + " var sourceSideFast = (cachedRopFast && cachedRopFast.selection && cachedRopFast.selection.srcRef && typeof cachedRopFast.selection.srcRef.side === \"number\")", + " ? cachedRopFast.selection.srcRef.side", + " : THREE.FrontSide;", + " var resolvedRenderSideFast = resolveRenderSide(sourceSideFast, cullingModeFastInfo.name);", + " var sideSuffixFast = buildSideSuffix(cullingModeFastInfo.code, resolvedRenderSideFast);", + " var fastCacheKey = sharedKeyFast + \"::\" + (materialNameFast || \"\") + \"::\" + swayTypeFast + \"::GPU::FD\" + (distanceFadeFast ? \"1\" : \"0\") + pbrSuffixFast + sideSuffixFast + \"_TSL\" + (twoSidedLightingFast ? \"1\" : \"0\") + \"_PA\" + (polyScaleAutoModeFast ? \"1\" : \"0\");", + " var fastCached = objectTypeCacheGet(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 = cache.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).", + " var repMeshFast = null;", + " var geoFast = fastCached._gpuGeometry;", + " threeObjFast.traverse(function(o) {", + " if (repMeshFast) return;", + " if (o && o.isMesh && o.geometry === geoFast) {", + " repMeshFast = o;", + " }", + " });", + " ", + " if (repMeshFast) {", + " // Dodaj u pending i vrati se - SKIP SVE OSTALO!", + " if (!inst.pending) inst.pending = [];", + " // Ensure cancellation tracking exists (critical for edge case: object destroyed before first update tick)", + " if (typeof inst.queueIdCounter !== \"number\") inst.queueIdCounter = 0;", + " if (!inst.cancelledQueueIds || typeof inst.cancelledQueueIds.has !== \"function\") {", + " inst.cancelledQueueIds = new Set();", + " }", + " // Assign queueId for cancellation tracking", + " var queueId = ++inst.queueIdCounter;", + " inst.pending.push({", + " gdObj: gdObj,", + " threeObj: threeObjFast,", + " repMesh: repMeshFast,", + " geometry: geoFast,", + " material: g.material,", + " parent: g.parent,", + " baseGroupKey: groupKey, // baseGroupKey (without super-chunk suffix)", + " behavior: behavior,", + " queueId: queueId", + " });", + " behavior.__foliageQueued = true; // Mark as queued to prevent duplicates", + " behavior.__foliageQueueId = queueId; // Store queueId for cancellation", + " delete behavior.__foliageNonInstancedRegistered;", + " // Set groupKey immediately so onDestroy can find it even before flush", + " behavior.__foliageInstancingGroupKey = groupKey;", + " threeObjFast.userData = threeObjFast.userData || {};", + " delete threeObjFast.userData.__foliageNonInstancedRegistered;", + " threeObjFast.userData.__foliageInstancingGroupKey = groupKey;", + " inst.dirty = true;", + " ", + " // Increment shared material refCount (important for cleanup).", + " var sharedMatKey = fastCached._sharedMaterialKey;", + " var entry = cache.sharedByKey.get(sharedMatKey);", + " if (entry) {", + " entry.refCount++;", + " }", + " ", + " // Store behavior reference for cleanup.", + " behavior.__foliageSharedKey = sharedMatKey;", + " threeObjFast.userData.__foliageSharedKey = sharedMatKey;", + " ", + " return; // Fast path complete: skip expensive setup.", + " }", + " }", + " }", + " }", + " }", + " }", + " // ============================================================", + " // Fall back to full path if fast path cannot be used.", + " // ============================================================", + " ", + " var uniformSway = parseBool(data.uniformSway, true);", + "", + " var swayType = \"grassSway\";", + " if (data.swayType !== undefined && data.swayType !== null && String(data.swayType).trim() !== \"\") {", + " swayType = String(data.swayType);", + " }", + "", + " var useColorGrading = parseBool(data._useColorGrading, false);", + " var gpuInstancing = parseBool(data.gpuInstancing, false);", + " var debugOutput = parseBool(data.debugOutput, false);", + "", + " // Distance fade properties (for dither dissolve culling)", + " var distanceFadeEnabled = parseBool(data.distanceFadeEnabled, false);", + " var fadeStart = readClampedNumber(data.fadeStart, 1200, 0, 100000);", + " var fadeEnd = readClampedNumber(data.fadeEnd, 1600, 0, 100000);", + " // Ensure fadeEnd > fadeStart", + " if (fadeEnd <= fadeStart) fadeEnd = fadeStart + 100;", + " var customLit = parseBool(data.customLit, false);", + " var twoSidedLighting = parseBool(data.twoSidedLighting, true);", + " if (swayType === \"treeTrunkSway\") twoSidedLighting = false;", + " var metallic = readClampedNumber(data.metallic, 0, 0, 1);", + " var roughness = readClampedNumber(data.roughness, 1, 0, 1);", + " var specular = readClampedNumber(data.specular, 0.1, 0, 1);", + " var normalStrength = readClampedNumber(data.normalStrength, 1, 0, 1);", + " var aoStrength = readClampedNumber(data.aoStrength, 1, 0, 1);", + " var envStrength = readClampedNumber(data.envStrength, 1, 0, 1);", + " var cullingModeInfo = normalizeCullingMode(data.cullingMode);", + " var cullingMode = cullingModeInfo.name;", + " var cullingModeCode = cullingModeInfo.code;", + " var pbrSuffix = buildPbrSuffix(customLit, metallic, roughness, specular, normalStrength, aoStrength, envStrength);", + " ", + " var materialNameRaw = data.materialName !== undefined && data.materialName !== null ? String(data.materialName) : \"\";", + " var materialName = materialNameRaw.trim();", + " ", + " 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);", + " }", + " }", + " ", + " 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;", + " }", + " ", + " 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;", + " }", + " ", + " var topColor = parseColor(data.colorTop, \"#f9372b\");", + " var bottomColor = parseColor(data.colorBottom, \"#ede060\");", + " var sat = readClampedNumber(data.uSat, 1.35, 0.0, 3.0);", + " var contrast = readClampedNumber(data.uContrast, 1.10, 0.0, 3.0);", + " ", + " // Allow polyScaleRaw up to 200 for complex models (auto-scale can exceed 100)", + " var polyScaleRaw = readClampedNumber(data.polyScale, 0, 0, 200);", + " var polyScaleAutoMode = (polyScaleRaw === 0);", + " var polyScale = polyScaleRaw / 50.0;", + "", + " var gradStart = readClampedNumber(data.gradStart, 0.0, 0.0, 1.0);", + " var gradEnd = readClampedNumber(data.gradEnd, 1.0, 0.0, 1.0);", + " 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 = parseBool(data.ignoreUV, false);", + " // 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 = readClampedNumber(data.gradHeight, 1.0, 0.001, 100000.0);", + " 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\";", + " ", + " function collectAndStats(root) {", + " var records = [];", + " var statsByRef = new Map();", + " var statsByName = new Map();", + " ", + " function ensureStats(map, key) {", + " var st = map.get(key);", + " if (!st) {", + " st = {", + " zMin: Infinity,", + " zMax: -Infinity,", + " // Relative world Z (world Z minus mesh origin Z) - for ignoreUV mode (GDevelop uses Z-up)", + " relZMin: Infinity,", + " relZMax: -Infinity,", + " hasGeom: false,", + " totalVerts: 0,", + " totalTris: 0,", + " meshCount: 0,", + " slotCount: 0,", + " planeLikeCount: 0,", + " // Root-local bounds are stable across scene/world transforms.", + " 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++;", + " }", + " }", + " ", + " // Ensure child world matrices are correct once", + " 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; }", + " ", + " root.traverse(function (o) {", + " if (!o || !o.isMesh) return;", + " ", + " var meshName = o.name && String(o.name).trim() !== \"\" ? String(o.name).trim() : \"(unnamed mesh)\";", + " if (!o.material) return;", + " ", + " var geom = o.geometry;", + " var bbox = null;", + " var vertCount = 0;", + " var triCount = 0;", + " var meshKey = isFinite(o.id) ? (\"M:\" + o.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;", + " }", + " // Read bbox from cache first so each geometry is computed once.", + " var cachedBBox = cache.geometryBBoxCache ? cache.geometryBBoxCache.get(geom) : null;", + " if (cachedBBox) {", + " bbox = cachedBBox;", + " } else {", + " // If geometry already has a boundingBox, reuse it.", + " if (geom.boundingBox) {", + " bbox = geom.boundingBox;", + " } else {", + " // Compute bounding box only when not cached.", + " try { geom.computeBoundingBox(); } catch (e) {}", + " bbox = geom.boundingBox || null;", + " }", + " // Cache result in WeakMap so entries are released with geometry.", + " if (bbox && cache.geometryBBoxCache) {", + " cache.geometryBBoxCache.set(geom, bbox);", + " }", + " }", + " }", + " ", + " // Precompute world AABB once per mesh (if bbox exists)", + " 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;", + " // Relative world Z (world Z minus mesh origin Z) - for ignoreUV mode (GDevelop uses Z-up)", + " var relMinZ = 0, relMaxZ = 0;", + "", + " if (bbox) {", + " tmpBox.copy(bbox);", + " tmpBox.applyMatrix4(o.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;", + " ", + " // Mesh origin in world space (translation component of matrixWorld)", + " // matrixWorld.elements[12] = X, [13] = Y, [14] = Z translation", + " var meshOriginZ = o.matrixWorld.elements[14];", + " relMinZ = wMinZ - meshOriginZ;", + " relMaxZ = wMaxZ - meshOriginZ;", + "", + " if (hasRootWorldInv) {", + " try {", + " meshToRoot.multiplyMatrices(rootWorldInv, o.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;", + " }", + " }", + " }", + " ", + " if (Array.isArray(o.material)) {", + " var mats = o.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)", + " });", + "", + " // Stats keyed by material ref", + " var stRef = ensureStats(statsByRef, m);", + " if (bbox) {", + " stRef.hasGeom = true;", + " if (bbox.min.z < stRef.zMin) stRef.zMin = bbox.min.z;", + " if (bbox.max.z > stRef.zMax) stRef.zMax = bbox.max.z;", + " }", + " if (vertShare) stRef.totalVerts += vertShare;", + " if (triShare) stRef.totalTris += triShare;", + " stRef.slotCount++;", + " if (isPlaneLike) stRef.planeLikeCount++;", + " markMeshUsage(stRef, meshKey);", + " if (hasLocal) {", + " if (lMinX < stRef.sizeLocalMin.x) stRef.sizeLocalMin.x = lMinX;", + " if (lMinY < stRef.sizeLocalMin.y) stRef.sizeLocalMin.y = lMinY;", + " if (lMinZ < stRef.sizeLocalMin.z) stRef.sizeLocalMin.z = lMinZ;", + "", + " if (lMaxX > stRef.sizeLocalMax.x) stRef.sizeLocalMax.x = lMaxX;", + " if (lMaxY > stRef.sizeLocalMax.y) stRef.sizeLocalMax.y = lMaxY;", + " if (lMaxZ > stRef.sizeLocalMax.z) stRef.sizeLocalMax.z = lMaxZ;", + " }", + " if (hasWorld) {", + " if (wMinX < stRef.sizeWorldMin.x) stRef.sizeWorldMin.x = wMinX;", + " if (wMinY < stRef.sizeWorldMin.y) stRef.sizeWorldMin.y = wMinY;", + " if (wMinZ < stRef.sizeWorldMin.z) stRef.sizeWorldMin.z = wMinZ;", + "", + " if (wMaxX > stRef.sizeWorldMax.x) stRef.sizeWorldMax.x = wMaxX;", + " if (wMaxY > stRef.sizeWorldMax.y) stRef.sizeWorldMax.y = wMaxY;", + " if (wMaxZ > stRef.sizeWorldMax.z) stRef.sizeWorldMax.z = wMaxZ;", + " ", + " // Relative world Z (for ignoreUV mode - GDevelop uses Z-up)", + " if (relMinZ < stRef.relZMin) stRef.relZMin = relMinZ;", + " if (relMaxZ > stRef.relZMax) stRef.relZMax = relMaxZ;", + " }", + "", + " // Stats keyed by material name (if name exists)", + " if (mName) {", + " var stName = ensureStats(statsByName, mName);", + " if (bbox) {", + " stName.hasGeom = true;", + " if (bbox.min.z < stName.zMin) stName.zMin = bbox.min.z;", + " if (bbox.max.z > stName.zMax) stName.zMax = bbox.max.z;", + " }", + " if (vertShare) stName.totalVerts += vertShare;", + " if (triShare) stName.totalTris += triShare;", + " stName.slotCount++;", + " if (isPlaneLike) stName.planeLikeCount++;", + " markMeshUsage(stName, meshKey);", + " if (hasLocal) {", + " if (lMinX < stName.sizeLocalMin.x) stName.sizeLocalMin.x = lMinX;", + " if (lMinY < stName.sizeLocalMin.y) stName.sizeLocalMin.y = lMinY;", + " if (lMinZ < stName.sizeLocalMin.z) stName.sizeLocalMin.z = lMinZ;", + "", + " if (lMaxX > stName.sizeLocalMax.x) stName.sizeLocalMax.x = lMaxX;", + " if (lMaxY > stName.sizeLocalMax.y) stName.sizeLocalMax.y = lMaxY;", + " if (lMaxZ > stName.sizeLocalMax.z) stName.sizeLocalMax.z = lMaxZ;", + " }", + " if (hasWorld) {", + " if (wMinX < stName.sizeWorldMin.x) stName.sizeWorldMin.x = wMinX;", + " if (wMinY < stName.sizeWorldMin.y) stName.sizeWorldMin.y = wMinY;", + " if (wMinZ < stName.sizeWorldMin.z) stName.sizeWorldMin.z = wMinZ;", + "", + " if (wMaxX > stName.sizeWorldMax.x) stName.sizeWorldMax.x = wMaxX;", + " if (wMaxY > stName.sizeWorldMax.y) stName.sizeWorldMax.y = wMaxY;", + " if (wMaxZ > stName.sizeWorldMax.z) stName.sizeWorldMax.z = wMaxZ;", + " ", + " // Relative world Z (for ignoreUV mode - GDevelop uses Z-up)", + " if (relMinZ < stName.relZMin) stName.relZMin = relMinZ;", + " if (relMaxZ > stName.relZMax) stName.relZMax = relMaxZ;", + " }", + " }", + " }", + " } else {", + " var mSingle = o.material;", + " if (mSingle) {", + " 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)", + " });", + "", + " var stRefSingle = ensureStats(statsByRef, mSingle);", + " if (bbox) {", + " stRefSingle.hasGeom = true;", + " if (bbox.min.z < stRefSingle.zMin) stRefSingle.zMin = bbox.min.z;", + " if (bbox.max.z > stRefSingle.zMax) stRefSingle.zMax = bbox.max.z;", + " }", + " if (vertCount) stRefSingle.totalVerts += vertCount;", + " if (triCount) stRefSingle.totalTris += triCount;", + " stRefSingle.slotCount++;", + " if (isPlaneLike) stRefSingle.planeLikeCount++;", + " markMeshUsage(stRefSingle, meshKey);", + " if (hasLocal) {", + " if (lMinX < stRefSingle.sizeLocalMin.x) stRefSingle.sizeLocalMin.x = lMinX;", + " if (lMinY < stRefSingle.sizeLocalMin.y) stRefSingle.sizeLocalMin.y = lMinY;", + " if (lMinZ < stRefSingle.sizeLocalMin.z) stRefSingle.sizeLocalMin.z = lMinZ;", + "", + " if (lMaxX > stRefSingle.sizeLocalMax.x) stRefSingle.sizeLocalMax.x = lMaxX;", + " if (lMaxY > stRefSingle.sizeLocalMax.y) stRefSingle.sizeLocalMax.y = lMaxY;", + " if (lMaxZ > stRefSingle.sizeLocalMax.z) stRefSingle.sizeLocalMax.z = lMaxZ;", + " }", + " if (hasWorld) {", + " if (wMinX < stRefSingle.sizeWorldMin.x) stRefSingle.sizeWorldMin.x = wMinX;", + " if (wMinY < stRefSingle.sizeWorldMin.y) stRefSingle.sizeWorldMin.y = wMinY;", + " if (wMinZ < stRefSingle.sizeWorldMin.z) stRefSingle.sizeWorldMin.z = wMinZ;", + "", + " if (wMaxX > stRefSingle.sizeWorldMax.x) stRefSingle.sizeWorldMax.x = wMaxX;", + " if (wMaxY > stRefSingle.sizeWorldMax.y) stRefSingle.sizeWorldMax.y = wMaxY;", + " if (wMaxZ > stRefSingle.sizeWorldMax.z) stRefSingle.sizeWorldMax.z = wMaxZ;", + " ", + " // Relative world Z (for ignoreUV mode - GDevelop uses Z-up)", + " if (relMinZ < stRefSingle.relZMin) stRefSingle.relZMin = relMinZ;", + " if (relMaxZ > stRefSingle.relZMax) stRefSingle.relZMax = relMaxZ;", + " }", + "", + " if (mNameSingle) {", + " var stNameSingle = ensureStats(statsByName, mNameSingle);", + " if (bbox) {", + " stNameSingle.hasGeom = true;", + " if (bbox.min.z < stNameSingle.zMin) stNameSingle.zMin = bbox.min.z;", + " if (bbox.max.z > stNameSingle.zMax) stNameSingle.zMax = bbox.max.z;", + " }", + " if (vertCount) stNameSingle.totalVerts += vertCount;", + " if (triCount) stNameSingle.totalTris += triCount;", + " stNameSingle.slotCount++;", + " if (isPlaneLike) stNameSingle.planeLikeCount++;", + " markMeshUsage(stNameSingle, meshKey);", + " if (hasLocal) {", + " if (lMinX < stNameSingle.sizeLocalMin.x) stNameSingle.sizeLocalMin.x = lMinX;", + " if (lMinY < stNameSingle.sizeLocalMin.y) stNameSingle.sizeLocalMin.y = lMinY;", + " if (lMinZ < stNameSingle.sizeLocalMin.z) stNameSingle.sizeLocalMin.z = lMinZ;", + "", + " if (lMaxX > stNameSingle.sizeLocalMax.x) stNameSingle.sizeLocalMax.x = lMaxX;", + " if (lMaxY > stNameSingle.sizeLocalMax.y) stNameSingle.sizeLocalMax.y = lMaxY;", + " if (lMaxZ > stNameSingle.sizeLocalMax.z) stNameSingle.sizeLocalMax.z = lMaxZ;", + " }", + " if (hasWorld) {", + " if (wMinX < stNameSingle.sizeWorldMin.x) stNameSingle.sizeWorldMin.x = wMinX;", + " if (wMinY < stNameSingle.sizeWorldMin.y) stNameSingle.sizeWorldMin.y = wMinY;", + " if (wMinZ < stNameSingle.sizeWorldMin.z) stNameSingle.sizeWorldMin.z = wMinZ;", + "", + " if (wMaxX > stNameSingle.sizeWorldMax.x) stNameSingle.sizeWorldMax.x = wMaxX;", + " if (wMaxY > stNameSingle.sizeWorldMax.y) stNameSingle.sizeWorldMax.y = wMaxY;", + " if (wMaxZ > stNameSingle.sizeWorldMax.z) stNameSingle.sizeWorldMax.z = wMaxZ;", + " ", + " // Relative world Z (for ignoreUV mode - GDevelop uses Z-up)", + " if (relMinZ < stNameSingle.relZMin) stNameSingle.relZMin = relMinZ;", + " if (relMaxZ > stNameSingle.relZMax) stNameSingle.relZMax = relMaxZ;", + " }", + " }", + " }", + " }", + " });", + "", + " 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 };", + " }", + " ", + " function 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;", + " }", + " ", + " function 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()", + " : \"\"", + " };", + " }", + " ", + " function resolveOrPick(root, wantedName, sharedKeyForCache) {", + " // Cache collectAndStats result per object type (sharedKey).", + " // collectAndStats scans the full object, so result is materialName-independent.", + " var collected = null;", + " if (sharedKeyForCache) {", + " var statsCacheKey = sharedKeyForCache + \"::collectAndStats\";", + " var cachedStats = objectTypeCacheGet(statsCacheKey);", + " if (cachedStats && cachedStats.records && cachedStats.statsByRef && cachedStats.statsByName) {", + " collected = cachedStats;", + " } else {", + " collected = collectAndStats(root);", + " // Cache by sharedKey only (without materialName).", + " objectTypeCacheSet(statsCacheKey, collected);", + " }", + " } else {", + " collected = collectAndStats(root);", + " }", + " ", + " var records = collected.records;", + " ", + " var selection = null;", + " if (wantedName && wantedName.trim() !== \"\") {", + " selection = resolveByName(records, wantedName.trim());", + " }", + " if (!selection) {", + " selection = pickBest(records);", + " }", + " ", + " return {", + " records: records,", + " selection: selection,", + " statsByRef: collected.statsByRef,", + " statsByName: collected.statsByName", + " };", + " }", + " ", + " function 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(\", \"));", + " // console.log(\"[\" + objName + \"] \" + meshSet.size + \" meshes: \" + Array.from(meshSet).join(\", \"));", + " } catch (e) {}", + " }", + " ", + " // Cache resolveOrPick result per object type.", + " // collectAndStats remains cached by sharedKey (without materialName).", + " // resolveOrPick is cached by sharedKey + materialName because selection depends on materialName.", + " var objectTypeCacheKey = sharedKey + \"::\" + (materialName || \"\");", + " var cachedRop = objectTypeCacheGet(objectTypeCacheKey);", + " ", + " 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 = resolveOrPick(threeObj, materialName, sharedKey);", + " if (rop && rop.selection && rop.selection.srcRef) {", + " objectTypeCacheSet(objectTypeCacheKey, rop);", + " }", + " }", + " ", + " if (!rop || !rop.selection || !rop.selection.srcRef) return;", + " ", + " if (debugOutput && !cache.debugPrinted.has(sharedKey)) {", + " cache.debugPrinted.add(sharedKey);", + " 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.", + "var sizeWorldMin = new THREE.Vector3(Infinity, Infinity, Infinity);", + "var sizeWorldMax = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + "var sizeLocalMin = new THREE.Vector3(Infinity, Infinity, Infinity);", + "var sizeLocalMax = new THREE.Vector3(-Infinity, -Infinity, -Infinity);", + "var totalVerts = 0;", + "var totalTris = 0;", + "var meshCountForScale = 0;", + "var slotCountForScale = 0;", + "var planeLikeCountForScale = 0;", + "", + "var srcRef = selection.srcRef;", + "var srcName = selection.matchName;", + "", + "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.copy(st.sizeLocalMin);", + " if (st.sizeLocalMax) sizeLocalMax.copy(st.sizeLocalMax);", + " if (st.sizeWorldMin) sizeWorldMin.copy(st.sizeWorldMin);", + " if (st.sizeWorldMax) sizeWorldMax.copy(st.sizeWorldMax);", + "}", + "", + "var useLocalBoundsForAuto =", + " 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 = autoBoundsMax.z - autoBoundsMin.z;", + " 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 (only when user sets polyScale = 0)", + " if (polyScaleAutoMode) {", + " var autoPolyKey = sharedKey + \"::AUTO_POLY::\" + (selection.pickedId || \"\") + \"::\" + swayType + \"::C3\";", + " var autoCached = cache.autoPolyScaleCache ? cache.autoPolyScaleCache.get(autoPolyKey) : null;", + " var autoFromCache = !!(autoCached && isFinite(autoCached.polyScale) && isFinite(autoCached.polyScaleRaw));", + " var sizeX = autoBoundsMax.x - autoBoundsMin.x;", + " var sizeY = autoBoundsMax.y - autoBoundsMin.y;", + " var sizeZ = autoBoundsMax.z - autoBoundsMin.z;", + " 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);", + "", + " if (cache.autoPolyScaleCache && typeof cache.autoPolyScaleCache.set === \"function\") {", + " cache.autoPolyScaleCache.set(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 && !cache.debugPrinted.has(autoDebugKey)) {", + " cache.debugPrinted.add(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;", + " var resolvedRenderSide = resolveRenderSide(sourceRenderSide, cullingMode);", + " var sideSuffix = buildSideSuffix(cullingModeCode, resolvedRenderSide);", + " ", + " // Key sanitization: replace :: in component strings to prevent key parsing issues", + " function sanitizeKeyPart(s) {", + " if (!s) return \"(empty)\";", + " return String(s).replace(/::/g, \"_\").trim() || \"(empty)\";", + " }", + "", + " // 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;", + " ", + " 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 precomputed local-space wind directions to avoid inverse(m) in base sway.", + "", + "// UV based gradient (your current behavior)", + " 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 = `", + " // Use precomputed local-space wind directions.", + " vec2 dir = normalize(uWindDir);", + " vec2 perp = vec2(-dir.y, dir.x);", + " // Local-space directions (precomputed on CPU to avoid inverse(m))", + " 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;", + "", + "// Use local-space direction path in gust-enabled branch.", + "#ifdef USE_GUST", + " // Gust push is defined in world wind direction, then transformed to instance-local space.", + " // Use inverse(m) only for gust push to account for instance rotation.", + " 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;", + " ", + " // Use local-space direction path in 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", + " ", + "// Use local-space wind direction directly.", + "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;", + " // Use local-space wind direction directly.", + " 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;", + " // Use local-space wind direction directly.", + " 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) - same as leaves, no sway/color grading", + " 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 ", + " `;", + " }", + " ", + " 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;", + " `;", + " }", + "", + " 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;", + " }", + "", + " 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;", + " }", + " if (!shader.defines) shader.defines = {};", + " 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) };", + " 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 (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 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;", + "#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);", + " shader.vertexShader = shader.vertexShader.replace(\"#include \", buildShadowVertexBody(cfgShadow));", + " };", + "", + " shadowMat.needsUpdate = true;", + " }", + "", + " 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);", + " }", + "", + " 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;", + " }", + "", + " 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);", + " }", + " }", + " ", + " 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) };", + " // Precompute local-space wind direction to reduce shader work.", + " // For now, use uWindDir directly (identity transform) - can be improved with modelMatrix transform", + " 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.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;", + " // Local-space wind direction is precomputed on CPU.", + " 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;", + " 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 && cfg2.swayType !== \"treeTrunkSway\") {", + " var twoSidedLightPatch =", + " \"#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\" + twoSidedLightPatch", + " );", + " } else if (shader.fragmentShader.indexOf(\"#include \") !== -1) {", + " shader.fragmentShader = shader.fragmentShader.replace(", + " \"#include \",", + " \"#include \\n\" + twoSidedLightPatch", + " );", + " }", + " }", + " };", + " ", + " 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);", + " if (!shader.defines) shader.defines = {};", + " // GPU fade smoothing: aFadePrev + uFadeInterpT", + " shader.uniforms.uFadeInterpT = { value: 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 \\nvarying float vFade;\"", + " );", + " var useDistanceFadeShader = cfgFadeOnly ? !!cfgFadeOnly.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,", + " 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);", + " 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;", + " 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.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;", + " ", + " function findFirstMatchingMesh(root, selection, srcRef, srcName) {", + " var found = null;", + " root.traverse(function (o) {", + " if (found) return;", + " if (!o || !o.isMesh || !o.geometry || !o.material) return;", + "", + " if (!Array.isArray(o.material)) {", + " var mSingle = o.material;", + " if (!mSingle) return;", + "", + " var matchSingle = false;", + " if (selection.matchMode === \"name\") {", + " matchSingle = (mSingle.name || \"\") === selection.matchName;", + " } else {", + " matchSingle = mSingle === srcRef;", + " if (!matchSingle && srcName && (mSingle.name || \"\") === srcName && isAlphaLikely(mSingle)) matchSingle = true;", + " }", + "", + " if (matchSingle) found = o;", + " return;", + " }", + "", + " var mats = o.material;", + " for (var i = 0; i < mats.length; i++) {", + " var m = mats[i];", + " if (!m) continue;", + "", + " var match = false;", + " if (selection.matchMode === \"name\") {", + " match = (m.name || \"\") === selection.matchName;", + " } else {", + " match = m === srcRef;", + " if (!match && srcName && (m.name || \"\") === srcName && isAlphaLikely(m)) match = true;", + " }", + "", + " if (match) {", + " found = o;", + " return;", + " }", + " }", + " });", + " return found;", + " }", + "", + " // GPU instancing path (za grassSway, bushSway, treeTrunkSway i leavesSway)", + " 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: if object has exactly 2 materials, the other one is trunk (static), leaves use materialName (sway).", + " 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 = objectTypeCacheGet(objectTypeCacheKeyTrunk);", + " var ropTrunk = cachedRopTrunk || (function() {", + " var r = resolveOrPick(threeObj, trunkMatName, sharedKey);", + " if (r && r.selection && r.selection.srcRef) objectTypeCacheSet(objectTypeCacheKeyTrunk, r);", + " return r;", + " })();", + " var selectionTrunk = ropTrunk && ropTrunk.selection ? ropTrunk.selection : null;", + " if (!selectionTrunk || !selectionTrunk.srcRef) {", + " useTwoPartTree = false;", + " } else {", + " var srcTrunk = selectionTrunk.srcRef;", + " var sourceRenderSideTrunk = (typeof srcTrunk.side === \"number\") ? srcTrunk.side : THREE.FrontSide;", + " 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;", + " 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", + " };", + " sharedTrunk.side = resolvedRenderSideTrunk;", + " applyCustomPBRIfSupported(sharedTrunk, customLit, metallic, roughness, specular, normalStrength, aoStrength, envStrength);", + " patchMaterialFadeOnly(sharedTrunk);", + " ensureFoliageShadowMaterials(sharedTrunk);", + " entryTrunk = { material: sharedTrunk, refCount: 0, _ownedByFoliage: true };", + " cache.sharedByKey.set(keyTrunk, entryTrunk);", + " 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;", + " }", + " if (typeof resolvedRenderSideTrunk === \"number\" && entryTrunk.material.side !== resolvedRenderSideTrunk) {", + " entryTrunk.material.side = resolvedRenderSideTrunk;", + " entryTrunk.material.needsUpdate = true;", + " }", + " applyCustomPBRIfSupported(entryTrunk.material, !!customLit, metallic, roughness, specular, normalStrength, aoStrength, envStrength);", + " ensureFoliageShadowMaterials(entryTrunk.material);", + " }", + " var sharedMatTrunk = entryTrunk.material;", + " var repMeshTrunk = findFirstMatchingMesh(threeObj, selectionTrunk, selectionTrunk.srcRef, selectionTrunk.matchName);", + " var repMeshLeaves = findFirstMatchingMesh(threeObj, selection, srcRef, srcName);", + " if (!repMeshTrunk || !repMeshTrunk.geometry || !repMeshLeaves || !repMeshLeaves.geometry) {", + " useTwoPartTree = false;", + " }", + " }", + " if (useTwoPartTree) {", + " // Global instancing: geoKey = stable asset+mesh identity (no ::P:: parent, no ::SC: super-chunk)", + " 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 || \"\"));", + " var geoKeyLeaves = leavesPickedId", + " ? (sanitizeKeyPart(sharedKey) + \"::\" + sanitizeKeyPart(repMeshLeaves.name || \"(unnamed)\") + \"::\" + sanitizeKeyPart(leavesPickedId))", + " : (repMeshLeaves.geometry.id !== undefined && repMeshLeaves.geometry.id !== null ? String(repMeshLeaves.geometry.id) : (repMeshLeaves.geometry.uuid || \"\"));", + " var baseGroupKeyTrunk = keyTrunk + \"::GEO::\" + geoKeyTrunk;", + " var baseGroupKeyLeaves = key + \"::GEO::\" + geoKeyLeaves;", + " var instTree = cache.instancing;", + " if (!instTree.pending || !Array.isArray(instTree.pending)) instTree.pending = [];", + " if (typeof instTree.queueIdCounter !== \"number\") instTree.queueIdCounter = 0;", + " if (!instTree.cancelledQueueIds || typeof instTree.cancelledQueueIds.has !== \"function\") instTree.cancelledQueueIds = new Set();", + " var queueIdTree = ++instTree.queueIdCounter;", + " instTree.pending.push({", + " gdObj: gdObj,", + " threeObj: threeObj,", + " parent: null, // FoliageRoot will be used as parent (set in flushPendingInstancing)", + " 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.dirty = true;", + " }", + " }", + "", + " // Single-item path (grass, bush, trunk, ili leavesSway s 1 materijalom)", + " 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 any mesh using it (faster).", + " geo = cachedRop._cachedGeometry;", + " threeObj.traverse(function(o) {", + " if (repMesh) return; // Early exit", + " if (o && o.isMesh && o.geometry === geo) {", + " repMesh = o;", + " }", + " });", + " }", + " ", + " // 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 = stable asset+mesh identity (no ::P:: parent, no ::SC: super-chunk)", + " 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 groupKey = key + \"::GEO::\" + geoKey;", + "", + " var inst = cache.instancing;", + " var g = inst.groups.get(groupKey);", + " if (!g) {", + " g = {", + " key: groupKey,", + " geometry: geo,", + " material: sharedMat,", + " parent: null, // FoliageRoot will be set in flushPendingInstancing", + " matricesBuffer: null,", + " matrixCount: 0,", + " aliveCount: 0,", + " freeIndices: [],", + " freeIndexSet: new Set(),", + " capacity: 0,", + " mesh: null,", + " centersXY: null,", + " instanceFade: null,", + " instanceFadePrev: null,", + " fadeEnabled: distanceFadeEnabled,", + " fadeStart: fadeStart,", + " fadeEnd: fadeEnd", + " };", + " inst.groups.set(groupKey, g);", + " } else {", + " g.geometry = geo;", + " g.material = sharedMat;", + " if (!g.matricesBuffer) g.matricesBuffer = null;", + " if (typeof g.matrixCount !== \"number\") g.matrixCount = 0;", + " if (typeof g.aliveCount !== \"number\") g.aliveCount = 0;", + " if (!g.freeIndices) g.freeIndices = [];", + " if (!g.freeIndexSet || typeof g.freeIndexSet.has !== \"function\") g.freeIndexSet = new Set();", + " if (typeof g.capacity !== \"number\") g.capacity = 0;", + " g.fadeEnabled = distanceFadeEnabled;", + " g.fadeStart = fadeStart;", + " g.fadeEnd = fadeEnd;", + " if (!g.centersXY) g.centersXY = null;", + " }", + "", + " // Fast cache entry for subsequent objects of same type", + " var fastCacheKey = sharedKey + \"::\" + (materialName || \"\") + \"::\" + swayType + \"::GPU::FD\" + (distanceFadeEnabled ? \"1\" : \"0\") + pbrSuffix + sideSuffix + \"_TSL\" + (twoSidedLighting ? \"1\" : \"0\") + \"_PA\" + (polyScaleAutoMode ? \"1\" : \"0\");", + " if (!objectTypeCacheGet(fastCacheKey)) {", + " objectTypeCacheSet(fastCacheKey, {", + " _gpuGroupKey: groupKey,", + " _gpuGeometry: geo,", + " _sharedMaterialKey: key,", + " _resolvedSide: resolvedRenderSide", + " });", + " }", + "", + " // Defer capture by one frame (GDevelop applies transform slightly later)", + " if (!inst.pending || !Array.isArray(inst.pending)) inst.pending = [];", + " if (typeof inst.queueIdCounter !== \"number\") inst.queueIdCounter = 0;", + " if (!inst.cancelledQueueIds || typeof inst.cancelledQueueIds.has !== \"function\") {", + " inst.cancelledQueueIds = new Set();", + " }", + " var queueId = ++inst.queueIdCounter;", + " inst.pending.push({", + " gdObj: gdObj,", + " threeObj: threeObj,", + " repMesh: repMesh,", + " geometry: geo,", + " material: sharedMat,", + " parent: null, // FoliageRoot will be used as parent", + " baseGroupKey: groupKey,", + " behavior: behavior,", + " queueId: queueId", + " });", + " behavior.__foliageQueued = true;", + " behavior.__foliageQueueId = queueId;", + " delete behavior.__foliageNonInstancedRegistered;", + " if (threeObj && threeObj.userData) delete threeObj.userData.__foliageNonInstancedRegistered;", + "", + " inst.dirty = true;", + " }", + " }", + " }", + " }", + "", + " // Non-instanced path (original behavior)", + " // Used when gpuInstancing is false or swayType is not supported for instancing.", + " if (!gpuInstancing || (swayType !== \"grassSway\" && swayType !== \"bushSway\" && swayType !== \"treeTrunkSway\" && swayType !== \"leavesSway\")) {", + " var matToAssign = sharedMat;", + " var useNonInstancedFade = distanceFadeEnabled && sharedMat.userData && sharedMat.userData.foliageConfig;", + " if (useNonInstancedFade) {", + " var fadeMat = sharedMat.clone();", + " fadeMat.userData = { foliageConfig: sharedMat.userData.foliageConfig };", + " if (typeof sharedMat.side === \"number\") fadeMat.side = sharedMat.side;", + " // 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) {", + " shader.vertexShader = shader.vertexShader.replace(\"varying float vFade;\", \"uniform float uFade;\\nvarying float vFade;\");", + " }", + " shader.vertexShader = shader.vertexShader.replace(\"#else\\n vFade = 1.0;\\n#endif\", \"#else\\n vFade = uFade;\\n#endif\");", + " };", + " fadeMat.needsUpdate = true;", + " ensureFoliageShadowMaterials(fadeMat);", + " matToAssign = fadeMat;", + " }", + " threeObj.traverse(function (o) {", + " if (!o || !o.isMesh || !o.material) return;", + "", + " // Single material: no allocations", + " if (!Array.isArray(o.material)) {", + " var mSingle2 = o.material;", + " if (!mSingle2) return;", + "", + " var matchSingle2 = false;", + " if (selection.matchMode === \"name\") {", + " matchSingle2 = (mSingle2.name || \"\") === selection.matchName;", + " } else {", + " matchSingle2 = mSingle2 === srcRef;", + " if (!matchSingle2 && srcName && (mSingle2.name || \"\") === srcName && isAlphaLikely(mSingle2)) matchSingle2 = true;", + " }", + "", + " if (matchSingle2) o.material = matToAssign;", + " if (matchSingle2) assignShadowMaterialsToMesh(o, matToAssign);", + " return;", + " }", + "", + " // Material array: first scan without allocating", + " var mats2 = o.material;", + " var needsChange2 = false;", + " for (var i2 = 0; i2 < mats2.length; i2++) {", + " var m2 = mats2[i2];", + " if (!m2) continue;", + "", + " var matchA = false;", + " if (selection.matchMode === \"name\") {", + " matchA = (m2.name || \"\") === selection.matchName;", + " } else {", + " matchA = m2 === srcRef;", + " if (!matchA && srcName && (m2.name || \"\") === srcName && isAlphaLikely(m2)) matchA = true;", + " }", + "", + " 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 = false;", + " if (selection.matchMode === \"name\") {", + " matchB = (mm2.name || \"\") === selection.matchName;", + " } else {", + " matchB = mm2 === srcRef;", + " if (!matchB && srcName && (mm2.name || \"\") === srcName && isAlphaLikely(mm2)) matchB = true;", + " }", + "", + " if (matchB) newMats2[j2] = matToAssign;", + " }", + "", + " o.material = newMats2;", + " });", + " if (useNonInstancedFade) {", + " var entryTrunkMaterial = null;", + " // leavesSway two-part: trunk must fade with leaves (same uFade)", + " if (swayType === \"leavesSway\") {", + " var trunkMatFound = null;", + " threeObj.traverse(function (o) {", + " if (trunkMatFound !== null || !o || !o.isMesh || !o.material) return;", + " if (!Array.isArray(o.material) && o.material !== fadeMat) {", + " trunkMatFound = o.material;", + " }", + " });", + " if (trunkMatFound) {", + " var trunkFadeMat = trunkMatFound.clone();", + " trunkFadeMat.userData = trunkFadeMat.userData || {};", + " trunkFadeMat.side = resolveRenderSide(trunkMatFound.side, cullingMode);", + " applyCustomPBRIfSupported(trunkFadeMat, customLit, metallic, roughness, specular, normalStrength, aoStrength, envStrength);", + " 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(\"#else\\n vFade = 1.0;\\n#endif\", \"#else\\n vFade = uFade;\\n#endif\");", + " 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;", + " ensureFoliageShadowMaterials(trunkFadeMat);", + " threeObj.traverse(function (o) {", + " if (!o || !o.isMesh || !o.material) return;", + " if (!Array.isArray(o.material) && o.material === trunkMatFound) {", + " o.material = trunkFadeMat;", + " assignShadowMaterialsToMesh(o, trunkFadeMat);", + " }", + " });", + " entryTrunkMaterial = trunkFadeMat;", + " }", + " }", + " var existingNonInstancedEntry = null;", + " if (cache.nonInstancedFadeObjects && Array.isArray(cache.nonInstancedFadeObjects)) {", + " for (var niFind = 0; niFind < cache.nonInstancedFadeObjects.length; niFind++) {", + " var candidateNi = cache.nonInstancedFadeObjects[niFind];", + " if (candidateNi && (candidateNi.gdObj === gdObj || candidateNi.threeObj === threeObj)) {", + " existingNonInstancedEntry = candidateNi;", + " break;", + " }", + " }", + " }", + " if (!existingNonInstancedEntry && cache.nonInstancedStaticObjects && Array.isArray(cache.nonInstancedStaticObjects)) {", + " for (var niFindStatic = 0; niFindStatic < cache.nonInstancedStaticObjects.length; niFindStatic++) {", + " var candidateNiStatic = cache.nonInstancedStaticObjects[niFindStatic];", + " if (candidateNiStatic && (candidateNiStatic.gdObj === gdObj || candidateNiStatic.threeObj === threeObj)) {", + " existingNonInstancedEntry = candidateNiStatic;", + " break;", + " }", + " }", + " }", + " if (existingNonInstancedEntry) {", + " if (existingNonInstancedEntry.material && existingNonInstancedEntry.material !== fadeMat) {", + " unregisterActiveMaterial(existingNonInstancedEntry.material);", + " if (typeof cache._disposeFoliageShadowMaterials === \"function\") {", + " cache._disposeFoliageShadowMaterials(existingNonInstancedEntry.material);", + " }", + " if (typeof existingNonInstancedEntry.material.dispose === \"function\") {", + " try { existingNonInstancedEntry.material.dispose(); } catch (eNiReplace) {}", + " }", + " }", + " if (existingNonInstancedEntry.trunkMaterial && existingNonInstancedEntry.trunkMaterial !== entryTrunkMaterial) {", + " unregisterActiveMaterial(existingNonInstancedEntry.trunkMaterial);", + " if (typeof cache._disposeFoliageShadowMaterials === \"function\") {", + " cache._disposeFoliageShadowMaterials(existingNonInstancedEntry.trunkMaterial);", + " }", + " if (typeof existingNonInstancedEntry.trunkMaterial.dispose === \"function\") {", + " try { existingNonInstancedEntry.trunkMaterial.dispose(); } catch (eNiReplaceTrunk) {}", + " }", + " }", + " existingNonInstancedEntry.gdObj = gdObj;", + " existingNonInstancedEntry.threeObj = threeObj;", + " existingNonInstancedEntry.material = fadeMat;", + " existingNonInstancedEntry.fadeStart = fadeStart;", + " existingNonInstancedEntry.fadeEnd = fadeEnd;", + " existingNonInstancedEntry.fadeEnabled = !!distanceFadeEnabled;", + " existingNonInstancedEntry.fadeBehaviorData = behavior ? (behavior._behaviorData || behavior) : null;", + " existingNonInstancedEntry.trunkMaterial = entryTrunkMaterial;", + " existingNonInstancedEntry._parkedNoFade = false;", + " if (cache.nonInstancedStaticObjects && Array.isArray(cache.nonInstancedStaticObjects)) {", + " for (var niStaticRm = cache.nonInstancedStaticObjects.length - 1; niStaticRm >= 0; niStaticRm--) {", + " if (cache.nonInstancedStaticObjects[niStaticRm] === existingNonInstancedEntry) {", + " cache.nonInstancedStaticObjects.splice(niStaticRm, 1);", + " break;", + " }", + " }", + " }", + " if (cache.nonInstancedFadeObjects && Array.isArray(cache.nonInstancedFadeObjects)) {", + " var existsInActive = false;", + " for (var niActiveChk = 0; niActiveChk < cache.nonInstancedFadeObjects.length; niActiveChk++) {", + " if (cache.nonInstancedFadeObjects[niActiveChk] === existingNonInstancedEntry) {", + " existsInActive = true;", + " break;", + " }", + " }", + " if (!existsInActive) cache.nonInstancedFadeObjects.push(existingNonInstancedEntry);", + " }", + " } else {", + " cache.nonInstancedFadeObjects.push({", + " gdObj: gdObj,", + " threeObj: threeObj,", + " material: fadeMat,", + " fadeStart: fadeStart,", + " fadeEnd: fadeEnd,", + " fadeEnabled: !!distanceFadeEnabled,", + " fadeBehaviorData: behavior ? (behavior._behaviorData || behavior) : null,", + " trunkMaterial: entryTrunkMaterial,", + " _parkedNoFade: false", + " });", + " }", + " registerActiveMaterial(fadeMat);", + " if (entryTrunkMaterial) registerActiveMaterial(entryTrunkMaterial);", + " 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;", + " }", + " })(runtimeScene, eventsFunctionContext);", + "" + ], + "parameterObjects": "", + "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": [ + "(function(runtimeScene, eventsFunctionContext) {", + " var cache = window.__FOLIAGE_SWAY__;", + " if (!cache || !cache.sharedByKey) return;", + "", + " function unregisterActiveMaterial(mat) {", + " if (!mat) return;", + " if (typeof cache._unregisterActiveMaterial === \"function\") {", + " cache._unregisterActiveMaterial(mat);", + " } else if (cache.activeMaterials && typeof cache.activeMaterials.delete === \"function\") {", + " cache.activeMaterials.delete(mat);", + " }", + " }", + "", + " function disposeFoliageShadowMaterials(mat) {", + " if (cache && typeof cache._disposeFoliageShadowMaterials === \"function\") {", + " cache._disposeFoliageShadowMaterials(mat);", + " return;", + " }", + " if (!mat || !mat.userData) return;", + " var ud = mat.userData;", + " var depthMat = ud._foliageDepthMat;", + " var distanceMat = ud._foliageDistanceMat;", + " if (depthMat && depthMat !== mat && typeof depthMat.dispose === \"function\") {", + " try { depthMat.dispose(); } catch (eDepthDispose) {}", + " }", + " if (distanceMat && distanceMat !== mat && distanceMat !== depthMat && typeof distanceMat.dispose === \"function\") {", + " try { distanceMat.dispose(); } catch (eDistanceDispose) {}", + " }", + " delete ud._foliageDepthMat;", + " delete ud._foliageDistanceMat;", + " delete ud._foliageShadowOwned;", + " }", + "", + " var objs = eventsFunctionContext.getObjects(\"Object\") || [];", + " var gdObj = objs[0];", + " if (!gdObj) return;", + "", + " var behavior = null;", + " try { behavior = gdObj.getBehavior(\"FoliageSwaying\"); } catch (e) {}", + " if (!behavior) { try { behavior = gdObj.getBehavior(\"NatureElements::FoliageSwaying\"); } catch (e2) {} }", + " 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;", + "", + " function removeNonInstancedEntryFromList(list) {", + " if (!list || !Array.isArray(list)) return;", + " for (var nf = list.length - 1; nf >= 0; nf--) {", + " var entryNf = list[nf];", + " if (!entryNf || (entryNf.gdObj !== gdObj && entryNf.threeObj !== threeObj)) continue;", + " unregisterActiveMaterial(entryNf.material);", + " if (entryNf.material) {", + " disposeFoliageShadowMaterials(entryNf.material);", + " if (typeof entryNf.material.dispose === \"function\") {", + " try { entryNf.material.dispose(); } catch (eFadeDispose) {}", + " }", + " }", + " unregisterActiveMaterial(entryNf.trunkMaterial);", + " if (entryNf.trunkMaterial) {", + " disposeFoliageShadowMaterials(entryNf.trunkMaterial);", + " if (typeof entryNf.trunkMaterial.dispose === \"function\") {", + " try { entryNf.trunkMaterial.dispose(); } catch (eTrunkFadeDispose) {}", + " }", + " }", + " list.splice(nf, 1);", + " }", + " }", + "", + " // Non-instanced entries can be in active fade list or parked static list.", + " removeNonInstancedEntryFromList(cache.nonInstancedFadeObjects);", + " removeNonInstancedEntryFromList(cache.nonInstancedStaticObjects);", + "", + " // 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 before flushPendingInstancing (queued but not yet assigned index)", + " // Mark queueId as cancelled so flushPendingInstancing can skip the item and do refCount rollback", + " // CRITICAL: Return here to avoid double decrement - flushPendingInstancing will handle refCount rollback", + " if (instIndex === null && behavior && behavior.__foliageQueued === true) {", + " var queueId = behavior.__foliageQueueId;", + " if (queueId !== undefined && isFinite(queueId) && cache.instancing) {", + " // Fallback: ensure cancelledQueueIds exists (should already exist from onCreated, but be defensive)", + " if (!cache.instancing.cancelledQueueIds || typeof cache.instancing.cancelledQueueIds.has !== \"function\") {", + " cache.instancing.cancelledQueueIds = new Set();", + " }", + " cache.instancing.cancelledQueueIds.add(queueId);", + " }", + " // Clean up queued flag and related flags (two-part: also 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 - flushPendingInstancing will handle refCount rollback to avoid double decrement", + " return;", + " }", + "", + " if (", + " instKey &&", + " instIndex !== null &&", + " cache.instancing &&", + " cache.instancing.groups &&", + " typeof cache.instancing.groups.get === \"function\"", + " ) {", + " try {", + " var g = cache.instancing.groups.get(instKey);", + " var gLeaves = instKeyLeaves ? cache.instancing.groups.get(instKeyLeaves) : null;", + "", + " if (g && typeof g.matrixCount === \"number\") {", + " // Robustness guard: bounds check", + " if (instIndex < 0 || instIndex >= g.matrixCount) {", + " // Stale/invalid index - skip", + " } else {", + " // Backward compatibility: init aliveCount if missing", + " 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 estimatedFreeL = gLeaves.freeIndices ? gLeaves.freeIndices.length : 0;", + " gLeaves.aliveCount = Math.max(0, gLeaves.matrixCount - estimatedFreeL);", + " }", + "", + " 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]);", + " }", + " }", + " }", + "", + " // Robustness guard: duplicate free check", + " if (g.freeIndexSet.has(instIndex)) {", + " // Already freed - skip to prevent double decrement", + " } else {", + " if (!g.freeIndices) g.freeIndices = [];", + " g.freeIndices.push(instIndex);", + " g.freeIndexSet.add(instIndex);", + " if (g.aliveCount > 0) g.aliveCount--;", + " // Two-part: same idx in both groups, freeIndices/freeIndexSet are shared", + " if (gLeaves && gLeaves.aliveCount > 0) gLeaves.aliveCount--;", + " }", + "", + " var isEmpty = (g.aliveCount === 0);", + "", + " if (isEmpty) {", + " // Dispose owned resources only", + " try {", + " if (g.mesh) {", + " if (g.mesh.parent) g.mesh.parent.remove(g.mesh);", + " // Dispose CLONED geometry (owned), NOT source geometry", + " if (g._ownedGeometry && typeof g._ownedGeometry.dispose === \"function\") {", + " try { g._ownedGeometry.dispose(); } catch (eGeo) {}", + " }", + " try { if (g.mesh.dispose) g.mesh.dispose(); } catch (eMesh) {}", + " }", + " } catch (eRm) {}", + " g.mesh = null;", + " g._ownedGeometry = null;", + " g.matricesBuffer = null;", + " g.centersXY = null;", + " g.centersZ = null;", + " g.instanceFade = null;", + " g.instanceFadePrev = null;", + " g.fadeBuffer = null;", + " g.prevFadeBuffer = null;", + " g._srcGeometryRef = null;", + " g._instanceCullRadius = 0;", + " g.matrixCount = 0;", + " g.aliveCount = 0;", + " g.freeIndices = [];", + " g.freeIndexSet = new Set();", + " cache.instancing.groups.delete(instKey);", + "", + " if (instKeyLeaves && gLeaves) {", + " try {", + " if (gLeaves.mesh) {", + " if (gLeaves.mesh.parent) gLeaves.mesh.parent.remove(gLeaves.mesh);", + " if (gLeaves._ownedGeometry && typeof gLeaves._ownedGeometry.dispose === \"function\") {", + " try { gLeaves._ownedGeometry.dispose(); } catch (eGeo2) {}", + " }", + " try { if (gLeaves.mesh.dispose) gLeaves.mesh.dispose(); } catch (eMesh2) {}", + " }", + " } catch (eRm2) {}", + " gLeaves.mesh = null;", + " gLeaves._ownedGeometry = null;", + " gLeaves.matricesBuffer = null;", + " gLeaves.centersXY = null;", + " gLeaves.centersZ = null;", + " gLeaves.instanceFade = null;", + " gLeaves.instanceFadePrev = null;", + " gLeaves.fadeBuffer = null;", + " gLeaves.prevFadeBuffer = null;", + " gLeaves._srcGeometryRef = null;", + " gLeaves._instanceCullRadius = 0;", + " gLeaves.matrixCount = 0;", + " gLeaves.aliveCount = 0;", + " gLeaves.freeIndices = [];", + " gLeaves.freeIndexSet = new Set();", + " cache.instancing.groups.delete(instKeyLeaves);", + " }", + " } else {", + " cache.instancing.dirty = true;", + " }", + " }", + " }", + " } catch (eInst) {}", + " }", + "", + " if (key) {", + " var entry = cache.sharedByKey.get(key);", + " if (entry) {", + " entry.refCount--;", + " if (entry.refCount <= 0) {", + " cache.sharedByKey.delete(key);", + "", + " unregisterActiveMaterial(entry.material);", + " disposeFoliageShadowMaterials(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;", + " if (keyTrunk) {", + " var entryTrunk = cache.sharedByKey.get(keyTrunk);", + " if (entryTrunk) {", + " entryTrunk.refCount--;", + " if (entryTrunk.refCount <= 0) {", + " cache.sharedByKey.delete(keyTrunk);", + " unregisterActiveMaterial(entryTrunk.material);", + " disposeFoliageShadowMaterials(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;", + " }", + "", + "})(runtimeScene, eventsFunctionContext);", + "" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Scene3D::Model3DObject", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NatureElements::FoliageSwaying", + "type": "behavior" + } + ], + "objectGroups": [] + } + ], + "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": [] +} \ No newline at end of file