Merge commit '3f27235' into lang/foundations
This commit is contained in:
commit
e53a9fc865
15 changed files with 427 additions and 107 deletions
11
changes/bake-images.md
Normal file
11
changes/bake-images.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
bump: minor
|
||||
type: feature
|
||||
**Bakes you can look at, and three more of the renderer's textures read from one.** `ludic.lab` writes
|
||||
raw 8-bit pixels (1 to 4 channels) as a PNG (`lab_png_write`, `png_write.ludic`, importable alone with its
|
||||
own `LabPngState`) and turns float textures into honest previews (`png_convert.ludic`: R32F min..max as
|
||||
grey, RG16F as red and green x255, HDR RGBA16F as x/(1+x) then sRGB). `ludic.render3d` takes three bakes
|
||||
the game names (`bake_load.ludic`) and makes each as before when one is missing or stale: an impostor's
|
||||
atlases as BC7 with their baked mips, through the compressed upload (`impostor_from_baked`,
|
||||
`impostor_fill_bc7`; a fog opening past its cards reads the bake again instead of painting), the sky's image-based light at the start yaw per prefilter width (`sky_baked_in`; any other
|
||||
turn of the sky is convolved), and the grass carpet (`carpet_from_baked`, `carpet_bytes`).
|
||||
`impostor_from_bytes` returns null, keeping nothing, when the bytes are not the impostor's shape.
|
||||
|
|
@ -61,6 +61,8 @@ showing its own models - adds them from its own module with `def LabScenes <name
|
|||
| `lab_settle(frames)`, `lab_font(dir)`, `lab_title(s)`, `lab_note(s)` | how long a shot settles; the overlay font for captions (none by default); the window's title; a line under the caption |
|
||||
| `lab_scene()`, `lab_frame()`, `lab_time()` | the scene, and the lab's clock |
|
||||
| `lab_ppm_to_png(ppm, png)` | a frame as a PNG (stored, not compressed) |
|
||||
| `lab_png_write(path, w, h, channels, px)`, `lab_png_write_from(..., px, at)` | 8-bit pixels (1 grey, 2 grey + alpha, 3 RGB, 4 RGBA) as a PNG, the same way; `png_write.ludic` needs nothing else of the lab, so a program with a lab of its own imports it alone (`import "ludic.lab/png_write.ludic"`, state `LabPngState`) |
|
||||
| `lab_px_r32f_norm`, `lab_px_rg16f`, `lab_px_rgba16f_hdr` (`png_convert.ludic`) | previews of float textures as 8-bit pixels: R32F min..max to black..white; RG16F as red and green x255, blue 0; HDR RGBA16F as x/(1+x) then sRGB, alpha dropped |
|
||||
|
||||
## The plate's files
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import "state.ludic"
|
|||
import "scenes.ludic"
|
||||
import "plate.ludic"
|
||||
import "shots.ludic"
|
||||
import "png_write.ludic"
|
||||
import "png_convert.ludic"
|
||||
import "png.ludic"
|
||||
import "boot.ludic"
|
||||
import "loop.ludic"
|
||||
|
|
|
|||
|
|
@ -1,39 +1,6 @@
|
|||
# png.ludic - the renderer writes a frame as a PPM; the lab keeps it as a PNG, stored rather than
|
||||
# compressed (a PNG anything opens, with no zlib here): the shot is the PPM's pixels, filter 0
|
||||
# on every row, in deflate's stored blocks, with its CRCs and its Adler-32.
|
||||
# compressed (png_write.ludic: a PNG anything opens, with no zlib here).
|
||||
|
||||
# the CRC-32 table, made once with the state
|
||||
function lp_crc_table() -> []long {
|
||||
let t = new []long
|
||||
let poly: long = 3988292384 # 0xEDB88320: eight hex digits would be a 32-bit pattern
|
||||
for n in 0 .. 256 {
|
||||
var c = long(n)
|
||||
for k in 0 .. 8 {
|
||||
if (c & long(1)) != long(0) { c = (c >> long(1)) ^ poly } else { c = c >> long(1) }
|
||||
}
|
||||
push(t, c)
|
||||
}
|
||||
return t
|
||||
}
|
||||
# CRC-32 of out[from .. from + n)
|
||||
function lp_crc(lab_st: mut LabState, out: []byte, from: int, n: int) -> long {
|
||||
let mask: long = 4294967295
|
||||
var c = mask
|
||||
for i in from .. from + n {
|
||||
let k = int((c ^ long(out[i])) & long(255))
|
||||
c = lab_st.lp_table[k] ^ (c >> long(8))
|
||||
}
|
||||
return c ^ mask
|
||||
}
|
||||
function lp_put32(out: []byte, at: int, v: long) -> void {
|
||||
out[at] = int((v >> long(24)) & long(255))
|
||||
out[at + 1] = int((v >> long(16)) & long(255))
|
||||
out[at + 2] = int((v >> long(8)) & long(255))
|
||||
out[at + 3] = int(v & long(255))
|
||||
}
|
||||
function lp_tag(out: []byte, at: int, tag: string) -> void {
|
||||
for i in 0 .. 4 { out[at + i] = tag[i] }
|
||||
}
|
||||
# a PPM's header: "P6", width, height, maxval, each after white space; returns the pixel offset
|
||||
function lp_ppm_header(lab_st: mut LabState, d: []byte) -> int {
|
||||
var i = 2
|
||||
|
|
@ -60,68 +27,10 @@ export function lab_ppm_to_png(lab_st: mut LabState, ppm: string, png: string) -
|
|||
let px = lp_ppm_header(lab_st, d)
|
||||
let w = lab_st.lp_w
|
||||
let h = lab_st.lp_h
|
||||
let row = w * 3 + 1
|
||||
let raw = row * h
|
||||
let blocks = (raw + 65534) / 65535
|
||||
let idat = 2 + blocks * 5 + raw + 4
|
||||
let total = 8 + 25 + 12 + idat + 12
|
||||
let out = buffer(total)
|
||||
let sig = [137, 80, 78, 71, 13, 10, 26, 10]
|
||||
for i in 0 .. 8 { out[i] = sig[i] }
|
||||
var at = 8
|
||||
lp_put32(out, at, long(13))
|
||||
lp_tag(out, at + 4, "IHDR")
|
||||
lp_put32(out, at + 8, long(w))
|
||||
lp_put32(out, at + 12, long(h))
|
||||
out[at + 16] = 8
|
||||
out[at + 17] = 2
|
||||
out[at + 18] = 0
|
||||
out[at + 19] = 0
|
||||
out[at + 20] = 0
|
||||
lp_put32(out, at + 21, lp_crc(lab_st, out, at + 4, 17))
|
||||
at += 25
|
||||
lp_put32(out, at, long(idat))
|
||||
lp_tag(out, at + 4, "IDAT")
|
||||
var o = at + 8
|
||||
out[o] = 120
|
||||
out[o + 1] = 1
|
||||
o += 2
|
||||
var a = 1
|
||||
var b = 0
|
||||
var left = raw
|
||||
var k = 0 # the byte of the raw stream being written
|
||||
while left > 0 {
|
||||
let n = Math.min(left, 65535)
|
||||
left -= n
|
||||
var fin = 0
|
||||
if left == 0 { fin = 1 }
|
||||
out[o] = fin
|
||||
out[o + 1] = n & 255
|
||||
out[o + 2] = (n >> 8) & 255
|
||||
out[o + 3] = (~n) & 255
|
||||
out[o + 4] = ((~n) >> 8) & 255
|
||||
o += 5
|
||||
for j in 0 .. n {
|
||||
let y = k / row
|
||||
let x = k % row
|
||||
var v = 0
|
||||
if x > 0 { v = d[px + y * w * 3 + x - 1] }
|
||||
out[o] = v
|
||||
a = (a + v) % 65521
|
||||
b = (b + a) % 65521
|
||||
o += 1
|
||||
k += 1
|
||||
}
|
||||
}
|
||||
lp_put32(out, o, long(b) * long(65536) + long(a))
|
||||
o += 4
|
||||
lp_put32(out, o, lp_crc(lab_st, out, at + 4, o - at - 4))
|
||||
o += 4
|
||||
lp_put32(out, o, long(0))
|
||||
lp_tag(out, o + 4, "IEND")
|
||||
lp_put32(out, o + 8, lp_crc(lab_st, out, o + 4, 4))
|
||||
o += 12
|
||||
if not Fs.write_bytes(png, out, o) { return false }
|
||||
if px + w * h * 3 > len(d) { return false }
|
||||
let out = buffer(lp_png_size(w, h, 3))
|
||||
let n = lp_encode(lab_st.lp_table, w, h, 3, d, px, out)
|
||||
if not Fs.write_bytes(png, out, n) { return false }
|
||||
Fs.remove(ppm)
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
71
packages/ludic.lab/png_convert.ludic
Normal file
71
packages/ludic.lab/png_convert.ludic
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# png_convert.ludic - previews of textures that are not 8-bit, as 8-bit pixels for lab_png_write. Each says
|
||||
# its mapping, because a preview is only honest when you know what a grey level means. Little-endian
|
||||
# samples as a GPU stores them; nothing here is ever read back into a texture.
|
||||
|
||||
# the 32-bit float at src[at]
|
||||
export function lab_f32_at(src: []byte, at: int) -> float {
|
||||
let bits = (src[at] & 255) | ((src[at + 1] & 255) << 8) | ((src[at + 2] & 255) << 16) | ((src[at + 3] & 255) << 24)
|
||||
return float_from_bits(bits)
|
||||
}
|
||||
# the 16-bit half float at src[at]; infinities and NaNs read as the largest half (65504)
|
||||
export function lab_f16_at(src: []byte, at: int) -> float {
|
||||
let h = (src[at] & 255) | ((src[at + 1] & 255) << 8)
|
||||
let e = (h >> 10) & 31
|
||||
let m = float(h & 1023)
|
||||
var v = 65504.0
|
||||
if e == 0 { v = m * Math.pow(2.0, -24.0) }
|
||||
if e > 0 and e < 31 { v = (1.0 + m / 1024.0) * Math.pow(2.0, float(e - 15)) }
|
||||
if (h & 32768) != 0 { return -v }
|
||||
return v
|
||||
}
|
||||
function lp_byte(x: float) -> int { return int(Math.clamp(x * 255.0 + 0.5, 0.0, 255.0)) }
|
||||
|
||||
# n R32F samples at src[at] as grey: the smallest value black, the largest white, linear between
|
||||
# (a flat field is black). The range is the image's own, so two previews are not on one scale
|
||||
@alloc_ok("a bake's preview, once")
|
||||
export function lab_px_r32f_norm(src: []byte, at: int, n: int) -> []byte {
|
||||
var lo = lab_f32_at(src, at)
|
||||
var hi = lo
|
||||
for i in 0 .. n {
|
||||
let v = lab_f32_at(src, at + i * 4)
|
||||
lo = Math.min(lo, v)
|
||||
hi = Math.max(hi, v)
|
||||
}
|
||||
let out = buffer(n)
|
||||
var span = hi - lo
|
||||
if span <= 0.0 { span = 1.0 }
|
||||
for i in 0 .. n { out[i] = lp_byte((lab_f32_at(src, at + i * 4) - lo) / span) }
|
||||
return out
|
||||
}
|
||||
|
||||
# n RG16F samples at src[at] as RGB: red and green each x255 clamped to 0..1, blue 0 (a table of two
|
||||
# numbers in 0..1, such as the BRDF lookup or a distance and a mask)
|
||||
@alloc_ok("a bake's preview, once")
|
||||
export function lab_px_rg16f(src: []byte, at: int, n: int) -> []byte {
|
||||
let out = buffer(n * 3)
|
||||
for i in 0 .. n {
|
||||
out[i * 3] = lp_byte(lab_f16_at(src, at + i * 4))
|
||||
out[i * 3 + 1] = lp_byte(lab_f16_at(src, at + i * 4 + 2))
|
||||
out[i * 3 + 2] = 0
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
# sRGB's encoding of a linear value in 0..1
|
||||
function lp_srgb(x: float) -> float {
|
||||
if x <= 0.0031308 { return x * 12.92 }
|
||||
return 1.055 * Math.pow(x, 1.0 / 2.4) - 0.055
|
||||
}
|
||||
# n RGBA16F samples at src[at] (linear HDR light) as RGB: each channel x/(1+x) (Reinhard: 1.0 lands at
|
||||
# half and nothing clips), then sRGB-encoded; alpha dropped. No exposure is applied
|
||||
@alloc_ok("a bake's preview, once")
|
||||
export function lab_px_rgba16f_hdr(src: []byte, at: int, n: int) -> []byte {
|
||||
let out = buffer(n * 3)
|
||||
for i in 0 .. n {
|
||||
for c in 0 .. 3 {
|
||||
let x = Math.max(lab_f16_at(src, at + i * 8 + c * 2), 0.0)
|
||||
out[i * 3 + c] = lp_byte(lp_srgb(x / (1.0 + x)))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
133
packages/ludic.lab/png_write.ludic
Normal file
133
packages/ludic.lab/png_write.ludic
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# png_write.ludic - raw 8-bit pixels (1 to 4 channels) as a PNG anything opens: filter 0 on every row,
|
||||
# deflate's stored blocks (no zlib here), the CRCs and the Adler-32. Needs nothing else of the lab, so a
|
||||
# program with a lab of its own imports this file alone: import "ludic.lab/png_write.ludic"
|
||||
|
||||
export state LabPngState {
|
||||
table: []long = lp_crc_table()
|
||||
}
|
||||
|
||||
# the CRC-32 table, made once with a state
|
||||
function lp_crc_table() -> []long {
|
||||
let t = new []long
|
||||
let poly: long = 3988292384 # 0xEDB88320: eight hex digits would be a 32-bit pattern
|
||||
for n in 0 .. 256 {
|
||||
var c = long(n)
|
||||
for k in 0 .. 8 {
|
||||
if (c & long(1)) != long(0) { c = (c >> long(1)) ^ poly } else { c = c >> long(1) }
|
||||
}
|
||||
push(t, c)
|
||||
}
|
||||
return t
|
||||
}
|
||||
# CRC-32 of out[from .. from + n)
|
||||
function lp_crc_of(table: []long, out: []byte, from: int, n: int) -> long {
|
||||
let mask: long = 4294967295
|
||||
var c = mask
|
||||
for i in from .. from + n {
|
||||
let k = int((c ^ long(out[i])) & long(255))
|
||||
c = table[k] ^ (c >> long(8))
|
||||
}
|
||||
return c ^ mask
|
||||
}
|
||||
function lp_put32(out: []byte, at: int, v: long) -> void {
|
||||
out[at] = int((v >> long(24)) & long(255))
|
||||
out[at + 1] = int((v >> long(16)) & long(255))
|
||||
out[at + 2] = int((v >> long(8)) & long(255))
|
||||
out[at + 3] = int(v & long(255))
|
||||
}
|
||||
function lp_tag(out: []byte, at: int, tag: string) -> void {
|
||||
for i in 0 .. 4 { out[at + i] = tag[i] }
|
||||
}
|
||||
# PNG's colour type for 1..4 channels: grey, grey + alpha, RGB, RGBA
|
||||
function lp_colour_type(channels: int) -> int {
|
||||
if channels == 1 { return 0 }
|
||||
if channels == 2 { return 4 }
|
||||
if channels == 3 { return 2 }
|
||||
return 6
|
||||
}
|
||||
|
||||
# the header, then the pixels, then the end, into one buffer; the length written, or -1
|
||||
@alloc_ok("a bake's preview or a lab shot: one image written, once")
|
||||
function lp_encode(table: []long, w: int, h: int, channels: int, px: []byte, at: int, out: []byte) -> int {
|
||||
let sig = [137, 80, 78, 71, 13, 10, 26, 10]
|
||||
for i in 0 .. 8 { out[i] = sig[i] }
|
||||
lp_put32(out, 8, long(13))
|
||||
lp_tag(out, 12, "IHDR")
|
||||
lp_put32(out, 16, long(w))
|
||||
lp_put32(out, 20, long(h))
|
||||
out[24] = 8
|
||||
out[25] = lp_colour_type(channels)
|
||||
out[26] = 0
|
||||
out[27] = 0
|
||||
out[28] = 0
|
||||
lp_put32(out, 29, lp_crc_of(table, out, 12, 17))
|
||||
let o = lp_idat(table, w, h, channels, px, at, out, 33)
|
||||
lp_put32(out, o, long(0))
|
||||
lp_tag(out, o + 4, "IEND")
|
||||
lp_put32(out, o + 8, lp_crc_of(table, out, o + 4, 4))
|
||||
return o + 12
|
||||
}
|
||||
# the IDAT chunk at `at0`: every row behind a filter byte of 0, in stored blocks; where it ends
|
||||
function lp_idat(table: []long, w: int, h: int, channels: int, px: []byte, at: int, out: []byte, at0: int) -> int {
|
||||
let stride = w * channels
|
||||
let row = stride + 1
|
||||
let raw = row * h
|
||||
let blocks = (raw + 65534) / 65535
|
||||
lp_put32(out, at0, long(2 + blocks * 5 + raw + 4))
|
||||
lp_tag(out, at0 + 4, "IDAT")
|
||||
var o = at0 + 8
|
||||
out[o] = 120
|
||||
out[o + 1] = 1
|
||||
o += 2
|
||||
var a = 1
|
||||
var b = 0
|
||||
var left = raw
|
||||
var k = 0 # the byte of the raw stream being written
|
||||
while left > 0 {
|
||||
let n = Math.min(left, 65535)
|
||||
left -= n
|
||||
var fin = 0
|
||||
if left == 0 { fin = 1 }
|
||||
out[o] = fin
|
||||
out[o + 1] = n & 255
|
||||
out[o + 2] = (n >> 8) & 255
|
||||
out[o + 3] = (~n) & 255
|
||||
out[o + 4] = ((~n) >> 8) & 255
|
||||
o += 5
|
||||
for j in 0 .. n {
|
||||
let x = k % row
|
||||
var v = 0
|
||||
if x > 0 { v = px[at + (k / row) * stride + x - 1] & 255 }
|
||||
out[o] = v
|
||||
a = (a + v) % 65521
|
||||
b = (b + a) % 65521
|
||||
o += 1
|
||||
k += 1
|
||||
}
|
||||
}
|
||||
lp_put32(out, o, long(b) * long(65536) + long(a))
|
||||
o += 4
|
||||
lp_put32(out, o, lp_crc_of(table, out, at0 + 4, o - at0 - 4))
|
||||
return o + 4
|
||||
}
|
||||
|
||||
# the bytes a PNG of this size takes, stored
|
||||
function lp_png_size(w: int, h: int, channels: int) -> int {
|
||||
let raw = (w * channels + 1) * h
|
||||
return 8 + 25 + 12 + 2 + ((raw + 65534) / 65535) * 5 + raw + 4 + 12
|
||||
}
|
||||
|
||||
# w x h pixels of `channels` 8-bit samples each (1 grey, 2 grey + alpha, 3 RGB, 4 RGBA), rows top down,
|
||||
# starting at px[at], written as a PNG at `path`
|
||||
@alloc_ok("a bake's preview or a lab shot: one image written, once")
|
||||
export function lab_png_write_from(lab_png_st: LabPngState, path: string, w: int, h: int, channels: int, px: []byte, at: int) -> bool {
|
||||
if channels < 1 or channels > 4 or w <= 0 or h <= 0 or at + w * h * channels > len(px) { return false }
|
||||
let out = buffer(lp_png_size(w, h, channels))
|
||||
let n = lp_encode(lab_png_st.table, w, h, channels, px, at, out)
|
||||
let ok = Fs.write_bytes(path, out, n)
|
||||
free(out)
|
||||
return ok
|
||||
}
|
||||
export function lab_png_write(lab_png_st: LabPngState, path: string, w: int, h: int, channels: int, px: []byte) -> bool {
|
||||
return lab_png_write_from(lab_png_st, path, w, h, channels, px, 0)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# lab_test.ludic - what the lab computes without a window: the PNG it keeps a shot as, the cameras
|
||||
# lab_test.ludic - what the lab computes without a window: the PNGs it writes (a shot, raw pixels), the cameras
|
||||
# a scene names, and the scenes a program adds
|
||||
import "ludic.lab"
|
||||
program LabTest {
|
||||
|
|
@ -35,6 +35,30 @@ program LabTest {
|
|||
expect(not Fs.exists(`{dir}/lab_test.ppm`))
|
||||
}
|
||||
|
||||
test "raw pixels become a PNG of their channel count" (lab_png_st: LabPngState) {
|
||||
let dir = Os.temp_dir()
|
||||
let px = buffer(3 * 2 * 4)
|
||||
for i in 0 .. 24 { px[i] = i * 10 }
|
||||
expect(lab_png_write(lab_png_st, `{dir}/lab_test_rgba.png`, 3, 2, 4, px))
|
||||
let p = Fs.read_bytes(`{dir}/lab_test_rgba.png`)
|
||||
expect_eq(p[19], 3) # width
|
||||
expect_eq(p[25], 6) # colour type RGBA
|
||||
expect_eq(len(p), lp_png_size(3, 2, 4))
|
||||
expect(not lab_png_write(lab_png_st, `{dir}/lab_test_short.png`, 9, 9, 4, px))
|
||||
}
|
||||
|
||||
test "a half float and a float read as the numbers they store" {
|
||||
let b = buffer(8)
|
||||
b[0] = 0; b[1] = 60 # half 1.0 = 0x3C00
|
||||
b[2] = 0; b[3] = 192 # half -2.0 = 0xC000
|
||||
b[4] = 0; b[5] = 0; b[6] = 128; b[7] = 63 # float 1.0 = 0x3F800000
|
||||
expect_near(lab_f16_at(b, 0), 1.0, 0.0001)
|
||||
expect_near(lab_f16_at(b, 2), -2.0, 0.0001)
|
||||
expect_near(lab_f32_at(b, 4), 1.0, 0.0001)
|
||||
let g = lab_px_rgba16f_hdr(b, 0, 1) # 1.0 -> 0.5 -> sRGB 188
|
||||
expect_eq(g[0] & 255, 188)
|
||||
}
|
||||
|
||||
test "a camera at a point looks back at it" (lab_st: mut LabState) {
|
||||
lab_shots_reset(lab_st)
|
||||
lab_shot_at(lab_st, "front", 0.0, 1.0, 0.0, 10.0, 0.0, 0.0)
|
||||
|
|
|
|||
139
packages/ludic.render3d/bake_load.ludic
Normal file
139
packages/ludic.render3d/bake_load.ludic
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# bake_load.ludic — the game names where a bake of the renderer's work lives, and the renderer takes the
|
||||
# textures from it when it is current and makes them as it always has when it is not: an impostor's
|
||||
# atlases (and again when a fog opens past its cards), the sky's image-based light, the grass carpet.
|
||||
|
||||
# ---- a bundle's parts, for whoever writes a preview of one (bake_pack's layout) ----
|
||||
function bake_part_count(data: []byte) -> int {
|
||||
if data == null or len(data) < 4 { return 0 }
|
||||
return Vk.get_i32(data_of(data), 0)
|
||||
}
|
||||
function bake_part_len(data: []byte, i: int) -> int { return Vk.get_i32(data_of(data), 4 + i * 4) }
|
||||
# where part i's bytes begin in the bundle
|
||||
function bake_part_off(data: []byte, i: int) -> int {
|
||||
var at = 4 + bake_part_count(data) * 4
|
||||
for k in 0 .. i { at += bake_part_len(data, k) }
|
||||
return at
|
||||
}
|
||||
|
||||
# ---- an impostor ----
|
||||
# where an impostor's bake lives (the game's name for it), whether or not its atlases came from it
|
||||
function impostor_source(im: Impostor, path: string, key: string, version: int) -> void {
|
||||
im.src_path = intern(path)
|
||||
im.src_key = intern(key)
|
||||
im.src_version = version
|
||||
}
|
||||
|
||||
# an impostor of this shape from its bake - both atlases BC7 with their mips, as tools/textures/bake_impostors.py
|
||||
# makes them - or null (missing, stale, not this shape, or a GPU without BC) for the caller to paint
|
||||
@alloc_ok("a bake read once at a map load")
|
||||
function impostor_from_baked(render3d_st: mut Render3dState, model: Model, tiles: int, tw: int, th: int, path: string, key: string, version: int) -> Impostor {
|
||||
let im = new Impostor
|
||||
im.tiles = tiles; im.radius = model.radius * 1.02; im.height = model.height
|
||||
im.model = model; im.tw = tw; im.th = th; im.flower = render3d_st.sc_bake_flower
|
||||
impostor_source(im, path, key, version)
|
||||
if not impostor_refill(render3d_st, im) { return null }
|
||||
im.src_ok = true
|
||||
return im
|
||||
}
|
||||
# the atlases made from the bake (at a load, and again when a fog opens past the cards); false, and none
|
||||
# kept, when it cannot be
|
||||
@alloc_ok("a bake read once at a load or when a fog opens")
|
||||
function impostor_refill(render3d_st: mut Render3dState, im: Impostor) -> bool {
|
||||
if not render3d_st.gvk_has_bc { return false }
|
||||
let b = r3d_baked_read(im.src_path, im.src_key, im.src_version)
|
||||
if b == null { return false }
|
||||
let ok = impostor_fill_bc7(render3d_st, im, b)
|
||||
free(b)
|
||||
return ok
|
||||
}
|
||||
# the two atlases from a bundle of two BC7 .dds (albedo, normal) whose size is the impostor's
|
||||
function impostor_fill_bc7(render3d_st: mut Render3dState, im: Impostor, data: []byte) -> bool {
|
||||
let was = render3d_st.gvk_tag
|
||||
render3d_st.gvk_tag = VKM_IMPOSTOR
|
||||
im.albedo = 0; im.normal = 0
|
||||
if bake_part_count(data) == 2 {
|
||||
im.albedo = impostor_bc7_part(render3d_st, im, data, 0)
|
||||
im.normal = impostor_bc7_part(render3d_st, im, data, 1)
|
||||
}
|
||||
let ok = im.albedo != 0 and im.normal != 0
|
||||
if not ok {
|
||||
gpu_tex_free(render3d_st, im.albedo); gpu_tex_free(render3d_st, im.normal)
|
||||
im.albedo = 0; im.normal = 0
|
||||
}
|
||||
render3d_st.gvk_tag = was
|
||||
return ok
|
||||
}
|
||||
# one atlas: its baked levels uploaded as they are (no gpu_tex_mips), sampled as a painted atlas is
|
||||
function impostor_bc7_part(render3d_st: mut Render3dState, im: Impostor, data: []byte, part: int) -> int {
|
||||
let at = bake_part_off(data, part)
|
||||
if at + bake_part_len(data, part) > len(data) { return 0 }
|
||||
let id = tex_load_dds_at(render3d_st, data, at, false)
|
||||
if id == 0 { return 0 }
|
||||
if render3d_st.tex_w != im.tiles * im.tw or render3d_st.tex_h != im.th {
|
||||
gpu_tex_free(render3d_st, id)
|
||||
return 0
|
||||
}
|
||||
# tex_load_dds_at repeats and filters anisotropically; a painted atlas (tex_target) is clamped, with neither
|
||||
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
|
||||
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
||||
gpu_tex_paramf(render3d_st, GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, fixed(1.0)) # 1.0 is off, as a painted atlas has it
|
||||
return id
|
||||
}
|
||||
|
||||
# ---- the sky's image-based light ----
|
||||
# the directory the sky's bakes are in, under the key and version they were baked as: before r3d_init.
|
||||
# A bake is one HDRI at the start yaw at one prefilter width; any other turn of the sky is convolved
|
||||
function sky_baked_in(render3d_st: mut Render3dState, dir: string, key: string, version: int) -> void {
|
||||
render3d_st.sky_bake_dir = intern(dir)
|
||||
render3d_st.sky_bake_key = intern(key)
|
||||
render3d_st.sky_bake_version = version
|
||||
}
|
||||
# the HDRI's name without its directory or ".hdr": what the sky's bakes are named by
|
||||
@alloc_ok("a bake's name, made at a load")
|
||||
function sky_hdri_name(render3d_st: Render3dState) -> string {
|
||||
let p = render3d_st.r3d_sky_path
|
||||
if p == null { return SKY_DEFAULT_HDRI }
|
||||
var from = 0
|
||||
for i in 0 .. len(p) { if p[i] == 47 { from = i + 1 } }
|
||||
var to = len(p)
|
||||
if to - from > 4 and p[to - 4] == 46 { to -= 4 }
|
||||
return p[from .. to]
|
||||
}
|
||||
# <dir>/<hdri>_y<start yaw in whole degrees>_w<prefilter width>.bin, the file one bake is
|
||||
@alloc_ok("a bake's name, made at a load")
|
||||
function sky_bake_file(render3d_st: Render3dState, w: int) -> string {
|
||||
let deg = int(Math.round(Math.rad_to_deg(render3d_st.sky_start_yaw)))
|
||||
return `{render3d_st.sky_bake_dir}/{sky_hdri_name(render3d_st)}_y{deg}_w{w}.bin`
|
||||
}
|
||||
# the IBL set from its bake when the sky is at the start yaw and a bake of this width is current
|
||||
@alloc_ok("a bake read once at a load or a new light quality")
|
||||
function sky_ibl_try_baked(render3d_st: mut Render3dState) -> bool {
|
||||
if len(render3d_st.sky_bake_dir) == 0 or render3d_st.sky_yaw != render3d_st.sky_start_yaw { return false }
|
||||
let path = sky_bake_file(render3d_st, render3d_st.sky_prefilter_w)
|
||||
let b = r3d_baked_read(path, render3d_st.sky_bake_key, render3d_st.sky_bake_version)
|
||||
var ok = false
|
||||
if b != null { ok = sky_ibl_from_bytes(render3d_st, b) }
|
||||
if b != null { free(b) }
|
||||
if not ok and not render3d_st.sky_bake_said { sky_say_missing(path) }
|
||||
if not ok { render3d_st.sky_bake_said = true }
|
||||
return ok
|
||||
}
|
||||
function sky_say_missing(path: string) -> void { print(`bake: {path} missing or stale - the sky's light made at run time (ludic bake)`) }
|
||||
|
||||
# ---- the grass carpet ----
|
||||
function carpet_bytes(render3d_st: mut Render3dState, tex: int) -> []byte { return bake_pack(render3d_st, bake_list1(tex)) }
|
||||
# the carpet texture (RGBA8, res square) from its bake, finished as carpet_bake finishes it; 0 when missing
|
||||
@alloc_ok("a bake read once at a map load")
|
||||
function carpet_from_baked(render3d_st: mut Render3dState, path: string, key: string, version: int, res: int) -> int {
|
||||
let b = r3d_baked_read(path, key, version)
|
||||
if b == null { return 0 }
|
||||
let was = render3d_st.gvk_tag
|
||||
render3d_st.gvk_tag = VKM_SCATTER
|
||||
let tex = tex_target(render3d_st, res, res, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
|
||||
let ok = bake_unpack(render3d_st, b, bake_list1(tex))
|
||||
free(b)
|
||||
if ok { carpet_finish(render3d_st, tex) } else { gpu_tex_free(render3d_st, tex) }
|
||||
render3d_st.gvk_tag = was
|
||||
if ok { return tex }
|
||||
return 0
|
||||
}
|
||||
|
|
@ -36,6 +36,12 @@ function bake_unpack(render3d_st: mut Render3dState, data: []byte, texs: words)
|
|||
return true
|
||||
}
|
||||
|
||||
@alloc_ok("a bake's list of textures, once")
|
||||
function bake_list1(a: int) -> words {
|
||||
let w = words(1)
|
||||
w[0] = a
|
||||
return w
|
||||
}
|
||||
@alloc_ok("a bake's list of textures, once")
|
||||
function bake_list2(a: int, b: int) -> words {
|
||||
let w = words(2)
|
||||
|
|
@ -52,13 +58,15 @@ function bake_list3(a: int, b: int, c: int) -> words {
|
|||
# ---- an impostor: its albedo and normal atlases (level 0; mips are made after) ----
|
||||
function impostor_bytes(render3d_st: mut Render3dState, im: Impostor) -> []byte { return bake_pack(render3d_st, bake_list2(im.albedo, im.normal)) }
|
||||
|
||||
# an impostor with the bake's shape (model, tiles and tile size as impostor_bake takes them) from bytes
|
||||
# an impostor with the bake's shape (model, tiles and tile size as impostor_bake takes them) from bytes;
|
||||
# null, and nothing kept, when the bytes are not that shape
|
||||
function impostor_from_bytes(render3d_st: mut Render3dState, model: Model, tiles: int, tw: int, th: int, data: []byte) -> Impostor {
|
||||
let im = new Impostor
|
||||
im.tiles = tiles; im.radius = model.radius * 1.02; im.height = model.height
|
||||
im.model = model; im.tw = tw; im.th = th; im.flower = render3d_st.sc_bake_flower
|
||||
impostor_fill(render3d_st, im, data)
|
||||
return im
|
||||
if impostor_fill(render3d_st, im, data) { return im }
|
||||
gpu_tex_free(render3d_st, im.albedo); gpu_tex_free(render3d_st, im.normal)
|
||||
return null
|
||||
}
|
||||
|
||||
# an impostor's atlases made and filled from bytes (at a load, or when a fog opens past its cards)
|
||||
|
|
@ -96,7 +104,7 @@ function terrain_shadow_from_bytes(render3d_st: mut Render3dState, data: []byte)
|
|||
# ---- the sky's image-based lighting: irradiance, the prefiltered levels, the BRDF table ----
|
||||
# for the HDRI already loaded and the yaw already set (the yaw is part of what a bake is keyed on)
|
||||
function sky_ibl_bytes(render3d_st: mut Render3dState) -> []byte {
|
||||
sky_precompute(render3d_st)
|
||||
sky_compute(render3d_st) # never from an older bake
|
||||
return bake_pack(render3d_st, bake_list3(render3d_st.sky_irradiance, render3d_st.sky_prefilter, render3d_st.sky_brdf))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -705,6 +705,10 @@ export state Render3dState {
|
|||
sky_p_pre: int = 0
|
||||
sky_p_brdf: int = 0
|
||||
sky_prefilter_w: int = 512
|
||||
sky_bake_dir: string = "" # where the sky's IBL bakes are (sky_baked_in, bake_load.ludic); "" = none
|
||||
sky_bake_key: string = ""
|
||||
sky_bake_version: int = 0
|
||||
sky_bake_said: bool = false # a missing bake said once
|
||||
STREAM_MAX_CHUNKS: int = 4096
|
||||
stream_all: []Stream = null
|
||||
stream_cap_read: bool = false
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# fog_impostors.ludic — a layer's impostor atlases are let go while the fog wall is nearer than its
|
||||
# cards begin (the layer's `near`), and baked again when the fog opens past it. Inside the wall every
|
||||
# cards begin (the layer's `near`), and baked again (or read again from its bake) when the fog opens past it. Inside the wall every
|
||||
# instance is a mesh, and the meshes' LOD2 cast its shadows, so a card is neither drawn nor cast.
|
||||
|
||||
# does the wall let any of this layer's instances be drawn as a card?
|
||||
|
|
@ -22,7 +22,8 @@ function fog_impostor(render3d_st: mut Render3dState, l: Layer) -> void {
|
|||
let t0 = gl_now_us()
|
||||
let was = render3d_st.gvk_tag
|
||||
render3d_st.gvk_tag = VKM_IMPOSTOR
|
||||
impostor_paint(render3d_st, im)
|
||||
# a baked impostor is read again rather than drawn again (bake_load.ludic)
|
||||
if not (im.src_ok and impostor_refill(render3d_st, im)) { impostor_paint(render3d_st, im) }
|
||||
render3d_st.gvk_tag = was
|
||||
render3d_st.fog_imp_rebake_us = render3d_st.fog_imp_rebake_us + (gl_now_us() - t0)
|
||||
render3d_st.fog_imp_rebaked += 1
|
||||
|
|
|
|||
|
|
@ -59,4 +59,5 @@ import "water.ludic"
|
|||
import "streamline.ludic"
|
||||
import "render.ludic"
|
||||
import "bake_parts.ludic"
|
||||
import "bake_load.ludic"
|
||||
import "safe_api.ludic"
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ function r3d_load_step(render3d_st: mut Render3dState, i: int) -> bool {
|
|||
var sky = render3d_st.r3d_sky_path
|
||||
var made: string = null
|
||||
if sky == null {
|
||||
made = render3d_st.r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr"
|
||||
made = render3d_st.r3d_assets + "/hdri/" + SKY_DEFAULT_HDRI + ".hdr"
|
||||
sky = made
|
||||
}
|
||||
let got = sky_load(render3d_st, sky)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@ property Impostor {
|
|||
model: Model, # what it was baked from, to bake it again (fog_impostors.ludic)
|
||||
tw: int = 0,
|
||||
th: int = 0,
|
||||
flower: bool = false
|
||||
flower: bool = false,
|
||||
src_path: string = "", # its bake file, where the game names one (bake_load.ludic): what a fog opening reads
|
||||
src_key: string = "",
|
||||
src_version: int = 0,
|
||||
src_ok: bool = false # its atlases came from that bake, so a re-open reads it rather than painting
|
||||
}
|
||||
property Layer {
|
||||
model: Model,
|
||||
|
|
@ -1230,14 +1234,18 @@ function carpet_bake__t(render3d_st: mut Render3dState, layers: []Layer, count:
|
|||
gpu_fb_free(render3d_st, fbo)
|
||||
gpu_rb_free(render3d_st, rb)
|
||||
gpu_buffer_free(render3d_st, buf)
|
||||
carpet_finish(render3d_st, tex)
|
||||
gpu_check(render3d_st, "carpet bake")
|
||||
return tex
|
||||
}
|
||||
# the carpet tile made ready to wear: repeat-wrapped and mipmapped (a bake's too, bake_load.ludic)
|
||||
function carpet_finish(render3d_st: mut Render3dState, tex: int) -> void {
|
||||
gpu_tex_bind(render3d_st, GPU_TEX2D, tex)
|
||||
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
|
||||
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
|
||||
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
|
||||
gpu_tex_paramf(render3d_st, GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, render3d_st.tex_anisotropy)
|
||||
gpu_tex_mips(render3d_st, GPU_TEX2D)
|
||||
gpu_check(render3d_st, "carpet bake")
|
||||
return tex
|
||||
}
|
||||
|
||||
# ---- another map at run time ------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
# ============================================================================
|
||||
|
||||
const SKY_PREFILTER_LEVELS: int = 6
|
||||
const SKY_DEFAULT_HDRI: string = "kloofendal_48d_partly_cloudy_puresky_4k" # under <assets>/hdri/, when no path is given
|
||||
|
||||
# The yaw to bake the light at the first time (radians, float bits). A game that turns the sky at
|
||||
# start sets it before r3d_init, so the image-based light is baked once at that yaw rather than at
|
||||
|
|
@ -90,7 +91,13 @@ function sky_set_quality(render3d_st: mut Render3dState, w: int) -> void {
|
|||
if render3d_st.sky_irradiance != 0 { sky_precompute(render3d_st) }
|
||||
}
|
||||
|
||||
# the IBL set for the sky as it is turned now: from its bake where there is a current one (bake_load.ludic)
|
||||
function sky_precompute(render3d_st: mut Render3dState) -> void {
|
||||
if sky_ibl_try_baked(render3d_st) { return }
|
||||
sky_compute(render3d_st)
|
||||
}
|
||||
# the IBL set convolved from the HDRI on the GPU
|
||||
function sky_compute(render3d_st: mut Render3dState) -> void {
|
||||
gpu_depth_test(render3d_st, false)
|
||||
sky_ibl_make(render3d_st)
|
||||
if render3d_st.sky_p_irr == 0 { render3d_st.sky_p_irr = r3d_program(render3d_st, "fullscreen.vert", "ibl_irradiance.frag", ""); render3d_st.sky_p_pre = r3d_program(render3d_st, "fullscreen.vert", "ibl_prefilter.frag", ""); render3d_st.sky_p_brdf = r3d_program(render3d_st, "fullscreen.vert", "ibl_brdf.frag", "") }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue