Skip to content

Commit f4661dc

Browse files
Copilotriccardobl
andcommitted
Make bounds update toggleable (off by default) and add visual example
Agent-Logs-Url: https://github.com/jMonkeyEngine/jmonkeyengine/sessions/1528c7f1-8585-400f-9b82-851054cf0408 Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com>
1 parent d20beb7 commit f4661dc

4 files changed

Lines changed: 265 additions & 18 deletions

File tree

jme3-core/src/main/java/com/jme3/anim/SkinningControl.java

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ public class SkinningControl extends AbstractControl implements JmeCloneable {
133133
*/
134134
private transient Matrix4f[] boneOffsetMatrices;
135135

136+
/**
137+
* When true, the bounding volumes of animated geometries are updated each
138+
* frame to match the current pose, ensuring correct frustum culling.
139+
* Disabled by default because it adds CPU cost every frame.
140+
*/
141+
private boolean updateBounds = false;
142+
136143
private MatParamOverride numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
137144
private MatParamOverride jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
138145

@@ -248,6 +255,32 @@ public boolean isHardwareSkinningUsed() {
248255
return hwSkinningEnabled;
249256
}
250257

258+
/**
259+
* Enables or disables per-frame bounding-volume updates for animated
260+
* geometries. When enabled, the bounding volume of each deformed geometry
261+
* is recomputed every render frame to match the current animation pose,
262+
* ensuring correct frustum culling at the cost of additional CPU work.
263+
* Disabled by default.
264+
*
265+
* @param updateBounds true to update bounds each frame, false to keep
266+
* static bind-pose bounds (default=false)
267+
* @see #isUpdateBounds()
268+
*/
269+
public void setUpdateBounds(boolean updateBounds) {
270+
this.updateBounds = updateBounds;
271+
}
272+
273+
/**
274+
* Returns whether per-frame bounding-volume updates are enabled for
275+
* animated geometries.
276+
*
277+
* @return true if bounds are updated each frame, false otherwise
278+
* @see #setUpdateBounds(boolean)
279+
*/
280+
public boolean isUpdateBounds() {
281+
return updateBounds;
282+
}
283+
251284
/**
252285
* Recursively finds and adds animated geometries to the targets list.
253286
*
@@ -299,8 +332,10 @@ private void controlRenderSoftware() {
299332
// NOTE: This assumes code higher up has already ensured this mesh is animated.
300333
// Otherwise, a crash will happen in skin update.
301334
applySoftwareSkinning(mesh, boneOffsetMatrices);
302-
// Update the mesh bounding volume to reflect the animated vertex positions.
303-
geometry.updateModelBound();
335+
if (updateBounds) {
336+
// Update the mesh bounding volume to reflect the animated vertex positions.
337+
geometry.updateModelBound();
338+
}
304339
}
305340
}
306341

@@ -311,13 +346,15 @@ private void controlRenderHardware() {
311346
boneOffsetMatrices = armature.computeSkinningMatrices();
312347
jointMatricesParam.setValue(boneOffsetMatrices);
313348

314-
// Hardware skinning transforms vertices on the GPU, so the CPU-side vertex
315-
// buffer is not updated. Compute the animated bounding volume from the bind
316-
// pose positions and the current skinning matrices so culling is correct.
317-
for (Geometry geometry : targets) {
318-
Mesh mesh = geometry.getMesh();
319-
if (mesh != null && mesh.isAnimated()) {
320-
updateSkinnedMeshBound(geometry, mesh, boneOffsetMatrices);
349+
if (updateBounds) {
350+
// Hardware skinning transforms vertices on the GPU, so the CPU-side vertex
351+
// buffer is not updated. Compute the animated bounding volume from the bind
352+
// pose positions and the current skinning matrices so culling is correct.
353+
for (Geometry geometry : targets) {
354+
Mesh mesh = geometry.getMesh();
355+
if (mesh != null && mesh.isAnimated()) {
356+
updateSkinnedMeshBound(geometry, mesh, boneOffsetMatrices);
357+
}
321358
}
322359
}
323360
}
@@ -879,6 +916,7 @@ public void write(JmeExporter ex) throws IOException {
879916
super.write(ex);
880917
OutputCapsule oc = ex.getCapsule(this);
881918
oc.write(armature, "armature", null);
919+
oc.write(updateBounds, "updateBounds", false);
882920
}
883921

884922
/**
@@ -893,6 +931,7 @@ public void read(JmeImporter im) throws IOException {
893931
super.read(im);
894932
InputCapsule in = im.getCapsule(this);
895933
armature = (Armature) in.readSavable("armature", null);
934+
updateBounds = in.readBoolean("updateBounds", false);
896935

897936
for (MatParamOverride mpo : spatial.getLocalMatParamOverrides().getArray()) {
898937
if (mpo.getName().equals("NumberOfBones") || mpo.getName().equals("BoneMatrices")) {

jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl
108108
*/
109109
private transient Matrix4f[] offsetMatrices;
110110

111+
/**
112+
* When true, the bounding volumes of animated geometries are updated each
113+
* frame to match the current pose, ensuring correct frustum culling.
114+
* Disabled by default because it adds CPU cost every frame.
115+
*/
116+
private boolean updateBounds = false;
117+
111118
private MatParamOverride numberOfBonesParam;
112119
private MatParamOverride boneMatricesParam;
113120

@@ -193,6 +200,32 @@ public boolean isHardwareSkinningUsed() {
193200
return hwSkinningEnabled;
194201
}
195202

203+
/**
204+
* Enables or disables per-frame bounding-volume updates for animated
205+
* geometries. When enabled, the bounding volume of each deformed geometry
206+
* is recomputed every render frame to match the current animation pose,
207+
* ensuring correct frustum culling at the cost of additional CPU work.
208+
* Disabled by default.
209+
*
210+
* @param updateBounds true to update bounds each frame, false to keep
211+
* static bind-pose bounds (default=false)
212+
* @see #isUpdateBounds()
213+
*/
214+
public void setUpdateBounds(boolean updateBounds) {
215+
this.updateBounds = updateBounds;
216+
}
217+
218+
/**
219+
* Returns whether per-frame bounding-volume updates are enabled for
220+
* animated geometries.
221+
*
222+
* @return true if bounds are updated each frame, false otherwise
223+
* @see #setUpdateBounds(boolean)
224+
*/
225+
public boolean isUpdateBounds() {
226+
return updateBounds;
227+
}
228+
196229
/**
197230
* Creates a skeleton control. The list of targets will be acquired
198231
* automatically when the control is attached to a node.
@@ -261,22 +294,26 @@ private void controlRenderSoftware() {
261294
// already ensured this mesh is animated.
262295
// Otherwise a crash will happen in skin update.
263296
softwareSkinUpdate(mesh, offsetMatrices);
264-
// Update the mesh bounding volume to reflect the animated vertex positions.
265-
geometry.updateModelBound();
297+
if (updateBounds) {
298+
// Update the mesh bounding volume to reflect the animated vertex positions.
299+
geometry.updateModelBound();
300+
}
266301
}
267302
}
268303

269304
private void controlRenderHardware() {
270305
offsetMatrices = skeleton.computeSkinningMatrices();
271306
boneMatricesParam.setValue(offsetMatrices);
272307

273-
// Hardware skinning transforms vertices on the GPU, so the CPU-side vertex
274-
// buffer is not updated. Compute the animated bounding volume from the bind
275-
// pose positions and the current skinning matrices so culling is correct.
276-
for (Geometry geometry : targets) {
277-
Mesh mesh = geometry.getMesh();
278-
if (mesh != null && mesh.isAnimated()) {
279-
updateSkinnedMeshBound(geometry, mesh, offsetMatrices);
308+
if (updateBounds) {
309+
// Hardware skinning transforms vertices on the GPU, so the CPU-side vertex
310+
// buffer is not updated. Compute the animated bounding volume from the bind
311+
// pose positions and the current skinning matrices so culling is correct.
312+
for (Geometry geometry : targets) {
313+
Mesh mesh = geometry.getMesh();
314+
if (mesh != null && mesh.isAnimated()) {
315+
updateSkinnedMeshBound(geometry, mesh, offsetMatrices);
316+
}
280317
}
281318
}
282319
}
@@ -827,6 +864,7 @@ public void write(JmeExporter ex) throws IOException {
827864

828865
oc.write(numberOfBonesParam, "numberOfBonesParam", null);
829866
oc.write(boneMatricesParam, "boneMatricesParam", null);
867+
oc.write(updateBounds, "updateBounds", false);
830868
}
831869

832870
@Override
@@ -837,6 +875,7 @@ public void read(JmeImporter im) throws IOException {
837875

838876
numberOfBonesParam = (MatParamOverride) in.readSavable("numberOfBonesParam", null);
839877
boneMatricesParam = (MatParamOverride) in.readSavable("boneMatricesParam", null);
878+
updateBounds = in.readBoolean("updateBounds", false);
840879

841880
if (numberOfBonesParam == null) {
842881
numberOfBonesParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);

jme3-core/src/test/java/com/jme3/test/PreventCoreIssueRegressions.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ public void testIssue343() {
150150

151151
// Force software skinning so bounds are computed from CPU vertex positions.
152152
sControl.setHardwareSkinningPreferred(false);
153+
// Enable per-frame bounds update (off by default).
154+
sControl.setUpdateBounds(true);
153155

154156
// Record the world bound in the bind pose.
155157
cgModel.updateGeometricState();
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright (c) 2009-2025 jMonkeyEngine
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are
7+
* met:
8+
*
9+
* * Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
*
12+
* * Redistributions in binary form must reproduce the above copyright
13+
* notice, this list of conditions and the following disclaimer in the
14+
* documentation and/or other materials provided with the distribution.
15+
*
16+
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17+
* may be used to endorse or promote products derived from this software
18+
* without specific prior written permission.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22+
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23+
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24+
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25+
* EXEMPLARY, OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26+
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package jme3test.animation;
31+
32+
import com.jme3.anim.AnimComposer;
33+
import com.jme3.anim.SkinningControl;
34+
import com.jme3.app.SimpleApplication;
35+
import com.jme3.bounding.BoundingBox;
36+
import com.jme3.bounding.BoundingVolume;
37+
import com.jme3.font.BitmapText;
38+
import com.jme3.input.KeyInput;
39+
import com.jme3.input.controls.ActionListener;
40+
import com.jme3.input.controls.KeyTrigger;
41+
import com.jme3.light.AmbientLight;
42+
import com.jme3.light.DirectionalLight;
43+
import com.jme3.material.Material;
44+
import com.jme3.math.ColorRGBA;
45+
import com.jme3.math.Vector3f;
46+
import com.jme3.scene.Geometry;
47+
import com.jme3.scene.Node;
48+
import com.jme3.scene.debug.WireBox;
49+
50+
/**
51+
* Demonstrates the toggleable per-frame bounding-volume update feature of
52+
* {@link SkinningControl}.
53+
*
54+
* <p>The Elephant model plays its "legUp" animation in a loop. A wireframe
55+
* box shows the world bounding volume of the model. Press {@code B} to
56+
* toggle {@link SkinningControl#setUpdateBounds(boolean) updateBounds}:
57+
*
58+
* <ul>
59+
* <li><b>updateBounds OFF (default):</b> The bounding box stays fixed at
60+
* the bind pose — it will not grow when the leg extends upward.</li>
61+
* <li><b>updateBounds ON:</b> The bounding box tracks the animated pose
62+
* correctly, expanding and contracting as the leg moves.</li>
63+
* </ul>
64+
*
65+
* @see SkinningControl#setUpdateBounds(boolean)
66+
*/
67+
public class TestAnimatedModelBound extends SimpleApplication {
68+
69+
/** Root node of the loaded model. */
70+
private Node modelRoot;
71+
/** SkinningControl whose updateBounds flag we toggle. */
72+
private SkinningControl skinningControl;
73+
/** Wireframe box visualizing the world bounding volume each frame. */
74+
private Geometry boundGeom;
75+
/** Label shown in the top-left corner. */
76+
private BitmapText statusText;
77+
78+
public static void main(String[] args) {
79+
TestAnimatedModelBound app = new TestAnimatedModelBound();
80+
app.start();
81+
}
82+
83+
@Override
84+
public void simpleInitApp() {
85+
// ---------- lighting ----------
86+
rootNode.addLight(new AmbientLight(new ColorRGBA(0.3f, 0.3f, 0.3f, 1f)));
87+
DirectionalLight sun = new DirectionalLight(
88+
new Vector3f(-1f, -1f, -1f).normalizeLocal(),
89+
ColorRGBA.White);
90+
rootNode.addLight(sun);
91+
92+
// ---------- camera ----------
93+
cam.setLocation(new Vector3f(0f, 2f, 8f));
94+
cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
95+
flyCam.setMoveSpeed(5f);
96+
97+
// ---------- model ----------
98+
modelRoot = (Node) assetManager.loadModel("Models/Elephant/Elephant.mesh.xml");
99+
float scale = 0.04f;
100+
modelRoot.scale(scale);
101+
rootNode.attachChild(modelRoot);
102+
103+
skinningControl = modelRoot.getControl(SkinningControl.class);
104+
// updateBounds is OFF by default — the bounding box will stay static.
105+
skinningControl.setHardwareSkinningPreferred(false); // easier to visualize with SW skinning
106+
107+
AnimComposer composer = modelRoot.getControl(AnimComposer.class);
108+
composer.setCurrentAction("legUp");
109+
110+
// ---------- bounding-box visualizer ----------
111+
// Create an unshaded wireframe geometry; we reposition it every frame.
112+
Material wireMat = new Material(assetManager,
113+
"Common/MatDefs/Misc/Unshaded.j3md");
114+
wireMat.setColor("Color", ColorRGBA.Yellow);
115+
wireMat.getAdditionalRenderState().setWireframe(true);
116+
117+
boundGeom = new Geometry("boundingBox", new WireBox(1f, 1f, 1f));
118+
boundGeom.setMaterial(wireMat);
119+
rootNode.attachChild(boundGeom);
120+
121+
// ---------- HUD ----------
122+
guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
123+
statusText = new BitmapText(guiFont);
124+
statusText.setSize(guiFont.getCharSet().getRenderedSize());
125+
statusText.setLocalTranslation(10f,
126+
settings.getHeight() - 10f, 0f);
127+
guiNode.attachChild(statusText);
128+
updateStatusText();
129+
130+
// ---------- key binding ----------
131+
inputManager.addMapping("ToggleBounds",
132+
new KeyTrigger(KeyInput.KEY_B));
133+
inputManager.addListener(new ActionListener() {
134+
@Override
135+
public void onAction(String name, boolean isPressed, float tpf) {
136+
if (isPressed) {
137+
boolean current = skinningControl.isUpdateBounds();
138+
skinningControl.setUpdateBounds(!current);
139+
updateStatusText();
140+
}
141+
}
142+
}, "ToggleBounds");
143+
}
144+
145+
@Override
146+
public void simpleUpdate(float tpf) {
147+
// Update the wireframe box to match the current world bounding volume.
148+
modelRoot.updateGeometricState();
149+
BoundingVolume wb = modelRoot.getWorldBound();
150+
if (wb instanceof BoundingBox) {
151+
BoundingBox bbox = (BoundingBox) wb;
152+
Vector3f center = bbox.getCenter();
153+
((WireBox) boundGeom.getMesh()).updatePositions(
154+
bbox.getXExtent(), bbox.getYExtent(), bbox.getZExtent());
155+
boundGeom.setLocalTranslation(center);
156+
}
157+
}
158+
159+
/** Refreshes the HUD label that shows the current updateBounds state. */
160+
private void updateStatusText() {
161+
boolean on = skinningControl.isUpdateBounds();
162+
statusText.setText(
163+
"Press B to toggle updateBounds\n"
164+
+ "updateBounds: " + (on ? "ON — bounding box tracks the animated pose"
165+
: "OFF — bounding box stays at bind pose (default)"));
166+
}
167+
}

0 commit comments

Comments
 (0)