fix(interior-mapping): correct room-space rays and atlas projection

The mapping looked flat/skewed because rays were cast on the wrong side of
the facade, world directions were mixed with room-local origins, and
B/(B+d) UV crush / center mullions distorted the pre-projected atlas.

Use per-window object space (window at z=+1 into -Z), Joost far-hit slab
tests, bounded B/(B+d) sampling, outer sash only, and a matching
perspective atlas bake.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>
This commit is contained in:
Cursor Agentand西风逍遥游 committed 2026-09-13 14:14:32 +00:00
1 parent 5fd7084e7d
commit 2a3bb8ab3e
5 files changed
+208 -233

No files matched your search

+11
View File
@@ -39,3 +39,14 @@ cd examples/interior-mapping && ../../build/linux-debug/src/engine/eve run
- `assets/room_atlas.png` — 预投影房间图集(4×4)
- `scripts/gen_room_atlas.py` — 重新生成图集
- `config.nut` — 窗口配置
## Mapping notes
The fragment shader builds a **per-window room space** (window on `z=+1`, back wall on `z=-1`)
from tiled mesh UVs, casts a view ray with Joost’s far-hit box test, then samples the
pre-projected atlas with `scale = B / (B + d)`.
Debug: set `fgSteps` to `99` via the live uniform path to visualize face hits
(floor=red, ceiling=cyan, walls=green/blue, back=yellow).
Controls: O orbit, A/D yaw, W/S pitch, [/] depth, -/= perspective, F furniture, R random rooms.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+9 -9
View File
@@ -21,9 +21,9 @@ persist imYaw = 0.35
persist imPitch = 0.18
persist imOrbit = true
persist imRoomDepth = 0.55
persist imPerspective = 1.15
persist imPerspective = 1.0
persist imRandomRoom = 1.0
persist imShowFg = 1.0
persist imShowFg = 0.0
persist imFgSteps = 16.0
persist imStatus = "orbit on"
@@ -48,7 +48,7 @@ function pushUniforms() {
imShader.sendFloat("fgSteps", imFgSteps);
imShader.sendFloat("time", imTime);
imShader.sendFloat("showFg", imShowFg);
imShader.sendFloat("frameWidth", 0.055);
imShader.sendFloat("frameWidth", 0.04);
}
function buildAtlas() {
@@ -94,15 +94,15 @@ function updateCamera() {
function applyPreset(which) {
if (which == 1) {
imRoomDepth = 0.35;
imPerspective = 0.85;
imPerspective = 1.2;
imStatus = "preset shallow";
} else if (which == 2) {
imRoomDepth = 0.55;
imPerspective = 1.15;
imPerspective = 1.0;
imStatus = "preset default";
} else {
imRoomDepth = 0.78;
imPerspective = 1.6;
imRoomDepth = 0.75;
imPerspective = 0.65;
imStatus = "preset deep";
}
}
@@ -190,11 +190,11 @@ eve_update = function(dt) {
imStatus = "depth " + imRoomDepth;
}
if (key_just_pressed("-") || key_just_pressed("_")) {
imPerspective = clampf(imPerspective - 0.1, 0.2, 3.0);
imPerspective = clampf(imPerspective - 0.1, 0.2, 2.0);
imStatus = "perspective " + imPerspective;
}
if (key_just_pressed("=") || key_just_pressed("+")) {
imPerspective = clampf(imPerspective + 0.1, 0.2, 3.0);
imPerspective = clampf(imPerspective + 0.1, 0.2, 2.0);
imStatus = "perspective " + imPerspective;
}
if (key_just_pressed("f") || key_just_pressed("F")) {
@@ -1,5 +1,9 @@
#!/usr/bin/env python3
"""Generate the pre-projected room atlas PNG for examples/interior-mapping."""
"""Perspective room atlas matching the shader's behind-facade convention.
Camera at window z=+1 looking toward -Z (into the building).
film (u,v) → ray origin (0,0,+1), direction (u, v, -1).
"""
from __future__ import annotations
import struct
@@ -13,84 +17,105 @@ def clamp01(v: float) -> float:
return 0.0 if v < 0.0 else 1.0 if v > 1.0 else v
def palette(room_id: int) -> tuple[float, float, float]:
def palette(room_id: int):
walls = [
(0.78, 0.74, 0.68),
(0.55, 0.68, 0.72),
(0.72, 0.58, 0.52),
(0.62, 0.66, 0.55),
(0.70, 0.62, 0.78),
(0.80, 0.72, 0.58),
(0.58, 0.60, 0.68),
(0.74, 0.70, 0.62),
(0.66, 0.74, 0.70),
(0.76, 0.66, 0.60),
(0.60, 0.70, 0.78),
(0.68, 0.60, 0.58),
(0.72, 0.72, 0.66),
(0.58, 0.64, 0.58),
(0.70, 0.58, 0.66),
(0.64, 0.68, 0.74),
(0.78, 0.74, 0.68), (0.55, 0.68, 0.72), (0.72, 0.58, 0.52), (0.62, 0.66, 0.55),
(0.70, 0.62, 0.78), (0.80, 0.72, 0.58), (0.58, 0.60, 0.68), (0.74, 0.70, 0.62),
(0.66, 0.74, 0.70), (0.76, 0.66, 0.60), (0.60, 0.70, 0.78), (0.68, 0.60, 0.58),
(0.72, 0.72, 0.66), (0.58, 0.64, 0.58), (0.70, 0.58, 0.66), (0.64, 0.68, 0.74),
]
return walls[room_id % len(walls)]
def paint_room(buf: bytearray, ox: int, oy: int, stride: int, room_id: int) -> None:
def ray_box_far(origin, direction):
ox, oy, oz = origin
dx, dy, dz = direction
eps = 1e-5
def safe(v):
if abs(v) < eps:
return -eps if v < 0.0 else eps
return v
dx, dy, dz = safe(dx), safe(dy), safe(dz)
inv = (1.0 / dx, 1.0 / dy, 1.0 / dz)
t_far = (abs(inv[0]) - ox * inv[0], abs(inv[1]) - oy * inv[1], abs(inv[2]) - oz * inv[2])
t = min(t_far)
return (ox + t * dx, oy + t * dy, oz + t * dz)
def face_of(hit):
ax, ay, az = abs(hit[0]), abs(hit[1]), abs(hit[2])
if az >= ax and az >= ay:
return "back" if hit[2] < 0.0 else "window"
if ay >= ax:
return "ceil" if hit[1] > 0.0 else "floor"
return "right" if hit[0] > 0.0 else "left"
def shade_room(room_id, hit, face):
wall = palette(room_id)
floor_c = (wall[0] * 0.55, wall[1] * 0.48, wall[2] * 0.42)
ceil_c = (wall[0] * 1.08, wall[1] * 1.06, wall[2] * 1.05)
floor_c = (wall[0] * 0.50, wall[1] * 0.42, wall[2] * 0.36)
ceil_c = (min(1.0, wall[0] * 1.12), min(1.0, wall[1] * 1.10), min(1.0, wall[2] * 1.08))
left_c = (wall[0] * 0.78, wall[1] * 0.78, wall[2] * 0.82)
right_c = (wall[0] * 0.86, wall[1] * 0.84, wall[2] * 0.80)
accent = (
0.35 + 0.4 * ((room_id * 17) % 5) / 4.0,
0.25 + 0.35 * ((room_id * 9) % 7) / 6.0,
0.20 + 0.45 * ((room_id * 13) % 4) / 3.0,
)
back_door = room_id % 3 == 0
window_side = room_id % 5 == 2
shelf = room_id % 4 != 1
hx, hy, hz = hit
if face == "floor":
board = 0.88 if int((hx + 1.0) * 4.0) % 2 == 0 else 1.0
return (floor_c[0] * board, floor_c[1] * board, floor_c[2] * board)
if face == "ceil":
if abs(hx) < 0.12 and abs(hz + 0.2) < 0.12:
return (0.95, 0.90, 0.70)
return ceil_c
if face == "left":
if room_id % 4 != 1 and -0.2 < hy < 0.55 and -0.7 < hz < 0.0:
if int((hy + 1.0) * 10.0) % 4 == 0:
return (accent[0] * 0.5, accent[1] * 0.5, accent[2] * 0.5)
return accent
return left_c
if face == "right":
if room_id % 5 == 2 and -0.15 < hy < 0.45 and -0.75 < hz < -0.15:
return (0.45, 0.62, 0.82)
return right_c
col = wall
if room_id % 3 == 0 and -0.35 < hx < 0.35 and -1.0 < hy < 0.55:
col = (0.22, 0.18, 0.14)
if 0.18 < hx < 0.28 and -0.05 < hy < 0.08:
col = (0.75, 0.65, 0.35)
if room_id % 5 == 1 and 0.35 < hx < 0.75 and -0.2 < hy < 0.45:
col = (0.40, 0.55, 0.75)
if room_id % 4 == 2 and -0.55 < hx < 0.55 and -1.0 < hy < -0.35 and hz < -0.85:
col = (0.42, 0.26, 0.14)
return col
def paint_room(buf, ox, oy, stride, room_id):
origin = (0.0, 0.0, 1.0)
for y in range(CELL):
for x in range(CELL):
u = (x + 0.5) / CELL
v = (y + 0.5) / CELL
if v < 0.18:
col = ceil_c
elif v > 0.78:
board = 0.92 if int(u * 8.0) % 2 == 0 else 1.0
col = (floor_c[0] * board, floor_c[1] * board, floor_c[2] * board)
elif u < 0.12:
col = (wall[0] * 0.82, wall[1] * 0.82, wall[2] * 0.85)
elif u > 0.88:
col = (wall[0] * 0.88, wall[1] * 0.86, wall[2] * 0.84)
else:
col = wall
if back_door and 0.38 < u < 0.62 and 0.28 < v < 0.78:
col = (0.22, 0.18, 0.14)
if 0.56 < u < 0.60 and 0.48 < v < 0.54:
col = (0.75, 0.65, 0.35)
if window_side and 0.70 < u < 0.86 and 0.32 < v < 0.62:
col = (0.45, 0.62, 0.82)
if shelf and 0.16 < u < 0.34 and 0.40 < v < 0.70:
if int(v * 20.0) % 4 == 0:
col = (accent[0] * 0.5, accent[1] * 0.5, accent[2] * 0.5)
else:
col = accent
if 0.46 < u < 0.54 and 0.20 < v < 0.28:
col = (0.95, 0.90, 0.70)
edge = (
clamp01(u * 4.0)
* clamp01((1.0 - u) * 4.0)
* clamp01(v * 3.0)
* clamp01((1.0 - v) * 3.0)
)
edge = 0.75 + 0.25 * edge
u = (x + 0.5) / CELL * 2.0 - 1.0
v = 1.0 - (y + 0.5) / CELL * 2.0 # PNG top = +Y
direction = (u, v, -1.0) # into the building
hit = ray_box_far(origin, direction)
face = face_of(hit)
if face == "window":
face = "back"
col = shade_room(room_id, hit, face)
edge = clamp01(1.0 - max(abs(u), abs(v)) * 0.15)
i = ((oy + y) * stride + (ox + x)) * 4
buf[i] = int(clamp01(col[0] * edge) * 255 + 0.5)
buf[i + 1] = int(clamp01(col[1] * edge) * 255 + 0.5)
buf[i + 2] = int(clamp01(col[2] * edge) * 255 + 0.5)
buf[i] = int(clamp01(col[0] * (0.85 + 0.15 * edge)) * 255 + 0.5)
buf[i + 1] = int(clamp01(col[1] * (0.85 + 0.15 * edge)) * 255 + 0.5)
buf[i + 2] = int(clamp01(col[2] * (0.85 + 0.15 * edge)) * 255 + 0.5)
buf[i + 3] = 255
def write_png(path: Path, width: int, height: int, rgba: bytes) -> None:
def chunk(tag: bytes, data: bytes) -> bytes:
def write_png(path, width, height, rgba):
def chunk(tag, data):
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
raw = bytearray()
@@ -103,15 +128,13 @@ def write_png(path: Path, width: int, height: int, rgba: bytes) -> None:
path.write_bytes(png)
def main() -> None:
def main():
w, h = COLS * CELL, ROWS * CELL
buf = bytearray(w * h * 4)
room_id = 0
for row in range(ROWS):
for col in range(COLS):
# Image top = GL V=1 after typical upload; paint row0 at top.
oy = row * CELL
paint_room(buf, col * CELL, oy, w, room_id)
paint_room(buf, col * CELL, row * CELL, w, room_id)
room_id += 1
out = Path(__file__).resolve().parent.parent / "assets" / "room_atlas.png"
out.parent.mkdir(parents=True, exist_ok=True)
@@ -1,8 +1,8 @@
#version 450
// Interior Mapping (pre-projected 2D atlas) — based on
// 「Interior Mapping室内映射技术拆解」: tangent-space ray–AABB,
// perspective correction, atlas room selection, optional foreground
// ray-march against procedural furniture bounds.
// Interior Mapping — per-window object space (XY facade, +Z outward).
// Avoids cotangentFrame handedness bugs by reconstructing room-space
// camera from tiled UVs. Window at z=+1, back at z=-1.
// Atlas: scale = B/(B+d), d = 1 - hit.z.
layout(location = 0) in vec3 vNormal;
layout(location = 1) in vec2 vUV;
@@ -12,139 +12,98 @@ layout(location = 4) in vec3 vCameraPos;
layout(location = 5) in vec3 vViewPos;
layout(location = 0) out vec4 outColor;
struct Light3D {
vec4 posRadius;
vec4 color;
};
struct Light3D { vec4 posRadius; vec4 color; };
layout(set = 0, binding = 0, std140) uniform Frame {
mat4 mvp;
mat4 model;
vec4 lightDirIntensity;
vec4 lightColor;
vec4 tint;
vec4 cameraPos;
vec4 ambient;
mat4 mvp; mat4 model;
vec4 lightDirIntensity; vec4 lightColor; vec4 tint; vec4 cameraPos; vec4 ambient;
Light3D lights[8];
vec4 texBomb;
vec4 parallax;
mat4 view;
vec4 clipInfo;
vec4 cloud;
vec4 cloudWind;
vec4 texBomb; vec4 parallax; mat4 view; vec4 clipInfo; vec4 cloud; vec4 cloudWind;
} ubo;
layout(set = 0, binding = 1) uniform sampler2D albedoSampler;
layout(push_constant) uniform Externals { float data[32]; } pc;
layout(push_constant) uniform Externals {
float data[32];
} pc;
// pc layout (declareFloat order):
// 0 atlasColumns 1 atlasRows 2 perspective 3 roomDepth
// 4 randomRoom 5 randomSeed 6 fgSteps 7 time
// 8 showFg 9 frameWidth
// Facade layout matches main.nut (WIN_X=6, WIN_Y=4, 1 world unit per window).
const float kWinX = 6.0;
const float kWinY = 4.0;
float hash21(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
vec2 hash22(vec2 p) {
float n = hash21(p);
return vec2(n, hash21(p + n + 19.19));
}
// Cotangent frame (Mesh3D has no mesh tangents).
mat3 cotangentFrame(vec3 N, vec3 p, vec2 uv) {
vec3 dp1 = dFdx(p);
vec3 dp2 = dFdy(p);
vec2 duv1 = dFdx(uv);
vec2 duv2 = dFdy(uv);
vec3 dp2perp = cross(dp2, N);
vec3 dp1perp = cross(N, dp1);
vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;
float invMax = inversesqrt(max(dot(T, T), dot(B, B)));
return mat3(T * invMax, B * invMax, N);
}
// Far-hit only ray vs unit box [-1,1]^3 (Joost / article form).
vec3 rayBoxFarHit(vec3 rayPos, vec3 rayDir) {
vec3 safeDir = vec3(
// Slab method: return the FAR intersection with the unit box [-1,1]^3.
vec3 d = vec3(
abs(rayDir.x) < 1e-5 ? (rayDir.x < 0.0 ? -1e-5 : 1e-5) : rayDir.x,
abs(rayDir.y) < 1e-5 ? (rayDir.y < 0.0 ? -1e-5 : 1e-5) : rayDir.y,
abs(rayDir.z) < 1e-5 ? (rayDir.z < 0.0 ? -1e-5 : 1e-5) : rayDir.z);
vec3 invDir = 1.0 / safeDir;
vec3 tFar = abs(invDir) - rayPos * invDir;
float t = min(min(tFar.x, tFar.y), tFar.z);
return rayPos + t * rayDir;
vec3 inv = 1.0 / d;
vec3 t0 = (-1.0 - rayPos) * inv;
vec3 t1 = ( 1.0 - rayPos) * inv;
vec3 tMin = min(t0, t1);
vec3 tMax = max(t0, t1);
float tNear = max(max(tMin.x, tMin.y), tMin.z);
float tFar = min(min(tMax.x, tMax.y), tMax.z);
float t = tFar > tNear ? tFar : max(tFar, 0.0);
return rayPos + t * d;
}
vec2 projectToAtlas(vec3 hit, float perspective) {
float d = max(1.0 - hit.z, 1e-3);
float B = max(perspective, 0.05);
float scale = B / (B + d);
vec2 uv = hit.xy * scale * 0.5 + 0.5;
uv.y = 1.0 - uv.y;
return clamp(uv, 0.001, 0.999);
}
vec3 faceColor(vec3 hit) {
vec3 a = abs(hit);
if (a.z >= a.x && a.z >= a.y)
return hit.z < 0.0 ? vec3(1.0, 0.85, 0.35) : vec3(1.0, 0.0, 1.0);
if (a.y >= a.x)
return hit.y > 0.0 ? vec3(0.2, 0.85, 1.0) : vec3(1.0, 0.25, 0.1);
return hit.x > 0.0 ? vec3(0.2, 1.0, 0.3) : vec3(0.3, 0.35, 1.0);
}
// Per-room furniture AABB in room space [-1,1], z from -1 (window) to +1 (back).
// Returns bounds as (minXYZ, maxXYZ) packed; empty if no furniture.
bool furnitureBounds(vec2 roomIndex, out vec3 bmin, out vec3 bmax) {
float style = hash21(roomIndex + 3.17);
if (style < 0.18) {
// empty room
return false;
}
if (style < 0.45) {
// desk against back wall
bmin = vec3(-0.55, -1.0, 0.35);
bmax = vec3(0.55, -0.35, 0.95);
return true;
}
if (style < 0.70) {
// sofa / cabinet left
bmin = vec3(-0.95, -1.0, -0.15);
bmax = vec3(-0.35, -0.15, 0.55);
return true;
}
// tall plant / column right-center
bmin = vec3(0.35, -1.0, -0.25);
bmax = vec3(0.75, 0.35, 0.35);
return true;
if (style < 0.22) return false;
if (style < 0.50) { bmin=vec3(-0.45,-1.0,-0.92); bmax=vec3(0.45,-0.40,-0.40); return true; }
if (style < 0.75) { bmin=vec3(-0.92,-1.0,-0.55); bmax=vec3(-0.40,-0.20,-0.05); return true; }
bmin=vec3(0.40,-1.0,-0.40); bmax=vec3(0.72,0.30,-0.05); return true;
}
bool insideAABB(vec3 p, vec3 bmin, vec3 bmax) {
return all(greaterThanEqual(p, bmin)) && all(lessThanEqual(p, bmax));
}
bool rayMarchFurniture(inout vec3 pos, vec3 rayDir, int steps, vec3 bmin, vec3 bmax) {
// March from window (z~-1) toward back; pos is room-space.
float maxZ = bmax.z + 0.02;
float stepDepth = max(maxZ - pos.z, 0.05) / float(max(steps, 1));
float minZ = bmin.z - 0.02;
float stepDepth = max(pos.z - minZ, 0.05) / float(max(steps, 1));
vec3 stepSize = rayDir * (stepDepth / max(abs(rayDir.z), 1e-5));
vec3 prev = pos;
for (int i = 0; i < 32; ++i) {
if (i >= steps) break;
if (pos.z > maxZ || abs(pos.x) > 1.05 || abs(pos.y) > 1.05) break;
if (pos.z < minZ || abs(pos.x) > 1.05 || abs(pos.y) > 1.05) break;
if (insideAABB(pos, bmin, bmax)) {
vec3 lo = prev;
vec3 hi = pos;
vec3 lo = prev, hi = pos;
for (int j = 0; j < 5; ++j) {
vec3 mid = 0.5 * (lo + hi);
if (insideAABB(mid, bmin, bmax)) hi = mid;
else lo = mid;
if (insideAABB(mid, bmin, bmax)) hi = mid; else lo = mid;
}
pos = hi;
return true;
pos = hi; return true;
}
prev = pos;
pos += stepSize;
prev = pos; pos += stepSize;
}
return false;
}
vec3 furnitureColor(vec2 roomIndex, vec3 hit) {
float h = hash21(roomIndex + 9.1);
vec3 wood = vec3(0.45, 0.28, 0.14);
vec3 cloth = vec3(0.25 + 0.35 * h, 0.22, 0.35 + 0.2 * (1.0 - h));
vec3 metal = vec3(0.55, 0.58, 0.62);
float band = smoothstep(-0.4, -0.2, hit.y);
return mix(wood, mix(cloth, metal, step(0.7, h)), band);
return mix(vec3(0.45,0.28,0.14),
mix(vec3(0.25+0.35*h,0.22,0.35), vec3(0.55,0.58,0.62), step(0.7,h)),
smoothstep(-0.4,-0.2,hit.y));
}
void main() {
@@ -154,88 +113,70 @@ void main() {
float roomDepth = clamp(pc.data[3], 0.05, 0.95);
float randomRoom = pc.data[4];
float randomSeed = pc.data[5];
int fgSteps = int(clamp(pc.data[6], 1.0, 32.0));
float fgStepsF = pc.data[6];
int fgSteps = int(clamp(fgStepsF, 1.0, 32.0));
float showFg = pc.data[8];
float frameWidth = clamp(pc.data[9], 0.0, 0.25);
bool debugFaces = fgStepsF > 90.0;
vec2 uv = vUV;
vec2 tileIndex = floor(uv);
vec2 localUV = fract(uv);
vec2 tileIndex = floor(vUV);
vec2 localUV = fract(vUV);
vec2 atlasSize = floor(max(vec2(atlasColumns, atlasRows), vec2(1.0)));
vec2 roomIndex = tileIndex - floor(tileIndex / atlasSize) * atlasSize;
if (randomRoom > 0.5) {
vec2 rnd = hash22(tileIndex + randomSeed * 7.31);
roomIndex = floor(rnd * atlasSize);
}
if (randomRoom > 0.5)
roomIndex = floor(hash22(tileIndex + randomSeed * 7.31) * atlasSize);
vec3 N = normalize(vNormal);
mat3 TBN = cotangentFrame(N, vWorldPos, uv);
// Article: view from camera to surface, then into tangent space.
vec3 viewWS = vWorldPos - vCameraPos;
vec3 tangentViewDir = transpose(TBN) * viewWS; // world→tangent (TBN columns are T,B,N)
// World XY of this window's center (mesh: UV0→(-kWinX/2,-kWinY/2), 1 unit/window).
vec2 winCenterWS = tileIndex + 0.5 - vec2(kWinX, kWinY) * 0.5;
// Depth scale: remap roomDepth like the cubemap path (farFrac).
// Camera in room space: XY relative to window center (window spans [-1,1]),
// Z so facade world z=0 maps to room z=+1 (window plane).
vec3 camRoom;
// Scale so window half-extent (0.5 world) maps to room 1.0.
camRoom.xy = (vCameraPos.xy - winCenterWS) * 2.0;
camRoom.z = vCameraPos.z + 1.0;
vec3 rayPos = vec3(localUV * 2.0 - 1.0, 0.999);
vec3 rayDir = rayPos - camRoom; // camera → surface (Joost)
// Depth control: compress/expand the into-room Z before abs.
float depthScale = 1.0 / max(1.0 - roomDepth, 1e-4) - 1.0;
tangentViewDir.z *= max(depthScale, 1e-3);
vec3 rayDir = tangentViewDir;
// March into the room (+Z of tangent space after abs on Z).
rayDir.z = max(abs(rayDir.z), 1e-4);
// Window plane at z = -1, XY in [-1,1].
vec3 rayPos = vec3(localUV * 2.0 - 1.0, -1.0);
rayDir.z *= max(depthScale, 1e-3);
rayDir.z = -max(abs(rayDir.z), 1e-4);
vec3 hitPos = rayBoxFarHit(rayPos, rayDir);
// Perspective correction (similar triangles): Scale = B / (B + d).
float hitDepthNorm = clamp(hitPos.z * 0.5 + 0.5, 0.0, 1.0);
float perspectiveScale = perspective / (perspective + hitDepthNorm);
vec2 roomUV = hitPos.xy * perspectiveScale;
roomUV = roomUV * 0.5 + 0.5;
roomUV = clamp(roomUV, 0.001, 0.999);
vec2 atlasUV = (roomIndex + roomUV) / atlasSize;
vec4 bgColor = texture(albedoSampler, atlasUV);
vec3 color = bgColor.rgb;
bool hitFg = false;
if (showFg > 0.5) {
vec3 bmin, bmax;
if (furnitureBounds(roomIndex, bmin, bmax)) {
vec3 fgPos = rayPos;
hitFg = rayMarchFurniture(fgPos, rayDir, fgSteps, bmin, bmax);
if (hitFg) {
color = furnitureColor(roomIndex, fgPos);
// Slight lighting from room-space normal of nearest furniture face.
float shade = 0.75 + 0.25 * max(-fgPos.z, 0.0);
color *= shade;
vec3 color;
if (debugFaces) {
color = faceColor(hitPos);
} else {
vec2 roomUV = projectToAtlas(hitPos, clamp(perspective, 0.05, 2.0));
color = texture(albedoSampler, (roomIndex + roomUV) / atlasSize).rgb;
if (showFg > 0.5) {
vec3 bmin, bmax;
if (furnitureBounds(roomIndex, bmin, bmax)) {
vec3 fgPos = rayPos;
if (rayMarchFurniture(fgPos, rayDir, fgSteps, bmin, bmax)) {
color = furnitureColor(roomIndex, fgPos);
color *= 0.75 + 0.25 * max(fgPos.z, 0.0);
}
}
}
}
// Window frame (mullions) in local UV.
float fx = min(localUV.x, 1.0 - localUV.x);
float fy = min(localUV.y, 1.0 - localUV.y);
float frame = 1.0 - smoothstep(frameWidth * 0.55, frameWidth, min(fx, fy));
float mullionX = 1.0 - smoothstep(frameWidth * 0.2, frameWidth * 0.45, abs(localUV.x - 0.5));
float mullionY = 1.0 - smoothstep(frameWidth * 0.2, frameWidth * 0.45, abs(localUV.y - 0.5));
float mullion = max(mullionX, mullionY) * 0.85;
float frameMask = max(frame, mullion);
vec3 frameCol = vec3(0.08, 0.09, 0.11);
color = mix(color, frameCol, frameMask);
float frameMask = 1.0 - smoothstep(frameWidth * 0.55, frameWidth, min(fx, fy));
color = mix(color, vec3(0.08, 0.09, 0.11), frameMask);
// Grazing-angle glass fresnel (FH4 tip from the article lineage).
vec3 viewDir = normalize(vCameraPos - vWorldPos);
float ndotv = clamp(dot(N, viewDir), 0.0, 1.0);
float fresnel = pow(1.0 - ndotv, 3.0);
vec3 sky = vec3(0.55, 0.68, 0.85);
color = mix(color, sky, fresnel * 0.35 * (1.0 - frameMask));
// Mild exterior lighting on the facade plane.
vec3 L = normalize(ubo.lightDirIntensity.xyz);
float diff = 0.55 + 0.45 * max(dot(N, L), 0.0);
color *= mix(1.0, diff, 0.25);
color += ubo.ambient.rgb * 0.08;
if (!debugFaces) {
vec3 N = normalize(vNormal);
float fresnel = pow(1.0 - clamp(dot(N, normalize(vCameraPos - vWorldPos)), 0.0, 1.0), 3.0);
color = mix(color, vec3(0.55, 0.68, 0.85), fresnel * 0.35 * (1.0 - frameMask));
float diff = 0.55 + 0.45 * max(dot(N, normalize(ubo.lightDirIntensity.xyz)), 0.0);
color *= mix(1.0, diff, 0.25);
color += ubo.ambient.rgb * 0.08;
}
outColor = vec4(color * vTint.rgb, vTint.a);
}