nx_scene_render.nx source
↩ module page · 295 lines · 12143 B
1// nx_scene_render.nx -- render ONE (SceneWorld, SceneEntity) pair under
2// any style (exceed-ladder R0; pairs with nx_scene_contract.nx).
3//
4// Side-elevation view: the camera slice is the entity's anchor row. Each
5// screen column maps to a world cell; ground height -> screen row; terrain
6// filled below, sky above; the synthetic human stands ON the ground at the
7// anchor (insertion is physical, not a paste).
8//
9// Style law (from the contract): the SAME entity + world render under all
10// four styles; only the named tables change the art:
11// REALISTIC -- continuous elevation gradient + slope shading
12// ANIME -- 5 cel bands, saturated, bright sky, eye dots
13// CARTOON -- 3 flat bands, dark outline around figure and band edges
14// VN -- sunset-gradient backdrop, terrain as far silhouette,
15// portrait-scale sprite with eyes
16//
17// Tier 0: nx_scene_quickstart(world_seed, human_seed, style, path)
18// -- one call -> a P6 PPM scene on disk.
19// license_tier: ORIGINAL
20
21import "nx_syscalls.nx"
22import "nx_tier.nx"
23import "nx_scene_contract.nx"
24import "nx_ppm_writer.nx"
25const NX_MAGIC_65536: i64 = 65536
26const NX_MAGIC_1024: i64 = 1024
27const NX_MAGIC_999999: i64 = 999999
28const NX_MAGIC_4096: i64 = 4096
29
30// ===== Screen geometry (named, not magic) =============================
31const NX_SCN_W: nx_int = 256
32const NX_SCN_H: nx_int = 192
33const NX_SCN_GROUND_BASE: nx_int = 160 // screen row of elevation 0
34const NX_SCN_ELEV_SCALE_Q10: nx_int = 24 // px per Q10 elevation unit, /1024
35// speed budget for one full scene render (measured in the KATs; the
36// "beat them on speed" number is logged every gate run)
37const NX_SCN_RENDER_BUDGET_MS: nx_int = 200
38
39// ===== tiny framebuffer helpers ======================================
40func _scn_px(fb: *u8, x: nx_int, y: nx_int, rgb: nx_int) -> nx_int {
41 if x < 0 { return 0 }
42 if x >= NX_SCN_W { return 0 }
43 if y < 0 { return 0 }
44 if y >= NX_SCN_H { return 0 }
45 let o: nx_int = (y * NX_SCN_W + x) * 3
46 fb[o] = (rgb / NX_MAGIC_65536) & 0xff
47 fb[o + 1] = (rgb / 256) & 0xff
48 fb[o + 2] = rgb & 0xff
49 return 0
50}
51
52func _scn_rect(fb: *u8, x0: nx_int, y0: nx_int, rw: nx_int, rh: nx_int, rgb: nx_int) -> nx_int {
53 var y: nx_int = y0
54 while y < y0 + rh {
55 var x: nx_int = x0
56 while x < x0 + rw {
57 _scn_px(fb, x, y, rgb)
58 x = x + 1
59 }
60 y = y + 1
61 }
62 return 0
63}
64
65func _scn_disc(fb: *u8, cx: nx_int, cy: nx_int, r: nx_int, rgb: nx_int) -> nx_int {
66 let r2: nx_int = r * r
67 var y: nx_int = cy - r
68 while y <= cy + r {
69 var x: nx_int = cx - r
70 while x <= cx + r {
71 let dx: nx_int = x - cx
72 let dy: nx_int = y - cy
73 if dx * dx + dy * dy <= r2 { _scn_px(fb, x, y, rgb) }
74 x = x + 1
75 }
76 y = y + 1
77 }
78 return 0
79}
80
81// integer lerp of two 0xRRGGBB colors, t in Q10
82func _scn_lerp_rgb(a: nx_int, b: nx_int, t_q10: nx_int) -> nx_int {
83 var t: nx_int = t_q10
84 if t < 0 { t = 0 }
85 if t > NX_MAGIC_1024 { t = NX_MAGIC_1024 }
86 let ar: nx_int = (a / NX_MAGIC_65536) & 0xff
87 let ag: nx_int = (a / 256) & 0xff
88 let ab: nx_int = a & 0xff
89 let br: nx_int = (b / NX_MAGIC_65536) & 0xff
90 let bg: nx_int = (b / 256) & 0xff
91 let bb: nx_int = b & 0xff
92 let r: nx_int = ar + ((br - ar) * t) / NX_MAGIC_1024
93 let g: nx_int = ag + ((bg - ag) * t) / NX_MAGIC_1024
94 let bl: nx_int = ab + ((bb - ab) * t) / NX_MAGIC_1024
95 return r * NX_MAGIC_65536 + g * 256 + bl
96}
97
98// ===== style palettes (Tier-2 data) ===================================
99func _scn_sky_top(style: nx_int) -> nx_int {
100 if style == NX_STYLE_ANIME { return 0x6EC6FF }
101 if style == NX_STYLE_CARTOON { return 0x4FC3F7 }
102 if style == NX_STYLE_VN { return 0xF7B26B } // sunset peach
103 return 0x7FB2E5
104}
105func _scn_sky_bottom(style: nx_int) -> nx_int {
106 if style == NX_STYLE_ANIME { return 0xDFF3FF }
107 if style == NX_STYLE_CARTOON { return 0x4FC3F7 } // flat cartoon sky
108 if style == NX_STYLE_VN { return 0xC58BC9 } // sunset lavender
109 return 0xE8F1F8
110}
111func _scn_ground_low(style: nx_int) -> nx_int {
112 if style == NX_STYLE_VN { return 0x2E3A55 } // silhouette navy
113 return 0x2F6B33
114}
115func _scn_ground_high(style: nx_int) -> nx_int {
116 if style == NX_STYLE_VN { return 0x44507A }
117 return 0xF2F2F2 // snowcap
118}
119func _scn_band_color(style: nx_int, band: nx_int, n_bands: nx_int) -> nx_int {
120 // quantized cel bands sample the same low->high ramp at band centers
121 let t: nx_int = ((band * 2 + 1) * NX_MAGIC_1024) / (n_bands * 2)
122 return _scn_lerp_rgb(_scn_ground_low(style), _scn_ground_high(style), t)
123}
124const NX_SCN_OUTLINE_RGB: nx_int = 0x1A1A1A
125
126// ===== terrain pass ===================================================
127// ground screen row for elevation h (Q10)
128func _scn_ground_row(h_q10: nx_int) -> nx_int {
129 var row: nx_int = NX_SCN_GROUND_BASE - (h_q10 * NX_SCN_ELEV_SCALE_Q10) / NX_MAGIC_1024
130 if row < 0 { row = 0 }
131 if row >= NX_SCN_H { row = NX_SCN_H - 1 }
132 return row
133}
134
135func _scn_terrain(fb: *u8, land: *Landscape, slice_y: nx_int, style: nx_int) -> nx_int {
136 let w: nx_int = land.width
137 let cols_per_cell: nx_int = NX_SCN_W / w
138 // elevation range over the slice (for band/gradient normalization)
139 var hmin: nx_int = NX_MAGIC_999999
140 var hmax: nx_int = 0 - NX_MAGIC_999999
141 var i: nx_int = 0
142 while i < w {
143 let v: nx_int = nx_landscape_height_at(land, i, slice_y)
144 if v < hmin { hmin = v }
145 if v > hmax { hmax = v }
146 i = i + 1
147 }
148 var range: nx_int = hmax - hmin
149 if range < 1 { range = 1 }
150 let bands: nx_int = nx_style_terrain_bands(style)
151
152 var sx: nx_int = 0
153 while sx < NX_SCN_W {
154 var cell: nx_int = sx / cols_per_cell
155 if cell >= w { cell = w - 1 }
156 let h: nx_int = nx_landscape_height_at(land, cell, slice_y)
157 let gr: nx_int = _scn_ground_row(h)
158 let t_q10: nx_int = ((h - hmin) * NX_MAGIC_1024) / range
159
160 // sky above the ground line
161 var y: nx_int = 0
162 while y < gr {
163 let st_q10: nx_int = (y * NX_MAGIC_1024) / NX_SCN_GROUND_BASE
164 _scn_px(fb, sx, y, _scn_lerp_rgb(_scn_sky_top(style), _scn_sky_bottom(style), st_q10))
165 y = y + 1
166 }
167 // terrain below
168 var gcol: nx_int = 0
169 if bands > 0 {
170 var b: nx_int = (t_q10 * bands) / NX_MAGIC_1024
171 if b >= bands { b = bands - 1 }
172 gcol = _scn_band_color(style, b, bands)
173 } else {
174 gcol = _scn_lerp_rgb(_scn_ground_low(style), _scn_ground_high(style), t_q10)
175 // realistic slope shading: darken by |dh| of the next cell
176 if style == NX_STYLE_REALISTIC {
177 var dh: nx_int = nx_landscape_height_at(land, cell + 1, slice_y) - h
178 if dh < 0 { dh = 0 - dh }
179 var sh: nx_int = NX_MAGIC_1024 - (dh * 2)
180 if sh < 640 { sh = 640 }
181 gcol = _scn_lerp_rgb(0x000000, gcol, sh)
182 }
183 }
184 while y < NX_SCN_H {
185 _scn_px(fb, sx, y, gcol)
186 y = y + 1
187 }
188 // cartoon law: dark edge where the band changes column-to-column
189 if style == NX_STYLE_CARTOON {
190 _scn_px(fb, sx, gr, NX_SCN_OUTLINE_RGB)
191 }
192 sx = sx + 1
193 }
194 return 0
195}
196
197// ===== entity pass ====================================================
198// Draw the synthetic human standing at its anchor. Proportions come from
199// the style tables; identity (palette/build) comes ONLY from the entity.
200func _scn_entity(fb: *u8, land: *Landscape, e: *i64, style: nx_int) -> nx_int {
201 let w: nx_int = land.width
202 let cols_per_cell: nx_int = NX_SCN_W / w
203 let ax: nx_int = e[NX_ENT_ANCHOR_X]
204 let ay: nx_int = e[NX_ENT_ANCHOR_Y]
205 let ground_h: nx_int = nx_landscape_height_at(land, ax, ay)
206 let feet_y: nx_int = _scn_ground_row(ground_h)
207 let cx: nx_int = ax * cols_per_cell + cols_per_cell / 2
208
209 // style-scaled proportions over the identity height
210 let px_total: nx_int = (nx_style_px_height(style) * e[NX_ENT_HEIGHT_Q10]) / NX_MAGIC_1024
211 let head_d: nx_int = (px_total * nx_style_head_ratio_q10(style)) / NX_MAGIC_1024
212 let body_h: nx_int = px_total - head_d
213 let leg_h: nx_int = (body_h * 470) / NX_MAGIC_1024
214 let torso_h: nx_int = body_h - leg_h
215 var torso_w: nx_int = (px_total * e[NX_ENT_BUILD_Q10]) / NX_MAGIC_4096
216 if torso_w < 3 { torso_w = 3 }
217 let leg_w: nx_int = (torso_w * 410) / NX_MAGIC_1024
218
219 let top_y: nx_int = feet_y - px_total
220 let torso_y: nx_int = top_y + head_d
221 let legs_y: nx_int = torso_y + torso_h
222
223 // cartoon outline: a slightly larger dark silhouette behind the figure
224 if nx_style_outline(style) == 1 {
225 _scn_rect(fb, cx - torso_w / 2 - 1, torso_y - 1, torso_w + 2, torso_h + 2, NX_SCN_OUTLINE_RGB)
226 _scn_rect(fb, cx - torso_w / 2 - 1, legs_y - 1, leg_w + 2, leg_h + 2, NX_SCN_OUTLINE_RGB)
227 _scn_rect(fb, cx + torso_w / 2 - leg_w - 1, legs_y - 1, leg_w + 2, leg_h + 2, NX_SCN_OUTLINE_RGB)
228 _scn_disc(fb, cx, top_y + head_d / 2, head_d / 2 + 1, NX_SCN_OUTLINE_RGB)
229 }
230
231 // legs (outfit), torso (outfit), arms (outfit), head (skin), hair cap
232 _scn_rect(fb, cx - torso_w / 2, legs_y, leg_w, leg_h, e[NX_ENT_OUTFIT_RGB])
233 _scn_rect(fb, cx + torso_w / 2 - leg_w, legs_y, leg_w, leg_h, e[NX_ENT_OUTFIT_RGB])
234 _scn_rect(fb, cx - torso_w / 2, torso_y, torso_w, torso_h, e[NX_ENT_OUTFIT_RGB])
235 let arm_w: nx_int = (leg_w * 717) / NX_MAGIC_1024
236 _scn_rect(fb, cx - torso_w / 2 - arm_w, torso_y, arm_w, (torso_h * 870) / NX_MAGIC_1024, e[NX_ENT_SKIN_RGB])
237 _scn_rect(fb, cx + torso_w / 2, torso_y, arm_w, (torso_h * 870) / NX_MAGIC_1024, e[NX_ENT_SKIN_RGB])
238 _scn_disc(fb, cx, top_y + head_d / 2, head_d / 2, e[NX_ENT_SKIN_RGB])
239 // hair: upper half-cap of the head disc
240 var hy: nx_int = top_y
241 while hy < top_y + head_d / 3 {
242 var hx: nx_int = cx - head_d / 2
243 while hx <= cx + head_d / 2 {
244 let ddx: nx_int = hx - cx
245 let ddy: nx_int = hy - (top_y + head_d / 2)
246 if ddx * ddx + ddy * ddy <= (head_d / 2) * (head_d / 2) {
247 _scn_px(fb, hx, hy, e[NX_ENT_HAIR_RGB])
248 }
249 hx = hx + 1
250 }
251 hy = hy + 1
252 }
253 // eyes (anime/VN): two dots of the identity eye color
254 if nx_style_eyes(style) == 1 {
255 let ey: nx_int = top_y + (head_d * 563) / NX_MAGIC_1024
256 let ex_off: nx_int = head_d / 5
257 if ex_off > 0 {
258 _scn_px(fb, cx - ex_off, ey, e[NX_ENT_EYE_RGB])
259 _scn_px(fb, cx - ex_off + 1, ey, e[NX_ENT_EYE_RGB])
260 _scn_px(fb, cx + ex_off, ey, e[NX_ENT_EYE_RGB])
261 _scn_px(fb, cx + ex_off - 1, ey, e[NX_ENT_EYE_RGB])
262 }
263 }
264 return 0
265}
266
267// ===== Tier-1: render a placed scene ==================================
268func nx_scene_render(sw: *i64, e: *i64, fb: *u8) -> nx_int {
269 let land: *Landscape = sw[NX_SW_LAND] as *Landscape
270 let style: nx_int = sw[NX_SW_STYLE]
271 _scn_terrain(fb, land, e[NX_ENT_ANCHOR_Y], style)
272 _scn_entity(fb, land, e, style)
273 return 0
274}
275
276// ===== Tier-0: one call -> PPM on disk ================================
277func nx_scene_quickstart(world_seed: nx_int, human_seed: nx_int,
278 style: nx_int, path: *u8) -> nx_int {
279 let sw: *i64 = nx_scene_world(world_seed, NX_PROCGEN_PRESET_MOUNTAIN, style)
280 let e: *i64 = nx_entity_human(human_seed)
281 let land: *Landscape = sw[NX_SW_LAND] as *Landscape
282 nx_scene_place(e, land.width / 2, land.height / 2)
283 let fb: *u8 = sys_mmap(NX_SCN_W * NX_SCN_H * 3)
284 nx_scene_render(sw, e, fb)
285 let cap: nx_int = NX_SCN_W * NX_SCN_H * 3 + 64
286 let out: *u8 = sys_mmap(cap)
287 let total: nx_int = ppm_write_p6(out, cap, NX_SCN_W, NX_SCN_H, fb)
288 if total <= 0 { return 0 - 1 }
289 let fd: nx_int = sys_openat_wr(path, 0x1a4)
290 if fd < 0 { return 0 - 2 }
291 let wr: nx_int = sys_write(fd, out, total)
292 sys_close(fd)
293 if wr != total { return 0 - 3 }
294 return 0
295}