6060import java .io .IOException ;
6161import java .nio .Buffer ;
6262import java .nio .FloatBuffer ;
63+ import java .util .HashMap ;
64+ import java .util .Map ;
6365import java .util .logging .Level ;
6466import java .util .logging .Logger ;
6567
@@ -140,6 +142,22 @@ public class SkinningControl extends AbstractControl implements JmeCloneable {
140142 */
141143 private boolean updateBounds = false ;
142144
145+ /**
146+ * Maximum number of vertices processed per frame when updating bounding
147+ * volumes during hardware skinning. Processing is spread across multiple
148+ * frames (the cursor resumes where it left off), keeping each frame's cost
149+ * bounded. {@link Integer#MAX_VALUE} (the default) means all vertices are
150+ * processed in one frame, matching the old behaviour.
151+ */
152+ private int boundingUpdateBudget = Integer .MAX_VALUE ;
153+
154+ /**
155+ * Per-geometry state for incremental bounding-volume updates. Only used
156+ * when {@link #boundingUpdateBudget} is smaller than the vertex count of
157+ * the mesh, allowing the work to be spread over several frames.
158+ */
159+ private transient Map <Geometry , BoundsUpdateState > boundsUpdateStates = new HashMap <>();
160+
143161 private MatParamOverride numberOfJointsParam = new MatParamOverride (VarType .Int , "NumberOfBones" , null );
144162 private MatParamOverride jointMatricesParam = new MatParamOverride (VarType .Matrix4Array , "BoneMatrices" , null );
145163
@@ -281,6 +299,43 @@ public boolean isUpdateBounds() {
281299 return updateBounds ;
282300 }
283301
302+ /**
303+ * Sets the maximum number of vertices considered per frame when updating
304+ * bounding volumes during hardware skinning. Use this to distribute the
305+ * CPU cost of the bounds update across several frames instead of paying the
306+ * full price in a single frame.
307+ *
308+ * <p>The update cursor advances by at most {@code budget} vertices each
309+ * frame and resumes where it left off the next frame. When a full pass over
310+ * all vertices is complete, the geometry's model bound is refreshed with
311+ * the newly computed values. In the meantime, the geometry keeps the bound
312+ * from the previous completed pass.
313+ *
314+ * <p>A value of {@link Integer#MAX_VALUE} (the default) processes all
315+ * vertices in one frame, matching the original behaviour. Values ≤ 0
316+ * are treated as {@link Integer#MAX_VALUE}.
317+ *
318+ * @param budget max vertices per frame (any positive integer; values
319+ * ≤ 0 are normalized to {@link Integer#MAX_VALUE})
320+ * @see #getBoundingUpdateBudget()
321+ * @see #setUpdateBounds(boolean)
322+ */
323+ public void setBoundingUpdateBudget (int budget ) {
324+ this .boundingUpdateBudget = (budget <= 0 ) ? Integer .MAX_VALUE : budget ;
325+ boundsUpdateStates .clear ();
326+ }
327+
328+ /**
329+ * Returns the maximum number of vertices processed per frame when updating
330+ * bounding volumes during hardware skinning.
331+ *
332+ * @return the bounding-update vertex budget
333+ * @see #setBoundingUpdateBudget(int)
334+ */
335+ public int getBoundingUpdateBudget () {
336+ return boundingUpdateBudget ;
337+ }
338+
284339 /**
285340 * Recursively finds and adds animated geometries to the targets list.
286341 *
@@ -803,17 +858,23 @@ private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexB
803858 }
804859
805860 /**
806- * Computes the bounding volume of an animated mesh from the bind pose
807- * positions and the current skinning matrices, then sets it on the geometry.
808- * This is used during hardware skinning to keep culling correct, since the
809- * GPU-transformed vertex positions are not reflected in the CPU-side vertex
810- * buffer.
861+ * Computes (or incrementally advances) the bounding volume of an animated
862+ * mesh from the bind-pose positions and the current skinning matrices, then
863+ * sets it on the geometry once a full pass is complete.
864+ *
865+ * <p>When {@link #boundingUpdateBudget} is smaller than the vertex count,
866+ * at most {@code boundingUpdateBudget} vertices are processed each call.
867+ * The per-geometry {@link BoundsUpdateState} records the cursor and the
868+ * in-progress min/max so subsequent calls resume from where they left off.
869+ * The geometry's bound is only updated after all vertices have been visited
870+ * in a single pass; in the meantime it keeps the bound from the last
871+ * completed pass.
811872 *
812873 * @param geometry the geometry whose bound needs to be updated
813874 * @param mesh the animated mesh
814875 * @param offsetMatrices the bone offset matrices for this frame
815876 */
816- private static void updateSkinnedMeshBound (Geometry geometry , Mesh mesh ,
877+ private void updateSkinnedMeshBound (Geometry geometry , Mesh mesh ,
817878 Matrix4f [] offsetMatrices ) {
818879 VertexBuffer bindPosVB = mesh .getBuffer (Type .BindPosePosition );
819880 if (bindPosVB == null ) {
@@ -831,27 +892,56 @@ private static void updateSkinnedMeshBound(Geometry geometry, Mesh mesh,
831892 int fourMinusMaxWeights = 4 - maxWeightsPerVert ;
832893
833894 FloatBuffer bindPos = (FloatBuffer ) bindPosVB .getData ();
834- bindPos .rewind ();
835895 IndexBuffer boneIndex = IndexBuffer .wrapIndexBuffer (boneIndexVB .getData ());
836896 FloatBuffer boneWeightBuf = (FloatBuffer ) boneWeightVB .getData ();
837- boneWeightBuf .rewind ();
838897 // Use array() when available (heap buffer), otherwise copy to a local array.
839898 float [] weights ;
840899 if (boneWeightBuf .hasArray ()) {
841900 weights = boneWeightBuf .array ();
842901 } else {
843902 weights = new float [boneWeightBuf .limit ()];
903+ boneWeightBuf .rewind ();
844904 boneWeightBuf .get (weights );
845905 }
846- int idxWeights = 0 ;
847906
848907 int numVerts = bindPos .limit () / 3 ;
849- float minX = Float .POSITIVE_INFINITY , minY = Float .POSITIVE_INFINITY ,
850- minZ = Float .POSITIVE_INFINITY ;
851- float maxX = Float .NEGATIVE_INFINITY , maxY = Float .NEGATIVE_INFINITY ,
852- maxZ = Float .NEGATIVE_INFINITY ;
853908
854- for (int v = 0 ; v < numVerts ; v ++) {
909+ // Decide whether we need incremental (multi-frame) processing.
910+ boolean incremental = (boundingUpdateBudget < numVerts );
911+ BoundsUpdateState state = null ;
912+ if (incremental ) {
913+ state = boundsUpdateStates .get (geometry );
914+ if (state == null ) {
915+ state = new BoundsUpdateState ();
916+ boundsUpdateStates .put (geometry , state );
917+ }
918+ }
919+
920+ // Starting vertex and accumulated min/max for this pass.
921+ int startVertex ;
922+ float minX , minY , minZ , maxX , maxY , maxZ ;
923+ if (state != null && state .nextVertex > 0 ) {
924+ // Resume an in-progress pass.
925+ startVertex = state .nextVertex ;
926+ minX = state .minX ; minY = state .minY ; minZ = state .minZ ;
927+ maxX = state .maxX ; maxY = state .maxY ; maxZ = state .maxZ ;
928+ } else {
929+ // Start a fresh pass.
930+ startVertex = 0 ;
931+ minX = Float .POSITIVE_INFINITY ; minY = Float .POSITIVE_INFINITY ;
932+ minZ = Float .POSITIVE_INFINITY ;
933+ maxX = Float .NEGATIVE_INFINITY ; maxY = Float .NEGATIVE_INFINITY ;
934+ maxZ = Float .NEGATIVE_INFINITY ;
935+ }
936+
937+ int budget = incremental ? boundingUpdateBudget : numVerts ;
938+ int endVertex = Math .min (startVertex + budget , numVerts );
939+
940+ // Position the bind-pose buffer at the correct vertex.
941+ bindPos .position (startVertex * 3 );
942+ int idxWeights = startVertex * 4 ;
943+
944+ for (int v = startVertex ; v < endVertex ; v ++) {
855945 float vtx = bindPos .get ();
856946 float vty = bindPos .get ();
857947 float vtz = bindPos .get ();
@@ -884,6 +974,19 @@ private static void updateSkinnedMeshBound(Geometry geometry, Mesh mesh,
884974 if (rz > maxZ ) maxZ = rz ;
885975 }
886976
977+ if (endVertex < numVerts ) {
978+ // Pass not yet complete – save state and wait for the next frame.
979+ state .nextVertex = endVertex ;
980+ state .minX = minX ; state .minY = minY ; state .minZ = minZ ;
981+ state .maxX = maxX ; state .maxY = maxY ; state .maxZ = maxZ ;
982+ return ;
983+ }
984+
985+ // Full pass complete – reset cursor and commit the bounding box.
986+ if (state != null ) {
987+ state .nextVertex = 0 ;
988+ }
989+
887990 // Reuse the existing BoundingBox if possible to avoid allocation.
888991 BoundingVolume bv = mesh .getBound ();
889992 BoundingBox bbox ;
@@ -904,6 +1007,21 @@ private static void updateSkinnedMeshBound(Geometry geometry, Mesh mesh,
9041007 geometry .setModelBound (bbox );
9051008 }
9061009
1010+ /**
1011+ * Holds the incremental bounding-volume update state for a single geometry
1012+ * when {@link #boundingUpdateBudget} limits processing to fewer than all
1013+ * vertices per frame.
1014+ */
1015+ private static final class BoundsUpdateState {
1016+ int nextVertex = 0 ;
1017+ float minX = Float .POSITIVE_INFINITY ;
1018+ float minY = Float .POSITIVE_INFINITY ;
1019+ float minZ = Float .POSITIVE_INFINITY ;
1020+ float maxX = Float .NEGATIVE_INFINITY ;
1021+ float maxY = Float .NEGATIVE_INFINITY ;
1022+ float maxZ = Float .NEGATIVE_INFINITY ;
1023+ }
1024+
9071025 /**
9081026 * Serialize this Control to the specified exporter, for example when saving
9091027 * to a J3O file.
@@ -917,6 +1035,7 @@ public void write(JmeExporter ex) throws IOException {
9171035 OutputCapsule oc = ex .getCapsule (this );
9181036 oc .write (armature , "armature" , null );
9191037 oc .write (updateBounds , "updateBounds" , false );
1038+ oc .write (boundingUpdateBudget , "boundingUpdateBudget" , Integer .MAX_VALUE );
9201039 }
9211040
9221041 /**
@@ -932,6 +1051,7 @@ public void read(JmeImporter im) throws IOException {
9321051 InputCapsule in = im .getCapsule (this );
9331052 armature = (Armature ) in .readSavable ("armature" , null );
9341053 updateBounds = in .readBoolean ("updateBounds" , false );
1054+ boundingUpdateBudget = in .readInt ("boundingUpdateBudget" , Integer .MAX_VALUE );
9351055
9361056 for (MatParamOverride mpo : spatial .getLocalMatParamOverrides ().getArray ()) {
9371057 if (mpo .getName ().equals ("NumberOfBones" ) || mpo .getName ().equals ("BoneMatrices" )) {
@@ -949,6 +1069,7 @@ public void read(JmeImporter im) throws IOException {
9491069 */
9501070 private void updateAnimationTargets (Spatial spatial ) {
9511071 targets .clear ();
1072+ boundsUpdateStates .clear ();
9521073 collectAnimatedGeometries (spatial );
9531074 }
9541075
0 commit comments