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.basic.texturedpolygon;
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;
16 import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon;
19 * A textured triangle renderer with perspective-correct texture mapping.
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>
24 * <p><b>Perspective-correct texture rendering:</b></p>
26 * <li>Small triangles are rendered without perspective correction</li>
27 * <li>Larger triangles are tessellated into smaller pieces for accurate perspective</li>
31 * @see Vertex#textureCoordinate
33 public class TexturedTriangle extends AbstractCoordinateShape {
35 private static final ThreadLocal<PolygonBorderInterpolator[]> INTERPOLATORS =
36 ThreadLocal.withInitial(() -> new PolygonBorderInterpolator[]{
37 new PolygonBorderInterpolator(), new PolygonBorderInterpolator(), new PolygonBorderInterpolator()
41 * The texture to apply to this triangle.
43 public final Texture texture;
45 private boolean backfaceCulling = false;
48 * Total UV distance between all texture coordinate pairs.
49 * Computed at construction time to determine appropriate mipmap level.
51 private double totalTextureDistance;
54 * Creates a textured triangle with the specified vertices and texture.
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
61 public TexturedTriangle(Vertex p1, Vertex p2, Vertex p3, final Texture texture) {
64 this.texture = texture;
65 computeTotalTextureDistance();
69 * Computes the total UV distance between all texture coordinate pairs.
70 * Used to determine appropriate mipmap level.
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);
79 * Draws a horizontal scanline between two edge interpolators with texture sampling.
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
87 private void drawHorizontalLine(final PolygonBorderInterpolator line1,
88 final PolygonBorderInterpolator line2, final int y,
89 final RenderingContext renderBuffer,
90 final TextureBitmap textureBitmap) {
95 int x1 = line1.getX();
96 int x2 = line2.getX();
98 final double tx2, ty2;
99 final double tx1, ty1;
103 tx1 = line1.getTX() * textureBitmap.multiplicationFactor;
104 ty1 = line1.getTY() * textureBitmap.multiplicationFactor;
106 tx2 = line2.getTX() * textureBitmap.multiplicationFactor;
107 ty2 = line2.getTY() * textureBitmap.multiplicationFactor;
114 tx1 = line2.getTX() * textureBitmap.multiplicationFactor;
115 ty1 = line2.getTY() * textureBitmap.multiplicationFactor;
117 tx2 = line1.getTX() * textureBitmap.multiplicationFactor;
118 ty2 = line1.getTY() * textureBitmap.multiplicationFactor;
121 final double realWidth = x2 - x1;
122 final double realX1 = x1;
127 if (x2 >= renderBuffer.width)
128 x2 = renderBuffer.width - 1;
130 int renderBufferOffset = (y * renderBuffer.width) + x1;
131 final int[] renderBufferPixels = renderBuffer.pixels;
133 final double twidth = tx2 - tx1;
134 final double theight = ty2 - ty1;
136 final double txStep = twidth / realWidth;
137 final double tyStep = theight / realWidth;
139 double tx = tx1 + txStep * (x1 - realX1);
140 double ty = ty1 + tyStep * (x1 - realX1);
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;
148 for (int x = x1; x < x2; x++) {
153 if (itx < 0) itx = 0;
154 else if (itx > texWMinus1) itx = texWMinus1;
156 if (ity < 0) ity = 0;
157 else if (ity > texHMinus1) ity = texHMinus1;
159 final int srcPixel = texPixels[ity * texW + itx];
160 final int srcAlpha = (srcPixel >> 24) & 0xff;
163 if (srcAlpha == 255) {
164 renderBufferPixels[renderBufferOffset] = srcPixel;
166 final int backgroundAlpha = 255 - srcAlpha;
168 final int srcR = ((srcPixel >> 16) & 0xff) * srcAlpha;
169 final int srcG = ((srcPixel >> 8) & 0xff) * srcAlpha;
170 final int srcB = (srcPixel & 0xff) * srcAlpha;
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;
177 final int r = ((destR * backgroundAlpha) + srcR) >> 8;
178 final int g = ((destG * backgroundAlpha) + srcG) >> 8;
179 final int b = ((destB * backgroundAlpha) + srcB) >> 8;
181 renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
187 renderBufferOffset++;
193 * Renders this textured triangle to the screen.
195 * <p>This method performs:</p>
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>
203 * @param renderBuffer the rendering context containing the pixel buffer
206 public void paint(final RenderingContext renderBuffer) {
208 final Point2D projectedPoint1 = vertices.get(0).onScreenCoordinate;
209 final Point2D projectedPoint2 = vertices.get(1).onScreenCoordinate;
210 final Point2D projectedPoint3 = vertices.get(2).onScreenCoordinate;
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);
221 if (mouseInteractionController != null)
222 if (renderBuffer.getMouseEvent() != null)
223 if (pointWithinPolygon(
224 renderBuffer.getMouseEvent().coordinate, projectedPoint1, projectedPoint2, projectedPoint3))
225 renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController);
227 // Show polygon boundaries (for debugging)
228 if (renderBuffer.developerTools != null && renderBuffer.developerTools.showPolygonBorders)
229 showBorders(renderBuffer);
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;
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;
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;
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;
250 double totalVisibleDistance = projectedPoint1.getDistanceTo(projectedPoint2);
251 totalVisibleDistance += projectedPoint1.getDistanceTo(projectedPoint3);
252 totalVisibleDistance += projectedPoint2.getDistanceTo(projectedPoint3);
254 final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d;
256 final TextureBitmap mipmap = texture.getMipmapForScale(scaleFactor);
258 final PolygonBorderInterpolator[] interpolators = INTERPOLATORS.get();
259 final PolygonBorderInterpolator pbi1 = interpolators[0];
260 final PolygonBorderInterpolator pbi2 = interpolators[1];
261 final PolygonBorderInterpolator pbi3 = interpolators[2];
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);
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);
282 * Checks if backface culling is enabled for this triangle.
284 * @return {@code true} if backface culling is enabled
286 public boolean isBackfaceCullingEnabled() {
287 return backfaceCulling;
291 * Enables or disables backface culling for this triangle.
293 * @param backfaceCulling {@code true} to enable backface culling
295 public void setBackfaceCulling(final boolean backfaceCulling) {
296 this.backfaceCulling = backfaceCulling;
300 * Draws the triangle border edges in yellow (for debugging).
302 * @param renderBuffer the rendering context
304 private void showBorders(final RenderingContext renderBuffer) {
306 final Point2D projectedPoint1 = vertices.get(0).onScreenCoordinate;
307 final Point2D projectedPoint2 = vertices.get(1).onScreenCoordinate;
308 final Point2D projectedPoint3 = vertices.get(2).onScreenCoordinate;
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;
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);