881e71684b67c041951bac18d18e6ddce60e4b6e
[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.basic.texturedpolygon;
6
7 import eu.svjatoslav.aukio.e3d.geometry.Point2D;
8 import eu.svjatoslav.aukio.e3d.geometry.Point3D;
9 import eu.svjatoslav.aukio.e3d.gui.RenderingContext;
10 import eu.svjatoslav.aukio.e3d.math.Vertex;
11 import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap;
12 import org.junit.Test;
13
14 import java.lang.reflect.Method;
15 import java.util.Random;
16
17 import static org.junit.Assert.assertEquals;
18
19 /**
20  * Pixel-exactness proof for the optimized textured scanline renderer:
21  * the one-multiply alpha blend and the clamp-free fast path must produce
22  * output identical to the legacy implementation, bit for bit.
23  */
24 public class TexturedTriangleBlendTest {
25
26     /** Legacy two-multiply blend, the original semantics. */
27     private static int legacyBlendChannel(final int src, final int dest, final int alpha) {
28         return ((dest * (255 - alpha)) + (src * alpha)) >> 8;
29     }
30
31     /** Optimized one-multiply blend, must equal legacy for every input. */
32     private static int fastBlendChannel(final int src, final int dest, final int alpha) {
33         return dest + ((alpha * (src - dest) - dest) >> 8);
34     }
35
36     @Test
37     public void oneMultiplyBlendMatchesLegacyBlend() {
38         final int[] channelValues = {0, 1, 2, 63, 127, 128, 200, 254, 255};
39         for (int alpha = 0; alpha <= 255; alpha++) {
40             for (final int src : channelValues) {
41                 for (final int dest : channelValues) {
42                     assertEquals("src=" + src + " dest=" + dest + " alpha=" + alpha,
43                             legacyBlendChannel(src, dest, alpha),
44                             fastBlendChannel(src, dest, alpha));
45                 }
46             }
47         }
48         final Random random = new Random(42);
49         for (int i = 0; i < 1_000_000; i++) {
50             final int src = random.nextInt(256);
51             final int dest = random.nextInt(256);
52             final int alpha = random.nextInt(256);
53             assertEquals("src=" + src + " dest=" + dest + " alpha=" + alpha,
54                     legacyBlendChannel(src, dest, alpha),
55                     fastBlendChannel(src, dest, alpha));
56         }
57     }
58
59     /**
60      * Legacy scanline implementation (pre-optimization), used as the
61      * oracle: the optimized drawHorizontalLineZ must match it exactly.
62      */
63     private static void legacyDrawHorizontalLine(
64             final PolygonBorderInterpolator line1, final PolygonBorderInterpolator line2,
65             final int y, final int[] renderBufferPixels, final int width,
66             final int renderMinX, final int renderMaxX,
67             final TextureBitmap textureBitmap) {
68         line1.setCurrentY(y);
69         line2.setCurrentY(y);
70
71         int x1 = line1.getX();
72         int x2 = line2.getX();
73
74         final double tx2, ty2;
75         final double tx1, ty1;
76
77         if (x1 <= x2) {
78             tx1 = line1.getTX() * textureBitmap.multiplicationFactor;
79             ty1 = line1.getTY() * textureBitmap.multiplicationFactor;
80             tx2 = line2.getTX() * textureBitmap.multiplicationFactor;
81             ty2 = line2.getTY() * textureBitmap.multiplicationFactor;
82         } else {
83             final int tmp = x1;
84             x1 = x2;
85             x2 = tmp;
86             tx1 = line2.getTX() * textureBitmap.multiplicationFactor;
87             ty1 = line2.getTY() * textureBitmap.multiplicationFactor;
88             tx2 = line1.getTX() * textureBitmap.multiplicationFactor;
89             ty2 = line1.getTY() * textureBitmap.multiplicationFactor;
90         }
91
92         final double realWidth = x2 - x1;
93         final double realX1 = x1;
94
95         if (x1 < renderMinX)
96             x1 = renderMinX;
97         if (x2 >= renderMaxX)
98             x2 = renderMaxX;
99
100         int renderBufferOffset = (y * width) + x1;
101
102         final double twidth = tx2 - tx1;
103         final double theight = ty2 - ty1;
104
105         final double txStep = twidth / realWidth;
106         final double tyStep = theight / realWidth;
107
108         double tx = tx1 + txStep * (x1 - realX1);
109         double ty = ty1 + tyStep * (x1 - realX1);
110
111         final int[] texPixels = textureBitmap.pixels;
112         final int texW = textureBitmap.width;
113         final int texH = textureBitmap.height;
114         final int texWMinus1 = texW - 1;
115         final int texHMinus1 = texH - 1;
116
117         for (int x = x1; x < x2; x++) {
118             int itx = (int) tx;
119             int ity = (int) ty;
120
121             if (itx < 0) itx = 0;
122             else if (itx > texWMinus1) itx = texWMinus1;
123
124             if (ity < 0) ity = 0;
125             else if (ity > texHMinus1) ity = texHMinus1;
126
127             final int srcPixel = texPixels[ity * texW + itx];
128             final int srcAlpha = (srcPixel >> 24) & 0xff;
129
130             if (srcAlpha != 0) {
131                 if (srcAlpha == 255) {
132                     renderBufferPixels[renderBufferOffset] = srcPixel;
133                 } else {
134                     final int destPixel = renderBufferPixels[renderBufferOffset];
135                     final int destR = (destPixel >> 16) & 0xff;
136                     final int destG = (destPixel >> 8) & 0xff;
137                     final int destB = destPixel & 0xff;
138
139                     final int r = legacyBlendChannel((srcPixel >> 16) & 0xff, destR, srcAlpha);
140                     final int g = legacyBlendChannel((srcPixel >> 8) & 0xff, destG, srcAlpha);
141                     final int b = legacyBlendChannel(srcPixel & 0xff, destB, srcAlpha);
142
143                     renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
144                 }
145             }
146
147             tx += txStep;
148             ty += tyStep;
149             renderBufferOffset++;
150         }
151     }
152
153     @Test
154     public void scanlineMatchesLegacyImplementation() throws Exception {
155         final int width = 96;
156         final int height = 8;
157         final Random random = new Random(1337);
158
159         // Texture with a mix of transparent, semi-transparent and opaque pixels
160         final int texW = 16, texH = 16;
161         final int[] texPixels = new int[texW * texH];
162         for (int i = 0; i < texPixels.length; i++) {
163             final int alpha;
164             switch (random.nextInt(4)) {
165                 case 0: alpha = 0; break;
166                 case 1: alpha = 255; break;
167                 default: alpha = 1 + random.nextInt(254);
168             }
169             texPixels[i] = (alpha << 24) | (random.nextInt(256) << 16)
170                     | (random.nextInt(256) << 8) | random.nextInt(256);
171         }
172         final TextureBitmap textureBitmap = new TextureBitmap(texW, texH, texPixels, 1.0);
173
174         final TexturedTriangle triangle = new TexturedTriangle(
175                 new Vertex(new Point3D(0, 0, 0), new Point2D(0, 0)),
176                 new Vertex(new Point3D(1, 0, 0), new Point2D(1, 0)),
177                 new Vertex(new Point3D(0, 1, 0), new Point2D(0, 1)), null);
178
179         final Method draw = TexturedTriangle.class.getDeclaredMethod("drawHorizontalLineZ",
180                 PolygonBorderInterpolator.class, PolygonBorderInterpolator.class,
181                 int.class, RenderingContext.class, TextureBitmap.class);
182         draw.setAccessible(true);
183
184         for (int iteration = 0; iteration < 5000; iteration++) {
185             // Random span endpoints, including out-of-texture and
186             // out-of-render-bounds cases, and reversed X order
187             final double sx1 = random.nextDouble() * width * 1.5 - width * 0.25;
188             final double sx2 = random.nextDouble() * width * 1.5 - width * 0.25;
189             final double u1 = random.nextDouble() * 2.0 - 0.5;
190             final double v1 = random.nextDouble() * 2.0 - 0.5;
191             final double u2 = random.nextDouble() * 2.0 - 0.5;
192             final double v2 = random.nextDouble() * 2.0 - 0.5;
193             final int y = 1 + random.nextInt(height - 2);
194
195             final PolygonBorderInterpolator line1 = new PolygonBorderInterpolator();
196             final PolygonBorderInterpolator line2 = new PolygonBorderInterpolator();
197             line1.setPoints(new Point2D(sx1, y), new Point2D(sx1, y + 1),
198                     new Point2D(u1, v1), new Point2D(u1, v1));
199             line2.setPoints(new Point2D(sx2, y), new Point2D(sx2, y + 1),
200                     new Point2D(u2, v2), new Point2D(u2, v2));
201
202             final int[] actual = new int[width * height];
203             final int[] expected = new int[width * height];
204             for (int i = 0; i < actual.length; i++) {
205                 actual[i] = expected[i] = 0xFF000000 | random.nextInt(0xFFFFFF);
206             }
207
208             final RenderingContext context = new RenderingContext(width, height, 1);
209             System.arraycopy(actual, 0, context.pixels, 0, actual.length);
210             context.renderMinX = 0;
211             context.renderMaxX = width;
212
213             // Fresh interpolators for the oracle (setCurrentY mutates them)
214             final PolygonBorderInterpolator oLine1 = new PolygonBorderInterpolator();
215             final PolygonBorderInterpolator oLine2 = new PolygonBorderInterpolator();
216             oLine1.setPoints(new Point2D(sx1, y), new Point2D(sx1, y + 1),
217                     new Point2D(u1, v1), new Point2D(u1, v1));
218             oLine2.setPoints(new Point2D(sx2, y), new Point2D(sx2, y + 1),
219                     new Point2D(u2, v2), new Point2D(u2, v2));
220
221             java.util.Arrays.fill(context.depth, Float.NEGATIVE_INFINITY);
222             draw.invoke(triangle, line1, line2, y, context, textureBitmap);
223             legacyDrawHorizontalLine(oLine1, oLine2, y, expected, width,
224                     0, width, textureBitmap);
225
226             for (int i = 0; i < expected.length; i++) {
227                 if (expected[i] != context.pixels[i]) {
228                     final int px = i % width, py = i / width;
229                     throw new AssertionError("iteration " + iteration
230                             + " pixel(" + px + "," + py + "): expected "
231                             + Integer.toHexString(expected[i]) + " but got "
232                             + Integer.toHexString(context.pixels[i])
233                             + " [span " + sx1 + ".." + sx2 + " uv ("
234                             + u1 + "," + v1 + ")->(" + u2 + "," + v2 + ")]");
235                 }
236             }
237         }
238     }
239 }