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