3131 */
3232package com .jme3 .scene .control ;
3333
34+ import com .jme3 .util .AreaUtils ;
3435import com .jme3 .bounding .BoundingVolume ;
3536import com .jme3 .export .InputCapsule ;
3637import com .jme3 .export .JmeExporter ;
4546import com .jme3 .scene .Spatial ;
4647import com .jme3 .util .clone .JmeCloneable ;
4748import java .io .IOException ;
49+ import java .util .logging .Level ;
50+ import java .util .logging .Logger ;
4851
4952/**
50- * Determines what Level of Detail a spatial should be, based on how many pixels
51- * on the screen the spatial is taking up. The more pixels covered, the more
52- * detailed the spatial should be. It calculates the area of the screen that the
53- * spatial covers by using its bounding box. When initializing, it will ask the
54- * spatial for how many triangles it has for each LOD. It then uses that, along
55- * with the trisPerPixel value to determine what LOD it should be at. It
56- * requires the camera to do this. The controlRender method is called each frame
57- * and will update the spatial's LOD if the camera has moved by a specified
58- * amount.
53+ * `LodControl` automatically adjusts the Level of Detail (LOD) for a
54+ * {@link com.jme3.scene.Geometry Geometry} based on its projected screen area.
55+ * The more pixels a spatial covers on the screen, the higher its detail level
56+ * will be. This control uses the geometry's bounding volume and the active
57+ * camera to estimate the screen coverage.
58+ * <p>
59+ * Upon attachment to a {@link com.jme3.scene.Geometry Geometry}, it queries
60+ * the mesh for the triangle counts at each available LOD level. During
61+ * rendering, it continuously monitors the geometry's screen size and
62+ * updates the LOD level if the change in screen area (or distance)
63+ * exceeds a specified tolerance.
64+ * </p>
65+ * <p>
66+ * This control is ideal for optimizing rendering performance by ensuring
67+ * that objects only use the necessary amount of detail based on their
68+ * visual prominence on screen.
69+ * </p>
5970 */
60- public class LodControl extends AbstractControl implements Cloneable , JmeCloneable {
71+ public class LodControl extends AbstractControl implements JmeCloneable {
72+
73+ private static final Logger logger = Logger .getLogger (LodControl .class .getName ());
6174
6275 private float trisPerPixel = 1f ;
6376 private float distTolerance = 1f ;
@@ -67,18 +80,22 @@ public class LodControl extends AbstractControl implements Cloneable, JmeCloneab
6780 private int [] numTris ;
6881
6982 /**
70- * Creates a new
71- * <code>LodControl</code>.
83+ * Creates a new `LodControl`.
84+ * <p>
85+ * You must attach this control to a {@link com.jme3.scene.Geometry}
86+ * for it to function correctly.
87+ * </p>
7288 */
7389 public LodControl () {
7490 }
7591
7692 /**
77- * Returns the distance tolerance for changing LOD.
78- *
79- * @return the distance tolerance for changing LOD.
93+ * Returns the distance tolerance for changing LOD. The LOD level will
94+ * only be changed if the geometry's distance from the camera has changed
95+ * by more than this tolerance since the last update. This prevents
96+ * frequent LOD flickering when the camera moves slightly.
8097 *
81- * @see #setDistTolerance(float)
98+ * @return The distance tolerance in world units.
8299 */
83100 public float getDistTolerance () {
84101 return distTolerance ;
@@ -87,105 +104,138 @@ public float getDistTolerance() {
87104 /**
88105 * Specifies the distance tolerance for changing the LOD level on the
89106 * geometry. The LOD level will only get changed if the geometry has moved
90- * this distance beyond the current LOD level.
107+ * this distance beyond the current LOD level. Setting a higher tolerance
108+ * reduces LOD changes but might make transitions less precise.
91109 *
92- * @param distTolerance distance tolerance for changing LOD
110+ * @param distTolerance The distance tolerance in world units (must be non-negative).
111+ * @throws IllegalArgumentException if `distTolerance` is negative.
93112 */
94113 public void setDistTolerance (float distTolerance ) {
114+ if (distTolerance < 0 ) {
115+ throw new IllegalArgumentException ("Distance tolerance cannot be negative." );
116+ }
95117 this .distTolerance = distTolerance ;
96118 }
97119
98120 /**
99121 * Returns the triangles per pixel value.
100122 *
101123 * @return the triangles per pixel value.
102- *
103- * @see #setTrisPerPixel(float)
104124 */
105125 public float getTrisPerPixel () {
106126 return trisPerPixel ;
107127 }
108128
109129 /**
110- * Sets the triangles per pixel value. The
111- * <code>LodControl</code> will use this value as an error metric to
112- * determine which LOD level to use based on the geometry's area on the
113- * screen.
130+ * Sets the triangles per pixel value. The `LodControl` uses this value
131+ * as an error metric to determine which LOD level to use based on the
132+ * geometry's projected area on the screen. A higher value means the
133+ * object will appear more detailed for a given screen size .
114134 *
115- * @param trisPerPixel triangles per pixel
135+ * @param trisPerPixel The desired number of triangles per screen pixel (must be positive).
136+ * @throws IllegalArgumentException if `trisPerPixel` is zero or negative.
116137 */
117138 public void setTrisPerPixel (float trisPerPixel ) {
139+ if (trisPerPixel <= 0 ) {
140+ throw new IllegalArgumentException ("Triangles per pixel must be positive." );
141+ }
118142 this .trisPerPixel = trisPerPixel ;
119143 }
120144
121145 @ Override
122146 public void setSpatial (Spatial spatial ) {
123147 if (spatial != null && !(spatial instanceof Geometry )) {
124- throw new IllegalArgumentException ("LodControl can only be attached to Geometry!" );
148+ throw new IllegalArgumentException ("Invalid Spatial type for LodControl. " +
149+ "Expected a Geometry, but got: " + spatial .getClass ().getSimpleName ());
125150 }
126151
127152 super .setSpatial (spatial );
128-
129- if (spatial != null ) {
153+
154+ if (spatial != null ) {
130155 Geometry geom = (Geometry ) spatial ;
131156 Mesh mesh = geom .getMesh ();
132157 numLevels = mesh .getNumLodLevels ();
133- numTris = new int [numLevels ];
134- for (int i = numLevels - 1 ; i >= 0 ; i --) {
135- numTris [i ] = mesh .getTriangleCount (i );
158+
159+ logger .log (Level .INFO , "LodControl attached to Geometry ''{0}''. Detected {1} LOD levels." ,
160+ new Object []{spatial .getName (), numLevels });
161+
162+ if (numLevels > 0 ) {
163+ numTris = new int [numLevels ];
164+ // Store triangle counts from lowest LOD (highest index) to highest LOD (index 0)
165+ // This makes the lookup loop in controlRender more efficient.
166+ for (int i = 0 ; i < numLevels ; i ++) {
167+ numTris [i ] = mesh .getTriangleCount (i );
168+ }
169+ } else {
170+ logger .log (Level .WARNING , "LodControl attached to Geometry {0} but its Mesh has no LOD levels defined. " +
171+ "Control will have no effect." , spatial .getName ());
136172 }
137173 } else {
138174 numLevels = 0 ;
139175 numTris = null ;
176+ lastDistance = 0f ;
177+ lastLevel = 0 ;
140178 }
141179 }
142180
143181 @ Override
144182 public Object jmeClone () {
145- LodControl clone = (LodControl )super .jmeClone ();
183+ LodControl clone = (LodControl ) super .jmeClone ();
146184 clone .lastDistance = 0 ;
147185 clone .lastLevel = 0 ;
148- clone .numTris = numTris != null ? numTris .clone () : null ;
186+ clone .numTris = ( numTris != null ) ? numTris .clone () : null ;
149187 return clone ;
150- }
188+ }
151189
152190 @ Override
153191 protected void controlUpdate (float tpf ) {
192+ // LOD is determined during controlRender to react to camera changes
154193 }
155194
156195 @ Override
157196 protected void controlRender (RenderManager rm , ViewPort vp ) {
197+ if (numLevels == 0 ) {
198+ return ;
199+ }
200+
158201 BoundingVolume bv = spatial .getWorldBound ();
159202
160203 Camera cam = vp .getCamera ();
161204 float atanNH = FastMath .atan (cam .getFrustumNear () * cam .getFrustumTop ());
162205 float ratio = (FastMath .PI / (8f * atanNH ));
163- float newDistance = bv .distanceTo (vp . getCamera () .getLocation ()) / ratio ;
164- int level ;
165-
166- if (Math .abs (newDistance - lastDistance ) <= distTolerance ) {
167- level = lastLevel ; // we haven't moved relative to the model, send the old measurement back.
168- } else if (lastDistance > newDistance && lastLevel == 0 ) {
169- level = lastLevel ; // we're already at the lowest setting and we just got closer to the model, no need to keep trying.
170- } else if (lastDistance < newDistance && lastLevel == numLevels - 1 ) {
171- level = lastLevel ; // we're already at the highest setting and we just got further from the model, no need to keep trying.
206+ float currDistance = bv .distanceTo (cam .getLocation ()) / ratio ;
207+ int newLevel ;
208+
209+ if (Math .abs (currDistance - lastDistance ) <= distTolerance ) {
210+ newLevel = lastLevel ; // we haven't moved relative to the model
211+ } else if (lastDistance > currDistance && lastLevel == 0 ) {
212+ newLevel = lastLevel ; // Getting closer, already at highest detail (level 0)
213+ } else if (lastDistance < currDistance && lastLevel == numLevels - 1 ) {
214+ newLevel = lastLevel ; // Getting further, already at lowest detail
172215 } else {
173- lastDistance = newDistance ;
216+ // Update lastDistance for subsequent checks
217+ lastDistance = currDistance ;
174218
175219 // estimate area of polygon via bounding volume
176220 float area = AreaUtils .calcScreenArea (bv , lastDistance , cam .getWidth ());
177221 float trisToDraw = area * trisPerPixel ;
178- level = numLevels - 1 ;
179- for (int i = numLevels ; --i >= 0 ;) {
180- if (trisToDraw - numTris [i ] < 0 ) {
181- break ;
222+
223+ // Find the appropriate LOD level
224+ // Start with the lowest detail (highest index)
225+ newLevel = numLevels - 1 ;
226+ // Iterate from highest detail (index 0) to lowest, breaking when trisToDraw is enough
227+ for (int i = 0 ; i < numLevels ; i ++) {
228+ if (trisToDraw >= numTris [i ]) {
229+ newLevel = i ;
230+ break ; // Found the highest detail level that satisfies the criteria
182231 }
183- level = i ;
184232 }
185- lastLevel = level ;
233+ // Only set LOD if it's actually changing to avoid unnecessary calls
234+ if (newLevel != lastLevel ) {
235+ spatial .setLodLevel (newLevel );
236+ }
237+ lastLevel = newLevel ;
186238 }
187-
188- spatial .setLodLevel (level );
189239 }
190240
191241 @ Override
0 commit comments