code wiki / (root) / nx_scene_render.nx

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}