2 * Sixth 3D engine. Author: Svjatoslav Agejenko.
3 * This project is released under Creative Commons Zero (CC0) license.
5 package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base;
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;
21 import java.util.ArrayList;
22 import java.util.List;
25 * A composite shape that groups multiple sub-shapes into a single logical unit.
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>
32 * <p><b>Usage example - creating a custom composite shape:</b></p>
34 * // Create a composite shape at position (0, 0, 200)
35 * AbstractCompositeShape myObject = new AbstractCompositeShape(
36 * new Point3D(0, 0, 200)
40 * myObject.addShape(new Line(
41 * new Point3D(-50, 0, 0), new Point3D(50, 0, 0),
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
51 * viewPanel.getRootShapeCollection().addShape(myObject);
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>
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>
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
67 public class AbstractCompositeShape extends AbstractShape {
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.
73 private final List<SubShape> originalSubShapes = new ArrayList<>();
76 * Tracks the distance and angle between the camera and this shape to compute
77 * an appropriate slice factor for level-of-detail adjustments.
79 private final ViewSpaceTracker viewSpaceTracker;
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.
87 double currentSliceFactor = 5;
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.
94 private List<AbstractShape> renderedSubShapes = new ArrayList<>();
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.
100 private boolean slicingOutdated = true;
103 * The position and orientation transform for this composite shape.
104 * Applied to all sub-shapes during the rendering transform pass.
106 private Transform transform;
109 * Creates a composite shape at the world origin with no rotation.
111 public AbstractCompositeShape() {
112 this(new Transform());
116 * Creates a composite shape at the specified location with no rotation.
118 * @param location the position in world space
120 public AbstractCompositeShape(final Point3D location) {
121 this(new Transform(location));
125 * Creates a composite shape with the specified transform (position and orientation).
127 * @param transform the initial transform defining position and rotation
129 public AbstractCompositeShape(final Transform transform) {
130 this.transform = transform;
131 viewSpaceTracker = new ViewSpaceTracker();
135 * Adds a sub-shape to this composite shape without a group identifier.
137 * @param shape the shape to add
139 public void addShape(final AbstractShape shape) {
140 addShape(shape, null);
144 * Adds a sub-shape to this composite shape with an optional group identifier.
146 * <p>Grouped shapes can be shown, hidden, or removed together using
147 * {@link #showGroup}, {@link #hideGroup}, and {@link #removeGroup}.</p>
149 * @param shape the shape to add
150 * @param groupId the group identifier, or {@code null} for ungrouped shapes
152 public void addShape(final AbstractShape shape, final String groupId) {
153 originalSubShapes.add(new SubShape(shape, groupId, true));
154 slicingOutdated = true;
158 * This method should be overridden by anyone wanting to customize the shape
159 * before it is rendered.
161 * @param transformPipe the current transform stack
162 * @param context the rendering context for the current frame
164 public void beforeTransformHook(final TransformStack transformPipe,
165 final RenderingContext context) {
169 * Returns the world-space position of this composite shape.
171 * @return the translation component of this shape's transform
173 public Point3D getLocation() {
174 return transform.getTranslation();
178 * Returns the list of all sub-shapes (including hidden ones).
180 * @return the internal list of sub-shapes
182 public List<SubShape> getOriginalSubShapes() {
183 return originalSubShapes;
187 * Returns the view-space tracker that monitors the distance
188 * and angle between the camera and this shape for level-of-detail adjustments.
190 * @return the view-space tracker for this shape
192 public ViewSpaceTracker getViewSpaceTracker() {
193 return viewSpaceTracker;
197 * Hides all sub-shapes belonging to the specified group.
198 * Hidden shapes are not rendered but remain in the collection.
200 * @param groupIdentifier the group to hide
201 * @see #showGroup(String)
202 * @see #removeGroup(String)
204 public void hideGroup(final String groupIdentifier) {
205 for (final SubShape subShape : originalSubShapes) {
206 if (subShape.matchesGroup(groupIdentifier)) {
207 subShape.setVisible(false);
208 slicingOutdated = true;
214 * Determines whether textured polygons need to be re-sliced based on slice factor change.
216 * Re-slicing is needed if the slicing state is marked outdated, or if the ratio between
217 * the larger and smaller slice factor exceeds 1.5x. This threshold prevents frequent
218 * re-slicing for minor view changes while ensuring significant LOD changes trigger updates.
220 * @param proposedNewSliceFactor the slice factor computed from current view distance
221 * @param currentSliceFactor the slice factor currently in use
222 * @return {@code true} if re-slicing should be performed
224 private boolean isReslicingNeeded(final double proposedNewSliceFactor, final double currentSliceFactor) {
229 // reslice if there is significant difference between proposed and current slice factor
230 final double larger = Math.max(proposedNewSliceFactor, currentSliceFactor);
231 final double smaller = Math.min(proposedNewSliceFactor, currentSliceFactor);
233 return (larger / smaller) > 1.5d;
237 * Permanently removes all sub-shapes belonging to the specified group.
239 * @param groupIdentifier the group to remove
240 * @see #hideGroup(String)
242 public void removeGroup(final String groupIdentifier) {
243 final java.util.Iterator<SubShape> iterator = originalSubShapes
246 while (iterator.hasNext()) {
247 final SubShape subShape = iterator.next();
248 if (subShape.matchesGroup(groupIdentifier)) {
250 slicingOutdated = true;
256 * Returns all sub-shapes belonging to the specified group.
258 * @param groupIdentifier the group identifier to match
259 * @return list of matching sub-shapes
261 public List<SubShape> getGroup(final String groupIdentifier) {
262 final List<SubShape> result = new ArrayList<>();
263 for (int i = 0; i < originalSubShapes.size(); i++) {
264 final SubShape subShape = originalSubShapes.get(i);
265 if (subShape.matchesGroup(groupIdentifier))
266 result.add(subShape);
272 * Checks if re-slicing is needed and performs it if so.
274 * @param context the rendering context for logging
276 private void resliceIfNeeded(final RenderingContext context) {
278 final double proposedSliceFactor = viewSpaceTracker.proposeSliceFactor();
280 if (isReslicingNeeded(proposedSliceFactor, currentSliceFactor)) {
281 currentSliceFactor = proposedSliceFactor;
287 * Paint solid elements of this composite shape into given color.
289 * @param color the color to apply to all solid sub-shapes
291 public void setColor(final Color color) {
292 for (final SubShape subShape : getOriginalSubShapes()) {
293 final AbstractShape shape = subShape.getShape();
295 if (shape instanceof SolidPolygon)
296 ((SolidPolygon) shape).setColor(color);
298 if (shape instanceof Line)
299 ((Line) shape).color = color;
304 * Assigns a group identifier to all sub-shapes that currently have no group.
306 * @param groupIdentifier the group to assign to ungrouped shapes
308 public void setGroupForUngrouped(final String groupIdentifier) {
309 for (int i = 0; i < originalSubShapes.size(); i++) {
310 final SubShape subShape = originalSubShapes.get(i);
311 if (subShape.isUngrouped())
312 subShape.setGroup(groupIdentifier);
317 public void setMouseInteractionController(
318 final MouseInteractionController mouseInteractionController) {
319 super.setMouseInteractionController(mouseInteractionController);
321 for (final SubShape subShape : originalSubShapes)
322 subShape.getShape().setMouseInteractionController(
323 mouseInteractionController);
325 slicingOutdated = true;
330 * Replaces this shape's transform (position and orientation).
332 * @param transform the new transform to apply
334 public void setTransform(final Transform transform) {
335 this.transform = transform;
339 * Enables or disables shading for all SolidPolygon sub-shapes.
340 * When enabled, polygons use the global lighting manager from the rendering
341 * context to calculate flat shading based on light sources.
343 * @param shadingEnabled {@code true} to enable shading, {@code false} to disable
345 public void setShadingEnabled(final boolean shadingEnabled) {
346 for (final SubShape subShape : getOriginalSubShapes()) {
347 final AbstractShape shape = subShape.getShape();
348 if (shape instanceof SolidPolygon) {
349 ((SolidPolygon) shape).setShadingEnabled(shadingEnabled);
352 // TODO: if shape is abstract composite, it seems that it would be good to enabled sharding recursively there too
357 * Enables or disables backface culling for all SolidPolygon and TexturedPolygon sub-shapes.
359 * @param backfaceCulling {@code true} to enable backface culling, {@code false} to disable
361 public void setBackfaceCulling(final boolean backfaceCulling) {
362 for (final SubShape subShape : getOriginalSubShapes()) {
363 final AbstractShape shape = subShape.getShape();
364 if (shape instanceof SolidPolygon) {
365 ((SolidPolygon) shape).setBackfaceCulling(backfaceCulling);
366 } else if (shape instanceof TexturedPolygon) {
367 ((TexturedPolygon) shape).setBackfaceCulling(backfaceCulling);
373 * Makes all sub-shapes belonging to the specified group visible.
375 * @param groupIdentifier the group to show
376 * @see #hideGroup(String)
378 public void showGroup(final String groupIdentifier) {
379 for (int i = 0; i < originalSubShapes.size(); i++) {
380 final SubShape subShape = originalSubShapes.get(i);
381 if (subShape.matchesGroup(groupIdentifier)) {
382 subShape.setVisible(true);
383 slicingOutdated = true;
389 * Re-slices all textured polygons and rebuilds the rendered sub-shapes list.
390 * Logs the operation to the debug log buffer if available.
392 * @param context the rendering context for logging, may be {@code null}
394 private void reslice(final RenderingContext context) {
395 slicingOutdated = false;
397 final List<AbstractShape> result = new ArrayList<>();
399 final Slicer slicer = new Slicer(currentSliceFactor);
400 int texturedPolygonCount = 0;
401 int otherShapeCount = 0;
403 for (int i = 0; i < originalSubShapes.size(); i++) {
404 final SubShape subShape = originalSubShapes.get(i);
405 if (subShape.isVisible()) {
406 if (subShape.getShape() instanceof TexturedPolygon) {
407 slicer.slice((TexturedPolygon) subShape.getShape());
408 texturedPolygonCount++;
410 result.add(subShape.getShape());
416 result.addAll(slicer.getResult());
418 renderedSubShapes = result;
420 // Log to developer tools console if available
421 if (context != null && context.debugLogBuffer != null) {
422 context.debugLogBuffer.log("reslice: " + getClass().getSimpleName()
423 + " sliceFactor=" + String.format("%.2f", currentSliceFactor)
424 + " texturedPolygons=" + texturedPolygonCount
425 + " otherShapes=" + otherShapeCount
426 + " resultingTexturedPolygons=" + slicer.getResult().size());
431 public void transform(final TransformStack transformPipe,
432 final RenderAggregator aggregator, final RenderingContext context) {
434 // Add the current composite shape transform to the end of the transform
436 transformPipe.addTransform(transform);
438 viewSpaceTracker.analyze(transformPipe, context);
440 beforeTransformHook(transformPipe, context);
442 resliceIfNeeded(context);
444 // transform rendered subshapes
445 for (final AbstractShape shape : renderedSubShapes)
446 shape.transform(transformPipe, aggregator, context);
448 transformPipe.dropTransform();