2 * Aukio 3D engine. Author: Svjatoslav Agejenko.
3 * This project is released under Creative Commons Zero (CC0) license.
5 package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon;
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;
14 import java.lang.reflect.Method;
15 import java.util.Random;
17 import static org.junit.Assert.assertEquals;
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.
24 public class TexturedTriangleBlendTest {
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;
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);
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));
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));
60 * Legacy scanline implementation (pre-optimization), used as the
61 * oracle: the optimized drawHorizontalLineZ must match it exactly.
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) {
71 int x1 = line1.getX();
72 int x2 = line2.getX();
74 final double tx2, ty2;
75 final double tx1, ty1;
78 tx1 = line1.getTX() * textureBitmap.multiplicationFactor;
79 ty1 = line1.getTY() * textureBitmap.multiplicationFactor;
80 tx2 = line2.getTX() * textureBitmap.multiplicationFactor;
81 ty2 = line2.getTY() * textureBitmap.multiplicationFactor;
86 tx1 = line2.getTX() * textureBitmap.multiplicationFactor;
87 ty1 = line2.getTY() * textureBitmap.multiplicationFactor;
88 tx2 = line1.getTX() * textureBitmap.multiplicationFactor;
89 ty2 = line1.getTY() * textureBitmap.multiplicationFactor;
92 final double realWidth = x2 - x1;
93 final double realX1 = x1;
100 int renderBufferOffset = (y * width) + x1;
102 final double twidth = tx2 - tx1;
103 final double theight = ty2 - ty1;
105 final double txStep = twidth / realWidth;
106 final double tyStep = theight / realWidth;
108 double tx = tx1 + txStep * (x1 - realX1);
109 double ty = ty1 + tyStep * (x1 - realX1);
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;
117 for (int x = x1; x < x2; x++) {
121 if (itx < 0) itx = 0;
122 else if (itx > texWMinus1) itx = texWMinus1;
124 if (ity < 0) ity = 0;
125 else if (ity > texHMinus1) ity = texHMinus1;
127 final int srcPixel = texPixels[ity * texW + itx];
128 final int srcAlpha = (srcPixel >> 24) & 0xff;
131 if (srcAlpha == 255) {
132 renderBufferPixels[renderBufferOffset] = srcPixel;
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;
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);
143 renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b;
149 renderBufferOffset++;
154 public void scanlineMatchesLegacyImplementation() throws Exception {
155 final int width = 96;
156 final int height = 8;
157 final Random random = new Random(1337);
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++) {
164 switch (random.nextInt(4)) {
165 case 0: alpha = 0; break;
166 case 1: alpha = 255; break;
167 default: alpha = 1 + random.nextInt(254);
169 texPixels[i] = (alpha << 24) | (random.nextInt(256) << 16)
170 | (random.nextInt(256) << 8) | random.nextInt(256);
172 final TextureBitmap textureBitmap = new TextureBitmap(texW, texH, texPixels, 1.0);
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);
179 final Method draw = TexturedTriangle.class.getDeclaredMethod("drawHorizontalLineZ",
180 PolygonBorderInterpolator.class, PolygonBorderInterpolator.class,
181 int.class, RenderingContext.class, TextureBitmap.class);
182 draw.setAccessible(true);
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);
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));
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);
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;
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));
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);
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 + ")]");