9aba6d48f696153abd3a40b30eef940e9000606d
[aukio-3d.git] /
1 /*
2  * Aukio 3D engine. Author: Svjatoslav Agejenko.
3  * This project is released under Creative Commons Zero (CC0) license.
4  */
5 package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite;
6
7 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
8 import eu.svjatoslav.aukio.e3d.renderer.raster.gi.LightmappedTriangle;
9 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape;
10 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape;
11 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
12 import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
13
14 import java.util.ArrayList;
15 import java.util.List;
16
17 /**
18  * Composite shape whose solid polygons can render as lightmapped
19  * triangles for the global illumination system.
20  *
21  * <p><b>Lightmapping:</b> when enabled, every {@link SolidPolygon}
22  * (including those inside nested composites, which are flattened) is
23  * wrapped as a {@link LightmappedTriangle} with its own lightmap texture,
24  * and the GI system paints light across its surface instead of leaving
25  * it a single flat color. Rebuild is automatic via the normal
26  * cache-invalidation path.</p>
27  *
28  * <p><b>Ordering:</b> none required — the engine's z-buffer resolves
29  * visibility per pixel, so this class needs no spatial ordering
30  * structure of its own. Nested composites are flattened assuming
31  * identity transforms (vertices are used in the composite's local
32  * space); animate transforms above this composite, not inside it.</p>
33  *
34  * @see LightmappedTriangle the per-fragment lightmap carrier
35  * @see AbstractCompositeShape the base composite (fan-triangulates
36  *      N-vertex polygons when building the render list)
37  */
38 public class LightmappedCompositeShape extends AbstractCompositeShape {
39
40     /**
41      * When true, the composite's polygons render as
42      * {@link LightmappedTriangle}s: each polygon gets its own lightmap
43      * texture and the GI system paints light across its surface.
44      */
45     private boolean lightmappingEnabled;
46
47     /** World units per lightmap texel (smaller = finer GI detail). */
48     private double lightmapUnitsPerTexel = 12.0;
49
50     /**
51      * Replaces solid polygons with lightmapped wrappers when lightmapping
52      * is enabled; otherwise returns the list unchanged.
53      *
54      * @param renderList the render list built from the shape registry
55      * @return lightmapped wrappers + non-polygon passthrough shapes
56      */
57     @Override
58     protected List<AbstractShape> postprocessRenderList(final List<AbstractShape> renderList) {
59         if (!lightmappingEnabled)
60             return renderList;
61
62         final List<AbstractShape> result = new ArrayList<>(renderList.size());
63         for (final AbstractShape shape : renderList) {
64             if (shape instanceof SolidPolygon) {
65                 wrapLightmapped((SolidPolygon) shape, result);
66             } else if (shape instanceof AbstractCompositeShape) {
67                 // Flatten nested composites. Assumes identity transforms
68                 // on the nested composite chain.
69                 for (final SolidPolygon polygon
70                         : ((AbstractCompositeShape) shape).extractSolidPolygons())
71                     wrapLightmapped(polygon, result);
72             } else {
73                 result.add(shape);
74             }
75         }
76         return result;
77     }
78
79     /**
80      * Enables or disables lightmapping. When enabled, the composite's
81      * polygons render as {@link LightmappedTriangle}s with per-polygon
82      * lightmap textures filled by the global illumination system.
83      * Rebuilds the render list on the next frame.
84      *
85      * @param enabled true to render polygons lightmapped
86      */
87     public void setLightmappingEnabled(final boolean enabled) {
88         if (lightmappingEnabled != enabled) {
89             lightmappingEnabled = enabled;
90             setCacheNeedsRebuild(true);
91         }
92     }
93
94     /**
95      * Returns whether lightmapping is enabled.
96      *
97      * @return true when polygons render as lightmapped triangles
98      */
99     public boolean isLightmappingEnabled() {
100         return lightmappingEnabled;
101     }
102
103     /**
104      * Sets the lightmap resolution. Default 12 world units per texel:
105      * a 100-unit wall cell gets an 8x8 lightmap. Halving the units
106      * quadruples the GI tracing work; finer texels are the ONLY way to
107      * smoother shadow edges (there is no upsampling). Takes effect on the
108      * next render list rebuild.
109      *
110      * @param unitsPerTexel world units per texel
111      */
112     public void setLightmapUnitsPerTexel(final double unitsPerTexel) {
113         lightmapUnitsPerTexel = unitsPerTexel;
114         setCacheNeedsRebuild(true);
115     }
116
117     /**
118      * Fan-triangulates a polygon (triangles pass through as-is) and wraps
119      * each triangle into a lightmapped textured triangle with the same
120      * geometry, color and culling. The initial texture is dim; the GI
121      * system converges it to full lighting within seconds.
122      *
123      * @param polygon the polygon to wrap (any vertex count >= 3)
124      * @param out     the list receiving the lightmapped triangles
125      */
126     private void wrapLightmapped(final SolidPolygon polygon, final List<AbstractShape> out) {
127         final int vertexCount = polygon.getVertexCount();
128         if (vertexCount == 3) {
129             out.add(wrapTriangle(polygon));
130             return;
131         }
132         // Fan: anchor vertex 0, then consecutive pairs
133         final Point3D anchor = polygon.vertices.get(0).coordinate;
134         for (int i = 1; i + 1 < vertexCount; i++) {
135             final SolidPolygon triangle = new SolidPolygon(
136                     anchor,
137                     polygon.vertices.get(i).coordinate,
138                     polygon.vertices.get(i + 1).coordinate,
139                     polygon.getColor());
140             triangle.setShadingEnabled(polygon.isShadingEnabled());
141             triangle.setBackfaceCulling(polygon.isBackfaceCullingEnabled());
142             triangle.setMouseInteractionController(polygon.mouseInteractionController);
143             out.add(wrapTriangle(triangle));
144         }
145     }
146
147     /**
148      * Wraps one triangle into a lightmapped textured triangle with the
149      * same geometry, color and culling.
150      */
151     private AbstractCoordinateShape wrapTriangle(final SolidPolygon polygon) {
152         final Point3D a = polygon.vertices.get(0).coordinate;
153         final Point3D b = polygon.vertices.get(1).coordinate;
154         final Point3D c = polygon.vertices.get(2).coordinate;
155
156         // Normal: right-handed cross(b-a, c-a).
157         final double e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
158         final double e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
159         double nx = e1y * e2z - e1z * e2y;
160         double ny = e1z * e2x - e1x * e2z;
161         double nz = e1x * e2y - e1y * e2x;
162         final double len = Math.sqrt(nx * nx + ny * ny + nz * nz);
163         if (len > 0) {
164             nx /= len;
165             ny /= len;
166             nz /= len;
167         }
168
169         final LightmappedTriangle triangle = new LightmappedTriangle(
170                 a, b, c, polygon.getColor(), lightmapUnitsPerTexel,
171                 nx, ny, nz);
172         triangle.setBackfaceCulling(polygon.isBackfaceCullingEnabled());
173         triangle.setMouseInteractionController(polygon.mouseInteractionController);
174         return triangle;
175     }
176 }