diff --git a/extensions/community/FoliageSwaying3D.json b/extensions/community/FoliageSwaying3D.json
index b8aab6482..24b1cb789 100644
--- a/extensions/community/FoliageSwaying3D.json
+++ b/extensions/community/FoliageSwaying3D.json
@@ -10,7 +10,7 @@
"name": "FoliageSwaying3D",
"previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/732ef90b7fcf5dd9171fe95d0cf262e09159b487304915a4693baa893d9ce16c_grass.svg",
"shortDescription": "Sway foliage of 3D objects according to the wind.",
- "version": "1.1.0",
+ "version": "1.2.0",
"description": [
"Sway foliage of 3D objects according to wind.",
"",
@@ -30,6 +30,10 @@
"* Hot reload supports all basic behavior property changes, excluding Sway settings (`Material to sway`, `Sway type`, `Enable uniform sway toggle`) and `Distance culling`, `Debug output`, `GPU instancing`, and `Custom PBR settings` toggles. However, if PBR settings are enabled as you start the preview, changes to specific PBR values will be updated on hot-reload.
",
""
],
+ "origin": {
+ "identifier": "FoliageSwaying3D",
+ "name": "gdevelop-extension-store"
+ },
"tags": [
"3D",
"shader",
@@ -123,7 +127,7 @@
" /** @typedef {FoliagePrimitiveValue|gdjs.Variable} FoliageArgumentValue */",
" /** @typedef {{ getAsBoolean?: () => boolean, getAsNumber?: () => number, getAsString?: () => string }} FoliageArgumentVariableLike */",
" /** @typedef {{ getArgument: (name: string) => FoliageArgumentValue }} FoliageEventsFunctionContext */",
- " /** @typedef {{ runtimeScene: gdjs.RuntimeScene|null, dt: number, time: number, windStrength: number, windSpeed: number, wx: number, wy: number, cam: THREE.Camera|null, camX: number, camY: number, camZ: number, frustum: THREE.Frustum|null, _frustumBuilt: boolean }} SharedFrameContext */",
+ " /** @typedef {{ runtimeScene: gdjs.RuntimeScene|null, inverseWorldScale: number, dt: number, time: number, windStrength: number, windSpeed: number, wx: number, wy: number, cam: THREE.Camera|null, camX: number, camY: number, camZ: number, frustum: THREE.Frustum|null, _frustumBuilt: boolean }} SharedFrameContext */",
" /** @typedef {{ _getMaterialName: () => string, _getSwayType: () => string, _getUseUniformSway: () => boolean, _getUseCustomPBR: () => boolean, _getMetallic: () => number, _getRoughness: () => number, _getSpecular: () => number, _getNormalStrength: () => number, _getAmbientOcclusionStrength: () => number, _getEnvironmentStrength: () => number, _getCullingMode: () => string, _getUseTwoSidedLighting: () => boolean, _getUseGPUInstancing: () => boolean, _getUseDistanceFade: () => boolean, _getDistanceFadeStart: () => number, _getDistanceFadeEnd: () => number, _getSwayResponse: () => number, _getIgnoreUV: () => boolean, _getGradientStart: () => number, _getGradientEnd: () => number, _getUseColorGradient: () => boolean, _getTopColor: () => string, _getBottomColor: () => string, _getColorContrast: () => number, _getColorSaturation: () => number, _getDebugOutput: () => boolean }} FoliageBehaviorGetterMethods */",
" /**",
" * Typed snapshot of behavior properties read once for a single runtime use-site.",
@@ -237,6 +241,7 @@
" registerActiveMaterial(mat) {",
" if (!mat) return;",
" var state = this.state;",
+ " state.syncMaterialGustDefine(mat);",
" state.activeMaterials.add(mat);",
" this.ensureBuckets().addUnknown(mat);",
" }",
@@ -280,11 +285,11 @@
" var state = this.state;",
" state.activeMaterials.forEach(function(mat) {",
" if (!mat) return;",
- " mat.needsUpdate = true;",
+ " state.syncMaterialGustDefine(mat);",
" var ud = mat.userData;",
" if (!ud) return;",
- " if (ud._foliageDepthMat) ud._foliageDepthMat.needsUpdate = true;",
- " if (ud._foliageDistanceMat) ud._foliageDistanceMat.needsUpdate = true;",
+ " state.syncMaterialGustDefine(ud._foliageDepthMat);",
+ " state.syncMaterialGustDefine(ud._foliageDistanceMat);",
" });",
" }",
" }",
@@ -296,10 +301,10 @@
" /** @typedef {{ srcRef: THREE.Material|null, pickedId: string, matchMode: \"name\"|\"auto\", matchName: string }} FoliageMaterialSelection */",
" /** @typedef {{ repMesh?: THREE.Object3D|null, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, baseGroupKey?: string }} FoliagePendingPart */",
" /** @typedef {{ gdObj?: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string, getX?: () => number, getY?: () => number, getZ?: () => number, setX?: (x: number) => void, setY?: (y: number) => void, setZ?: (z: number) => void, getScaleX?: () => number, getScaleY?: () => number, getScaleZ?: () => number, getRotationX?: () => number, getRotationY?: () => number, getRotationZ?: () => number, getAngle?: () => number, hide?: (enable?: boolean) => void, isHidden?: () => boolean, deleteFromScene?: (runtimeScene?: gdjs.RuntimeScene) => void })|null, threeObj?: THREE.Object3D|null, repMesh?: THREE.Object3D|null, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, parent?: THREE.Object3D|null, baseGroupKey?: string, groupKey?: string, swayType?: string, behavior?: FoliageBehavior|null, queueId?: number, objectName?: string, behaviorName?: string, parts?: FoliagePendingPart[] }} FoliagePendingItem */",
- " /** @typedef {{ gdObj?: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string, getX?: () => number, getY?: () => number, getZ?: () => number, getScaleX?: () => number, getScaleY?: () => number, getScaleZ?: () => number, getRotationX?: () => number, getRotationY?: () => number, getRotationZ?: () => number, getAngle?: () => number, hide?: (enable?: boolean) => void, isHidden?: () => boolean, deleteFromScene?: (runtimeScene?: gdjs.RuntimeScene) => void })|null, threeObj?: THREE.Object3D|null, material?: THREE.Material|null, trunkMaterial?: THREE.Material|null, fadeStart?: number, fadeEnd?: number, fadeEnabled?: boolean, fadeBehavior?: FoliageBehavior|null, _wasHidden?: boolean, _parkedNoFade?: boolean, _firstHideWarmupDone?: boolean }} FoliageNonInstancedEntry */",
- " /** @typedef {{ key?: string, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, parent?: THREE.Object3D|null, matricesBuffer?: Float32Array|null, centersXY?: Float32Array|null, centersZ?: Float32Array|null, cullRadii?: Float32Array|null, matrixCount?: number, aliveCount?: number, freeIndices?: number[], freeIndexSet?: Set, capacity?: number, mesh?: THREE.InstancedMesh|null, castShadow?: boolean, receiveShadow?: boolean, fadeEnabled?: boolean, fadeStart?: number, fadeEnd?: number, _fadeBehavior?: FoliageBehavior|null, _fadeSig?: string, _instanceCullRadius?: number, instanceFade?: Float32Array|null, instanceFadePrev?: Float32Array|null, _distanceCandidateIndices?: Uint32Array|null, _distanceCandidateCount?: number, _fadeDisabledApplied?: boolean, _lastFadeEnabled?: boolean, visibleCount?: number, instanceDetSign?: number, _srcGeometryRef?: THREE.BufferGeometry|null, _ownedGeometry?: THREE.BufferGeometry|null }} FoliageInstancingGroup */",
+ " /** @typedef {{ gdObj?: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string, getX?: () => number, getY?: () => number, getZ?: () => number, getScaleX?: () => number, getScaleY?: () => number, getScaleZ?: () => number, getRotationX?: () => number, getRotationY?: () => number, getRotationZ?: () => number, getAngle?: () => number, hide?: (enable?: boolean) => void, isHidden?: () => boolean, deleteFromScene?: (runtimeScene?: gdjs.RuntimeScene) => void })|null, threeObj?: THREE.Object3D|null, material?: THREE.Material|null, trunkMaterial?: THREE.Material|null, fadeStart?: number, fadeEnd?: number, fadeEnabled?: boolean, fadeBehavior?: FoliageBehavior|null, gradientSources?: FoliageGradientSource[], _wasHidden?: boolean, _parkedNoFade?: boolean, _firstHideWarmupDone?: boolean }} FoliageNonInstancedEntry */",
+ " /** @typedef {{ key?: string, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, parent?: THREE.Object3D|null, matricesBuffer?: Float32Array|null, gradientBounds?: Float32Array|null, centersXY?: Float32Array|null, centersZ?: Float32Array|null, cullRadii?: Float32Array|null, matrixCount?: number, aliveCount?: number, freeIndices?: number[], freeIndexSet?: Set, capacity?: number, mesh?: THREE.InstancedMesh|null, castShadow?: boolean, receiveShadow?: boolean, fadeEnabled?: boolean, fadeStart?: number, fadeEnd?: number, _fadeBehavior?: FoliageBehavior|null, _fadeSig?: string, _instanceCullRadius?: number, instanceFade?: Float32Array|null, instanceFadePrev?: Float32Array|null, _distanceCandidateIndices?: Uint32Array|null, _distanceCandidateCount?: number, _fadeDisabledApplied?: boolean, _lastFadeEnabled?: boolean, visibleCount?: number, instanceDetSign?: number, _srcGeometryRef?: THREE.BufferGeometry|null, _ownedGeometry?: THREE.BufferGeometry|null }} FoliageInstancingGroup */",
" /** @typedef {{ groups: Map, typeGroupKeys: Map>, groupTypeKeys: Map>, typeRepresentatives: Map, dirty: boolean, sceneTag: string|null, pending: FoliagePendingItem[], queueIdCounter: number, cancelledQueueIds: Set, foliageRoot: THREE.Object3D|null, _cachedSceneRoot: THREE.Object3D|null, _isReady?: boolean, _tmpRootSet?: Set|null, _tmpMat4?: THREE.Matrix4, _tmpObj3D?: THREE.Object3D, _tmpVec3_pos?: THREE.Vector3, _tmpVec3_scale?: THREE.Vector3, _tmpEuler?: THREE.Euler, _tmpQuat?: THREE.Quaternion, _tmpMat4_objWorld?: THREE.Matrix4, _tmpMat4_objWorldInv?: THREE.Matrix4, _tmpMat4_repWorld?: THREE.Matrix4, _repRelByMesh?: WeakMap, _itemsByParentRepMesh?: Map, _repRelMatrixCache?: Map, _tmpMat4_local?: THREE.Matrix4, _tmpMat4_foliageRootInv?: THREE.Matrix4 }} FoliageInstancingStateShape */",
- " /** @typedef {{ records?: FoliageObjectRecord[], statsByRef?: Map, statsByName?: Map, selection?: FoliageMaterialSelection|null, _cachedGeometry?: THREE.BufferGeometry|null, _gpuGeometry?: THREE.BufferGeometry|null, _gpuGroupKey?: string, _sharedMaterialKey?: string, _resolvedSide?: THREE.Side, _gpuFastMode?: \"single\"|\"splitLeavesTree\", _gpuLeavesGeometry?: THREE.BufferGeometry|null, _gpuTrunkGeometry?: THREE.BufferGeometry|null, _gpuLeavesBaseGroupKey?: string, _gpuTrunkBaseGroupKey?: string, _gpuLeavesSharedMaterialKey?: string, _gpuTrunkSharedMaterialKey?: string, _gpuLeavesResolvedSide?: THREE.Side, _gpuTrunkResolvedSide?: THREE.Side }} FoliageObjectTypeCacheEntry */",
+ " /** @typedef {{ records?: FoliageObjectRecord[], statsByRef?: Map, statsByName?: Map, selection?: FoliageMaterialSelection|null, _cachedGeometry?: THREE.BufferGeometry|null, _gpuSourceGeometry?: THREE.BufferGeometry|null, _gpuGeometry?: THREE.BufferGeometry|null, _gpuGroupKey?: string, _sharedMaterialKey?: string, _resolvedSide?: THREE.Side, _gpuFastMode?: \"single\"|\"splitLeavesTree\", _gpuLeavesSourceGeometry?: THREE.BufferGeometry|null, _gpuTrunkSourceGeometry?: THREE.BufferGeometry|null, _gpuLeavesGeometry?: THREE.BufferGeometry|null, _gpuTrunkGeometry?: THREE.BufferGeometry|null, _gpuLeavesBaseGroupKey?: string, _gpuTrunkBaseGroupKey?: string, _gpuLeavesSharedMaterialKey?: string, _gpuTrunkSharedMaterialKey?: string, _gpuLeavesResolvedSide?: THREE.Side, _gpuTrunkResolvedSide?: THREE.Side }} FoliageObjectTypeCacheEntry */",
" /**",
" * Cached result of the automatic sway response heuristic.",
" * The `swayResponse` field is the shader multiplier; when the behavior sets",
@@ -308,11 +313,14 @@
" * @typedef {{ swayResponse: number, swayResponseRaw: number, sizeFactor: number, complexityFactor: number, vertexFactor: number, triFactor: number, structureFactor: number, planeFactor: number, responseGain: number, planeLikeRatio: number, baseByType: number, boundsMode: string }} FoliageAutoSwayResponseEntry",
" */",
" /** @typedef {{ isAlphaLikely: (mat: FoliageInspectableMaterial|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterial|null|undefined) => number }} FoliageObjectTypeAnalysisHelpers */",
- " /** @typedef {{ patchedMaterials: WeakSet, sharedByKey: Map, activeMaterials: Set, materialRegistry: FoliageSharedMaterialRegistry, objectTypeCacheRegistry: FoliageObjectTypeCache, nonInstancedRegistry: FoliageNonInstancedRegistry, instancingState: FoliageInstancingState, instancingCoordinator: FoliageInstancingCoordinator, cullingCoordinator: FoliageCullingCoordinator, shadowUniformSync: FoliageShadowUniformSync, time: number, gustVersion: number, debugPrinted: Set, gustEnabled: boolean, gustStrength: number, gustScale: number, gustSpeed: number, gustThreshold: number, gustContrast: number, gustTexture: THREE.Texture|null, gustTextureResourceName: string, gustFallbackTex: THREE.Texture|null, _gustFallbackIsStripe: boolean, _gustDefineDirty: boolean, fadeUpdateHz?: number, cullAccum?: number, cullInterval?: number, _lastLoggedFadeHz?: number, lastCullTime?: number, _lastFadeUpdateHz?: number, lastCamX?: number, lastCamY?: number, lastCamZ?: number, _fadeParamsDirty?: boolean, objectTypeCache: Map, _objectTypeCacheState: FoliageObjectTypeCacheState, geometryBBoxCache: WeakMap, autoSwayResponseCache: Map, nonInstancedFadeObjects: FoliageNonInstancedEntry[], nonInstancedStaticObjects: FoliageNonInstancedEntry[], _nonInstancedStaticCheckAccum: number, instancing: FoliageInstancingState, GUST_DEFAULTS: GustDefaults, _materialBuckets: FoliageMaterialBuckets, _frameCtx: SharedFrameContext|null, _frustumShared: THREE.Frustum|null, _projViewMatrixShared: THREE.Matrix4|null, _behaviorPropsCache?: WeakMap|null, _tmpInstanceCullSphere?: { cx: number, cy: number, cz: number, radius: number }, _tmpInstanceCullSphereA?: { cx: number, cy: number, cz: number, radius: number }, _tmpInstanceCullSphereB?: { cx: number, cy: number, cz: number, radius: number }, _tmpInstanceCullSphereShared?: { cx: number, cy: number, cz: number, radius: number }, _tmpVec3NonInstancedFade: THREE.Vector3|null, _libReady: boolean, getBehaviorProps: (behavior: FoliageBehavior|null|undefined) => FoliageBehaviorProps|null, syncObjectHotReload: (gdObj: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string })|null|undefined, behavior: FoliageBehavior|null|undefined, behaviorName?: string) => boolean, resetMaterialBuckets: () => void, registerActiveMaterial: (mat: THREE.Material|null|undefined) => void, unregisterActiveMaterial: (mat: THREE.Material|null|undefined) => void, disposeShadowMaterials: (mat: THREE.Material|null|undefined) => void, markGustForRecompile: () => void, ensureGustFallbackTex: () => THREE.Texture|null, disposeNonFallbackTex: (tex: THREE.Texture|null|undefined) => void, cleanupForSceneChange: () => void, _syncObjectTypeCacheAliases: () => FoliageSceneState, _syncNonInstancedAliases: () => FoliageSceneState, _syncInstancingAliases: () => FoliageSceneState, _readBoolArg: (efc: FoliageEventsFunctionContext, name: string, fallback: boolean) => boolean, _readNumArg: (efc: FoliageEventsFunctionContext, name: string, fallback: number) => number, _readStringArg: (efc: FoliageEventsFunctionContext, name: string, fallback: string) => string, buildFrameContext: (runtimeScene: gdjs.RuntimeScene, eventsFunctionContext: FoliageEventsFunctionContext) => SharedFrameContext, _ensureFrustum: (ctx: SharedFrameContext) => THREE.Frustum|null }} FoliageSceneStateShape */",
- " /** @typedef {{ _libReady: boolean, getSceneState: (runtimeScene: gdjs.RuntimeScene) => FoliageSceneState, normalizeCullingMode: (v: FoliagePrimitiveValue) => { name: string, code: number }, resolveRenderSide: (sourceSide: THREE.Side|undefined|null, cullingModeName: string) => THREE.Side, buildSideSuffix: (cullingModeCode: number, renderSide: THREE.Side|undefined|null) => string, buildPbrSuffix: (customPBREnabledValue: boolean, metallicValue: number, roughnessValue: number, specularValue: number, normalStrengthValue: number, ambientOcclusionStrengthValue: number, environmentStrengthValue: number) => string, buildGpuFastCacheKey: (localSharedKey: string, localMaterialName: string, localSwayType: string, localDistanceFadeEnabled: boolean, localPbrSuffix: string, localSideSuffix: string, localTwoSidedLighting: boolean, localSwayResponseAutoMode: boolean) => string, parseColor: (str: string, fallbackHex: string) => THREE.Color, isAlphaLikely: (mat: FoliageInspectableMaterial|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterial|null|undefined) => number, sanitizeKeyPart: (s: string) => string, meshMatchesSelection: (mesh: FoliageMeshLike, selection: FoliageMaterialSelection, srcRef: THREE.Material|null, srcName: string) => boolean, findFirstMatchingMesh: (root: THREE.Object3D, selection: FoliageMaterialSelection, srcRef: THREE.Material|null, srcName: string) => FoliageMeshLike|null, findSplitTreeMeshes: (root: THREE.Object3D, trunkMatcher: (mesh: FoliageMeshLike) => boolean, leavesMatcher: (mesh: FoliageMeshLike) => boolean) => { trunk: FoliageMeshLike|null, leaves: FoliageMeshLike|null } }} FoliageExtensionApi */",
+ " /** @typedef {{ patchedMaterials: WeakSet, sharedByKey: Map, activeMaterials: Set, materialRegistry: FoliageSharedMaterialRegistry, objectTypeCacheRegistry: FoliageObjectTypeCache, nonInstancedRegistry: FoliageNonInstancedRegistry, instancingState: FoliageInstancingState, instancingCoordinator: FoliageInstancingCoordinator, cullingCoordinator: FoliageCullingCoordinator, shadowUniformSync: FoliageShadowUniformSync, time: number, gustVersion: number, debugPrinted: Set, gustEnabled: boolean, gustStrength: number, gustScale: number, gustSpeed: number, gustThreshold: number, gustContrast: number, gustTexture: THREE.Texture|null, gustTextureResourceName: string, gustFallbackTex: THREE.Texture|null, _gustFallbackIsStripe: boolean, _gustDefineDirty: boolean, fadeUpdateHz?: number, cullAccum?: number, cullInterval?: number, _lastLoggedFadeHz?: number, lastCullTime?: number, _lastFadeUpdateHz?: number, lastCamX?: number, lastCamY?: number, lastCamZ?: number, _fadeParamsDirty?: boolean, objectTypeCache: Map, _objectTypeCacheState: FoliageObjectTypeCacheState, geometryBBoxCache: WeakMap, autoSwayResponseCache: Map, nonInstancedFadeObjects: FoliageNonInstancedEntry[], nonInstancedStaticObjects: FoliageNonInstancedEntry[], _nonInstancedStaticCheckAccum: number, instancing: FoliageInstancingState, GUST_DEFAULTS: GustDefaults, _materialBuckets: FoliageMaterialBuckets, _frameCtx: SharedFrameContext|null, _frustumShared: THREE.Frustum|null, _projViewMatrixShared: THREE.Matrix4|null, _behaviorPropsCache?: WeakMap|null, _tmpInstanceCullSphere?: { cx: number, cy: number, cz: number, radius: number }, _tmpInstanceCullSphereA?: { cx: number, cy: number, cz: number, radius: number }, _tmpInstanceCullSphereB?: { cx: number, cy: number, cz: number, radius: number }, _tmpInstanceCullSphereShared?: { cx: number, cy: number, cz: number, radius: number }, _tmpVec3NonInstancedFade: THREE.Vector3|null, _libReady: boolean, getBehaviorProps: (behavior: FoliageBehavior|null|undefined) => FoliageBehaviorProps|null, syncObjectHotReload: (gdObj: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string })|null|undefined, behavior: FoliageBehavior|null|undefined, behaviorName?: string) => boolean, resetMaterialBuckets: () => void, registerActiveMaterial: (mat: THREE.Material|null|undefined) => void, unregisterActiveMaterial: (mat: THREE.Material|null|undefined) => void, disposeShadowMaterials: (mat: THREE.Material|null|undefined) => void, markGustForRecompile: () => void, syncMaterialGustDefine: (mat: THREE.Material|null|undefined) => void, ensureGustFallbackTex: () => THREE.Texture|null, disposeNonFallbackTex: (tex: THREE.Texture|null|undefined) => void, cleanupForSceneChange: () => void, _syncObjectTypeCacheAliases: () => FoliageSceneState, _syncNonInstancedAliases: () => FoliageSceneState, _syncInstancingAliases: () => FoliageSceneState, _readBoolArg: (efc: FoliageEventsFunctionContext, name: string, fallback: boolean) => boolean, _readNumArg: (efc: FoliageEventsFunctionContext, name: string, fallback: number) => number, _readStringArg: (efc: FoliageEventsFunctionContext, name: string, fallback: string) => string, buildFrameContext: (runtimeScene: gdjs.RuntimeScene, eventsFunctionContext: FoliageEventsFunctionContext) => SharedFrameContext, _ensureFrustum: (ctx: SharedFrameContext) => THREE.Frustum|null }} FoliageSceneStateShape */",
+ " /** @typedef {{ _libReady: boolean, getSceneState: (runtimeScene: gdjs.RuntimeScene) => FoliageSceneState, normalizeCullingMode: (v: FoliagePrimitiveValue) => { name: string, code: number }, resolveRenderSide: (sourceSide: THREE.Side|undefined|null, cullingModeName: string) => THREE.Side, buildSideSuffix: (cullingModeCode: number, renderSide: THREE.Side|undefined|null) => string, buildPbrSuffix: (customPBREnabledValue: boolean, metallicValue: number, roughnessValue: number, specularValue: number, normalStrengthValue: number, ambientOcclusionStrengthValue: number, environmentStrengthValue: number) => string, buildGpuFastCacheKey: (localSharedKey: string, localMaterialName: string, localSwayType: string, localDistanceFadeEnabled: boolean, localPbrSuffix: string, localSideSuffix: string, localTwoSidedLighting: boolean, localSwayResponseAutoMode: boolean) => string, parseColor: (str: string, fallbackHex: string) => THREE.Color, isAlphaLikely: (mat: FoliageInspectableMaterial|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterial|null|undefined) => number, sanitizeKeyPart: (s: string) => string, refreshGradientBounds: (sources: FoliageGradientSource[], bounds: { worldMin: number, worldMax: number }, inverseWorldScale: number) => boolean, meshMatchesSelection: (mesh: FoliageMeshLike, selection: FoliageMaterialSelection, srcRef: THREE.Material|null, srcName: string) => boolean, findFirstMatchingMesh: (root: THREE.Object3D, selection: FoliageMaterialSelection, srcRef: THREE.Material|null, srcName: string) => FoliageMeshLike|null, findSplitTreeMeshes: (root: THREE.Object3D, trunkMatcher: (mesh: FoliageMeshLike) => boolean, leavesMatcher: (mesh: FoliageMeshLike) => boolean) => { trunk: FoliageMeshLike|null, leaves: FoliageMeshLike|null } }} FoliageExtensionApi */",
" /** @typedef {typeof gdjs & { _foliageSwaying3D?: FoliageExtensionApi }} FoliageGdjs */",
" /** @typedef {gdjs.RuntimeScene & { _foliageSwaying3D?: FoliageSceneState }} FoliageRuntimeScene */",
" ",
+ " /** @typedef {{ bounds: THREE.Box3, indices: number[], geometry: THREE.BufferGeometry|null }} FoliageMaterialSubset */",
+ " /** @typedef {{ mesh: FoliageMeshLike, bounds: THREE.Box3, zRow: number[] }} FoliageGradientSource */",
+ "",
" /**",
" * Cache for expensive per-object-type mesh/material analysis.",
" * onCreated uses this to reuse selection, stats, and fast-path metadata across",
@@ -330,6 +338,10 @@
" this.autoSwayResponseCache = new Map();",
" /** @type {Set} */",
" this.debugPrinted = new Set();",
+ " /** @type {WeakMap>} */",
+ " this.materialSubsets = new WeakMap();",
+ " /** @type {Set} */",
+ " this.ownedSubsetGeometries = new Set();",
" this.reset();",
" }",
"",
@@ -347,6 +359,11 @@
" }",
" entry._cachedGeometry = null;",
" entry._gpuGeometry = null;",
+ " entry._gpuSourceGeometry = null;",
+ " entry._gpuLeavesGeometry = null;",
+ " entry._gpuTrunkGeometry = null;",
+ " entry._gpuLeavesSourceGeometry = null;",
+ " entry._gpuTrunkSourceGeometry = null;",
" }",
"",
" /**",
@@ -359,6 +376,10 @@
" this._clearEntryCachedRefs(entry);",
" }",
" }",
+ " // Scene cleanup removes instancing groups before releasing their shared subsets.",
+ " for (var geometry of this.ownedSubsetGeometries) geometry.dispose();",
+ " this.ownedSubsetGeometries.clear();",
+ " this.materialSubsets = new WeakMap();",
" this.entries = new Map();",
" this.state = { tick: 0, setCount: 0, meta: new Map() };",
" this.geometryBBoxCache = new WeakMap();",
@@ -367,6 +388,123 @@
" }",
"",
" /**",
+ " * Cache the triangles actually drawn for a material selection. A single material",
+ " * ignores groups, just like Three.js; material arrays render only their groups.",
+ " * @param {THREE.BufferGeometry} source",
+ " * @param {number[]|null} slots",
+ " * @returns {FoliageMaterialSubset}",
+ " */",
+ " getMaterialSubset(source, slots) {",
+ " var key = slots === null ? \"single\" : slots.join(\",\");",
+ " var selections = this.materialSubsets.get(source);",
+ " if (!selections) {",
+ " selections = new Map();",
+ " this.materialSubsets.set(source, selections);",
+ " }",
+ " var cached = selections.get(key);",
+ " if (cached) return cached;",
+ " var position = source.getAttribute(\"position\");",
+ " var index = source.getIndex();",
+ " var count = position ? (index ? index.count : position.count) : 0;",
+ " var drawStart = Math.max(0, source.drawRange.start);",
+ " var drawEnd = Math.min(count, source.drawRange.start + source.drawRange.count);",
+ " var ranges = slots === null",
+ " ? [{ start: drawStart, count: drawEnd - drawStart, materialIndex: 0 }]",
+ " : source.groups.filter(function(group) { return slots.indexOf(group.materialIndex || 0) !== -1; });",
+ " var bounds = new THREE.Box3();",
+ " var point = new THREE.Vector3();",
+ " /** @type {number[]} */",
+ " var indices = [];",
+ " for (var range of ranges) {",
+ " var start = Math.max(drawStart, range.start);",
+ " var end = Math.min(drawEnd, range.start + range.count);",
+ " for (var i = start; i + 2 < end; i += 3) {",
+ " for (var j = 0; j < 3; j++) {",
+ " var vertex = index ? index.getX(i + j) : i + j;",
+ " indices.push(vertex);",
+ " point.fromBufferAttribute(position, vertex);",
+ " bounds.expandByPoint(point);",
+ " }",
+ " }",
+ " }",
+ " cached = { bounds: bounds, indices: indices, geometry: null };",
+ " selections.set(key, cached);",
+ " return cached;",
+ " }",
+ "",
+ " /**",
+ " * Prepare a shared, compact geometry, retaining every vertex/morph attribute.",
+ " * Source geometry and materials remain owned by GDevelop.",
+ " * @param {FoliageMeshLike} mesh",
+ " * @param {FoliageMaterialSelection} selection",
+ " * @returns {THREE.BufferGeometry}",
+ " */",
+ " getRenderGeometry(mesh, selection) {",
+ " var subset = this.getMaterialSubset(mesh.geometry, selectedMaterialSlots(mesh, selection));",
+ " if (!subset.geometry) {",
+ " var indexed = mesh.geometry.clone();",
+ " indexed.setIndex(subset.indices);",
+ " indexed.clearGroups();",
+ " indexed.setDrawRange(0, subset.indices.length);",
+ " subset.geometry = indexed.toNonIndexed();",
+ " indexed.dispose();",
+ " subset.geometry.computeBoundingBox();",
+ " subset.geometry.computeBoundingSphere();",
+ " this.ownedSubsetGeometries.add(subset.geometry);",
+ " }",
+ " return subset.geometry;",
+ " }",
+ "",
+ " /**",
+ " * Capture mesh references before their source materials are replaced.",
+ " * @param {THREE.Object3D} root",
+ " * @param {FoliageMaterialSelection} selection",
+ " * @returns {FoliageGradientSource[]}",
+ " */",
+ " collectGradientSources(root, selection) {",
+ " /** @type {FoliageGradientSource[]} */",
+ " var sources = [];",
+ " var registry = this;",
+ " root.traverse(function(node) {",
+ " var mesh = /** @type {FoliageMeshLike} */ (node);",
+ " if (!meshMatchesSelection(mesh, selection, selection.srcRef, selection.matchName)) return;",
+ " var subset = registry.getMaterialSubset(mesh.geometry, selectedMaterialSlots(mesh, selection));",
+ " if (!subset.bounds.isEmpty()) sources.push({ mesh: mesh, bounds: subset.bounds, zRow: [NaN, NaN, NaN, NaN] });",
+ " });",
+ " return sources;",
+ " }",
+ "",
+ " /**",
+ " * The current batch layout supports one draw, or one trunk and one leaves draw.",
+ " * Keep more complex assets intact rather than silently dropping their meshes.",
+ " * @param {THREE.Object3D} root",
+ " * @param {FoliageMaterialSelection} selection",
+ " * @param {string} swayType",
+ " * @returns {boolean}",
+ " */",
+ " supportsInstancing(root, selection, swayType) {",
+ " var registry = this;",
+ " var draws = 0, selectedDraws = 0, slots = 0;",
+ " var supported = true;",
+ " root.traverse(function(node) {",
+ " var mesh = /** @type {FoliageMeshLike & { isSkinnedMesh?: boolean, isInstancedMesh?: boolean }} */ (node);",
+ " if (!mesh.isMesh || !mesh.geometry || !mesh.material) return;",
+ " if (mesh.isSkinnedMesh || mesh.isInstancedMesh || Object.keys(mesh.geometry.morphAttributes).length) supported = false;",
+ " var materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];",
+ " for (var i = 0; i < materials.length; i++) {",
+ " if (!materials[i]) continue;",
+ " slots++;",
+ " var subset = registry.getMaterialSubset(mesh.geometry, Array.isArray(mesh.material) ? [i] : null);",
+ " if (!subset.indices.length) continue;",
+ " draws++;",
+ " if (materialMatchesSelection(materials[i], selection)) selectedDraws++;",
+ " else if (!materials[i].name) supported = false;",
+ " }",
+ " });",
+ " return supported && draws === slots && selectedDraws === 1 && (draws === 1 || (swayType === \"Leaves\" && draws === 2));",
+ " }",
+ "",
+ " /**",
" * Classify a cache key so pruning can cap each cache family independently.",
" * @param {string} key",
" * @returns {\"rop\"|\"stats\"|\"fast\"}",
@@ -635,7 +773,8 @@
" st.meshCount++;",
" }",
" }",
- " root.updateMatrixWorld(true);",
+ " // Include ancestors: the layer's world scale may not have reached matrixWorld yet.",
+ " root.updateWorldMatrix(true, true);",
" var tmpBox = new THREE.Box3();",
" var tmpBoxLocal = new THREE.Box3();",
" var rootWorldInv = new THREE.Matrix4();",
@@ -1072,10 +1211,28 @@
" cfg.colorContrast = props.colorContrast;",
" cfg.useColorGradient = !!props.useColorGradient;",
" cfg.twoSidedLighting = !!props.twoSidedLighting;",
- " cfg.ignoreUV = !!props.ignoreUV;",
+ " cfg.ignoreUV = cfg.useColorGradient && !!props.ignoreUV;",
" cfg.gradientStart = props.gradientStart;",
" cfg.gradientEnd = props.gradientEnd;",
"",
+ " // Signed shadow variants own config copies, so switch their bounds as well.",
+ " var gradientMaterials = [mat, mat.userData._foliageDepthMat, mat.userData._foliageDistanceMat];",
+ " for (var gi = 0; gi < gradientMaterials.length; gi++) {",
+ " var gradientMat = gradientMaterials[gi];",
+ " var gradientCfg = gradientMat && gradientMat.userData ? gradientMat.userData.foliageConfig : null;",
+ " if (!gradientCfg || !gradientCfg.gradientBounds) continue;",
+ " var bounds = gradientCfg.gradientBounds;",
+ " gradientCfg.ignoreUV = cfg.ignoreUV;",
+ " gradientCfg.gradientLocalZMin = cfg.ignoreUV ? bounds.worldMin : bounds.localMin;",
+ " gradientCfg.gradientLocalZMax = cfg.ignoreUV ? bounds.worldMax : bounds.localMax;",
+ " var gradientUniforms = gradientMat.userData.foliageUniforms;",
+ " if (gradientUniforms) {",
+ " if (gradientUniforms.uUseWorldGradient) gradientUniforms.uUseWorldGradient.value = cfg.ignoreUV ? 1.0 : 0.0;",
+ " if (gradientUniforms.uGradientLocalZMin) gradientUniforms.uGradientLocalZMin.value = gradientCfg.gradientLocalZMin;",
+ " if (gradientUniforms.uGradientLocalZMax) gradientUniforms.uGradientLocalZMax.value = gradientCfg.gradientLocalZMax;",
+ " }",
+ " }",
+ "",
" var u = mat.userData.foliageUniforms;",
" var shouldSyncSwayResponse = !!cfg.swayType || typeof cfg.swayResponse === \"number\" || !!(u && u.uSwayResponse);",
" if (shouldSyncSwayResponse) {",
@@ -1358,6 +1515,61 @@
" return String(s).replace(/::/g, \"_\").trim() || \"(empty)\";",
" }",
"",
+ " /** @param {THREE.Material} material @param {FoliageMaterialSelection} selection @returns {boolean} */",
+ " function materialMatchesSelection(material, selection) {",
+ " return selection.matchMode === \"name\"",
+ " ? material.name === selection.matchName",
+ " : material === selection.srcRef || !!(selection.matchName && material.name === selection.matchName && isAlphaLikely(material));",
+ " }",
+ "",
+ " /** @param {FoliageMeshLike} mesh @param {FoliageMaterialSelection} selection @returns {number[]|null} */",
+ " function selectedMaterialSlots(mesh, selection) {",
+ " if (!Array.isArray(mesh.material)) return null;",
+ " var slots = [];",
+ " for (var i = 0; i < mesh.material.length; i++) {",
+ " if (mesh.material[i] && materialMatchesSelection(mesh.material[i], selection)) slots.push(i);",
+ " }",
+ " return slots;",
+ " }",
+ "",
+ " /**",
+ " * Project cached local boxes onto world Z. Only the Z matrix row affects this",
+ " * gradient; checking it avoids vertex scans and uniform writes for static trees.",
+ " * @param {FoliageGradientSource[]} sources",
+ " * @param {{ worldMin: number, worldMax: number }} bounds",
+ " * @param {number} inverseWorldScale",
+ " * @returns {boolean}",
+ " */",
+ " function refreshGradientBounds(sources, bounds, inverseWorldScale) {",
+ " var changed = false;",
+ " for (var source of sources) {",
+ " var e = source.mesh.matrixWorld.elements;",
+ " var row = source.zRow;",
+ " if (row[0] !== e[2] || row[1] !== e[6] || row[2] !== e[10] || row[3] !== e[14]) changed = true;",
+ " }",
+ " if (!changed) return false;",
+ " var min = Infinity, max = -Infinity;",
+ " for (var source of sources) {",
+ " var e = source.mesh.matrixWorld.elements, box = source.bounds;",
+ " var row = source.zRow;",
+ " row[0] = e[2]; row[1] = e[6]; row[2] = e[10]; row[3] = e[14];",
+ " var x0 = e[2] * box.min.x, x1 = e[2] * box.max.x;",
+ " var y0 = e[6] * box.min.y, y1 = e[6] * box.max.y;",
+ " var z0 = e[10] * box.min.z, z1 = e[10] * box.max.z;",
+ " min = Math.min(min, e[14] + Math.min(x0, x1) + Math.min(y0, y1) + Math.min(z0, z1));",
+ " max = Math.max(max, e[14] + Math.max(x0, x1) + Math.max(y0, y1) + Math.max(z0, z1));",
+ " }",
+ " if (!isFinite(min) || !isFinite(max)) return false;",
+ " // Degenerate horizontal cards still need a nonzero range in the shader.",
+ " if (max <= min) {",
+ " min -= inverseWorldScale * 0.5;",
+ " max += inverseWorldScale * 0.5;",
+ " }",
+ " bounds.worldMin = min;",
+ " bounds.worldMax = max;",
+ " return true;",
+ " }",
+ "",
" /**",
" * @param {FoliageMeshLike} mesh",
" * @param {FoliageMaterialSelection} selection",
@@ -1627,6 +1839,7 @@
" entry.fadeEnd = entryData.fadeEnd;",
" entry.fadeEnabled = !!entryData.fadeEnabled;",
" entry.fadeBehavior = entryData.fadeBehavior || null;",
+ " entry.gradientSources = entryData.gradientSources || [];",
" entry._parkedNoFade = false;",
" if (typeof entry._firstHideWarmupDone !== \"boolean\") entry._firstHideWarmupDone = false;",
" this._removeFromList(this.staticEntries, entry, false);",
@@ -1641,6 +1854,7 @@
" fadeEnd: entryData.fadeEnd,",
" fadeEnabled: !!entryData.fadeEnabled,",
" fadeBehavior: entryData.fadeBehavior || null,",
+ " gradientSources: entryData.gradientSources || [],",
" _parkedNoFade: false,",
" _firstHideWarmupDone: false",
" };",
@@ -1652,6 +1866,41 @@
" return entry;",
" }",
"",
+ " /** @returns {void} */",
+ " syncGradientBounds() {",
+ " this._syncGradientList(this.fadeEntries);",
+ " this._syncGradientList(this.staticEntries);",
+ " }",
+ "",
+ " /** @param {FoliageNonInstancedEntry[]} entries @returns {void} */",
+ " _syncGradientList(entries) {",
+ " for (var entry of entries) {",
+ " if (!entry.threeObj || !entry.material || !entry.gradientSources || !entry.gradientSources.length) continue;",
+ " var cfg = entry.material.userData.foliageConfig;",
+ " if (!cfg || !cfg.gradientBounds) continue;",
+ " entry.threeObj.updateWorldMatrix(true, true);",
+ " if (!refreshGradientBounds(entry.gradientSources, cfg.gradientBounds, this.state._frameCtx ? this.state._frameCtx.inverseWorldScale : 1)) continue;",
+ " this._syncGradientMaterial(entry.material, cfg.gradientBounds);",
+ " this._syncGradientMaterial(entry.material.userData._foliageDepthMat, cfg.gradientBounds);",
+ " this._syncGradientMaterial(entry.material.userData._foliageDistanceMat, cfg.gradientBounds);",
+ " }",
+ " }",
+ "",
+ " /** @param {THREE.Material|null|undefined} material @param {{ localMin: number, localMax: number, worldMin: number, worldMax: number }} bounds @returns {void} */",
+ " _syncGradientMaterial(material, bounds) {",
+ " if (!material) return;",
+ " var cfg = material.userData.foliageConfig;",
+ " if (!cfg) return;",
+ " cfg.gradientBounds = bounds;",
+ " cfg.gradientLocalZMin = cfg.ignoreUV ? bounds.worldMin : bounds.localMin;",
+ " cfg.gradientLocalZMax = cfg.ignoreUV ? bounds.worldMax : bounds.localMax;",
+ " var uniforms = material.userData.foliageUniforms;",
+ " if (uniforms) {",
+ " if (uniforms.uGradientLocalZMin) uniforms.uGradientLocalZMin.value = cfg.gradientLocalZMin;",
+ " if (uniforms.uGradientLocalZMax) uniforms.uGradientLocalZMax.value = cfg.gradientLocalZMax;",
+ " }",
+ " }",
+ "",
" /**",
" * @param {FoliageNonInstancedEntry|null|undefined} entry",
" * @returns {FoliageNonInstancedEntry|null}",
@@ -2104,6 +2353,7 @@
" g.centersXY = null;",
" g.centersZ = null;",
" g.cullRadii = null;",
+ " g.gradientBounds = null;",
" g.instanceFade = null;",
" g.instanceFadePrev = null;",
" g._srcGeometryRef = null;",
@@ -2367,6 +2617,7 @@
" sideKey",
" ].join(\"|\");",
" };",
+ " this.state.syncMaterialGustDefine(targetMat);",
" targetMat.needsUpdate = true;",
" return targetMat;",
" }",
@@ -2771,6 +3022,36 @@
" }",
"",
" /**",
+ " * Store mesh-relative world Z bounds in scene units, independent of renderer scale.",
+ " * @param {FoliageInstancingGroup} group",
+ " * @param {number} idx",
+ " * @param {THREE.Matrix4} worldMat",
+ " * @param {THREE.BufferGeometry} geometry",
+ " * @param {number} inverseWorldScale",
+ " */",
+ " writeInstanceGradientBounds(group, idx, worldMat, geometry, inverseWorldScale) {",
+ " var required = (idx + 1) * 2;",
+ " if (!group.gradientBounds || group.gradientBounds.length < required) {",
+ " var previous = group.gradientBounds;",
+ " group.gradientBounds = new Float32Array(Math.max(required, previous ? previous.length * 2 : 512));",
+ " if (previous) group.gradientBounds.set(previous);",
+ " }",
+ " if (!geometry.boundingBox) geometry.computeBoundingBox();",
+ " var box = geometry.boundingBox;",
+ " var lo = 0, hi = 0;",
+ " if (box) {",
+ " var e = worldMat.elements;",
+ " var x0 = e[2] * box.min.x, x1 = e[2] * box.max.x;",
+ " var y0 = e[6] * box.min.y, y1 = e[6] * box.max.y;",
+ " var z0 = e[10] * box.min.z, z1 = e[10] * box.max.z;",
+ " lo = (Math.min(x0, x1) + Math.min(y0, y1) + Math.min(z0, z1)) / inverseWorldScale;",
+ " hi = (Math.max(x0, x1) + Math.max(y0, y1) + Math.max(z0, z1)) / inverseWorldScale;",
+ " }",
+ " group.gradientBounds[idx * 2] = isFinite(lo) && hi > lo ? lo : -0.5;",
+ " group.gradientBounds[idx * 2 + 1] = isFinite(hi) && hi > lo ? hi : 0.5;",
+ " }",
+ "",
+ " /**",
" * @param {THREE.Material|null|undefined} mat",
" * @param {number} determinant",
" * @returns {void}",
@@ -2905,6 +3186,8 @@
" localM = new THREE.Matrix4();",
" inst._tmpMat4_local = localM;",
" }",
+ " var leavesWorld = new THREE.Matrix4();",
+ " var leavesLocal = new THREE.Matrix4();",
"",
" var foliageRootMWInv = inst._tmpMat4_foliageRootInv;",
" if (!foliageRootMWInv) {",
@@ -3006,11 +3289,17 @@
"",
" localM.multiplyMatrices(foliageRootMWInv, repW2P);",
" var detSignParts = this._getDetSignFromMatrix(localM);",
- " var detSuffixParts = this._getDetSignSuffix(detSignParts);",
+ " var leavesRel = this.getOrComputeRepRelMatrix(gdObjParts, part1.repMesh);",
+ " if (leavesRel) leavesWorld.multiplyMatrices(objW2P, leavesRel);",
+ " else leavesWorld.copy(part1.repMesh.matrixWorld);",
+ " leavesLocal.multiplyMatrices(foliageRootMWInv, leavesWorld);",
+ " var detSignLeaves = this._getDetSignFromMatrix(leavesLocal);",
+ " // Both groups share slot indices, so partition by the pair of signs.",
+ " var detSuffixParts = \"::PAIR_T\" + detSignParts + \"_L\" + detSignLeaves;",
" var groupKeyTrunk = part0.baseGroupKey + detSuffixParts;",
" var groupKeyLeaves = part1.baseGroupKey + detSuffixParts;",
" var materialTrunk = this._getSignedInstancedMaterial(part0.material, detSignParts) || part0.material;",
- " var materialLeaves = this._getSignedInstancedMaterial(part1.material, detSignParts) || part1.material;",
+ " var materialLeaves = this._getSignedInstancedMaterial(part1.material, detSignLeaves) || part1.material;",
" this._logDetSignDiagnostic(\"split\", part0.baseGroupKey, detSignParts, behaviorParts);",
" var gTrunk = inst.ensureGroup(groupKeyTrunk, {",
" key: groupKeyTrunk,",
@@ -3034,7 +3323,7 @@
" material: materialLeaves,",
" parent: inst.foliageRoot",
" });",
- " gLeaves.instanceDetSign = detSignParts;",
+ " gLeaves.instanceDetSign = detSignLeaves;",
" if (!gLeaves.freeIndices || gLeaves.freeIndices !== gTrunk.freeIndices) gLeaves.freeIndices = gTrunk.freeIndices;",
" if (!gLeaves.freeIndexSet || gLeaves.freeIndexSet !== gTrunk.freeIndexSet) gLeaves.freeIndexSet = gTrunk.freeIndexSet;",
" gLeaves.castShadow = !!gLeaves.castShadow || !!(part1.repMesh && part1.repMesh.castShadow);",
@@ -3105,10 +3394,10 @@
" gLeaves.matricesBuffer = newBufferL;",
" }",
" for (var mL = 0; mL < 16; mL++) {",
- " gLeaves.matricesBuffer[offsetP + mL] = srcP[mL];",
+ " gLeaves.matricesBuffer[offsetP + mL] = leavesLocal.elements[mL];",
" }",
" var cullSphereLeaves = this.computeWorldCullSphere(",
- " repW2P,",
+ " leavesWorld,",
" part1.geometry,",
" gLeaves._instanceCullRadius,",
" state._tmpInstanceCullSphereB || (state._tmpInstanceCullSphereB = { cx: 0, cy: 0, cz: 0, radius: 0 })",
@@ -3120,6 +3409,7 @@
" );",
" this.writeInstanceCullValues(gTrunk, idxP, cullSphereShared.cx, cullSphereShared.cy, cullSphereShared.cz, cullSphereShared.radius);",
" this.writeInstanceCullValues(gLeaves, idxP, cullSphereShared.cx, cullSphereShared.cy, cullSphereShared.cz, cullSphereShared.radius);",
+ " this.writeInstanceGradientBounds(gLeaves, idxP, leavesWorld, part1.geometry, ctx.inverseWorldScale);",
" if (gLeaves.instanceFade && idxP < gLeaves.instanceFade.length) gLeaves.instanceFade[idxP] = 1.0;",
" if (gLeaves.instanceFadePrev && idxP < gLeaves.instanceFadePrev.length) gLeaves.instanceFadePrev[idxP] = 1.0;",
"",
@@ -3277,6 +3567,7 @@
" }",
"",
" this.writeInstanceCullData(g, idx, repW2, geometry, g._instanceCullRadius);",
+ " this.writeInstanceGradientBounds(g, idx, repW2, geometry, ctx.inverseWorldScale);",
" if (g.instanceFade && idx < g.instanceFade.length) g.instanceFade[idx] = 1.0;",
" if (g.instanceFadePrev && idx < g.instanceFadePrev.length) g.instanceFadePrev[idx] = 1.0;",
"",
@@ -3420,6 +3711,10 @@
" for (var fpi = 0; fpi < newCap; fpi++) fadePrevAttr.array[fpi] = 1.0;",
" mesh.geometry.setAttribute(\"aFadePrev\", fadePrevAttr);",
"",
+ " var gradientAttr = new THREE.InstancedBufferAttribute(new Float32Array(newCap * 2), 2);",
+ " gradientAttr.setUsage(THREE.DynamicDrawUsage);",
+ " mesh.geometry.setAttribute(\"aFoliageGradientBounds\", gradientAttr);",
+ "",
" mesh.count = 0;",
" mesh.visible = false;",
" var attachParent = g.parent || inst.foliageRoot;",
@@ -3541,10 +3836,12 @@
" var prevCamY = isFinite(this._prevFrustumPassCamY) ? this._prevFrustumPassCamY : camY;",
" var prevCamZ = isFinite(this._prevFrustumPassCamZ) ? this._prevFrustumPassCamZ : camZ;",
" var camDelta = Math.abs(camX - prevCamX) + Math.abs(camY - prevCamY) + Math.abs(camZ - prevCamZ);",
- " var camMoved = camDelta > 0.5;",
- " this._prevFrustumPassCamX = camX;",
- " this._prevFrustumPassCamY = camY;",
- " this._prevFrustumPassCamZ = camZ;",
+ " var camMoved = !isFinite(this._prevFrustumPassCamX) || camDelta > 0.5 * frameCam.inverseWorldScale;",
+ " if (camMoved) {",
+ " this._prevFrustumPassCamX = camX;",
+ " this._prevFrustumPassCamY = camY;",
+ " this._prevFrustumPassCamZ = camZ;",
+ " }",
"",
" var dirChanged = false;",
" if (typeof cam.getWorldDirection === \"function\" && typeof THREE.Vector3 !== \"undefined\") {",
@@ -3576,6 +3873,7 @@
" var inst = state.instancing;",
" if (!inst || !inst.groups) return false;",
"",
+ " var inverseWorldScale = ctx ? ctx.inverseWorldScale : 1;",
" var cam = ctx && ctx.cam ? ctx.cam : null;",
" var camX = ctx && isFinite(ctx.camX) ? ctx.camX : 0;",
" var camY = ctx && isFinite(ctx.camY) ? ctx.camY : 0;",
@@ -3589,10 +3887,7 @@
" var prevCamY = isFinite(this._prevCullCamY) ? this._prevCullCamY : camY;",
" var prevCamZ = isFinite(this._prevCullCamZ) ? this._prevCullCamZ : camZ;",
" var camDelta = Math.abs(camX - prevCamX) + Math.abs(camY - prevCamY) + Math.abs(camZ - prevCamZ);",
- " var camMoved = camDelta > 0.5;",
- " this._prevCullCamX = camX;",
- " this._prevCullCamY = camY;",
- " this._prevCullCamZ = camZ;",
+ " var camMoved = !isFinite(this._prevCullCamX) || camDelta > 0.5 * inverseWorldScale;",
"",
" var camDirChanged = false;",
" if (cam && typeof cam.getWorldDirection === \"function\" && typeof THREE.Vector3 !== \"undefined\") {",
@@ -3618,8 +3913,8 @@
" refreshFadeState(g, g._fadeBehavior || null, false);",
" }",
" var sigEnabled = !!g.fadeEnabled;",
- " var sigStart = isFinite(g.fadeStart) ? Math.round(g.fadeStart) : 1200;",
- " var sigEnd = isFinite(g.fadeEnd) ? Math.round(g.fadeEnd) : 1600;",
+ " var sigStart = isFinite(g.fadeStart) ? g.fadeStart : 1200;",
+ " var sigEnd = isFinite(g.fadeEnd) ? g.fadeEnd : 1600;",
" var newSig = (sigEnabled ? 1 : 0) + \"|\" + sigStart + \"|\" + sigEnd;",
" if (g._fadeSig !== newSig) {",
" g._fadeSig = newSig;",
@@ -3633,6 +3928,11 @@
" return false;",
" }",
"",
+ " // Accumulate slow movement against the last processed distance tick.",
+ " this._prevCullCamX = camX;",
+ " this._prevCullCamY = camY;",
+ " this._prevCullCamZ = camZ;",
+ "",
" inst.groups.forEach(function(group) {",
" /** @type {FoliageInstancingGroup} */",
" var g = group;",
@@ -3706,13 +4006,13 @@
"",
" var freeSet = g.freeIndexSet;",
" var totalSlots = g.matrixCount || 0;",
- " var startD = g.fadeStart || 1200;",
- " var endD = g.fadeEnd || 1600;",
+ " var startD = (isFinite(g.fadeStart) ? g.fadeStart : 1200) * inverseWorldScale;",
+ " var endD = (isFinite(g.fadeEnd) ? g.fadeEnd : 1600) * inverseWorldScale;",
" var range = endD - startD;",
- " if (range <= 0) range = 100;",
+ " if (range <= 0) range = 100 * inverseWorldScale;",
" var invRange = 1.0 / range;",
" var startDSq = startD * startD;",
- " var margin = 256;",
+ " var margin = 256 * inverseWorldScale;",
" var endPlusMargin = endD + margin;",
" var endPlusMarginSq = endPlusMargin * endPlusMargin;",
"",
@@ -3823,7 +4123,7 @@
" tmpSphereInstancedCull = this._tmpSphereInstancedCull;",
" }",
" var frustumRadiusScale = 1.10;",
- " var frustumRadiusMargin = 32.0;",
+ " var frustumRadiusMargin = 32.0 * (ctx ? ctx.inverseWorldScale : 1);",
"",
" var anyProcessed = false;",
" inst.groups.forEach(function(group) {",
@@ -3856,6 +4156,7 @@
" 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 renderGradientAttr = mesh.geometry ? mesh.geometry.getAttribute(\"aFoliageGradientBounds\") : null;",
" var storCenters = g.centersXY;",
" var storCentersZ = g.centersZ;",
" var storRadii = g.cullRadii;",
@@ -3923,6 +4224,10 @@
"",
" if (renderFadeAttr && renderFadeAttr.array) renderFadeAttr.array[visibleCount] = fadeEnabled ? storFade[i] : 1.0;",
" if (renderFadePrevAttr && renderFadePrevAttr.array) renderFadePrevAttr.array[visibleCount] = fadeEnabled ? storFadePrev[i] : 1.0;",
+ " if (renderGradientAttr && g.gradientBounds) {",
+ " renderGradientAttr.array[visibleCount * 2] = g.gradientBounds[i * 2];",
+ " renderGradientAttr.array[visibleCount * 2 + 1] = g.gradientBounds[i * 2 + 1];",
+ " }",
" visibleCount++;",
" }",
"",
@@ -3930,6 +4235,7 @@
" mesh.instanceMatrix.needsUpdate = true;",
" if (renderFadeAttr) renderFadeAttr.needsUpdate = true;",
" if (renderFadePrevAttr) renderFadePrevAttr.needsUpdate = true;",
+ " if (renderGradientAttr) renderGradientAttr.needsUpdate = true;",
" mesh.visible = (visibleCount > 0);",
" g.visibleCount = visibleCount;",
" });",
@@ -3974,6 +4280,7 @@
" }",
"",
" if (su.uFoliageTime) su.uFoliageTime.value = ctx.time;",
+ " if (su.uFoliageWorldScale) su.uFoliageWorldScale.value = 1 / ctx.inverseWorldScale;",
" if (su.uWindStrength) su.uWindStrength.value = ctx.windStrength;",
" if (su.uWindSpeed) su.uWindSpeed.value = ctx.windSpeed;",
" if (su.uWindDirection) su.uWindDirection.value.set(ctx.wx, ctx.wy);",
@@ -4218,6 +4525,7 @@
" if (!ctx) {",
" ctx = cache._frameCtx = {",
" runtimeScene: null,",
+ " inverseWorldScale: 1,",
" dt: 0,",
" time: 0,",
" windStrength: 6,",
@@ -4233,6 +4541,9 @@
" };",
" }",
" ctx.runtimeScene = runtimeScene;",
+ " // Camera and mesh world positions use renderer units, not editor distances.",
+ " ctx.inverseWorldScale = typeof runtimeScene.getRenderer3DInverseWorldScale === \"function\"",
+ " ? runtimeScene.getRenderer3DInverseWorldScale() : 1;",
" ctx.frustum = null;",
" ctx._frustumBuilt = false;",
" ",
@@ -4311,6 +4622,7 @@
" this.sharedByKey = new Map();",
" this.activeMaterials = new Set();",
" this.time = 0;",
+ " this._inverseWorldScale = 1;",
" this.gustVersion = 0;",
" this.gustEnabled = false;",
" this.gustStrength = 3.0;",
@@ -4388,11 +4700,96 @@
" this.ensureGustFallbackTex();",
" }",
"",
+ " /**",
+ " * Rebase renderer-space caches only when GDevelop changes its scene scale.",
+ " * Instance matrices and gradient attributes stay in scene/local units.",
+ " * @param {gdjs.RuntimeScene} runtimeScene",
+ " */",
+ " syncWorldScale(runtimeScene) {",
+ " var next = typeof runtimeScene.getRenderer3DInverseWorldScale === \"function\"",
+ " ? runtimeScene.getRenderer3DInverseWorldScale() : 1;",
+ " if (!isFinite(next) || next <= 0 || next === this._inverseWorldScale) return;",
+ " var ratio = next / this._inverseWorldScale;",
+ " this._inverseWorldScale = next;",
+ " var inst = this.instancingState;",
+ " inst._repRelByMesh = new WeakMap();",
+ " inst._repRelMatrixCache.clear();",
+ " for (const group of inst.groups.values()) {",
+ " var count = group.matrixCount || 0;",
+ " for (var i = 0; i < count; i++) {",
+ " if (group.centersXY) {",
+ " group.centersXY[i * 2] *= ratio;",
+ " group.centersXY[i * 2 + 1] *= ratio;",
+ " }",
+ " if (group.centersZ) group.centersZ[i] *= ratio;",
+ " if (group.cullRadii) group.cullRadii[i] *= ratio;",
+ " }",
+ " }",
+ " var seenStats = new Set();",
+ " for (const entry of this.objectTypeCacheRegistry.entries.values()) {",
+ " for (const statsMap of [entry.statsByRef, entry.statsByName]) {",
+ " if (!statsMap) continue;",
+ " for (const stats of statsMap.values()) {",
+ " if (seenStats.has(stats)) continue;",
+ " seenStats.add(stats);",
+ " stats.relZMin *= ratio;",
+ " stats.relZMax *= ratio;",
+ " stats.sizeWorldMin.multiplyScalar(ratio);",
+ " stats.sizeWorldMax.multiplyScalar(ratio);",
+ " }",
+ " }",
+ " }",
+ " var materials = new Set(this.activeMaterials);",
+ " for (const shared of this.sharedByKey.values()) materials.add(shared.material);",
+ " var seenBounds = new Set();",
+ " for (const mat of materials) {",
+ " if (!mat || !mat.userData) continue;",
+ " if (mat.userData._foliageDepthMat) materials.add(mat.userData._foliageDepthMat);",
+ " if (mat.userData._foliageDistanceMat) materials.add(mat.userData._foliageDistanceMat);",
+ " var cfg = mat.userData.foliageConfig;",
+ " if (!cfg || !cfg.gradientBounds) continue;",
+ " var bounds = cfg.gradientBounds;",
+ " if (!seenBounds.has(bounds)) {",
+ " seenBounds.add(bounds);",
+ " bounds.worldMin *= ratio;",
+ " bounds.worldMax *= ratio;",
+ " }",
+ " cfg.gradientLocalZMin = cfg.ignoreUV ? bounds.worldMin : bounds.localMin;",
+ " cfg.gradientLocalZMax = cfg.ignoreUV ? bounds.worldMax : bounds.localMax;",
+ " var u = mat.userData.foliageUniforms;",
+ " if (u) {",
+ " if (u.uFoliageWorldScale) u.uFoliageWorldScale.value = 1 / next;",
+ " if (u.uGradientLocalZMin) u.uGradientLocalZMin.value = cfg.gradientLocalZMin;",
+ " if (u.uGradientLocalZMax) u.uGradientLocalZMax.value = cfg.gradientLocalZMax;",
+ " }",
+ " }",
+ " var renderer = runtimeScene.getLayer(\"\").getRenderer();",
+ " var scene = renderer.getThreeScene ? renderer.getThreeScene() : null;",
+ " if (scene) scene.updateWorldMatrix(true, true);",
+ " this.cullingCoordinator.reset();",
+ " this._fadeParamsDirty = true;",
+ " }",
+ "",
" resetMaterialBuckets() { return this.materialRegistry.resetBuckets(); }",
" registerActiveMaterial(mat) { return this.materialRegistry.registerActiveMaterial(mat); }",
" unregisterActiveMaterial(mat) { return this.materialRegistry.unregisterActiveMaterial(mat); }",
" disposeShadowMaterials(mat) { return this.materialRegistry.disposeShadowMaterials(mat); }",
" markGustForRecompile() { return this.materialRegistry.markGustForRecompile(); }",
+ " /**",
+ " * Set the gust define before Three.js chooses a program, not in onBeforeCompile.",
+ " * @param {THREE.Material|null|undefined} mat",
+ " */",
+ " syncMaterialGustDefine(mat) {",
+ " if (!mat || !mat.userData || !mat.userData.foliageConfig || !mat.userData.foliageConfig.swayType) return;",
+ " var material = /** @type {THREE.Material & { defines?: Record }} */ (mat);",
+ " var enabled = !!this.gustEnabled;",
+ " var hasGust = !!material.defines && Object.prototype.hasOwnProperty.call(material.defines, \"USE_GUST\");",
+ " if (hasGust === enabled) return;",
+ " material.defines = Object.assign({}, material.defines || {});",
+ " if (enabled) material.defines.USE_GUST = \"\";",
+ " else delete material.defines.USE_GUST;",
+ " material.needsUpdate = true;",
+ " }",
" syncObjectHotReload(gdObj, behavior, behaviorName) { return syncObjectHotReload.call(this, gdObj, behavior, behaviorName); }",
" _syncObjectTypeCacheAliases() {",
" var registry = this.objectTypeCacheRegistry;",
@@ -4443,6 +4840,7 @@
" if (!scene._foliageSwaying3D) {",
" scene._foliageSwaying3D = createSceneState(runtimeScene);",
" }",
+ " scene._foliageSwaying3D.syncWorldScale(runtimeScene);",
" return scene._foliageSwaying3D;",
" }",
" ",
@@ -4463,6 +4861,7 @@
" isAlphaLikely: isAlphaLikely,",
" scoreMaterial: scoreMaterial,",
" sanitizeKeyPart: sanitizeKeyPart,",
+ " refreshGradientBounds: refreshGradientBounds,",
" meshMatchesSelection: meshMatchesSelection,",
" findFirstMatchingMesh: findFirstMatchingMesh,",
" findSplitTreeMeshes: findSplitTreeMeshes,",
@@ -4513,12 +4912,12 @@
" /** @typedef {{ swayResponse: number, swayResponseRaw: number, sizeFactor: number, complexityFactor: number, vertexFactor: number, triFactor: number, structureFactor: number, planeFactor: number, responseGain: number, planeLikeRatio: number, baseByType: number, boundsMode: string }} FoliageAutoSwayResponseEntryBridge */",
" /** @typedef {{ key?: string, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, parent?: THREE.Object3D|null, matricesBuffer?: Float32Array|null, centersXY?: Float32Array|null, centersZ?: Float32Array|null, cullRadii?: Float32Array|null, matrixCount?: number, aliveCount?: number, freeIndices?: number[], freeIndexSet?: Set, capacity?: number, mesh?: THREE.InstancedMesh|null, castShadow?: boolean, receiveShadow?: boolean, fadeEnabled?: boolean, fadeStart?: number, fadeEnd?: number, _fadeBehavior?: FoliageBehaviorBridge|null, _fadeSig?: string, _instanceCullRadius?: number, instanceFade?: Float32Array|null, instanceFadePrev?: Float32Array|null, _distanceCandidateIndices?: Uint32Array|null, _distanceCandidateCount?: number, _fadeDisabledApplied?: boolean, _lastFadeEnabled?: boolean, visibleCount?: number, _srcGeometryRef?: THREE.BufferGeometry|null, _ownedGeometry?: THREE.BufferGeometry|null }} FoliageInstancingGroupBridge */",
" /** @typedef {{ groups: Map, dirty: boolean, sceneTag: string|null, pending: FoliagePendingItemBridge[], queueIdCounter: number, cancelledQueueIds: Set, foliageRoot?: THREE.Object3D|null, _cachedSceneRoot?: THREE.Object3D|null, _tmpRootSet?: Set|null, _tmpMat4?: THREE.Matrix4, _tmpObj3D?: THREE.Object3D, _tmpVec3_pos?: THREE.Vector3, _tmpVec3_scale?: THREE.Vector3, _tmpEuler?: THREE.Euler, _tmpQuat?: THREE.Quaternion, _tmpMat4_objWorld?: THREE.Matrix4, _tmpMat4_objWorldInv?: THREE.Matrix4, _tmpMat4_repWorld?: THREE.Matrix4, _repRelByMesh?: WeakMap, _itemsByParentRepMesh?: Map, _repRelMatrixCache?: Map, _tmpMat4_local?: THREE.Matrix4, _tmpMat4_foliageRootInv?: THREE.Matrix4, ensureReady: () => FoliageInstancingStateBridge, markDirty: () => FoliageInstancingStateBridge }} FoliageInstancingStateBridge */",
- " /** @typedef {{ fadeEntries: FoliageNonInstancedEntryBridge[], staticEntries: FoliageNonInstancedEntryBridge[], staticCheckAccum: number, removeInvalidEntry: (entry: FoliageNonInstancedEntryBridge|null|undefined, fromList: \"fade\"|\"static\") => boolean, parkEntry: (entry: FoliageNonInstancedEntryBridge|null|undefined) => FoliageNonInstancedEntryBridge|null, reactivateEntry: (entry: FoliageNonInstancedEntryBridge|null|undefined) => FoliageNonInstancedEntryBridge|null }} FoliageNonInstancedRegistryBridge */",
+ " /** @typedef {{ syncGradientBounds: () => void, fadeEntries: FoliageNonInstancedEntryBridge[], staticEntries: FoliageNonInstancedEntryBridge[], staticCheckAccum: number, removeInvalidEntry: (entry: FoliageNonInstancedEntryBridge|null|undefined, fromList: \"fade\"|\"static\") => boolean, parkEntry: (entry: FoliageNonInstancedEntryBridge|null|undefined) => FoliageNonInstancedEntryBridge|null, reactivateEntry: (entry: FoliageNonInstancedEntryBridge|null|undefined) => FoliageNonInstancedEntryBridge|null }} FoliageNonInstancedRegistryBridge */",
" /** @typedef {{ timeWind: Set, fadeInterp: Set, gust: Set, shadowHost: Set, unknown: Set, rebuildNeeded: boolean }} FoliageMaterialBucketsBridge */",
" /** @typedef {{ flushPending: (ctx: SharedFrameContextBridge) => void, rebuildDirtyGroups: () => void }} FoliageInstancingCoordinatorBridge */",
" /** @typedef {{ reset: () => FoliageCullingCoordinatorBridge, didFrustumCameraChange: (frameCam: SharedFrameContextBridge|null|undefined) => boolean, performDistanceCullTick: (ctx: SharedFrameContextBridge|null|undefined, forceRun: boolean, refreshFadeState: (target: FoliageFadeStateTargetBridge|null|undefined, behavior: FoliageBehaviorBridge|null|undefined, defaultEnabled: boolean) => void) => boolean, performFrustumCompactionPass: (ctx: SharedFrameContextBridge|null|undefined) => boolean }} FoliageCullingCoordinatorBridge */",
" /** @typedef {{ syncMaterial: (hostMat: THREE.Material|null|undefined, shadowMat: THREE.Material|null|undefined, ctx: SharedFrameContextBridge, fadeInterpT: number) => void, syncShadowHostBucket: (bucket: Set|null|undefined, activeMaterials: Set|null|undefined, unregisterActiveMaterial: (mat: THREE.Material|null|undefined) => void, ctx: SharedFrameContextBridge, fadeInterpT: number) => void }} FoliageShadowUniformSyncBridge */",
- " /** @typedef {{ runtimeScene: gdjs.RuntimeScene|null, dt: number, time: number, windStrength: number, windSpeed: number, wx: number, wy: number, cam: THREE.Camera|null, camX: number, camY: number, camZ: number, frustum: THREE.Frustum|null, _frustumBuilt: boolean }} SharedFrameContextBridge */",
+ " /** @typedef {{ runtimeScene: gdjs.RuntimeScene|null, inverseWorldScale: number, dt: number, time: number, windStrength: number, windSpeed: number, wx: number, wy: number, cam: THREE.Camera|null, camX: number, camY: number, camZ: number, frustum: THREE.Frustum|null, _frustumBuilt: boolean }} SharedFrameContextBridge */",
" /** @typedef {{ _libReady?: boolean, time: number, gustVersion: number, fadeUpdateHz?: number, cullAccum?: number, cullInterval?: number, _lastLoggedFadeHz?: number, lastCullTime?: number, _lastFadeUpdateHz?: number, lastCamX?: number, lastCamY?: number, lastCamZ?: number, _fadeParamsDirty?: boolean, nonInstancedFadeObjects: FoliageNonInstancedEntryBridge[], nonInstancedStaticObjects: FoliageNonInstancedEntryBridge[], nonInstancedRegistry: FoliageNonInstancedRegistryBridge, autoSwayResponseCache: Map, _nonInstancedStaticCheckAccum: number, _readBoolArg: (efc: FoliageEventsFunctionContextBridge, name: string, fallback: boolean) => boolean, _readNumArg: (efc: FoliageEventsFunctionContextBridge, name: string, fallback: number) => number, _readStringArg: (efc: FoliageEventsFunctionContextBridge, name: string, fallback: string) => string, _gustDefineDirty?: boolean, markGustForRecompile: () => void, instancing: FoliageInstancingStateBridge, instancingState: FoliageInstancingStateBridge, instancingCoordinator: FoliageInstancingCoordinatorBridge, cullingCoordinator: FoliageCullingCoordinatorBridge, shadowUniformSync: FoliageShadowUniformSyncBridge, cleanupForSceneChange: () => void, buildFrameContext: (runtimeScene: gdjs.RuntimeScene, eventsFunctionContext: FoliageEventsFunctionContextBridge) => SharedFrameContextBridge, activeMaterials: Set, _materialBuckets: FoliageMaterialBucketsBridge, resetMaterialBuckets: () => void, unregisterActiveMaterial: (mat: THREE.Material|null|undefined) => void, disposeShadowMaterials: (mat: THREE.Material|null|undefined) => void, sharedByKey: Map, _ensureFrustum: (ctx: SharedFrameContextBridge) => THREE.Frustum|null, gustEnabled: boolean, gustStrength: number, gustScale: number, gustSpeed: number, gustThreshold: number, gustContrast: number, gustTexture: THREE.Texture|null, gustFallbackTex: THREE.Texture|null, _tmpVec3NonInstancedFade?: THREE.Vector3|null, getBehaviorProps: (behavior: FoliageBehaviorBridge|null|undefined) => FoliageBehaviorFadePropsBridge|null }} FoliageSceneStateBridge */",
" /** @typedef {{ _libReady?: boolean, getSceneState: (runtimeScene: gdjs.RuntimeScene) => FoliageSceneStateBridge }} FoliageExtensionBridge */",
" /** @typedef {typeof gdjs & { _foliageSwaying3D?: FoliageExtensionBridge }} FoliageGdjsBridge */",
@@ -4660,6 +5059,8 @@
" instancingOps.flushPending(ctx);",
" var hadDirty = cache.instancing && cache.instancing.dirty;",
" instancingOps.rebuildDirtyGroups();",
+ " // Includes fade-disabled objects and final transforms applied after onCreated.",
+ " cache.nonInstancedRegistry.syncGradientBounds();",
" ",
" var arrNi = cache.nonInstancedFadeObjects;",
" var arrNiLen = (arrNi && Array.isArray(arrNi)) ? arrNi.length : 0;",
@@ -4755,7 +5156,8 @@
" 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;",
+ " var inverseWorldScaleNi = ctx.inverseWorldScale;",
+ " var marginNi = 256.0 * inverseWorldScaleNi;",
" // 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];",
@@ -4793,6 +5195,8 @@
" try {",
" objX = entryNi.gdObj.getX ? entryNi.gdObj.getX() : 0;",
" objY = entryNi.gdObj.getY ? entryNi.gdObj.getY() : 0;",
+ " objX *= inverseWorldScaleNi;",
+ " objY *= -inverseWorldScaleNi;",
" tmpVecNi.set(objX, objY, 0);",
" posOk = true;",
" } catch (eGd) {}",
@@ -4805,6 +5209,8 @@
" var startNi = entryNi.fadeStart != null ? entryNi.fadeStart : 1200;",
" var endNi = entryNi.fadeEnd != null ? entryNi.fadeEnd : 1600;",
" if (endNi <= startNi) endNi = startNi + 100;",
+ " startNi *= inverseWorldScaleNi;",
+ " endNi *= inverseWorldScaleNi;",
" var dxNi = objX - camX;",
" var dyNi = objY - camY;",
" var distSqNi = dxNi * dxNi + dyNi * dyNi;",
@@ -4812,7 +5218,7 @@
" var endNiWithMargin = endNi + marginNi;",
" var endNiWithMarginSq = endNiWithMargin * endNiWithMargin;",
" var rangeNi = endNi - startNi;",
- " if (rangeNi <= 0) rangeNi = 100;",
+ " if (rangeNi <= 0) rangeNi = 100 * inverseWorldScaleNi;",
" var invRangeNi = 1.0 / rangeNi;",
" var fadeNi;",
" if (distSqNi > endNiWithMarginSq) {",
@@ -4902,6 +5308,10 @@
" }",
" var uUnknown = udUnknown.foliageUniforms;",
" if (!uUnknown) continue;",
+ " // Recompilation can remove uniforms as well as add them.",
+ " buckets.timeWind.delete(matUnknown);",
+ " buckets.fadeInterp.delete(matUnknown);",
+ " buckets.gust.delete(matUnknown);",
" if (uUnknown.uFoliageTime || uUnknown.uWindStrength || uUnknown.uWindSpeed || uUnknown.uWindDirection || uUnknown.uLocalWindDirection || uUnknown.uLocalWindPerpendicular) {",
" buckets.timeWind.add(matUnknown);",
" }",
@@ -4924,9 +5334,11 @@
" continue;",
" }",
" var uTW = udTW.foliageUniforms;",
+ " if (uTW.uFoliageWorldScale) uTW.uFoliageWorldScale.value = 1 / ctx.inverseWorldScale;",
" var last = udTW._lastUniforms;",
- " if (!last) {",
+ " if (!last || last.uniforms !== uTW) {",
" last = udTW._lastUniforms = {",
+ " uniforms: uTW,",
" time: undefined,",
" windStrength: undefined,",
" windSpeed: undefined,",
@@ -5018,8 +5430,7 @@
" ctx,",
" fadeInterpT",
" );",
- " ",
- " ",
+ "",
""
],
"parameterObjects": "",
@@ -6202,23 +6613,29 @@
" /** @typedef {{ material: THREE.Material, refCount: number, _ownedByFoliage?: boolean }} FoliageSharedMaterialEntryBridge */",
" /** @typedef {THREE.Material & { alphaMap?: THREE.Texture|null, map?: THREE.Texture|null, alphaTest?: number, transparent?: boolean, opacity?: number, name?: string }} FoliageInspectableMaterialBridge */",
" /** @typedef {{ swayResponse: number, swayResponseRaw: number, sizeFactor: number, complexityFactor: number, vertexFactor: number, triFactor: number, structureFactor: number, planeFactor: number, responseGain: number, planeLikeRatio: number, baseByType: number, boundsMode: string }} FoliageAutoSwayResponseEntryBridge */",
- " /** @typedef {{ _cachedGeometry?: THREE.BufferGeometry|null, _gpuGeometry?: THREE.BufferGeometry|null, _gpuGroupKey?: string, _sharedMaterialKey?: string, _resolvedSide?: THREE.Side, _gpuFastMode?: \"single\"|\"splitLeavesTree\", _gpuLeavesGeometry?: THREE.BufferGeometry|null, _gpuTrunkGeometry?: THREE.BufferGeometry|null, _gpuLeavesBaseGroupKey?: string, _gpuTrunkBaseGroupKey?: string, _gpuLeavesSharedMaterialKey?: string, _gpuTrunkSharedMaterialKey?: string, _gpuLeavesResolvedSide?: THREE.Side, _gpuTrunkResolvedSide?: THREE.Side, records?: FoliageMaterialRecordBridge[], statsByRef?: Map, statsByName?: Map, selection?: FoliageMaterialSelectionBridge|null }} FoliageObjectTypeCacheEntryBridge */",
+ " /** @typedef {{ mesh: FoliageMeshLike, bounds: THREE.Box3, zRow: number[] }} FoliageGradientSourceBridge */",
+ " /** @typedef {{ _cachedGeometry?: THREE.BufferGeometry|null, _gpuSourceGeometry?: THREE.BufferGeometry|null, _gpuGeometry?: THREE.BufferGeometry|null, _gpuGroupKey?: string, _sharedMaterialKey?: string, _resolvedSide?: THREE.Side, _gpuFastMode?: \"single\"|\"splitLeavesTree\", _gpuLeavesSourceGeometry?: THREE.BufferGeometry|null, _gpuTrunkSourceGeometry?: THREE.BufferGeometry|null, _gpuLeavesGeometry?: THREE.BufferGeometry|null, _gpuTrunkGeometry?: THREE.BufferGeometry|null, _gpuLeavesBaseGroupKey?: string, _gpuTrunkBaseGroupKey?: string, _gpuLeavesSharedMaterialKey?: string, _gpuTrunkSharedMaterialKey?: string, _gpuLeavesResolvedSide?: THREE.Side, _gpuTrunkResolvedSide?: THREE.Side, records?: FoliageMaterialRecordBridge[], statsByRef?: Map, statsByName?: Map, selection?: FoliageMaterialSelectionBridge|null }} FoliageObjectTypeCacheEntryBridge */",
" /** @typedef {{ repMesh?: THREE.Object3D|null, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, baseGroupKey?: string }} FoliagePendingPartBridge */",
" /** @typedef {{ gdObj?: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string })|null, threeObj?: THREE.Object3D|null, repMesh?: THREE.Object3D|null, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, parent?: THREE.Object3D|null, baseGroupKey?: string, groupKey?: string, swayType?: string, behavior?: FoliageBehaviorBridge|null, queueId?: number, objectName?: string, behaviorName?: string, parts?: FoliagePendingPartBridge[] }} FoliagePendingItemBridge */",
" /** @typedef {{ key?: string, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|null, parent?: THREE.Object3D|null, matricesBuffer?: Float32Array|null, centersXY?: Float32Array|null, matrixCount?: number, aliveCount?: number, freeIndices?: number[], freeIndexSet?: Set, capacity?: number, mesh?: THREE.InstancedMesh|null, centersZ?: Float32Array|null, cullRadii?: Float32Array|null, fadeEnabled?: boolean, fadeStart?: number, fadeEnd?: number, instanceFade?: Float32Array|null, instanceFadePrev?: Float32Array|null }} FoliageInstancingGroupBridge */",
" /** @typedef {{ groups: Map, dirty: boolean, sceneTag: string|null, pending: FoliagePendingItemBridge[], queueIdCounter: number, cancelledQueueIds: Set, foliageRoot?: THREE.Object3D|null, _cachedSceneRoot?: THREE.Object3D|null, ensureReady: () => FoliageInstancingStateBridge, nextQueueId: () => number, enqueue: (item: FoliagePendingItemBridge) => FoliagePendingItemBridge, ensureGroup: (groupKey: string, seedData?: FoliageInstancingGroupBridge) => FoliageInstancingGroupBridge, markDirty: () => FoliageInstancingStateBridge }} FoliageInstancingStateBridge */",
- " /** @typedef {{ gdObj?: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string })|null, threeObj?: THREE.Object3D|null, material?: THREE.Material|null, trunkMaterial?: THREE.Material|null, fadeStart?: number, fadeEnd?: number, fadeEnabled?: boolean, fadeBehavior?: FoliageBehaviorBridge|null, _wasHidden?: boolean, _parkedNoFade?: boolean, _firstHideWarmupDone?: boolean }} FoliageNonInstancedEntryBridge */",
+ " /** @typedef {{ gdObj?: (gdjs.RuntimeObject3D & { get3DRendererObject?: () => THREE.Object3D|null, getName?: () => string })|null, threeObj?: THREE.Object3D|null, material?: THREE.Material|null, trunkMaterial?: THREE.Material|null, fadeStart?: number, fadeEnd?: number, fadeEnabled?: boolean, fadeBehavior?: FoliageBehaviorBridge|null, gradientSources?: FoliageGradientSourceBridge[], _wasHidden?: boolean, _parkedNoFade?: boolean, _firstHideWarmupDone?: boolean }} FoliageNonInstancedEntryBridge */",
" /** @typedef {{ upsertFadeEntry: (entryData: FoliageNonInstancedEntryBridge) => FoliageNonInstancedEntryBridge|null }} FoliageNonInstancedRegistryBridge */",
- " /** @typedef {{ get: (key: string) => FoliageObjectTypeCacheEntryBridge|undefined, set: (key: string, value: FoliageObjectTypeCacheEntryBridge, kindHint?: string) => void, getCachedBBox: (geometry: THREE.BufferGeometry) => THREE.Box3|null|undefined, setCachedBBox: (geometry: THREE.BufferGeometry, bbox: THREE.Box3) => void, getAutoSwayResponse: (key: string) => FoliageAutoSwayResponseEntryBridge|null|undefined, setAutoSwayResponse: (key: string, value: FoliageAutoSwayResponseEntryBridge) => void, hasDebugKey: (key: string) => boolean, markDebugKey: (key: string) => void, resolveOrPick: (root: THREE.Object3D, wantedName: string, sharedKeyForCache: string, helpers: { isAlphaLikely: (mat: FoliageInspectableMaterialBridge|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterialBridge|null|undefined) => number }) => FoliageObjectTypeCacheEntryBridge, debugPrintFromRecords: (objName: string, records: FoliageMaterialRecordBridge[]) => void }} FoliageObjectTypeCacheRegistryBridge */",
+ " /** @typedef {{ get: (key: string) => FoliageObjectTypeCacheEntryBridge|undefined, set: (key: string, value: FoliageObjectTypeCacheEntryBridge, kindHint?: string) => void, collectGradientSources: (root: THREE.Object3D, selection: FoliageMaterialSelectionBridge) => FoliageGradientSourceBridge[], getRenderGeometry: (mesh: FoliageMeshLike, selection: FoliageMaterialSelectionBridge) => THREE.BufferGeometry, supportsInstancing: (root: THREE.Object3D, selection: FoliageMaterialSelectionBridge, swayType: string) => boolean, getCachedBBox: (geometry: THREE.BufferGeometry) => THREE.Box3|null|undefined, setCachedBBox: (geometry: THREE.BufferGeometry, bbox: THREE.Box3) => void, getAutoSwayResponse: (key: string) => FoliageAutoSwayResponseEntryBridge|null|undefined, setAutoSwayResponse: (key: string, value: FoliageAutoSwayResponseEntryBridge) => void, hasDebugKey: (key: string) => boolean, markDebugKey: (key: string) => void, resolveOrPick: (root: THREE.Object3D, wantedName: string, sharedKeyForCache: string, helpers: { isAlphaLikely: (mat: FoliageInspectableMaterialBridge|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterialBridge|null|undefined) => number }) => FoliageObjectTypeCacheEntryBridge, debugPrintFromRecords: (objName: string, records: FoliageMaterialRecordBridge[]) => void }} FoliageObjectTypeCacheRegistryBridge */",
" /** @typedef {THREE.Object3D & { isMesh?: boolean, geometry?: THREE.BufferGeometry|null, material?: THREE.Material|THREE.Material[]|null, castShadow?: boolean, receiveShadow?: boolean, id?: number, name?: string }} FoliageMeshLike */",
- " /** @typedef {{ _libReady?: boolean, getSceneState: (runtimeScene: gdjs.RuntimeScene) => FoliageSceneStateBridge, normalizeCullingMode: (v: FoliagePrimitiveValueBridge) => { name: string, code: number }, resolveRenderSide: (sourceSide: THREE.Side|undefined|null, cullingModeName: string) => THREE.Side, buildSideSuffix: (cullingModeCode: number, renderSide: THREE.Side|undefined|null) => string, buildPbrSuffix: (customPBREnabledValue: boolean, metallicValue: number, roughnessValue: number, specularValue: number, normalStrengthValue: number, ambientOcclusionStrengthValue: number, environmentStrengthValue: number) => string, buildGpuFastCacheKey: (localSharedKey: string, localMaterialName: string, localSwayType: string, localDistanceFadeEnabled: boolean, localPbrSuffix: string, localSideSuffix: string, localTwoSidedLighting: boolean, localSwayResponseAutoMode: boolean) => string, parseColor: (str: string, fallbackHex: string) => THREE.Color, isAlphaLikely: (mat: FoliageInspectableMaterialBridge|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterialBridge|null|undefined) => number, sanitizeKeyPart: (s: string) => string, meshMatchesSelection: (mesh: FoliageMeshLike, selection: FoliageMaterialSelectionBridge, srcRef: THREE.Material|null, srcName: string) => boolean, findFirstMatchingMesh: (root: THREE.Object3D, selection: FoliageMaterialSelectionBridge, srcRef: THREE.Material|null, srcName: string) => FoliageMeshLike|null, findSplitTreeMeshes: (root: THREE.Object3D, trunkMatcher: (mesh: FoliageMeshLike) => boolean, leavesMatcher: (mesh: FoliageMeshLike) => boolean) => { trunk: FoliageMeshLike|null, leaves: FoliageMeshLike|null } }} FoliageExtensionBridge */",
+ " /** @typedef {{ _libReady?: boolean, getSceneState: (runtimeScene: gdjs.RuntimeScene) => FoliageSceneStateBridge, normalizeCullingMode: (v: FoliagePrimitiveValueBridge) => { name: string, code: number }, resolveRenderSide: (sourceSide: THREE.Side|undefined|null, cullingModeName: string) => THREE.Side, buildSideSuffix: (cullingModeCode: number, renderSide: THREE.Side|undefined|null) => string, buildPbrSuffix: (customPBREnabledValue: boolean, metallicValue: number, roughnessValue: number, specularValue: number, normalStrengthValue: number, ambientOcclusionStrengthValue: number, environmentStrengthValue: number) => string, buildGpuFastCacheKey: (localSharedKey: string, localMaterialName: string, localSwayType: string, localDistanceFadeEnabled: boolean, localPbrSuffix: string, localSideSuffix: string, localTwoSidedLighting: boolean, localSwayResponseAutoMode: boolean) => string, parseColor: (str: string, fallbackHex: string) => THREE.Color, isAlphaLikely: (mat: FoliageInspectableMaterialBridge|null|undefined) => boolean, scoreMaterial: (mat: FoliageInspectableMaterialBridge|null|undefined) => number, sanitizeKeyPart: (s: string) => string, refreshGradientBounds: (sources: FoliageGradientSourceBridge[], bounds: { worldMin: number, worldMax: number }, inverseWorldScale: number) => boolean, meshMatchesSelection: (mesh: FoliageMeshLike, selection: FoliageMaterialSelectionBridge, srcRef: THREE.Material|null, srcName: string) => boolean, findFirstMatchingMesh: (root: THREE.Object3D, selection: FoliageMaterialSelectionBridge, srcRef: THREE.Material|null, srcName: string) => FoliageMeshLike|null, findSplitTreeMeshes: (root: THREE.Object3D, trunkMatcher: (mesh: FoliageMeshLike) => boolean, leavesMatcher: (mesh: FoliageMeshLike) => boolean) => { trunk: FoliageMeshLike|null, leaves: FoliageMeshLike|null } }} FoliageExtensionBridge */",
" /** @typedef {typeof gdjs & { _foliageSwaying3D?: FoliageExtensionBridge }} FoliageGdjsBridge */",
- " /** @typedef {{ _libReady?: boolean, instancingState: FoliageInstancingStateBridge, nonInstancedRegistry: FoliageNonInstancedRegistryBridge, cleanupForSceneChange: () => void, disposeShadowMaterials: (mat: THREE.Material|null|undefined) => void, ensureGustFallbackTex: () => THREE.Texture|null, getBehaviorProps: (behavior: FoliageBehaviorBridge|null|undefined) => FoliageBehaviorPropsBridge|null, gustContrast: number, gustEnabled: boolean, gustFallbackTex: THREE.Texture|null, gustScale: number, gustSpeed: number, gustStrength: number, gustTexture: THREE.Texture|null, gustThreshold: number, objectTypeCacheRegistry: FoliageObjectTypeCacheRegistryBridge, patchedMaterials: WeakSet, registerActiveMaterial: (mat: THREE.Material|null|undefined) => void, sharedByKey: Map, unregisterActiveMaterial: (mat: THREE.Material|null|undefined) => void, _defaultTopColor?: THREE.Color, _defaultBottomColor?: THREE.Color }} FoliageSceneStateBridge */",
+ " /** @typedef {{ _libReady?: boolean, instancingState: FoliageInstancingStateBridge, nonInstancedRegistry: FoliageNonInstancedRegistryBridge, cleanupForSceneChange: () => void, disposeShadowMaterials: (mat: THREE.Material|null|undefined) => void, ensureGustFallbackTex: () => THREE.Texture|null, getBehaviorProps: (behavior: FoliageBehaviorBridge|null|undefined) => FoliageBehaviorPropsBridge|null, gustContrast: number, gustEnabled: boolean, gustFallbackTex: THREE.Texture|null, gustScale: number, gustSpeed: number, gustStrength: number, gustTexture: THREE.Texture|null, gustThreshold: number, objectTypeCacheRegistry: FoliageObjectTypeCacheRegistryBridge, patchedMaterials: WeakSet, registerActiveMaterial: (mat: THREE.Material|null|undefined) => void, syncMaterialGustDefine: (mat: THREE.Material|null|undefined) => void, _frameCtx?: { time: number, windStrength: number, windSpeed: number, wx: number, wy: number }, sharedByKey: Map, unregisterActiveMaterial: (mat: THREE.Material|null|undefined) => void, _defaultTopColor?: THREE.Color, _defaultBottomColor?: THREE.Color }} FoliageSceneStateBridge */",
" var gdjsFoliage = /** @type {FoliageGdjsBridge} */ (gdjs);",
" var foliageExt = gdjsFoliage._foliageSwaying3D;",
" if (!foliageExt || !foliageExt._libReady) return;",
" /** @type {FoliageSceneStateBridge} */",
" var cache = foliageExt.getSceneState(runtimeScene);",
+ " /** @returns {number} */",
+ " function getFoliageWorldScale() {",
+ " return typeof runtimeScene.getRenderer3DInverseWorldScale === \"function\"",
+ " ? 1 / runtimeScene.getRenderer3DInverseWorldScale() : 1;",
+ " }",
" /** @type {FoliageNonInstancedRegistryBridge|null|undefined} */",
" var nonInstanced = cache.nonInstancedRegistry;",
" if (!nonInstanced) return;",
@@ -6328,7 +6745,7 @@
" threeObjFast.traverse(function(o) {",
" if (repMeshFast) return;",
" var meshFast = /** @type {FoliageMeshLike} */ (o);",
- " if (meshFast && meshFast.isMesh && meshFast.geometry === geoFast) {",
+ " if (meshFast && meshFast.isMesh && meshFast.geometry === fastCached._gpuSourceGeometry) {",
" repMeshFast = meshFast;",
" }",
" });",
@@ -6406,10 +6823,12 @@
"\t var splitFastMeshes = findSplitTreeMeshes(",
"\t threeObjFastLeaves,",
"\t function(mesh) {",
- "\t return mesh.geometry === geoFastTrunk;",
+ "\t return mesh.geometry === fastCachedLeaves._gpuTrunkSourceGeometry &&",
+ " meshMatchesSelection(mesh, { matchMode: \"name\", matchName: entryFastTrunk.material.name, srcRef: null, pickedId: \"\" }, null, entryFastTrunk.material.name);",
"\t },",
"\t function(mesh) {",
- "\t return mesh.geometry === geoFastLeaves;",
+ "\t return mesh.geometry === fastCachedLeaves._gpuLeavesSourceGeometry &&",
+ " meshMatchesSelection(mesh, { matchMode: \"name\", matchName: entryFastLeaves.material.name, srcRef: null, pickedId: \"\" }, null, entryFastLeaves.material.name);",
"\t }",
"\t );",
"\t var repMeshFastTrunk = splitFastMeshes.trunk;",
@@ -6559,13 +6978,21 @@
" }",
" ",
" var selection = rop.selection;",
+ " if (gpuInstancing && !objectTypeCache.supportsInstancing(threeObj, selection, swayType)) {",
+ " gpuInstancing = false;",
+ " var unsupportedKey = sharedKey + \"::UNSUPPORTED_INSTANCING_LAYOUT\";",
+ " if (debugOutput && !objectTypeCache.hasDebugKey(unsupportedKey)) {",
+ " objectTypeCache.markDebugKey(unsupportedKey);",
+ " console.warn(\"[FoliageSwaying3D] Keeping '\" + sharedKey + \"' non-instanced: the current layout supports one mesh/material, or one trunk and one leaves part.\");",
+ " }",
+ " }",
+ "",
+ "",
" ",
- " // 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 gradientLocalZMin = Infinity;",
- " var gradientLocalZMax = -Infinity;",
+ " // Keep selected-polygon bounds and mesh references; GDevelop applies scene",
+ " // transforms after onCreated, so the non-instanced registry refreshes world Z.",
+ " var gradientSources = objectTypeCache.collectGradientSources(threeObj, selection);",
+ " var gradientBounds = { localMin: Infinity, localMax: -Infinity, worldMin: Infinity, worldMax: -Infinity };",
" var hasGeometry = false;",
" ",
" // For automatic sway response (size + density). Prefer root-local bounds for stability.",
@@ -6597,17 +7024,6 @@
" ",
" if (st) {",
" hasGeometry = !!st.hasGeom;",
- " // World-gradient mode from the IgnoreUV property: 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) {",
- " gradientLocalZMin = st.relZMin;",
- " gradientLocalZMax = st.relZMax;",
- " } else if (!ignoreUV) {",
- " gradientLocalZMin = st.zMin;",
- " gradientLocalZMax = st.zMax;",
- " }",
" totalVerts = st.totalVerts || 0;",
" totalTris = st.totalTris || 0;",
" meshCountForScale = st.meshCount || 0;",
@@ -6619,35 +7035,48 @@
" if (st.sizeWorldMax) sizeWorldMax = st.sizeWorldMax;",
" }",
" ",
+ " var localSizeX = (sizeLocalMin && sizeLocalMax) ? sizeLocalMax.x - sizeLocalMin.x : NaN;",
+ " var localSizeY = (sizeLocalMin && sizeLocalMax) ? sizeLocalMax.y - sizeLocalMin.y : NaN;",
+ " var localSizeZ = (sizeLocalMin && sizeLocalMax) ? sizeLocalMax.z - sizeLocalMin.z : NaN;",
+ " // Inverting rotated/scaled matrices can give flat cards a tiny artificial thickness.",
+ " var localBoundsEpsilon = Math.max(localSizeX, localSizeY, localSizeZ) * 1e-8;",
" var useLocalBoundsForAuto =",
" !!sizeLocalMin && !!sizeLocalMax &&",
" isFinite(sizeLocalMin.x) && isFinite(sizeLocalMin.y) && isFinite(sizeLocalMin.z) &&",
" isFinite(sizeLocalMax.x) && isFinite(sizeLocalMax.y) && isFinite(sizeLocalMax.z) &&",
- " sizeLocalMax.x > sizeLocalMin.x &&",
- " sizeLocalMax.y > sizeLocalMin.y &&",
- " sizeLocalMax.z > sizeLocalMin.z;",
+ " localSizeX > localBoundsEpsilon &&",
+ " localSizeY > localBoundsEpsilon &&",
+ " localSizeZ > localBoundsEpsilon;",
" ",
" var autoBoundsMin = useLocalBoundsForAuto ? sizeLocalMin : sizeWorldMin;",
" var autoBoundsMax = useLocalBoundsForAuto ? sizeLocalMax : sizeWorldMax;",
- " ",
- " // Fallback if no geometry found or invalid bounds",
- " if (!hasGeometry || !isFinite(gradientLocalZMin) || !isFinite(gradientLocalZMax) || gradientLocalZMax <= gradientLocalZMin) {",
- " // For ignoreUV, try using world height (Z dimension) as fallback",
- " if (ignoreUV) {",
- " var fallbackHeight = (autoBoundsMin && autoBoundsMax) ? (autoBoundsMax.z - autoBoundsMin.z) : NaN;",
- " if (isFinite(fallbackHeight) && fallbackHeight > 0) {",
- " // Assume origin is at center",
- " gradientLocalZMin = -fallbackHeight * 0.5;",
- " gradientLocalZMax = fallbackHeight * 0.5;",
- " } else {",
- " gradientLocalZMin = -gradientHeight * 0.5;",
- " gradientLocalZMax = gradientHeight * 0.5;",
- " }",
- " } else {",
- " gradientLocalZMin = -gradientHeight * 0.5;",
- " gradientLocalZMax = gradientHeight * 0.5;",
- " }",
+ "",
+ " hasGeometry = gradientSources.length > 0;",
+ " for (var gradientSource of gradientSources) {",
+ " gradientBounds.localMin = Math.min(gradientBounds.localMin, gradientSource.bounds.min.z);",
+ " gradientBounds.localMax = Math.max(gradientBounds.localMax, gradientSource.bounds.max.z);",
+ " }",
+ " threeObj.updateWorldMatrix(true, true);",
+ " foliageExt.refreshGradientBounds(gradientSources, gradientBounds, 1 / getFoliageWorldScale());",
+ " if (gpuInstancing && gradientSources.length === 1) {",
+ " // Instanced shaders use their per-instance attribute and mesh-relative fallback.",
+ " var gradientOriginZ = gradientSources[0].mesh.matrixWorld.elements[14];",
+ " gradientBounds.worldMin -= gradientOriginZ;",
+ " gradientBounds.worldMax -= gradientOriginZ;",
" }",
+ "",
+ " if (!hasGeometry || !isFinite(gradientBounds.localMin) || !isFinite(gradientBounds.localMax) || gradientBounds.localMax <= gradientBounds.localMin) {",
+ " gradientBounds.localMin = -gradientHeight * 0.5;",
+ " gradientBounds.localMax = gradientHeight * 0.5;",
+ " }",
+ " if (!hasGeometry || !isFinite(gradientBounds.worldMin) || !isFinite(gradientBounds.worldMax) || gradientBounds.worldMax <= gradientBounds.worldMin) {",
+ " var fallbackHeight = (sizeWorldMin && sizeWorldMax) ? sizeWorldMax.z - sizeWorldMin.z : NaN;",
+ " if (!isFinite(fallbackHeight) || fallbackHeight <= 0) fallbackHeight = gradientHeight / getFoliageWorldScale();",
+ " gradientBounds.worldMin = -fallbackHeight * 0.5;",
+ " gradientBounds.worldMax = fallbackHeight * 0.5;",
+ " }",
+ " var gradientLocalZMin = ignoreUV ? gradientBounds.worldMin : gradientBounds.localMin;",
+ " var gradientLocalZMax = ignoreUV ? gradientBounds.worldMax : gradientBounds.localMax;",
" ",
" // Automatic sway response estimates bend response from bounds + mesh complexity.",
" // It exists so very different assets can share one behavior preset while still",
@@ -6665,6 +7094,13 @@
" var sizeX = (autoBoundsMin && autoBoundsMax) ? (autoBoundsMax.x - autoBoundsMin.x) : NaN;",
" var sizeY = (autoBoundsMin && autoBoundsMax) ? (autoBoundsMax.y - autoBoundsMin.y) : NaN;",
" var sizeZ = (autoBoundsMin && autoBoundsMax) ? (autoBoundsMax.z - autoBoundsMin.z) : NaN;",
+ " if (!useLocalBoundsForAuto) {",
+ " // Bounds from matrixWorld are in renderer units; the heuristic uses scene units.",
+ " var worldScaleForAuto = getFoliageWorldScale();",
+ " sizeX *= worldScaleForAuto;",
+ " sizeY *= worldScaleForAuto;",
+ " sizeZ *= worldScaleForAuto;",
+ " }",
" var maxSize = Math.max(sizeX, sizeY, sizeZ);",
" if (!isFinite(maxSize) || maxSize <= 0) maxSize = 1.0;",
" ",
@@ -6826,6 +7262,8 @@
" #endif",
" ",
" vec3 worldPos = (m * vec4(transformed, 1.0)).xyz;",
+ " // Wind uses scene distances while the gradient stays in renderer units.",
+ " vec3 swayWorldPos = worldPos * uFoliageWorldScale;",
" // Use the XY wind-basis uniforms so the common sway path avoids inverse(m).",
" ",
" // UV-based gradient path.",
@@ -6842,10 +7280,22 @@
" // 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)",
- " // uGradientLocalZMin/Max contain: for world-gradient mode - relative Z range, for UV mode - local Z range.",
- " float h = (uUseWorldGradient > 0.5) ? relativeWorldZ : position.z;",
- " float localN = (h - uGradientLocalZMin) / max(uGradientLocalZMax - uGradientLocalZMin, 0.0001);",
+ " // Plain meshes share absolute canopy bounds, even with different local origins.",
+ " // Instances use mesh-relative bounds; UV mode keeps its existing local Z path.",
+ " #ifdef USE_INSTANCING",
+ " float gradientWorldZ = relativeWorldZ;",
+ " #else",
+ " float gradientWorldZ = worldPos.z;",
+ " #endif",
+ " float h = (uUseWorldGradient > 0.5) ? gradientWorldZ : position.z;",
+ " vec2 gradientRange = vec2(uGradientLocalZMin, uGradientLocalZMax);",
+ " #ifdef USE_INSTANCING",
+ " // Per-instance bounds are stored in scene units; precompile meshes can omit them.",
+ " if (uUseWorldGradient > 0.5 && aFoliageGradientBounds.y > aFoliageGradientBounds.x) {",
+ " gradientRange = aFoliageGradientBounds / uFoliageWorldScale;",
+ " }",
+ " #endif",
+ " float localN = (h - gradientRange.x) / max(gradientRange.y - gradientRange.x, 0.0001);",
" localN = clamp(localN, 0.0, 1.0);",
" float gRawW = (uUseWorldGradient > 0.5) ? localN : (1.0 - localN);",
" ",
@@ -6861,7 +7311,7 @@
" // 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;",
+ " float phase = swayWorldPos.x * 0.18 + swayWorldPos.y * 0.18;",
" `;",
" ",
" var nodePhaseBlock = `",
@@ -6869,7 +7319,7 @@
" `;",
" if (!cfg.uniformSway) {",
" nodePhaseBlock = `",
- " vec3 t = vec3(m[3].x, m[3].y, m[3].z);",
+ " vec3 t = m[3].xyz * uFoliageWorldScale;",
" float nodeSeed = fract(sin(dot(t, vec3(12.9898, 78.233, 37.719))) * 43758.5453);",
" float nodePhase = nodeSeed * 6.2831853;",
" `;",
@@ -6893,7 +7343,7 @@
" float gustAmp = 0.0;",
" ",
" if (uWindGustEnabled > 0.5 && uWindGustStrength > 0.0) {",
- " vec2 p = worldPos.xy;",
+ " vec2 p = swayWorldPos.xy;",
" ",
" // Gust wave travels along wind direction",
" // gustSamplePos = distance along wind direction (U coordinate)",
@@ -6966,7 +7416,7 @@
" // Transform only the extra gust push from world wind into instance space.",
" #ifdef USE_GUST",
" // Reuse the base wind inverse so gust direction matches the same object basis.",
- " vec3 worldGustMove = vec3(dir.x * gustPush, dir.y * gustPush, 0.0);",
+ " vec3 worldGustMove = vec3(dir.x * gustPush, dir.y * gustPush, 0.0) / uFoliageWorldScale;",
" vec3 localGustMove = (baseInvM * vec4(worldGustMove, 0.0)).xyz;",
" transformed += localGustMove;",
" #endif",
@@ -6991,7 +7441,7 @@
" var leavesFlutter = `",
" if (uLeavesFlutterStrength > 0.0) {",
" float leafMask = pow(vGrad, 2.0);",
- " float fPhase = (worldPos.x + worldPos.y) * 0.35;",
+ " float fPhase = (swayWorldPos.x + swayWorldPos.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(uFoliageTime * flutterSpeed + fPhase + uSwayPhase * 1.7 + nodePhase);",
@@ -7033,7 +7483,7 @@
" ",
" // Use only world-space phase for tree trunk (no uSwayPhase/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 trunkPhase = swayWorldPos.x * 0.18 + swayWorldPos.y * 0.18;",
" ",
" float t1 = sin(uFoliageTime * (uWindSpeed * 0.40) + trunkPhase * 0.30);",
" float t2 = sin(uFoliageTime * (uWindSpeed * 0.60) + trunkPhase * 0.45) * 0.30;",
@@ -7044,7 +7494,7 @@
" float twist = sin(uFoliageTime * (uWindSpeed * 0.50) + trunkPhase * 0.25) * (uWindStrength * 0.18) * h2 * uSwayResponse * 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);",
+ " vec3 worldGustTrunkPush = vec3(dir.x * gustPush * 0.3, dir.y * gustPush * 0.3, 0.0) / uFoliageWorldScale;",
" mat4 invM = inverse(m);",
" vec3 localGustTrunkPush = (invM * vec4(worldGustTrunkPush, 0.0)).xyz;",
" // Apply the XY wind basis directly in vertex space.",
@@ -7178,21 +7628,35 @@
" #endif",
" ",
" vec3 worldPos = (m * vec4(transformed, 1.0)).xyz;",
+ " // Wind uses scene distances while the gradient stays in renderer units.",
+ " vec3 swayWorldPos = worldPos * uFoliageWorldScale;",
" ",
" 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 = (uUseWorldGradient > 0.5) ? relativeWorldZ : position.z;",
- " float localN = (h - uGradientLocalZMin) / max(uGradientLocalZMax - uGradientLocalZMin, 0.0001);",
+ " #ifdef USE_INSTANCING",
+ " float gradientWorldZ = relativeWorldZ;",
+ " #else",
+ " float gradientWorldZ = worldPos.z;",
+ " #endif",
+ " float h = (uUseWorldGradient > 0.5) ? gradientWorldZ : position.z;",
+ " vec2 gradientRange = vec2(uGradientLocalZMin, uGradientLocalZMax);",
+ " #ifdef USE_INSTANCING",
+ " // Per-instance bounds are stored in scene units; precompile meshes can omit them.",
+ " if (uUseWorldGradient > 0.5 && aFoliageGradientBounds.y > aFoliageGradientBounds.x) {",
+ " gradientRange = aFoliageGradientBounds / uFoliageWorldScale;",
+ " }",
+ " #endif",
+ " float localN = (h - gradientRange.x) / max(gradientRange.y - gradientRange.x, 0.0001);",
" localN = clamp(localN, 0.0, 1.0);",
" float gRawW = (uUseWorldGradient > 0.5) ? localN : (1.0 - localN);",
" float gRaw = (uUseWorldGradient > 0.5) ? gRawW : gRawUV;",
" float swayGrad = smoothstep(uGradientStart, uGradientEnd, gRaw);",
" float tip = pow(gRawUV, 3.0);",
" ",
- " float phase = worldPos.x * 0.18 + worldPos.y * 0.18;",
+ " float phase = swayWorldPos.x * 0.18 + swayWorldPos.y * 0.18;",
" `;",
" ",
" var nodePhaseBlock = `",
@@ -7200,7 +7664,7 @@
" `;",
" if (!cfg.uniformSway) {",
" nodePhaseBlock = `",
- " vec3 t = vec3(m[3].x, m[3].y, m[3].z);",
+ " vec3 t = m[3].xyz * uFoliageWorldScale;",
" float nodeSeed = fract(sin(dot(t, vec3(12.9898, 78.233, 37.719))) * 43758.5453);",
" float nodePhase = nodeSeed * 6.2831853;",
" `;",
@@ -7221,7 +7685,7 @@
" float gustAmp = 0.0;",
" ",
" if (uWindGustEnabled > 0.5 && uWindGustStrength > 0.0) {",
- " vec2 p = worldPos.xy;",
+ " vec2 p = swayWorldPos.xy;",
" float gustSamplePos = dot(p, dir);",
" float wavePhase = gustSamplePos * uWindGustScale - uFoliageTime * uWindGustSpeed;",
" float gustPerpPos = dot(p, perp);",
@@ -7272,7 +7736,7 @@
" transformed += localMove;",
" ",
" #ifdef USE_GUST",
- " vec3 worldGustMove = vec3(dir.x * gustPush, dir.y * gustPush, 0.0);",
+ " vec3 worldGustMove = vec3(dir.x * gustPush, dir.y * gustPush, 0.0) / uFoliageWorldScale;",
" vec3 localGustMove = (baseInvM * vec4(worldGustMove, 0.0)).xyz;",
" transformed += localGustMove;",
" #endif",
@@ -7290,7 +7754,7 @@
" var leavesFlutter = `",
" if (uLeavesFlutterStrength > 0.0) {",
" float leafMask = pow(swayGrad, 2.0);",
- " float fPhase = (worldPos.x + worldPos.y) * 0.35;",
+ " float fPhase = (swayWorldPos.x + swayWorldPos.y) * 0.35;",
" float flutterSpeed = uWindSpeed * 4.0;",
" float f1 = sin(uFoliageTime * flutterSpeed + fPhase + uSwayPhase * 1.7 + nodePhase);",
" float f2 = sin(uFoliageTime * (flutterSpeed * 1.9) + fPhase * 2.3 + uSwayPhase * 0.9 + nodePhase);",
@@ -7322,13 +7786,13 @@
" ",
" var trunkBlock = `",
" float h2 = tip;",
- " float trunkPhase = worldPos.x * 0.18 + worldPos.y * 0.18;",
+ " float trunkPhase = swayWorldPos.x * 0.18 + swayWorldPos.y * 0.18;",
" float t1 = sin(uFoliageTime * (uWindSpeed * 0.40) + trunkPhase * 0.30);",
" float t2 = sin(uFoliageTime * (uWindSpeed * 0.60) + trunkPhase * 0.45) * 0.30;",
" #ifdef USE_GUST",
" float bend = (t1 + t2) * (uWindStrength * 0.40) * h2 * uSwayResponse * gustMul;",
" float twist = sin(uFoliageTime * (uWindSpeed * 0.50) + trunkPhase * 0.25) * (uWindStrength * 0.18) * h2 * uSwayResponse * gustMul;",
- " vec3 worldGustTrunkPush = vec3(dir.x * gustPush * 0.3, dir.y * gustPush * 0.3, 0.0);",
+ " vec3 worldGustTrunkPush = vec3(dir.x * gustPush * 0.3, dir.y * gustPush * 0.3, 0.0) / uFoliageWorldScale;",
" 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);",
@@ -7364,6 +7828,7 @@
" if (!shadowMat) return;",
" shadowMat.userData = shadowMat.userData || {};",
" shadowMat.userData.foliageConfig = cfgSource;",
+ " cache.syncMaterialGustDefine(shadowMat);",
" if (shadowMat.userData.__foliageShadowPatched) return;",
" ",
" shadowMat.userData.__foliageShadowPatched = true;",
@@ -7378,19 +7843,21 @@
" return;",
" }",
" var useShadowFade = !!cfgShadow.distanceFadeEnabled;",
- " if (!shader.defines) shader.defines = {};",
- " if (cache.gustEnabled) shader.defines.USE_GUST = \"\";",
- " else delete shader.defines.USE_GUST;",
"",
" var pendingShadowFade = (this.userData && typeof this.userData._pendingFade === \"number\")",
" ? this.userData._pendingFade",
" : 1.0;",
- " shader.uniforms.uFoliageTime = { value: 0.0 };",
- " shader.uniforms.uWindStrength = { value: 0.5 };",
- " shader.uniforms.uWindSpeed = { value: 1.0 };",
- " shader.uniforms.uWindDirection = { value: new THREE.Vector2(1, 0) };",
- " shader.uniforms.uLocalWindDirection = { value: new THREE.Vector2(1, 0) };",
- " shader.uniforms.uLocalWindPerpendicular = { value: new THREE.Vector2(0, 1) };",
+ " // A recompiled variant must start with the current frame, not reset wind values.",
+ " var windFrame = cache._frameCtx;",
+ " var windX = windFrame ? windFrame.wx : 1;",
+ " var windY = windFrame ? windFrame.wy : 0;",
+ " shader.uniforms.uFoliageTime = { value: windFrame ? windFrame.time : 0.0 };",
+ " shader.uniforms.uFoliageWorldScale = { value: getFoliageWorldScale() };",
+ " shader.uniforms.uWindStrength = { value: windFrame ? windFrame.windStrength : 0.5 };",
+ " shader.uniforms.uWindSpeed = { value: windFrame ? windFrame.windSpeed : 1.0 };",
+ " shader.uniforms.uWindDirection = { value: new THREE.Vector2(windX, windY) };",
+ " shader.uniforms.uLocalWindDirection = { value: new THREE.Vector2(windX, windY) };",
+ " shader.uniforms.uLocalWindPerpendicular = { value: new THREE.Vector2(-windY, windX) };",
" shader.uniforms.uSwayPhase = { value: cfgShadow.phase || 0.0 };",
" shader.uniforms.uSwayResponse = { value: cfgShadow.swayResponse || 1.0 };",
" shader.uniforms.uLeavesFlutterStrength = { value: cfgShadow.swayType === \"Leaves\" && cfgShadow.swayResponse >= 0.6 ? 0.4 * cfgShadow.swayResponse : 0.0 };",
@@ -7408,17 +7875,18 @@
" shader.uniforms.uFadeInterpolation = { value: 1.0 };",
" }",
"",
- " if (cache.gustEnabled) {",
- " shader.uniforms.uWindGustTexture = { value: cache.gustTexture || cache.gustFallbackTex };",
- " shader.uniforms.uWindGustEnabled = { value: 1.0 };",
- " shader.uniforms.uWindGustStrength = { value: cache.gustStrength };",
- " shader.uniforms.uWindGustScale = { value: cache.gustScale };",
- " shader.uniforms.uWindGustSpeed = { value: cache.gustSpeed };",
- " shader.uniforms.uWindGustThreshold = { value: cache.gustThreshold };",
- " shader.uniforms.uWindGustContrast = { value: cache.gustContrast };",
- " }",
+ " // Cached plain/instanced programs can be reused without onBeforeCompile.",
+ " // Keep gust uniforms available even when USE_GUST strips the shader branch.",
+ " shader.uniforms.uWindGustTexture = { value: cache.gustTexture || cache.gustFallbackTex };",
+ " shader.uniforms.uWindGustEnabled = { value: cache.gustEnabled ? 1.0 : 0.0 };",
+ " shader.uniforms.uWindGustStrength = { value: cache.gustEnabled ? cache.gustStrength : 0.0 };",
+ " shader.uniforms.uWindGustScale = { value: cache.gustScale };",
+ " shader.uniforms.uWindGustSpeed = { value: cache.gustSpeed };",
+ " shader.uniforms.uWindGustThreshold = { value: cache.gustThreshold };",
+ " shader.uniforms.uWindGustContrast = { value: cache.gustContrast };",
" ",
" this.userData.foliageUniforms = shader.uniforms;",
+ " delete this.userData.__gustVersionApplied;",
" ",
" var shadowFadeVertexDecl = useShadowFade ? `",
" #ifdef USE_INSTANCING",
@@ -7431,7 +7899,11 @@
" ` : ``;",
" var commonShadowVertex = `",
" #include ",
+ " #ifdef USE_INSTANCING",
+ " attribute vec2 aFoliageGradientBounds;",
+ " #endif",
" uniform float uFoliageTime;",
+ " uniform float uFoliageWorldScale;",
" uniform float uWindStrength;",
" uniform float uWindSpeed;",
" uniform vec2 uWindDirection;",
@@ -7527,6 +7999,8 @@
" function computeInitialNonInstancedFade(gdObject, threeObject, startDistance, endDistance) {",
" if (!runtimeScene || !distanceFadeEnabled) return 1.0;",
"",
+ " var inverseWorldScaleInit = typeof runtimeScene.getRenderer3DInverseWorldScale === \"function\"",
+ " ? runtimeScene.getRenderer3DInverseWorldScale() : 1;",
" var camXInit = 0;",
" var camYInit = 0;",
" var hasCamera = false;",
@@ -7559,6 +8033,8 @@
" try {",
" objXInit = gdObject.getX ? gdObject.getX() : 0;",
" objYInit = gdObject.getY ? gdObject.getY() : 0;",
+ " objXInit *= inverseWorldScaleInit;",
+ " objYInit *= -inverseWorldScaleInit;",
" gotPos = true;",
" } catch (eInitPos2D) {}",
" }",
@@ -7567,19 +8043,21 @@
" var safeStart = isFinite(startDistance) ? startDistance : 1200;",
" var safeEnd = isFinite(endDistance) ? endDistance : 1600;",
" if (safeEnd <= safeStart) safeEnd = safeStart + 100;",
+ " safeStart *= inverseWorldScaleInit;",
+ " safeEnd *= inverseWorldScaleInit;",
"",
" var dxInit = objXInit - camXInit;",
" var dyInit = objYInit - camYInit;",
" var distSqInit = dxInit * dxInit + dyInit * dyInit;",
" var startSqInit = safeStart * safeStart;",
- " var marginInit = 256.0;",
+ " var marginInit = 256.0 * inverseWorldScaleInit;",
" var endWithMarginInit = safeEnd + marginInit;",
" var endWithMarginSqInit = endWithMarginInit * endWithMarginInit;",
" if (distSqInit > endWithMarginSqInit) return 0.0;",
" if (distSqInit < startSqInit) return 1.0;",
"",
" var rangeInit = safeEnd - safeStart;",
- " if (rangeInit <= 0) rangeInit = 100;",
+ " if (rangeInit <= 0) rangeInit = 100 * inverseWorldScaleInit;",
" var distInit = Math.sqrt(distSqInit);",
" var tInit = (distInit - safeStart) / rangeInit;",
" if (tInit < 0) tInit = 0;",
@@ -7788,6 +8266,7 @@
" * @returns {void}",
" */",
" function patchMaterialIfNeeded(mat) {",
+ " cache.syncMaterialGustDefine(mat);",
" if (cache.patchedMaterials.has(mat)) return;",
" cache.patchedMaterials.add(mat);",
" ",
@@ -7811,24 +8290,19 @@
" 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.uFoliageTime = { value: 0.0 };",
- " shader.uniforms.uWindStrength = { value: 0.5 };",
- " shader.uniforms.uWindSpeed = { value: 1.0 };",
- " shader.uniforms.uWindDirection = { value: new THREE.Vector2(1, 0) };",
+ " // A recompiled variant must start with the current frame, not reset wind values.",
+ " var windFrame = cache._frameCtx;",
+ " var windX = windFrame ? windFrame.wx : 1;",
+ " var windY = windFrame ? windFrame.wy : 0;",
+ " shader.uniforms.uFoliageTime = { value: windFrame ? windFrame.time : 0.0 };",
+ " shader.uniforms.uFoliageWorldScale = { value: getFoliageWorldScale() };",
+ " shader.uniforms.uWindStrength = { value: windFrame ? windFrame.windStrength : 0.5 };",
+ " shader.uniforms.uWindSpeed = { value: windFrame ? windFrame.windSpeed : 1.0 };",
+ " shader.uniforms.uWindDirection = { value: new THREE.Vector2(windX, windY) };",
" // Keep a secondary XY wind basis uniform so the common sway path avoids",
" // per-vertex inverse(m). It currently mirrors the world XY wind direction.",
- " var localWindDir = new THREE.Vector2(1, 0);",
- " var localWindPerp = new THREE.Vector2(0, 1);",
+ " var localWindDir = new THREE.Vector2(windX, windY);",
+ " var localWindPerp = new THREE.Vector2(-windY, windX);",
" shader.uniforms.uLocalWindDirection = { value: localWindDir };",
" shader.uniforms.uLocalWindPerpendicular = { value: localWindPerp };",
" shader.uniforms.uSwayPhase = { value: cfg2.phase };",
@@ -7863,25 +8337,29 @@
" var flutterAllowed = cfg2.swayType === \"Leaves\" && cfg2.swayResponse >= 0.6;",
" shader.uniforms.uLeavesFlutterStrength = { value: flutterAllowed ? 0.4 * cfg2.swayResponse : 0.0 };",
" ",
- " // Set gust uniforms only when gust is enabled.",
- " if (cache.gustEnabled) {",
- " shader.uniforms.uWindGustTexture = { value: gustTex };",
- " shader.uniforms.uWindGustEnabled = { value: 1.0 };",
- " shader.uniforms.uWindGustStrength = { value: cache.gustStrength };",
- " shader.uniforms.uWindGustScale = { value: cache.gustScale };",
- " shader.uniforms.uWindGustSpeed = { value: cache.gustSpeed };",
- " shader.uniforms.uWindGustThreshold = { value: cache.gustThreshold };",
- " shader.uniforms.uWindGustContrast = { value: cache.gustContrast };",
- " }",
+ " // Cached plain/instanced programs can be reused without onBeforeCompile.",
+ " // Keep gust uniforms available even when USE_GUST strips the shader branch.",
+ " shader.uniforms.uWindGustTexture = { value: gustTex };",
+ " shader.uniforms.uWindGustEnabled = { value: cache.gustEnabled ? 1.0 : 0.0 };",
+ " shader.uniforms.uWindGustStrength = { value: cache.gustEnabled ? cache.gustStrength : 0.0 };",
+ " shader.uniforms.uWindGustScale = { value: cache.gustScale };",
+ " shader.uniforms.uWindGustSpeed = { value: cache.gustSpeed };",
+ " shader.uniforms.uWindGustThreshold = { value: cache.gustThreshold };",
+ " shader.uniforms.uWindGustContrast = { value: cache.gustContrast };",
" ",
" // GPU fade smoothing: interpolation factor updated every frame",
" shader.uniforms.uFadeInterpolation = { value: 1.0 };",
" ",
" this.userData.foliageUniforms = shader.uniforms;",
+ " delete this.userData._lastUniforms;",
+ " delete this.userData.__gustVersionApplied;",
+ " // Reclassify variants compiled after gusts are enabled or disabled.",
+ " cache.registerActiveMaterial(this);",
" ",
" var commonVertex = `",
" #include ",
" uniform float uFoliageTime;",
+ " uniform float uFoliageWorldScale;",
" uniform float uWindStrength;",
" uniform float uWindSpeed;",
" uniform vec2 uWindDirection;",
@@ -7918,6 +8396,7 @@
" #ifdef USE_INSTANCING",
" attribute float aFade;",
" attribute float aFadePrev;",
+ " attribute vec2 aFoliageGradientBounds;",
" #endif",
" uniform float uFadeInterpolation;",
" varying float vFade;",
@@ -8066,6 +8545,7 @@
" // World-gradient config.",
" ignoreUV: !!ignoreUV,",
" gradientHeight: gradientHeight,",
+ " gradientBounds: gradientBounds,",
" gradientLocalZMin: gradientLocalZMin,",
" gradientLocalZMax: gradientLocalZMax,",
" receiveShadow: false",
@@ -8117,6 +8597,7 @@
" // Keep world-gradient config in sync for shared material.",
" cfg3.ignoreUV = !!ignoreUV;",
" cfg3.gradientHeight = gradientHeight;",
+ " cfg3.gradientBounds = gradientBounds;",
" cfg3.gradientLocalZMin = gradientLocalZMin;",
" cfg3.gradientLocalZMax = gradientLocalZMax;",
" applyCustomPBRIfSupported(entry.material, cfg3.customPBREnabled, cfg3.metallic, cfg3.roughness, cfg3.specular, cfg3.normalStrength, cfg3.ambientOcclusionStrength, cfg3.environmentStrength);",
@@ -8286,6 +8767,8 @@
"\t }",
" }",
" if (useTwoPartTree) {",
+ " var trunkGeometry = objectTypeCache.getRenderGeometry(repMeshTrunk, selectionTrunk);",
+ " var leavesGeometry = objectTypeCache.getRenderGeometry(repMeshLeaves, selection);",
" // Global instancing: geoKey stays a stable asset+mesh identity without runtime-instance suffixes.",
" var trunkPickedId = selectionTrunk && selectionTrunk.pickedId ? String(selectionTrunk.pickedId) : \"\";",
" var leavesPickedId = selection && selection.pickedId ? String(selection.pickedId) : \"\";",
@@ -8301,8 +8784,10 @@
"\t /** @type {FoliageObjectTypeCacheEntryBridge} */",
"\t var splitFastCacheEntry = objectTypeCache.get(fastCacheKeySplit) || {};",
"\t splitFastCacheEntry._gpuFastMode = \"splitLeavesTree\";",
- "\t splitFastCacheEntry._gpuTrunkGeometry = repMeshTrunk.geometry;",
- "\t splitFastCacheEntry._gpuLeavesGeometry = repMeshLeaves.geometry;",
+ "\t splitFastCacheEntry._gpuTrunkGeometry = trunkGeometry;",
+ " splitFastCacheEntry._gpuTrunkSourceGeometry = repMeshTrunk.geometry;",
+ "\t splitFastCacheEntry._gpuLeavesGeometry = leavesGeometry;",
+ " splitFastCacheEntry._gpuLeavesSourceGeometry = repMeshLeaves.geometry;",
"\t splitFastCacheEntry._gpuTrunkBaseGroupKey = baseGroupKeyTrunk;",
"\t splitFastCacheEntry._gpuLeavesBaseGroupKey = baseGroupKeyLeaves;",
"\t splitFastCacheEntry._gpuTrunkSharedMaterialKey = keyTrunk;",
@@ -8322,8 +8807,8 @@
" behaviorName: \"FoliageSwaying\",",
" queueId: queueIdTree,",
" parts: [",
- " { repMesh: repMeshTrunk, geometry: repMeshTrunk.geometry, material: sharedMatTrunk, baseGroupKey: baseGroupKeyTrunk },",
- " { repMesh: repMeshLeaves, geometry: repMeshLeaves.geometry, material: sharedMat, baseGroupKey: baseGroupKeyLeaves }",
+ " { repMesh: repMeshTrunk, geometry: trunkGeometry, material: sharedMatTrunk, baseGroupKey: baseGroupKeyTrunk },",
+ " { repMesh: repMeshLeaves, geometry: leavesGeometry, material: sharedMat, baseGroupKey: baseGroupKeyLeaves }",
" ]",
" });",
" behavior.__foliageQueued = true;",
@@ -8374,7 +8859,7 @@
" // Fallback to non-instanced behavior if we cannot resolve geometry",
" gpuInstancing = false;",
" } else {",
- " if (!geo) geo = repMesh.geometry;",
+ " geo = objectTypeCache.getRenderGeometry(repMesh, selection);",
" {",
" // Global instancing: geoKey stays a stable asset+mesh identity without runtime-instance suffixes.",
" var materialPickedId = selection && selection.pickedId ? String(selection.pickedId) : \"\";",
@@ -8401,6 +8886,7 @@
"\t _gpuFastMode: \"single\",",
"\t _gpuGroupKey: geoGroupKey,",
"\t _gpuGeometry: geo,",
+ " _gpuSourceGeometry: repMesh.geometry,",
"\t _sharedMaterialKey: key,",
" _resolvedSide: resolvedRenderSide",
" });",
@@ -8448,15 +8934,21 @@
" var trunkMatFound = null;",
" /** @type {FoliageMeshLike[]} */",
" var trunkMeshes = [];",
- " if (useNonInstancedFade) {",
+ " // Plain objects need independent gradient configs even when distance fade is off.",
+ " {",
" var fadeMat = sharedMat.clone();",
- " fadeMat.userData = { foliageConfig: sharedMat.userData.foliageConfig };",
+ " fadeMat.userData = { foliageConfig: Object.assign({}, sharedMat.userData.foliageConfig, {",
+ " gradientBounds: gradientBounds,",
+ " topColor: topColor.clone(),",
+ " bottomColor: bottomColor.clone()",
+ " }) };",
" if (typeof sharedMat.side === \"number\") fadeMat.side = sharedMat.side;",
" applyPendingFadeToMaterial(fadeMat, initialNonInstancedFade);",
" // Explicitly chain sharedMat's onBeforeCompile (full patch) then add uDistanceFade; clone() may not copy onBeforeCompile.",
" var sharedOnBeforeCompile = sharedMat.onBeforeCompile;",
" fadeMat.onBeforeCompile = function (shader) {",
" if (sharedOnBeforeCompile) sharedOnBeforeCompile.call(fadeMat, shader);",
+ " if (!fadeMat.userData.foliageConfig.distanceFadeEnabled) return;",
" var pendingFade = (fadeMat.userData && typeof fadeMat.userData._pendingFade === \"number\") ? fadeMat.userData._pendingFade : 1.0;",
" shader.uniforms.uDistanceFade = { value: pendingFade };",
" var hasUFadeUniform = /uniform\\s+float\\s+uDistanceFade\\s*;/.test(shader.vertexShader);",
@@ -8545,7 +9037,7 @@
" ",
" mesh.material = newMats2;",
" });",
- " if (useNonInstancedFade) {",
+ " {",
" // Leaves two-part: trunk must fade with leaves (same uDistanceFade).",
" if (useSplitLeavesFade && trunkMatFound) {",
" /** @type {THREE.Material} */",
@@ -8600,6 +9092,7 @@
" fadeEnd: fadeEnd,",
" fadeEnabled: !!distanceFadeEnabled,",
" fadeBehavior: behavior || null,",
+ " gradientSources: gradientSources,",
" _parkedNoFade: false,",
" _firstHideWarmupDone: false",
" });",