094ec42f7a7df31ca0f462762c973a109c1f0848
[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.basic.texturedpolygon;
6
7 import eu.svjatoslav.sixth.e3d.geometry.Point2D;
8 import eu.svjatoslav.sixth.e3d.gui.RenderingContext;
9 import eu.svjatoslav.sixth.e3d.math.Vertex;
10 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape;
11 import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture;
12 import eu.svjatoslav.sixth.e3d.renderer.raster.texture.TextureBitmap;
13
14 import java.awt.Color;
15
16 import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon;
17
18 /**
19  * A textured triangle renderer with perspective-correct texture mapping.
20  *
21  * <p>This class renders triangles with UV-mapped textures. For large triangles,
22  * the rendering may be sliced into smaller pieces for better perspective correction.</p>
23  *
24  * <p><b>Perspective-correct texture rendering:</b></p>
25  * <ul>
26  *   <li>Small polygons are rendered without perspective correction</li>
27  *   <li>Larger polygons are sliced into smaller pieces for accurate perspective</li>
28  * </ul>
29  *
30  * @see Texture
31  * @see Vertex#textureCoordinate
32  */
33 public class TexturedPolygon extends AbstractCoordinateShape {
34
35     private static final ThreadLocal<PolygonBorderInterpolator[]> INTERPOLATORS =
36             ThreadLocal.withInitial(() -> new PolygonBorderInterpolator[]{
37                     new PolygonBorderInterpolator(), new PolygonBorderInterpolator(), new PolygonBorderInterpolator()
38             });
39
40     /**
41      * The texture to apply to this polygon.
42      */
43     public final Texture texture;
44
45     private boolean backfaceCulling = false;
46
47     private double totalTextureDistance = -1;
48
49     /**
50      * Creates a textured triangle with the specified vertices and texture.
51      *
52      * @param p1      the first vertex (must have textureCoordinate set)
53      * @param p2      the second vertex (must have textureCoordinate set)
54      * @param p3      the third vertex (must have textureCoordinate set)
55      * @param texture the texture to apply
56      */
57     public TexturedPolygon(Vertex p1, Vertex p2, Vertex p3, final Texture texture) {
58
59         super(p1, p2, p3);
60         this.texture = texture;
61     }
62
63     /**
64      * Computes the total UV distance between all texture coordinate pairs.
65      * Used to determine appropriate mipmap level.
66      */
67     private void computeTotalTextureDistance() {
68         // compute total texture distance
69         totalTextureDistance = coordinates[0].textureCoordinate.getDistanceTo(coordinates[1].textureCoordinate);
70         totalTextureDistance += coordinates[0].textureCoordinate.getDistanceTo(coordinates[2].textureCoordinate);
71         totalTextureDistance += coordinates[1].textureCoordinate.getDistanceTo(coordinates[2].textureCoordinate);
72     }
73
74     /**
75      * Draws a horizontal scanline between two edge interpolators with texture sampling.
76      *
77      * @param line1         the left edge interpolator
78      * @param line2         the right edge interpolator
79      * @param y             the Y coordinate of the scanline
80      * @param renderBuffer  the rendering context to draw into
81      * @param textureBitmap the texture bitmap to sample from
82      */
83     private void drawHorizontalLine(final PolygonBorderInterpolator line1,
84                                     final PolygonBorderInterpolator line2, final int y,
85                                     final RenderingContext renderBuffer,
86                                     final TextureBitmap textureBitmap) {
87
88         line1.setCurrentY(y);
89         line2.setCurrentY(y);
90
91         int x1 = line1.getX();
92         int x2 = line2.getX();
93
94         final double tx2, ty2;
95         final double tx1, ty1;
96
97         if (x1 <= x2) {
98
99             tx1 = line1.getTX() * textureBitmap.multiplicationFactor;
100             ty1 = line1.getTY() * textureBitmap.multiplicationFactor;
101
102             tx2 = line2.getTX() * textureBitmap.multiplicationFactor;
103             ty2 = line2.getTY() * textureBitmap.multiplicationFactor;
104
105         } else {
106             final int tmp = x1;
107             x1 = x2;
108             x2 = tmp;
109
110             tx1 = line2.getTX() * textureBitmap.multiplicationFactor;
111             ty1 = line2.getTY() * textureBitmap.multiplicationFactor;
112
113             tx2 = line1.getTX() * textureBitmap.multiplicationFactor;
114             ty2 = line1.getTY() * textureBitmap.multiplicationFactor;
115         }
116
117         final double realWidth = x2 - x1;
118         final double realX1 = x1;
119
120         if (x1 < 0)
121             x1 = 0;
122
123         if (x2 >= renderBuffer.width)
124             x2 = renderBuffer.width - 1;
125
126         int renderBufferOffset = (y * renderBuffer.width) + x1;
127         final int[] renderBufferPixels = renderBuffer.pixels;
128
129         final double twidth = tx2 - tx1;
130         final double theight = ty2 - ty1;
131
132         final double txStep = twidth / realWidth;
133         final double tyStep = theight / realWidth;
134
135         double tx = tx1 + txStep * (x1 - realX1);
136         double ty = ty1 + tyStep * (x1 - realX1);
137
138         final int[] texPixels = textureBitmap.pixels;
139         final int texW = textureBitmap.width;
140         final int texH = textureBitmap.height;
141         final int texWMinus1 = texW - 1;
142         final int texHMinus1 = texH - 1;
143
144         for (int x = x1; x < x2; x++) {
145
146             int itx = (int) tx;
147             int ity = (int) ty;
148
149             if (itx < 0) itx = 0;
150             else if (itx > texWMinus1) itx = texWMinus1;
151
152             if (ity < 0) ity = 0;
153             else if (ity > texHMinus1) ity = texHMinus1;
154
155             final int srcPixel = texPixels[ity * texW + itx];
156             final int srcAlpha = (srcPixel >> 24) & 0xff;
157
158             if (srcAlpha != 0) {
159                 if (srcAlpha == 255) {
160                     renderBufferPixels[renderBufferOffset] = srcPixel;
161                 } else {
162                     final int backgroundAlpha = 255 - srcAlpha;
163
164                     final int srcR = (srcPixel >> 16) & 0xff;
165                     final int srcG = (srcPixel >> 8) & 0xff;
166                     final int srcB = srcPixel & 0xff;
167
168                     final int destPixel = renderBufferPixels[renderBufferOffset];
169                     final int destR = (destPixel >> 16) & 0xff;
170                     final int destG = (destPixel >> 8) & 0xff;
171                     final int destB = destPixel & 0xff;
172
173                     final int r = ((destR * backgroundAlpha) + (srcR * srcAlpha)) / 256;
174                     final int g = ((destG * backgroundAlpha) + (srcG * srcAlpha)) / 256;
175                     final int b = ((destB * backgroundAlpha) + (srcB * srcAlpha)) / 256;
176
177                     renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
178                 }
179             }
180
181             tx += txStep;
182             ty += tyStep;
183             renderBufferOffset++;
184         }
185
186     }
187
188     /**
189      * Renders this textured triangle to the screen.
190      *
191      * <p>This method performs:</p>
192      * <ul>
193      *   <li>Backface culling check (if enabled)</li>
194      *   <li>Mouse interaction detection</li>
195      *   <li>Mipmap level selection based on screen coverage</li>
196      *   <li>Scanline rasterization with texture sampling</li>
197      * </ul>
198      *
199      * @param renderBuffer the rendering context containing the pixel buffer
200      */
201     @Override
202     public void paint(final RenderingContext renderBuffer) {
203
204         final Point2D projectedPoint1 = coordinates[0].onScreenCoordinate;
205         final Point2D projectedPoint2 = coordinates[1].onScreenCoordinate;
206         final Point2D projectedPoint3 = coordinates[2].onScreenCoordinate;
207
208         if (backfaceCulling) {
209             final double signedArea = (projectedPoint2.x - projectedPoint1.x)
210                     * (projectedPoint3.y - projectedPoint1.y)
211                     - (projectedPoint3.x - projectedPoint1.x)
212                     * (projectedPoint2.y - projectedPoint1.y);
213             if (signedArea >= 0)
214                 return;
215         }
216
217         projectedPoint1.roundToInteger();
218         projectedPoint2.roundToInteger();
219         projectedPoint3.roundToInteger();
220
221         if (mouseInteractionController != null)
222             if (renderBuffer.getMouseEvent() != null)
223                 if (pointWithinPolygon(
224                         renderBuffer.getMouseEvent().coordinate, projectedPoint1,
225                         projectedPoint2, projectedPoint3))
226                     renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController);
227
228         // Show polygon boundaries (for debugging)
229         if (renderBuffer.developerTools != null && renderBuffer.developerTools.showPolygonBorders)
230             showBorders(renderBuffer);
231
232         // find top-most point
233         int yTop = (int) projectedPoint1.y;
234
235         if (projectedPoint2.y < yTop)
236             yTop = (int) projectedPoint2.y;
237
238         if (projectedPoint3.y < yTop)
239             yTop = (int) projectedPoint3.y;
240
241         if (yTop < 0)
242             yTop = 0;
243
244         // find bottom-most point
245         int yBottom = (int) projectedPoint1.y;
246
247         if (projectedPoint2.y > yBottom)
248             yBottom = (int) projectedPoint2.y;
249
250         if (projectedPoint3.y > yBottom)
251             yBottom = (int) projectedPoint3.y;
252
253         if (yBottom >= renderBuffer.height)
254             yBottom = renderBuffer.height - 1;
255
256         // clamp to render Y bounds
257         yTop = Math.max(yTop, renderBuffer.renderMinY);
258         yBottom = Math.min(yBottom, renderBuffer.renderMaxY);
259         if (yTop >= yBottom)
260             return;
261
262         // paint
263         double totalVisibleDistance = projectedPoint1.getDistanceTo(projectedPoint2);
264         totalVisibleDistance += projectedPoint1.getDistanceTo(projectedPoint3);
265         totalVisibleDistance += projectedPoint2.getDistanceTo(projectedPoint3);
266
267         if (totalTextureDistance == -1)
268             computeTotalTextureDistance();
269         final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d;
270
271         final TextureBitmap zoomedBitmap = texture.getZoomedBitmap(scaleFactor);
272
273         final PolygonBorderInterpolator[] interp = INTERPOLATORS.get();
274         final PolygonBorderInterpolator polygonBorder1 = interp[0];
275         final PolygonBorderInterpolator polygonBorder2 = interp[1];
276         final PolygonBorderInterpolator polygonBorder3 = interp[2];
277
278         polygonBorder1.setPoints(projectedPoint1, projectedPoint2,
279                 coordinates[0].textureCoordinate,
280                 coordinates[1].textureCoordinate);
281         polygonBorder2.setPoints(projectedPoint1, projectedPoint3,
282                 coordinates[0].textureCoordinate,
283                 coordinates[2].textureCoordinate);
284         polygonBorder3.setPoints(projectedPoint2, projectedPoint3,
285                 coordinates[1].textureCoordinate,
286                 coordinates[2].textureCoordinate);
287
288         // Inline sort for 3 elements to avoid array allocation
289         PolygonBorderInterpolator a = polygonBorder1;
290         PolygonBorderInterpolator b = polygonBorder2;
291         PolygonBorderInterpolator c = polygonBorder3;
292         PolygonBorderInterpolator t;
293         if (a.compareTo(b) > 0) { t = a; a = b; b = t; }
294         if (b.compareTo(c) > 0) { t = b; b = c; c = t; }
295         if (a.compareTo(b) > 0) { t = a; a = b; b = t; }
296
297         for (int y = yTop; y < yBottom; y++)
298             if (a.containsY(y)) {
299                 if (b.containsY(y))
300                     drawHorizontalLine(a, b, y, renderBuffer, zoomedBitmap);
301                 else if (c.containsY(y))
302                     drawHorizontalLine(a, c, y, renderBuffer, zoomedBitmap);
303             } else if (b.containsY(y))
304                 if (c.containsY(y))
305                     drawHorizontalLine(b, c, y, renderBuffer, zoomedBitmap);
306
307     }
308
309     /**
310      * Checks if backface culling is enabled for this polygon.
311      *
312      * @return {@code true} if backface culling is enabled
313      */
314     public boolean isBackfaceCullingEnabled() {
315         return backfaceCulling;
316     }
317
318     /**
319      * Enables or disables backface culling for this polygon.
320      *
321      * @param backfaceCulling {@code true} to enable backface culling
322      */
323     public void setBackfaceCulling(final boolean backfaceCulling) {
324         this.backfaceCulling = backfaceCulling;
325     }
326
327     /**
328      * Draws the polygon border edges in yellow (for debugging).
329      *
330      * @param renderBuffer the rendering context
331      */
332     private void showBorders(final RenderingContext renderBuffer) {
333
334         final Point2D projectedPoint1 = coordinates[0].onScreenCoordinate;
335         final Point2D projectedPoint2 = coordinates[1].onScreenCoordinate;
336         final Point2D projectedPoint3 = coordinates[2].onScreenCoordinate;
337
338         final int x1 = (int) projectedPoint1.x;
339         final int y1 = (int) projectedPoint1.y;
340         final int x2 = (int) projectedPoint2.x;
341         final int y2 = (int) projectedPoint2.y;
342         final int x3 = (int) projectedPoint3.x;
343         final int y3 = (int) projectedPoint3.y;
344
345         renderBuffer.executeWithGraphics(g -> {
346             g.setColor(Color.YELLOW);
347             g.drawLine(x1, y1, x2, y2);
348             g.drawLine(x3, y3, x2, y2);
349             g.drawLine(x1, y1, x3, y3);
350         });
351     }
352
353 }