d3455a047aacdc60c747011c4b36ad773f77cdcc
[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.lighting.LightingManager;
16 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape;
17 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line;
18 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
19 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon;
20 import eu.svjatoslav.sixth.e3d.renderer.raster.slicer.Slicer;
21
22 import java.util.ArrayList;
23 import java.util.List;
24
25 /**
26  * A composite shape that groups multiple sub-shapes into a single logical unit.
27  *
28  * <p>Use {@code AbstractCompositeShape} to build complex 3D objects by combining
29  * primitive shapes (lines, polygons, textured polygons) into a group that can be
30  * positioned, rotated, and manipulated as one entity. Sub-shapes can be organized
31  * into named groups for selective visibility toggling.</p>
32  *
33  * <p><b>Usage example - creating a custom composite shape:</b></p>
34  * <pre>{@code
35  * // Create a composite shape at position (0, 0, 200)
36  * AbstractCompositeShape myObject = new AbstractCompositeShape(
37  *     new Point3D(0, 0, 200)
38  * );
39  *
40  * // Add sub-shapes
41  * myObject.addShape(new Line(
42  *     new Point3D(-50, 0, 0), new Point3D(50, 0, 0),
43  *     Color.RED, 2.0
44  * ));
45  *
46  * // Add shapes to a named group for toggling visibility
47  * myObject.addShape(labelShape, "labels");
48  * myObject.hideGroup("labels");  // hide all shapes in "labels" group
49  * myObject.showGroup("labels");  // show them again
50  *
51  * // Add to scene
52  * viewPanel.getRootShapeCollection().addShape(myObject);
53  * }</pre>
54  *
55  * <p><b>Level-of-detail slicing:</b></p>
56  * <p>Textured polygons within the composite shape are automatically sliced into smaller
57  * triangles based on distance from the viewer. This provides perspective-correct texture
58  * mapping without requiring hardware support. The slicing factor adapts dynamically.</p>
59  *
60  * <p><b>Extending this class:</b></p>
61  * <p>Override {@link #beforeTransformHook} to customize shape appearance or behavior
62  * on each frame (e.g., animations, dynamic geometry updates).</p>
63  *
64  * @see SubShape wrapper for individual sub-shapes with group and visibility support
65  * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape the base shape class
66  * @see eu.svjatoslav.sixth.e3d.renderer.raster.slicer.Slicer the level-of-detail polygon slicer
67  */
68 public class AbstractCompositeShape extends AbstractShape {
69     private final List<SubShape> originalSubShapes = new ArrayList<>();
70     private final ViewSpaceTracker viewSpaceTracker;
71     double currentSliceFactor = 5;
72     private List<AbstractShape> renderedSubShapes = new ArrayList<>();
73     private boolean slicingOutdated = true;
74     private Transform transform;
75     private LightingManager lightingManager;
76
77     /**
78      * Creates a composite shape at the world origin with no rotation.
79      */
80     public AbstractCompositeShape() {
81         this(new Transform());
82     }
83
84     /**
85      * Creates a composite shape at the specified location with no rotation.
86      *
87      * @param location the position in world space
88      */
89     public AbstractCompositeShape(final Point3D location) {
90         this(new Transform(location));
91     }
92
93     /**
94      * Creates a composite shape with the specified transform (position and orientation).
95      *
96      * @param transform the initial transform defining position and rotation
97      */
98     public AbstractCompositeShape(final Transform transform) {
99         this.transform = transform;
100         viewSpaceTracker = new ViewSpaceTracker();
101     }
102
103     /**
104      * Adds a sub-shape to this composite shape without a group identifier.
105      *
106      * @param shape the shape to add
107      */
108     public void addShape(final AbstractShape shape) {
109         addShape(shape, null);
110     }
111
112     /**
113      * Adds a sub-shape to this composite shape with an optional group identifier.
114      *
115      * <p>Grouped shapes can be shown, hidden, or removed together using
116      * {@link #showGroup}, {@link #hideGroup}, and {@link #removeGroup}.</p>
117      *
118      * @param shape   the shape to add
119      * @param groupId the group identifier, or {@code null} for ungrouped shapes
120      */
121     public void addShape(final AbstractShape shape, final String groupId) {
122         final SubShape subShape = new SubShape(shape);
123         subShape.setGroup(groupId);
124         subShape.setVisible(true);
125         originalSubShapes.add(subShape);
126         slicingOutdated = true;
127     }
128
129     /**
130      * This method should be overridden by anyone wanting to customize shape
131      * before it is rendered.
132      *
133      * @param transformPipe the current transform stack
134      * @param context       the rendering context for the current frame
135      */
136     public void beforeTransformHook(final TransformStack transformPipe,
137                                     final RenderingContext context) {
138     }
139
140     /**
141      * Returns the world-space position of this composite shape.
142      *
143      * @return the translation component of this shape's transform
144      */
145     public Point3D getLocation() {
146         return transform.getTranslation();
147     }
148
149     /**
150      * Returns the list of all sub-shapes (including hidden ones).
151      *
152      * @return the internal list of sub-shapes
153      */
154     public List<SubShape> getOriginalSubShapes() {
155         return originalSubShapes;
156     }
157
158     /**
159      * Returns the view-space tracker that monitors the distance
160      * and angle between the camera and this shape for level-of-detail adjustments.
161      *
162      * @return the view-space tracker for this shape
163      */
164     public ViewSpaceTracker getViewSpaceTracker() {
165         return viewSpaceTracker;
166     }
167
168     /**
169      * Hides all sub-shapes belonging to the specified group.
170      * Hidden shapes are not rendered but remain in the collection.
171      *
172      * @param groupIdentifier the group to hide
173      * @see #showGroup(String)
174      * @see #removeGroup(String)
175      */
176     public void hideGroup(final String groupIdentifier) {
177         for (int i = 0; i < originalSubShapes.size(); i++) {
178             final SubShape subShape = originalSubShapes.get(i);
179             if (subShape.matchesGroup(groupIdentifier)) {
180                 subShape.setVisible(false);
181                 slicingOutdated = true;
182             }
183         }
184     }
185
186     private boolean isReslicingNeeded(double proposedNewSliceFactor, double currentSliceFactor) {
187
188         if (slicingOutdated)
189             return true;
190
191         // reslice if there is significant difference between proposed and current slice factor
192         if (proposedNewSliceFactor > currentSliceFactor) {
193             final double tmp = proposedNewSliceFactor;
194             proposedNewSliceFactor = currentSliceFactor;
195             currentSliceFactor = tmp;
196         }
197
198         return (currentSliceFactor / proposedNewSliceFactor) > 1.5d;
199     }
200
201     /**
202      * Permanently removes all sub-shapes belonging to the specified group.
203      *
204      * @param groupIdentifier the group to remove
205      * @see #hideGroup(String)
206      */
207     public void removeGroup(final String groupIdentifier) {
208         final java.util.Iterator<SubShape> iterator = originalSubShapes
209                 .iterator();
210
211         while (iterator.hasNext()) {
212             final SubShape subShape = iterator.next();
213             if (subShape.matchesGroup(groupIdentifier)) {
214                 iterator.remove();
215                 slicingOutdated = true;
216             }
217         }
218     }
219
220     /**
221      * Returns all sub-shapes belonging to the specified group.
222      *
223      * @param groupIdentifier the group identifier to match
224      * @return list of matching sub-shapes
225      */
226     public List<SubShape> getGroup(final String groupIdentifier) {
227         final List<SubShape> result = new ArrayList<>();
228         for (int i = 0; i < originalSubShapes.size(); i++) {
229             final SubShape subShape = originalSubShapes.get(i);
230             if (subShape.matchesGroup(groupIdentifier))
231                 result.add(subShape);
232         }
233         return result;
234     }
235
236     private void resliceIfNeeded() {
237
238         final double proposedSliceFactor = viewSpaceTracker.proposeSliceFactor();
239
240         if (isReslicingNeeded(proposedSliceFactor, currentSliceFactor)) {
241             currentSliceFactor = proposedSliceFactor;
242             reslice();
243         }
244     }
245
246     /**
247      * Paint solid elements of this composite shape into given color.
248      *
249      * @param color the color to apply to all solid sub-shapes
250      */
251     public void setColor(final Color color) {
252         for (final SubShape subShape : getOriginalSubShapes()) {
253             final AbstractShape shape = subShape.getShape();
254
255             if (shape instanceof SolidPolygon)
256                 ((SolidPolygon) shape).setColor(color);
257
258             if (shape instanceof Line)
259                 ((Line) shape).color = color;
260         }
261     }
262
263     /**
264      * Assigns a group identifier to all sub-shapes that currently have no group.
265      *
266      * @param groupIdentifier the group to assign to ungrouped shapes
267      */
268     public void setGroupForUngrouped(final String groupIdentifier) {
269         for (int i = 0; i < originalSubShapes.size(); i++) {
270             final SubShape subShape = originalSubShapes.get(i);
271             if (subShape.isUngrouped())
272                 subShape.setGroup(groupIdentifier);
273         }
274     }
275
276     @Override
277     public void setMouseInteractionController(
278             final MouseInteractionController mouseInteractionController) {
279         super.setMouseInteractionController(mouseInteractionController);
280
281         for (final SubShape subShape : originalSubShapes)
282             subShape.getShape().setMouseInteractionController(
283                     mouseInteractionController);
284
285         slicingOutdated = true;
286
287     }
288
289     /**
290      * Replaces this shape's transform (position and orientation).
291      *
292      * @param transform the new transform to apply
293      */
294     public void setTransform(final Transform transform) {
295         this.transform = transform;
296     }
297
298     /**
299      * Sets the lighting manager for this composite shape and enables shading on all SolidPolygon sub-shapes.
300      *
301      * @param lightingManager the lighting manager to use for shading calculations
302      */
303     public void setLightingManager(final LightingManager lightingManager) {
304         this.lightingManager = lightingManager;
305         applyShadingToPolygons();
306     }
307
308     /**
309      * Enables or disables shading for all SolidPolygon sub-shapes.
310      *
311      * @param shadingEnabled true to enable shading, false to disable
312      */
313     public void setShadingEnabled(final boolean shadingEnabled) {
314         for (final SubShape subShape : getOriginalSubShapes()) {
315             final AbstractShape shape = subShape.getShape();
316             if (shape instanceof SolidPolygon) {
317                 ((SolidPolygon) shape).setShadingEnabled(shadingEnabled, lightingManager);
318             }
319         }
320     }
321
322     private void applyShadingToPolygons() {
323         if (lightingManager == null)
324             return;
325
326         for (final SubShape subShape : getOriginalSubShapes()) {
327             final AbstractShape shape = subShape.getShape();
328             if (shape instanceof SolidPolygon) {
329                 ((SolidPolygon) shape).setShadingEnabled(true, lightingManager);
330             }
331         }
332     }
333
334     /**
335      * Enables or disables backface culling for all SolidPolygon and TexturedPolygon sub-shapes.
336      *
337      * @param backfaceCulling {@code true} to enable backface culling, {@code false} to disable
338      */
339     public void setBackfaceCulling(final boolean backfaceCulling) {
340         for (final SubShape subShape : getOriginalSubShapes()) {
341             final AbstractShape shape = subShape.getShape();
342             if (shape instanceof SolidPolygon) {
343                 ((SolidPolygon) shape).setBackfaceCulling(backfaceCulling);
344             } else if (shape instanceof TexturedPolygon) {
345                 ((TexturedPolygon) shape).setBackfaceCulling(backfaceCulling);
346             }
347         }
348     }
349
350     /**
351      * Makes all sub-shapes belonging to the specified group visible.
352      *
353      * @param groupIdentifier the group to show
354      * @see #hideGroup(String)
355      */
356     public void showGroup(final String groupIdentifier) {
357         for (int i = 0; i < originalSubShapes.size(); i++) {
358             final SubShape subShape = originalSubShapes.get(i);
359             if (subShape.matchesGroup(groupIdentifier)) {
360                 subShape.setVisible(true);
361                 slicingOutdated = true;
362             }
363         }
364     }
365
366     private void reslice() {
367         slicingOutdated = false;
368
369         final List<AbstractShape> result = new ArrayList<>();
370
371         final Slicer slicer = new Slicer(currentSliceFactor);
372         for (int i = 0; i < originalSubShapes.size(); i++) {
373             final SubShape subShape = originalSubShapes.get(i);
374             if (subShape.isVisible()) {
375                 if (subShape.getShape() instanceof TexturedPolygon)
376                     slicer.slice((TexturedPolygon) subShape.getShape());
377                 else
378                     result.add(subShape.getShape());
379             }
380         }
381
382         result.addAll(slicer.getResult());
383
384         renderedSubShapes = result;
385     }
386
387     @Override
388     public void transform(final TransformStack transformPipe,
389                           final RenderAggregator aggregator, final RenderingContext context) {
390
391         // add current composite shape transform to the end of the transform
392         // pipeline
393         transformPipe.addTransform(transform);
394
395         viewSpaceTracker.analyze(transformPipe, context);
396
397         beforeTransformHook(transformPipe, context);
398
399         // hack, to get somewhat perspective correct textures
400         resliceIfNeeded();
401
402         // transform rendered subshapes
403         for (final AbstractShape shape : renderedSubShapes)
404             shape.transform(transformPipe, aggregator, context);
405
406         transformPipe.dropTransform();
407     }
408
409 }