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.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;
22 import java.util.ArrayList;
23 import java.util.List;
26 * A composite shape that groups multiple sub-shapes into a single logical unit.
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>
33 * <p><b>Usage example - creating a custom composite shape:</b></p>
35 * // Create a composite shape at position (0, 0, 200)
36 * AbstractCompositeShape myObject = new AbstractCompositeShape(
37 * new Point3D(0, 0, 200)
41 * myObject.addShape(new Line(
42 * new Point3D(-50, 0, 0), new Point3D(50, 0, 0),
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
52 * viewPanel.getRootShapeCollection().addShape(myObject);
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>
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>
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
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;
78 * Creates a composite shape at the world origin with no rotation.
80 public AbstractCompositeShape() {
81 this(new Transform());
85 * Creates a composite shape at the specified location with no rotation.
87 * @param location the position in world space
89 public AbstractCompositeShape(final Point3D location) {
90 this(new Transform(location));
94 * Creates a composite shape with the specified transform (position and orientation).
96 * @param transform the initial transform defining position and rotation
98 public AbstractCompositeShape(final Transform transform) {
99 this.transform = transform;
100 viewSpaceTracker = new ViewSpaceTracker();
104 * Adds a sub-shape to this composite shape without a group identifier.
106 * @param shape the shape to add
108 public void addShape(final AbstractShape shape) {
109 addShape(shape, null);
113 * Adds a sub-shape to this composite shape with an optional group identifier.
115 * <p>Grouped shapes can be shown, hidden, or removed together using
116 * {@link #showGroup}, {@link #hideGroup}, and {@link #removeGroup}.</p>
118 * @param shape the shape to add
119 * @param groupId the group identifier, or {@code null} for ungrouped shapes
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;
130 * This method should be overridden by anyone wanting to customize shape
131 * before it is rendered.
133 * @param transformPipe the current transform stack
134 * @param context the rendering context for the current frame
136 public void beforeTransformHook(final TransformStack transformPipe,
137 final RenderingContext context) {
141 * Returns the world-space position of this composite shape.
143 * @return the translation component of this shape's transform
145 public Point3D getLocation() {
146 return transform.getTranslation();
150 * Returns the list of all sub-shapes (including hidden ones).
152 * @return the internal list of sub-shapes
154 public List<SubShape> getOriginalSubShapes() {
155 return originalSubShapes;
159 * Returns the view-space tracker that monitors the distance
160 * and angle between the camera and this shape for level-of-detail adjustments.
162 * @return the view-space tracker for this shape
164 public ViewSpaceTracker getViewSpaceTracker() {
165 return viewSpaceTracker;
169 * Hides all sub-shapes belonging to the specified group.
170 * Hidden shapes are not rendered but remain in the collection.
172 * @param groupIdentifier the group to hide
173 * @see #showGroup(String)
174 * @see #removeGroup(String)
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;
186 private boolean isReslicingNeeded(double proposedNewSliceFactor, double currentSliceFactor) {
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;
198 return (currentSliceFactor / proposedNewSliceFactor) > 1.5d;
202 * Permanently removes all sub-shapes belonging to the specified group.
204 * @param groupIdentifier the group to remove
205 * @see #hideGroup(String)
207 public void removeGroup(final String groupIdentifier) {
208 final java.util.Iterator<SubShape> iterator = originalSubShapes
211 while (iterator.hasNext()) {
212 final SubShape subShape = iterator.next();
213 if (subShape.matchesGroup(groupIdentifier)) {
215 slicingOutdated = true;
221 * Returns all sub-shapes belonging to the specified group.
223 * @param groupIdentifier the group identifier to match
224 * @return list of matching sub-shapes
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);
236 private void resliceIfNeeded() {
238 final double proposedSliceFactor = viewSpaceTracker.proposeSliceFactor();
240 if (isReslicingNeeded(proposedSliceFactor, currentSliceFactor)) {
241 currentSliceFactor = proposedSliceFactor;
247 * Paint solid elements of this composite shape into given color.
249 * @param color the color to apply to all solid sub-shapes
251 public void setColor(final Color color) {
252 for (final SubShape subShape : getOriginalSubShapes()) {
253 final AbstractShape shape = subShape.getShape();
255 if (shape instanceof SolidPolygon)
256 ((SolidPolygon) shape).setColor(color);
258 if (shape instanceof Line)
259 ((Line) shape).color = color;
264 * Assigns a group identifier to all sub-shapes that currently have no group.
266 * @param groupIdentifier the group to assign to ungrouped shapes
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);
277 public void setMouseInteractionController(
278 final MouseInteractionController mouseInteractionController) {
279 super.setMouseInteractionController(mouseInteractionController);
281 for (final SubShape subShape : originalSubShapes)
282 subShape.getShape().setMouseInteractionController(
283 mouseInteractionController);
285 slicingOutdated = true;
290 * Replaces this shape's transform (position and orientation).
292 * @param transform the new transform to apply
294 public void setTransform(final Transform transform) {
295 this.transform = transform;
299 * Sets the lighting manager for this composite shape and enables shading on all SolidPolygon sub-shapes.
301 * @param lightingManager the lighting manager to use for shading calculations
303 public void setLightingManager(final LightingManager lightingManager) {
304 this.lightingManager = lightingManager;
305 applyShadingToPolygons();
309 * Enables or disables shading for all SolidPolygon sub-shapes.
311 * @param shadingEnabled true to enable shading, false to disable
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);
322 private void applyShadingToPolygons() {
323 if (lightingManager == null)
326 for (final SubShape subShape : getOriginalSubShapes()) {
327 final AbstractShape shape = subShape.getShape();
328 if (shape instanceof SolidPolygon) {
329 ((SolidPolygon) shape).setShadingEnabled(true, lightingManager);
335 * Enables or disables backface culling for all SolidPolygon and TexturedPolygon sub-shapes.
337 * @param backfaceCulling {@code true} to enable backface culling, {@code false} to disable
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);
351 * Makes all sub-shapes belonging to the specified group visible.
353 * @param groupIdentifier the group to show
354 * @see #hideGroup(String)
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;
366 private void reslice() {
367 slicingOutdated = false;
369 final List<AbstractShape> result = new ArrayList<>();
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());
378 result.add(subShape.getShape());
382 result.addAll(slicer.getResult());
384 renderedSubShapes = result;
388 public void transform(final TransformStack transformPipe,
389 final RenderAggregator aggregator, final RenderingContext context) {
391 // add current composite shape transform to the end of the transform
393 transformPipe.addTransform(transform);
395 viewSpaceTracker.analyze(transformPipe, context);
397 beforeTransformHook(transformPipe, context);
399 // hack, to get somewhat perspective correct textures
402 // transform rendered subshapes
403 for (final AbstractShape shape : renderedSubShapes)
404 shape.transform(transformPipe, aggregator, context);
406 transformPipe.dropTransform();