d9bfe23107c8c97faa64995b76f41d31ad5af6b8
[sixth-3d.git] /
1 /*
2  * Sixth 3D engine. Author: Svjatoslav Agejenko.
3  * This project is released under Creative Commons Zero (CC0) license.
4  */
5 package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base;
6
7 import eu.svjatoslav.sixth.e3d.geometry.Point3D;
8 import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
9 import eu.svjatoslav.sixth.e3d.gui.ViewSpaceTracker;
10 import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
11 import eu.svjatoslav.sixth.e3d.math.Transform;
12 import eu.svjatoslav.sixth.e3d.math.TransformStack;
13 import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
14 import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator;
15 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape;
16 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line;
17 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
18 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon;
19 import eu.svjatoslav.sixth.e3d.renderer.raster.slicer.Slicer;
20
21 import java.util.ArrayList;
22 import java.util.List;
23
24 /**
25  * A composite shape that groups multiple sub-shapes into a single logical unit.
26  *
27  * <p>Use {@code AbstractCompositeShape} to build complex 3D objects by combining
28  * primitive shapes (lines, polygons, textured polygons) into a group that can be
29  * positioned, rotated, and manipulated as one entity. Sub-shapes can be organized
30  * into named groups for selective visibility toggling.</p>
31  *
32  * <p><b>Usage example - creating a custom composite shape:</b></p>
33  * <pre>{@code
34  * // Create a composite shape at position (0, 0, 200)
35  * AbstractCompositeShape myObject = new AbstractCompositeShape(
36  *     new Point3D(0, 0, 200)
37  * );
38  *
39  * // Add sub-shapes
40  * myObject.addShape(new Line(
41  *     new Point3D(-50, 0, 0), new Point3D(50, 0, 0),
42  *     Color.RED, 2.0
43  * ));
44  *
45  * // Add shapes to a named group for toggling visibility
46  * myObject.addShape(labelShape, "labels");
47  * myObject.hideGroup("labels");  // hide all shapes in "labels" group
48  * myObject.showGroup("labels");  // show them again
49  *
50  * // Add to scene
51  * viewPanel.getRootShapeCollection().addShape(myObject);
52  * }</pre>
53  *
54  * <p><b>Level-of-detail slicing:</b></p>
55  * <p>Textured polygons within the composite shape are automatically sliced into smaller
56  * triangles based on distance from the viewer. This provides perspective-correct texture
57  * mapping without requiring hardware support. The slicing factor adapts dynamically.</p>
58  *
59  * <p><b>Extending this class:</b></p>
60  * <p>Override {@link #beforeTransformHook} to customize shape appearance or behavior
61  * on each frame (e.g., animations, dynamic geometry updates).</p>
62  *
63  * @see SubShape wrapper for individual sub-shapes with group and visibility support
64  * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape the base shape class
65  * @see eu.svjatoslav.sixth.e3d.renderer.raster.slicer.Slicer the level-of-detail polygon slicer
66  */
67 public class AbstractCompositeShape extends AbstractShape {
68     /**
69      * The original sub-shapes added to this composite, each wrapped with group
70      * identifier and visibility state. Shapes are stored in insertion order and
71      * remain in this collection even when hidden.
72      */
73     private final List<SubShape> originalSubShapes = new ArrayList<>();
74
75     /**
76      * Tracks the distance and angle between the camera and this shape to compute
77      * an appropriate slice factor for level-of-detail adjustments.
78      */
79     private final ViewSpaceTracker viewSpaceTracker;
80
81     /**
82      * The current slice factor used for tessellating textured polygons into smaller
83      * triangles for perspective-correct rendering. Higher values produce more triangles
84      * for distant objects; lower values for nearby objects. Updated dynamically based
85      * on view-space analysis.
86      */
87     double currentSliceFactor = 5;
88
89     /**
90      * The processed list of sub-shapes ready for rendering. Contains non-textured
91      * shapes directly, and sliced triangles for textured polygons. Regenerated when
92      * {@link #slicingOutdated} is true.
93      */
94     private List<AbstractShape> renderedSubShapes = new ArrayList<>();
95
96     /**
97      * Flag indicating whether the rendered sub-shapes need to be regenerated.
98      * Set to true when sub-shapes are added, removed, or when group visibility changes.
99      */
100     private boolean slicingOutdated = true;
101
102     /**
103      * The position and orientation transform for this composite shape.
104      * Applied to all sub-shapes during the rendering transform pass.
105      */
106     private Transform transform;
107
108     /**
109      * Creates a composite shape at the world origin with no rotation.
110      */
111     public AbstractCompositeShape() {
112         this(new Transform());
113     }
114
115     /**
116      * Creates a composite shape at the specified location with no rotation.
117      *
118      * @param location the position in world space
119      */
120     public AbstractCompositeShape(final Point3D location) {
121         this(new Transform(location));
122     }
123
124     /**
125      * Creates a composite shape with the specified transform (position and orientation).
126      *
127      * @param transform the initial transform defining position and rotation
128      */
129     public AbstractCompositeShape(final Transform transform) {
130         this.transform = transform;
131         viewSpaceTracker = new ViewSpaceTracker();
132     }
133
134     /**
135      * Adds a sub-shape to this composite shape without a group identifier.
136      *
137      * @param shape the shape to add
138      */
139     public void addShape(final AbstractShape shape) {
140         addShape(shape, null);
141     }
142
143     /**
144      * Adds a sub-shape to this composite shape with an optional group identifier.
145      *
146      * <p>Grouped shapes can be shown, hidden, or removed together using
147      * {@link #showGroup}, {@link #hideGroup}, and {@link #removeGroup}.</p>
148      *
149      * @param shape   the shape to add
150      * @param groupId the group identifier, or {@code null} for ungrouped shapes
151      */
152     public void addShape(final AbstractShape shape, final String groupId) {
153         originalSubShapes.add(new SubShape(shape, groupId, true));
154         slicingOutdated = true;
155     }
156
157     /**
158      * This method should be overridden by anyone wanting to customize the shape
159      * before it is rendered.
160      *
161      * @param transformPipe the current transform stack
162      * @param context       the rendering context for the current frame
163      */
164     public void beforeTransformHook(final TransformStack transformPipe,
165                                     final RenderingContext context) {
166     }
167
168     /**
169      * Returns the world-space position of this composite shape.
170      *
171      * @return the translation component of this shape's transform
172      */
173     public Point3D getLocation() {
174         return transform.getTranslation();
175     }
176
177     /**
178      * Returns the list of all sub-shapes (including hidden ones).
179      *
180      * @return the internal list of sub-shapes
181      */
182     public List<SubShape> getOriginalSubShapes() {
183         return originalSubShapes;
184     }
185
186     /**
187      * Extracts all SolidPolygon triangles from this composite shape.
188      *
189      * <p>Recursively traverses the shape hierarchy and collects all
190      * {@link SolidPolygon} instances. Useful for CSG operations where
191      * you need the raw triangles from a composite shape like
192      * {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube}
193      * or {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonSphere}.</p>
194      *
195      * <p><b>Example:</b></p>
196      * <pre>{@code
197      * SolidPolygonCube cube = new SolidPolygonCube(new Point3D(0, 0, 0), 50, Color.RED);
198      * List<SolidPolygon> triangles = cube.extractSolidPolygons();
199      * CSG csg = CSG.fromSolidPolygons(triangles);
200      * }</pre>
201      *
202      * @return list of all SolidPolygon sub-shapes
203      */
204     public List<SolidPolygon> extractSolidPolygons() {
205         final List<SolidPolygon> result = new ArrayList<>();
206         for (final SubShape subShape : originalSubShapes) {
207             final AbstractShape shape = subShape.getShape();
208             if (shape instanceof SolidPolygon) {
209                 result.add((SolidPolygon) shape);
210             } else if (shape instanceof AbstractCompositeShape) {
211                 result.addAll(((AbstractCompositeShape) shape).extractSolidPolygons());
212             }
213         }
214         return result;
215     }
216
217     /**
218      * Returns the view-space tracker that monitors the distance
219      * and angle between the camera and this shape for level-of-detail adjustments.
220      *
221      * @return the view-space tracker for this shape
222      */
223     public ViewSpaceTracker getViewSpaceTracker() {
224         return viewSpaceTracker;
225     }
226
227     /**
228      * Hides all sub-shapes belonging to the specified group.
229      * Hidden shapes are not rendered but remain in the collection.
230      *
231      * @param groupIdentifier the group to hide
232      * @see #showGroup(String)
233      * @see #removeGroup(String)
234      */
235     public void hideGroup(final String groupIdentifier) {
236         for (final SubShape subShape : originalSubShapes) {
237             if (subShape.matchesGroup(groupIdentifier)) {
238                 subShape.setVisible(false);
239                 slicingOutdated = true;
240             }
241         }
242     }
243
244     /**
245      * Determines whether textured polygons need to be re-sliced based on slice factor change.
246      * <p>
247      * Re-slicing is needed if the slicing state is marked outdated, or if the ratio between
248      * the larger and smaller slice factor exceeds 1.5x. This threshold prevents frequent
249      * re-slicing for minor view changes while ensuring significant LOD changes trigger updates.
250      *
251      * @param proposedNewSliceFactor the slice factor computed from current view distance
252      * @param currentSliceFactor     the slice factor currently in use
253      * @return {@code true} if re-slicing should be performed
254      */
255     private boolean isReslicingNeeded(final double proposedNewSliceFactor, final double currentSliceFactor) {
256
257         if (slicingOutdated)
258             return true;
259
260         // reslice if there is significant difference between proposed and current slice factor
261         final double larger = Math.max(proposedNewSliceFactor, currentSliceFactor);
262         final double smaller = Math.min(proposedNewSliceFactor, currentSliceFactor);
263
264         return (larger / smaller) > 1.5d;
265     }
266
267     /**
268      * Permanently removes all sub-shapes belonging to the specified group.
269      *
270      * @param groupIdentifier the group to remove
271      * @see #hideGroup(String)
272      */
273     public void removeGroup(final String groupIdentifier) {
274         final java.util.Iterator<SubShape> iterator = originalSubShapes
275                 .iterator();
276
277         while (iterator.hasNext()) {
278             final SubShape subShape = iterator.next();
279             if (subShape.matchesGroup(groupIdentifier)) {
280                 iterator.remove();
281                 slicingOutdated = true;
282             }
283         }
284     }
285
286     /**
287      * Returns all sub-shapes belonging to the specified group.
288      *
289      * @param groupIdentifier the group identifier to match
290      * @return list of matching sub-shapes
291      */
292     public List<SubShape> getGroup(final String groupIdentifier) {
293         final List<SubShape> result = new ArrayList<>();
294         for (int i = 0; i < originalSubShapes.size(); i++) {
295             final SubShape subShape = originalSubShapes.get(i);
296             if (subShape.matchesGroup(groupIdentifier))
297                 result.add(subShape);
298         }
299         return result;
300     }
301
302     /**
303      * Checks if re-slicing is needed and performs it if so.
304      *
305      * @param context the rendering context for logging
306      */
307     private void resliceIfNeeded(final RenderingContext context) {
308
309         final double proposedSliceFactor = viewSpaceTracker.proposeSliceFactor();
310
311         if (isReslicingNeeded(proposedSliceFactor, currentSliceFactor)) {
312             currentSliceFactor = proposedSliceFactor;
313             reslice(context);
314         }
315     }
316
317     /**
318      * Paint solid elements of this composite shape into given color.
319      *
320      * @param color the color to apply to all solid sub-shapes
321      */
322     public void setColor(final Color color) {
323         for (final SubShape subShape : getOriginalSubShapes()) {
324             final AbstractShape shape = subShape.getShape();
325
326             if (shape instanceof SolidPolygon)
327                 ((SolidPolygon) shape).setColor(color);
328
329             if (shape instanceof Line)
330                 ((Line) shape).color = color;
331         }
332     }
333
334     /**
335      * Assigns a group identifier to all sub-shapes that currently have no group.
336      *
337      * @param groupIdentifier the group to assign to ungrouped shapes
338      */
339     public void setGroupForUngrouped(final String groupIdentifier) {
340         for (int i = 0; i < originalSubShapes.size(); i++) {
341             final SubShape subShape = originalSubShapes.get(i);
342             if (subShape.isUngrouped())
343                 subShape.setGroup(groupIdentifier);
344         }
345     }
346
347     @Override
348     public void setMouseInteractionController(
349             final MouseInteractionController mouseInteractionController) {
350         super.setMouseInteractionController(mouseInteractionController);
351
352         for (final SubShape subShape : originalSubShapes)
353             subShape.getShape().setMouseInteractionController(
354                     mouseInteractionController);
355
356         slicingOutdated = true;
357
358     }
359
360     /**
361      * Replaces this shape's transform (position and orientation).
362      *
363      * @param transform the new transform to apply
364      */
365     public void setTransform(final Transform transform) {
366         this.transform = transform;
367     }
368
369     /**
370      * Enables or disables shading for all SolidPolygon sub-shapes.
371      * When enabled, polygons use the global lighting manager from the rendering
372      * context to calculate flat shading based on light sources.
373      *
374      * @param shadingEnabled {@code true} to enable shading, {@code false} to disable
375      */
376     public void setShadingEnabled(final boolean shadingEnabled) {
377         for (final SubShape subShape : getOriginalSubShapes()) {
378             final AbstractShape shape = subShape.getShape();
379             if (shape instanceof SolidPolygon) {
380                 ((SolidPolygon) shape).setShadingEnabled(shadingEnabled);
381             }
382
383             // TODO: if shape is abstract composite, it seems that it would be good to enabled sharding recursively there too
384         }
385     }
386
387     /**
388      * Enables or disables backface culling for all SolidPolygon and TexturedPolygon sub-shapes.
389      *
390      * @param backfaceCulling {@code true} to enable backface culling, {@code false} to disable
391      */
392     public void setBackfaceCulling(final boolean backfaceCulling) {
393         for (final SubShape subShape : getOriginalSubShapes()) {
394             final AbstractShape shape = subShape.getShape();
395             if (shape instanceof SolidPolygon) {
396                 ((SolidPolygon) shape).setBackfaceCulling(backfaceCulling);
397             } else if (shape instanceof TexturedPolygon) {
398                 ((TexturedPolygon) shape).setBackfaceCulling(backfaceCulling);
399             }
400         }
401     }
402
403     /**
404      * Makes all sub-shapes belonging to the specified group visible.
405      *
406      * @param groupIdentifier the group to show
407      * @see #hideGroup(String)
408      */
409     public void showGroup(final String groupIdentifier) {
410         for (int i = 0; i < originalSubShapes.size(); i++) {
411             final SubShape subShape = originalSubShapes.get(i);
412             if (subShape.matchesGroup(groupIdentifier)) {
413                 subShape.setVisible(true);
414                 slicingOutdated = true;
415             }
416         }
417     }
418
419     /**
420      * Re-slices all textured polygons and rebuilds the rendered sub-shapes list.
421      * Logs the operation to the debug log buffer if available.
422      *
423      * @param context the rendering context for logging, may be {@code null}
424      */
425     private void reslice(final RenderingContext context) {
426         slicingOutdated = false;
427
428         final List<AbstractShape> result = new ArrayList<>();
429
430         final Slicer slicer = new Slicer(currentSliceFactor);
431         int texturedPolygonCount = 0;
432         int otherShapeCount = 0;
433
434         for (int i = 0; i < originalSubShapes.size(); i++) {
435             final SubShape subShape = originalSubShapes.get(i);
436             if (subShape.isVisible()) {
437                 if (subShape.getShape() instanceof TexturedPolygon) {
438                     slicer.slice((TexturedPolygon) subShape.getShape());
439                     texturedPolygonCount++;
440                 } else {
441                     result.add(subShape.getShape());
442                     otherShapeCount++;
443                 }
444             }
445         }
446
447         result.addAll(slicer.getResult());
448
449         renderedSubShapes = result;
450
451         // Log to developer tools console if available
452         if (context != null && context.debugLogBuffer != null) {
453             context.debugLogBuffer.log("reslice: " + getClass().getSimpleName()
454                     + " sliceFactor=" + String.format("%.2f", currentSliceFactor)
455                     + " texturedPolygons=" + texturedPolygonCount
456                     + " otherShapes=" + otherShapeCount
457                     + " resultingTexturedPolygons=" + slicer.getResult().size());
458         }
459     }
460
461     @Override
462     public void transform(final TransformStack transformPipe,
463                           final RenderAggregator aggregator, final RenderingContext context) {
464
465         // Add the current composite shape transform to the end of the transform
466         // pipeline.
467         transformPipe.addTransform(transform);
468
469         viewSpaceTracker.analyze(transformPipe, context);
470
471         beforeTransformHook(transformPipe, context);
472
473         resliceIfNeeded(context);
474
475         // transform rendered subshapes
476         for (final AbstractShape shape : renderedSubShapes)
477             shape.transform(transformPipe, aggregator, context);
478
479         transformPipe.dropTransform();
480     }
481
482 }