|
| 1 | +package fastterminal; |
| 2 | + |
| 3 | +import java.util.Random; |
| 4 | + |
| 5 | +/** |
| 6 | + * Premium 3D Solid Shaded Cube Demo. |
| 7 | + * Features real-time scanline triangle rasterization, custom back-face culling, |
| 8 | + * Lambertian diffuse flat shading with a dynamic orbiting 3D light source, |
| 9 | + * and a drifting starfield background running at a locked 120 FPS. |
| 10 | + */ |
| 11 | +public class ShadedDemo { |
| 12 | + |
| 13 | + // 3D Cube Vertices (centered at 0, 0, 0) |
| 14 | + private static final double[][] VERTICES = { |
| 15 | + {-1.0, -1.0, -1.0}, // 0 |
| 16 | + { 1.0, -1.0, -1.0}, // 1 |
| 17 | + { 1.0, 1.0, -1.0}, // 2 |
| 18 | + {-1.0, 1.0, -1.0}, // 3 |
| 19 | + {-1.0, -1.0, 1.0}, // 4 |
| 20 | + { 1.0, -1.0, 1.0}, // 5 |
| 21 | + { 1.0, 1.0, 1.0}, // 6 |
| 22 | + {-1.0, 1.0, 1.0} // 7 |
| 23 | + }; |
| 24 | + |
| 25 | + // 6 Cube Faces (each defined by 4 vertex indices in counter-clockwise order) |
| 26 | + private static final int[][] FACES = { |
| 27 | + {4, 5, 6, 7}, // Front face (normal: 0, 0, 1) |
| 28 | + {1, 0, 3, 2}, // Back face (normal: 0, 0, -1) |
| 29 | + {3, 2, 6, 7}, // Top face (normal: 0, 1, 0) |
| 30 | + {0, 1, 5, 4}, // Bottom face(normal: 0, -1, 0) |
| 31 | + {1, 5, 6, 2}, // Right face (normal: 1, 0, 0) |
| 32 | + {4, 0, 3, 7} // Left face (normal: -1, 0, 0) |
| 33 | + }; |
| 34 | + |
| 35 | + // Original normals of the 6 faces |
| 36 | + private static final double[][] NORMALS = { |
| 37 | + { 0.0, 0.0, 1.0}, // Front |
| 38 | + { 0.0, 0.0, -1.0}, // Back |
| 39 | + { 0.0, 1.0, 0.0}, // Top |
| 40 | + { 0.0, -1.0, 0.0}, // Bottom |
| 41 | + { 1.0, 0.0, 0.0}, // Right |
| 42 | + {-1.0, 0.0, 0.0} // Left |
| 43 | + }; |
| 44 | + |
| 45 | + // Base colors for each face of the cube (warm violet, royal blue, cool teal, electric crimson, magenta, emerald) |
| 46 | + private static final int[] FACE_COLORS = { |
| 47 | + 0x7C3AED, // Front (Amethyst Purple) |
| 48 | + 0x1D4ED8, // Back (Royal Blue) |
| 49 | + 0x0D9488, // Top (Teal) |
| 50 | + 0xE11D48, // Bottom (Crimson) |
| 51 | + 0xDB2777, // Right (Magenta) |
| 52 | + 0x059669 // Left (Emerald Green) |
| 53 | + }; |
| 54 | + |
| 55 | + // Starfield representation |
| 56 | + private static final int STAR_COUNT = 50; |
| 57 | + private static final double[] STAR_X = new double[STAR_COUNT]; |
| 58 | + private static final double[] STAR_Y = new double[STAR_COUNT]; |
| 59 | + private static final double[] STAR_SPEED = new double[STAR_COUNT]; |
| 60 | + private static final int[] STAR_COLOR = new int[STAR_COUNT]; |
| 61 | + |
| 62 | + static { |
| 63 | + Random rand = new Random(); |
| 64 | + for (int i = 0; i < STAR_COUNT; i++) { |
| 65 | + STAR_X[i] = rand.nextDouble(); |
| 66 | + STAR_Y[i] = rand.nextDouble(); |
| 67 | + STAR_SPEED[i] = 0.001 + rand.nextDouble() * 0.003; |
| 68 | + int choice = rand.nextInt(3); |
| 69 | + if (choice == 0) STAR_COLOR[i] = 0x64748B; // Faint gray |
| 70 | + else if (choice == 1) STAR_COLOR[i] = 0x94A3B8; // Slate |
| 71 | + else STAR_COLOR[i] = 0xE2E8F0; // Bright star |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + public static void main(String[] args) { |
| 76 | + System.out.println("Initializing FastTerminal 3D Shaded Cube Demo..."); |
| 77 | + |
| 78 | + // Register JVM Shutdown Hook to safely restore the console on exit |
| 79 | + Runtime.getRuntime().addShutdownHook(new Thread(() -> { |
| 80 | + System.out.print("\033[?1049l\033[?25h\033[0m"); |
| 81 | + System.out.flush(); |
| 82 | + })); |
| 83 | + |
| 84 | + // Enter Alternate Screen Buffer, Hide Cursor |
| 85 | + System.out.print("\033[?1049h\033[?25l"); |
| 86 | + System.out.flush(); |
| 87 | + |
| 88 | + int cols = 80; |
| 89 | + int rows = 30; |
| 90 | + |
| 91 | + // Try to query starting size natively |
| 92 | + try { |
| 93 | + int[] size = FastTerminal.getTerminalSize(); |
| 94 | + if (size != null && size[0] > 0 && size[1] > 0) { |
| 95 | + cols = size[0]; |
| 96 | + rows = size[1]; |
| 97 | + } |
| 98 | + } catch (Throwable ignored) {} |
| 99 | + |
| 100 | + TerminalRenderer renderer = null; |
| 101 | + TerminalScene canvas = null; |
| 102 | + |
| 103 | + double angleX = 0.0; |
| 104 | + double angleY = 0.0; |
| 105 | + double angleZ = 0.0; |
| 106 | + double lightPhase = 0.0; |
| 107 | + long frameTimeMs = 1000 / 120; // 120 FPS target |
| 108 | + |
| 109 | + while (true) { |
| 110 | + long startTime = System.currentTimeMillis(); |
| 111 | + |
| 112 | + // 1. DYNAMIC RESIZE DETECTION via JNI |
| 113 | + int currentCols = cols; |
| 114 | + int currentRows = rows; |
| 115 | + try { |
| 116 | + int[] size = FastTerminal.getTerminalSize(); |
| 117 | + if (size != null && size[0] > 0 && size[1] > 0) { |
| 118 | + currentCols = size[0]; |
| 119 | + currentRows = size[1]; |
| 120 | + } |
| 121 | + } catch (Throwable ignored) {} |
| 122 | + |
| 123 | + // Recreate viewport scene if resized |
| 124 | + if (renderer == null || canvas == null || currentCols != cols || currentRows != rows) { |
| 125 | + cols = currentCols; |
| 126 | + rows = currentRows; |
| 127 | + renderer = new TerminalRenderer(cols, rows); |
| 128 | + canvas = new TerminalScene(0, 0, cols, rows); |
| 129 | + renderer.addScene(canvas); |
| 130 | + } |
| 131 | + |
| 132 | + canvas.clear(); |
| 133 | + |
| 134 | + // Slow down angles for majestic rotations (1/3 of original speed) |
| 135 | + angleX += 0.025 / 3.0; |
| 136 | + angleY += 0.035 / 3.0; |
| 137 | + angleZ += 0.015 / 3.0; |
| 138 | + lightPhase += 0.01; // Orbiting light speed |
| 139 | + |
| 140 | + // 2. FILL CANVAS BACKGROUND WITH OBSIDIAN SPACE BLACK |
| 141 | + for (int r = 0; r < rows; r++) { |
| 142 | + for (int c = 0; c < cols; c++) { |
| 143 | + canvas.writeCell(c, r, ' ', 0x000000, 0x05070A); |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + // 3. DRAW AND DRIFT STARFIELD BACKDROP |
| 148 | + for (int i = 0; i < STAR_COUNT; i++) { |
| 149 | + STAR_X[i] -= STAR_SPEED[i]; |
| 150 | + if (STAR_X[i] < 0) { |
| 151 | + STAR_X[i] = 1.0; |
| 152 | + } |
| 153 | + int sx = (int) (STAR_X[i] * cols); |
| 154 | + int sy = (int) (STAR_Y[i] * rows); |
| 155 | + |
| 156 | + if (sx >= 0 && sx < cols && sy >= 0 && sy < rows) { |
| 157 | + int cp = (STAR_SPEED[i] > 0.003) ? '*' : '.'; |
| 158 | + canvas.writeCell(sx, sy, cp, STAR_COLOR[i], 0x05070A); |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + // 4. DYNAMIC ORBITING LIGHT SOURCE |
| 163 | + double lx = Math.cos(lightPhase); |
| 164 | + double ly = -0.5; |
| 165 | + double lz = Math.sin(lightPhase); |
| 166 | + // Normalize light vector |
| 167 | + double len = Math.sqrt(lx * lx + ly * ly + lz * lz); |
| 168 | + lx /= len; |
| 169 | + ly /= len; |
| 170 | + lz /= len; |
| 171 | + |
| 172 | + // 5. TRANSFORM AND ROTATE VERTICES |
| 173 | + int[][] projected = new int[VERTICES.length][2]; |
| 174 | + double[] rotatedZ = new double[VERTICES.length]; |
| 175 | + double cameraDistance = 3.2; |
| 176 | + |
| 177 | + for (int i = 0; i < VERTICES.length; i++) { |
| 178 | + double x = VERTICES[i][0]; |
| 179 | + double y = VERTICES[i][1]; |
| 180 | + double z = VERTICES[i][2]; |
| 181 | + |
| 182 | + // Pitch (X rotation) |
| 183 | + double y1 = y * Math.cos(angleX) - z * Math.sin(angleX); |
| 184 | + double z1 = y * Math.sin(angleX) + z * Math.cos(angleX); |
| 185 | + |
| 186 | + // Yaw (Y rotation) |
| 187 | + double x2 = x * Math.cos(angleY) + z1 * Math.sin(angleY); |
| 188 | + double z2 = -x * Math.sin(angleY) + z1 * Math.cos(angleY); |
| 189 | + |
| 190 | + // Roll (Z rotation) |
| 191 | + double x3 = x2 * Math.cos(angleZ) - y1 * Math.sin(angleZ); |
| 192 | + double y3 = x2 * Math.sin(angleZ) + y1 * Math.cos(angleZ); |
| 193 | + |
| 194 | + rotatedZ[i] = z2; |
| 195 | + |
| 196 | + // Perspective projection with terminal cell aspect ratio correction (2.1x width scaling) |
| 197 | + double scale = (rows * 0.5) / (cameraDistance + z2); |
| 198 | + projected[i][0] = (int) (cols / 2.0 + x3 * scale * 2.1); |
| 199 | + projected[i][1] = (int) (rows / 2.0 + y3 * scale); |
| 200 | + } |
| 201 | + |
| 202 | + // 6. CALCULATE, CULL, SHADE, AND RENDER FACE POLYGONS |
| 203 | + // We use simple painter's sorting by z-depth to prevent rendering overlaps (Z-Buffering) |
| 204 | + Integer[] faceOrder = {0, 1, 2, 3, 4, 5}; |
| 205 | + double[] faceAverageZ = new double[6]; |
| 206 | + for (int f = 0; f < 6; f++) { |
| 207 | + double avgZ = 0.0; |
| 208 | + for (int vIdx : FACES[f]) { |
| 209 | + avgZ += rotatedZ[vIdx]; |
| 210 | + } |
| 211 | + faceAverageZ[f] = avgZ / 4.0; |
| 212 | + } |
| 213 | + |
| 214 | + // Sort faces from furthest to nearest (descending order of Z depth) |
| 215 | + java.util.Arrays.sort(faceOrder, (a, b) -> Double.compare(faceAverageZ[b], faceAverageZ[a])); |
| 216 | + |
| 217 | + for (int f : faceOrder) { |
| 218 | + // Calculate rotated normal of this face |
| 219 | + double nx = NORMALS[f][0]; |
| 220 | + double ny = NORMALS[f][1]; |
| 221 | + double nz = NORMALS[f][2]; |
| 222 | + |
| 223 | + // Pitch (X rotation) |
| 224 | + double ny1 = ny * Math.cos(angleX) - nz * Math.sin(angleX); |
| 225 | + double nz1 = ny * Math.sin(angleX) + nz * Math.cos(angleX); |
| 226 | + |
| 227 | + // Yaw (Y rotation) |
| 228 | + double nx2 = nx * Math.cos(angleY) + nz1 * Math.sin(angleY); |
| 229 | + double nz2 = -nx * Math.sin(angleY) + nz1 * Math.cos(angleY); |
| 230 | + |
| 231 | + // Roll (Z rotation) |
| 232 | + double nx3 = nx2 * Math.cos(angleZ) - ny1 * Math.sin(angleZ); |
| 233 | + double ny3 = nx2 * Math.sin(angleZ) + ny1 * Math.cos(angleZ); |
| 234 | + |
| 235 | + // Back-face Culling: If normal points away from observer (camera faces Z-), do not draw! |
| 236 | + if (nz2 < 0) continue; |
| 237 | + |
| 238 | + // Lambertian Diffuse shading: Dot product of normal and light source direction |
| 239 | + double dot = nx3 * lx + ny3 * ly + nz2 * lz; |
| 240 | + double ambient = 0.18; // Base shadow brightness |
| 241 | + double shade = Math.max(0.0, dot); // Clamped lighting |
| 242 | + double intensity = ambient + (1.0 - ambient) * shade; |
| 243 | + |
| 244 | + // Compute shaded face color (24-bit True Color) |
| 245 | + int baseColor = FACE_COLORS[f]; |
| 246 | + int r = (int) (((baseColor >> 16) & 0xFF) * intensity); |
| 247 | + int g = (int) (((baseColor >> 8) & 0xFF) * intensity); |
| 248 | + int b = (int) ((baseColor & 0xFF) * intensity); |
| 249 | + int shadedColor = (r << 16) | (g << 8) | b; |
| 250 | + |
| 251 | + // Draw the Quad Face by splitting it into two triangles |
| 252 | + int[] v = FACES[f]; |
| 253 | + fillTriangle(canvas, projected[v[0]][0], projected[v[0]][1], |
| 254 | + projected[v[1]][0], projected[v[1]][1], |
| 255 | + projected[v[2]][0], projected[v[2]][1], shadedColor); |
| 256 | + |
| 257 | + fillTriangle(canvas, projected[v[0]][0], projected[v[0]][1], |
| 258 | + projected[v[2]][0], projected[v[2]][1], |
| 259 | + projected[v[3]][0], projected[v[3]][1], shadedColor); |
| 260 | + } |
| 261 | + |
| 262 | + // Blit standard composite buffers to screen |
| 263 | + canvas.setDirty(true); |
| 264 | + renderer.render(); |
| 265 | + |
| 266 | + // 120 FPS sleep throttle |
| 267 | + long elapsed = System.currentTimeMillis() - startTime; |
| 268 | + long sleepTime = frameTimeMs - elapsed; |
| 269 | + if (sleepTime > 0) { |
| 270 | + try { |
| 271 | + Thread.sleep(sleepTime); |
| 272 | + } catch (InterruptedException ignored) {} |
| 273 | + } |
| 274 | + } |
| 275 | + } |
| 276 | + |
| 277 | + // High-Performance Scanline Triangle Rasterizer filling cells with '█' blocks |
| 278 | + private static void fillTriangle(TerminalScene canvas, int x0, int y0, int x1, int y1, int x2, int y2, int color) { |
| 279 | + // Sort vertices by Y coordinate (y0 <= y1 <= y2) |
| 280 | + if (y0 > y1) { int t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; } |
| 281 | + if (y0 > y2) { int t = y0; y0 = y2; y2 = t; t = x0; x0 = x2; x2 = t; } |
| 282 | + if (y1 > y2) { int t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; } |
| 283 | + |
| 284 | + int totalHeight = y2 - y0; |
| 285 | + if (totalHeight == 0) return; |
| 286 | + |
| 287 | + for (int y = y0; y <= y2; y++) { |
| 288 | + if (y < 0 || y >= canvas.getHeight()) continue; |
| 289 | + |
| 290 | + boolean secondHalf = y > y1 || y1 == y0; |
| 291 | + int segmentHeight = secondHalf ? y2 - y1 : y1 - y0; |
| 292 | + if (segmentHeight == 0) continue; |
| 293 | + |
| 294 | + double alpha = (double) (y - y0) / totalHeight; |
| 295 | + double beta = (double) (secondHalf ? (y - y1) : (y - y0)) / segmentHeight; |
| 296 | + |
| 297 | + int ax = x0 + (int) ((x2 - x0) * alpha); |
| 298 | + int bx = secondHalf ? x1 + (int) ((x2 - x1) * beta) : x0 + (int) ((x1 - x0) * beta); |
| 299 | + |
| 300 | + if (ax > bx) { int t = ax; ax = bx; bx = t; } |
| 301 | + |
| 302 | + for (int x = ax; x <= bx; x++) { |
| 303 | + if (x >= 0 && x < canvas.getWidth()) { |
| 304 | + canvas.writeCell(x, y, '█', color, 0x05070A); |
| 305 | + } |
| 306 | + } |
| 307 | + } |
| 308 | + } |
| 309 | +} |
0 commit comments