Skip to content

Commit c73a6a9

Browse files
committed
Add ShadedDemo rendering a real-time, flat-shaded 3D solid cube with scanline triangle rasterization, custom back-face culling, and a dynamic orbiting light source at 120 FPS
1 parent 6f02d22 commit c73a6a9

2 files changed

Lines changed: 327 additions & 0 deletions

File tree

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
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+
}

run-shaded-demo.bat

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@echo off
2+
:: Configure Windows Console to UTF-8 to support True-Color and Grid rendering cleanly
3+
chcp 65001 > nul
4+
5+
echo [INFO] Building Main FastTerminal Library...
6+
call mvn -q clean package -DskipTests
7+
if %ERRORLEVEL% NEQ 0 ( pause & exit /b )
8+
9+
echo [INFO] Compiling Demo Classes...
10+
cd examples\Demo
11+
call mvn -q compile -DskipTests
12+
if %ERRORLEVEL% NEQ 0 ( cd ..\.. & pause & exit /b )
13+
14+
echo [INFO] Running Fullscreen 3D Shaded Solid Cube Demo...
15+
java --enable-native-access=ALL-UNNAMED -cp "target/classes;../../target/fastterminal-0.1.0.jar" fastterminal.ShadedDemo
16+
17+
cd ..\..
18+
pause

0 commit comments

Comments
 (0)