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;
14 import java.awt.Color;
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 sliced into smaller pieces for better perspective correction.</p>
24 * <p><b>Perspective-correct texture rendering:</b></p>
26 * <li>Small polygons are rendered without perspective correction</li>
27 * <li>Larger polygons are sliced into smaller pieces for accurate perspective</li>
31 * @see Vertex#textureCoordinate
33 public class TexturedPolygon 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 polygon.
43 public final Texture texture;
45 private boolean backfaceCulling = false;
47 private double totalTextureDistance = -1;
50 * Creates a textured triangle with the specified vertices and texture.
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
57 public TexturedPolygon(Vertex p1, Vertex p2, Vertex p3, final Texture texture) {
60 this.texture = texture;
64 * Computes the total UV distance between all texture coordinate pairs.
65 * Used to determine appropriate mipmap level.
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);
75 * Draws a horizontal scanline between two edge interpolators with texture sampling.
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
83 private void drawHorizontalLine(final PolygonBorderInterpolator line1,
84 final PolygonBorderInterpolator line2, final int y,
85 final RenderingContext renderBuffer,
86 final TextureBitmap textureBitmap) {
91 int x1 = line1.getX();
92 int x2 = line2.getX();
94 final double tx2, ty2;
95 final double tx1, ty1;
99 tx1 = line1.getTX() * textureBitmap.multiplicationFactor;
100 ty1 = line1.getTY() * textureBitmap.multiplicationFactor;
102 tx2 = line2.getTX() * textureBitmap.multiplicationFactor;
103 ty2 = line2.getTY() * textureBitmap.multiplicationFactor;
110 tx1 = line2.getTX() * textureBitmap.multiplicationFactor;
111 ty1 = line2.getTY() * textureBitmap.multiplicationFactor;
113 tx2 = line1.getTX() * textureBitmap.multiplicationFactor;
114 ty2 = line1.getTY() * textureBitmap.multiplicationFactor;
117 final double realWidth = x2 - x1;
118 final double realX1 = x1;
123 if (x2 >= renderBuffer.width)
124 x2 = renderBuffer.width - 1;
126 int renderBufferOffset = (y * renderBuffer.width) + x1;
127 final int[] renderBufferPixels = renderBuffer.pixels;
129 final double twidth = tx2 - tx1;
130 final double theight = ty2 - ty1;
132 final double txStep = twidth / realWidth;
133 final double tyStep = theight / realWidth;
135 double tx = tx1 + txStep * (x1 - realX1);
136 double ty = ty1 + tyStep * (x1 - realX1);
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;
144 for (int x = x1; x < x2; x++) {
149 if (itx < 0) itx = 0;
150 else if (itx > texWMinus1) itx = texWMinus1;
152 if (ity < 0) ity = 0;
153 else if (ity > texHMinus1) ity = texHMinus1;
155 final int srcPixel = texPixels[ity * texW + itx];
156 final int srcAlpha = (srcPixel >> 24) & 0xff;
159 if (srcAlpha == 255) {
160 renderBufferPixels[renderBufferOffset] = srcPixel;
162 final int backgroundAlpha = 255 - srcAlpha;
164 final int srcR = (srcPixel >> 16) & 0xff;
165 final int srcG = (srcPixel >> 8) & 0xff;
166 final int srcB = srcPixel & 0xff;
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;
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;
177 renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
183 renderBufferOffset++;
189 * Renders this textured triangle to the screen.
191 * <p>This method performs:</p>
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>
199 * @param renderBuffer the rendering context containing the pixel buffer
202 public void paint(final RenderingContext renderBuffer) {
204 final Point2D projectedPoint1 = coordinates[0].onScreenCoordinate;
205 final Point2D projectedPoint2 = coordinates[1].onScreenCoordinate;
206 final Point2D projectedPoint3 = coordinates[2].onScreenCoordinate;
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);
217 projectedPoint1.roundToInteger();
218 projectedPoint2.roundToInteger();
219 projectedPoint3.roundToInteger();
221 if (mouseInteractionController != null)
222 if (renderBuffer.getMouseEvent() != null)
223 if (pointWithinPolygon(
224 renderBuffer.getMouseEvent().coordinate, projectedPoint1,
225 projectedPoint2, projectedPoint3))
226 renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController);
228 // Show polygon boundaries (for debugging)
229 if (renderBuffer.developerTools != null && renderBuffer.developerTools.showPolygonBorders)
230 showBorders(renderBuffer);
232 // find top-most point
233 int yTop = (int) projectedPoint1.y;
235 if (projectedPoint2.y < yTop)
236 yTop = (int) projectedPoint2.y;
238 if (projectedPoint3.y < yTop)
239 yTop = (int) projectedPoint3.y;
244 // find bottom-most point
245 int yBottom = (int) projectedPoint1.y;
247 if (projectedPoint2.y > yBottom)
248 yBottom = (int) projectedPoint2.y;
250 if (projectedPoint3.y > yBottom)
251 yBottom = (int) projectedPoint3.y;
253 if (yBottom >= renderBuffer.height)
254 yBottom = renderBuffer.height - 1;
256 // clamp to render Y bounds
257 yTop = Math.max(yTop, renderBuffer.renderMinY);
258 yBottom = Math.min(yBottom, renderBuffer.renderMaxY);
263 double totalVisibleDistance = projectedPoint1.getDistanceTo(projectedPoint2);
264 totalVisibleDistance += projectedPoint1.getDistanceTo(projectedPoint3);
265 totalVisibleDistance += projectedPoint2.getDistanceTo(projectedPoint3);
267 if (totalTextureDistance == -1)
268 computeTotalTextureDistance();
269 final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d;
271 final TextureBitmap zoomedBitmap = texture.getZoomedBitmap(scaleFactor);
273 final PolygonBorderInterpolator[] interp = INTERPOLATORS.get();
274 final PolygonBorderInterpolator polygonBorder1 = interp[0];
275 final PolygonBorderInterpolator polygonBorder2 = interp[1];
276 final PolygonBorderInterpolator polygonBorder3 = interp[2];
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);
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; }
297 for (int y = yTop; y < yBottom; y++)
298 if (a.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))
305 drawHorizontalLine(b, c, y, renderBuffer, zoomedBitmap);
310 * Checks if backface culling is enabled for this polygon.
312 * @return {@code true} if backface culling is enabled
314 public boolean isBackfaceCullingEnabled() {
315 return backfaceCulling;
319 * Enables or disables backface culling for this polygon.
321 * @param backfaceCulling {@code true} to enable backface culling
323 public void setBackfaceCulling(final boolean backfaceCulling) {
324 this.backfaceCulling = backfaceCulling;
328 * Draws the polygon border edges in yellow (for debugging).
330 * @param renderBuffer the rendering context
332 private void showBorders(final RenderingContext renderBuffer) {
334 final Point2D projectedPoint1 = coordinates[0].onScreenCoordinate;
335 final Point2D projectedPoint2 = coordinates[1].onScreenCoordinate;
336 final Point2D projectedPoint3 = coordinates[2].onScreenCoordinate;
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;
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);