feat(examples): add Interior Mapping (pre-projected room atlas)
Implement the Zhihu Interior Mapping technique as a Mesh3D example: tangent-space ray-AABB, perspective correction, runtime room atlas, procedural foreground furniture ray-march, window frames and fresnel. Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>
This commit is contained in:
4 files changed
+605
No files matched your search
@@ -0,0 +1,39 @@
|
||||
# Interior Mapping(室内映射)
|
||||
|
||||
实现知乎专栏 [Interior Mapping室内映射技术拆解](https://zhuanlan.zhihu.com/p/2071667769171513389)
|
||||
中的预投影 2D Atlas 室内映射:在无真实室内几何的情况下,透过窗户看到有透视深度的房间。
|
||||
|
||||
## 技术要点
|
||||
|
||||
1. **切线空间射线–AABB 求交**:每个窗户 tile 视为单位房间盒,用视线与远平面交点采样。
|
||||
2. **伪透视缩放**:`scale = B / (B + depthNorm)`,对预投影贴图做相似三角形校正。
|
||||
3. **房间图集**:运行时生成 4×4 正交预投影房间 Atlas,支持顺序 / 随机选房。
|
||||
4. **前景遮挡**:文章中的深度图 Ray March 在此用**过程化家具 AABB** 代替(同算法:步进 + 二分求精),便于示例自包含、不依赖额外 set-1 深度贴图。
|
||||
5. **窗框 + 掠射菲涅尔**:浅角时偏反射,掩盖预投影极限。
|
||||
|
||||
## 运行
|
||||
|
||||
```sh
|
||||
make run/linux-debug GAME=examples/interior-mapping
|
||||
# 或
|
||||
cd examples/interior-mapping && ../../build/linux-debug/src/engine/eve run
|
||||
```
|
||||
|
||||
## 操作
|
||||
|
||||
| 键 | 作用 |
|
||||
| --- | --- |
|
||||
| `O` | 开关自动环绕 |
|
||||
| `A` / `D` | 偏航 |
|
||||
| `W` / `S` | 俯仰 |
|
||||
| `[` / `]` | 房间深度 |
|
||||
| `-` / `=` | 透视强度 |
|
||||
| `F` | 开关过程化家具遮挡 |
|
||||
| `R` | 开关随机房间 |
|
||||
| `1` / `2` / `3` | 浅 / 默认 / 深预设 |
|
||||
|
||||
## 文件
|
||||
|
||||
- `shaders/interior_mapping.frag` — Mesh3D 自定义片元着色器
|
||||
- `main.nut` — 场景、Atlas 生成、参数绑定
|
||||
- `config.nut` — 窗口配置
|
||||
@@ -0,0 +1,8 @@
|
||||
config = {
|
||||
width = 1280
|
||||
height = 720
|
||||
title = "EVEngine — Interior Mapping"
|
||||
debug = true
|
||||
hotReload = true
|
||||
modules = ["gfx", "win", "fs", "image"]
|
||||
};
|
||||
@@ -0,0 +1,317 @@
|
||||
// Interior Mapping demo — pre-projected 2D room atlas + tangent-space
|
||||
// ray–AABB (Zhihu: 「Interior Mapping室内映射技术拆解」).
|
||||
//
|
||||
// Controls:
|
||||
// O toggle auto-orbit
|
||||
// A/D yaw
|
||||
// W/S pitch
|
||||
// [ / ] room depth
|
||||
// - / = perspective strength
|
||||
// F toggle procedural foreground furniture
|
||||
// R toggle random room selection
|
||||
// 1/2/3 preset looks (shallow / default / deep)
|
||||
|
||||
persist imShader = null
|
||||
persist imCamera = null
|
||||
persist imFacade = null
|
||||
persist imGround = null
|
||||
persist imAtlas = null
|
||||
persist imTime = 0.0
|
||||
persist imYaw = 0.35
|
||||
persist imPitch = 0.18
|
||||
persist imOrbit = true
|
||||
persist imRoomDepth = 0.55
|
||||
persist imPerspective = 1.15
|
||||
persist imRandomRoom = 1.0
|
||||
persist imShowFg = 1.0
|
||||
persist imFgSteps = 16.0
|
||||
persist imStatus = "orbit on"
|
||||
|
||||
const ATLAS_COLS = 4;
|
||||
const ATLAS_ROWS = 4;
|
||||
const CELL = 128;
|
||||
const WIN_X = 6;
|
||||
const WIN_Y = 4;
|
||||
|
||||
function clampf(v, a, b) {
|
||||
if (v < a) return a;
|
||||
if (v > b) return b;
|
||||
return v;
|
||||
}
|
||||
|
||||
function pushUniforms() {
|
||||
imShader.sendFloat("atlasColumns", ATLAS_COLS.tofloat());
|
||||
imShader.sendFloat("atlasRows", ATLAS_ROWS.tofloat());
|
||||
imShader.sendFloat("perspective", imPerspective);
|
||||
imShader.sendFloat("roomDepth", imRoomDepth);
|
||||
imShader.sendFloat("randomRoom", imRandomRoom);
|
||||
imShader.sendFloat("randomSeed", 3.0);
|
||||
imShader.sendFloat("fgSteps", imFgSteps);
|
||||
imShader.sendFloat("time", imTime);
|
||||
imShader.sendFloat("showFg", imShowFg);
|
||||
imShader.sendFloat("frameWidth", 0.055);
|
||||
}
|
||||
|
||||
function roomPalette(id) {
|
||||
// Distinct wall / floor / ceiling / accent per room cell.
|
||||
local 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]
|
||||
];
|
||||
return walls[id % walls.len()];
|
||||
}
|
||||
|
||||
function paintRoom(img, ox, oy, id) {
|
||||
local wall = roomPalette(id);
|
||||
local floorC = [wall[0] * 0.55, wall[1] * 0.48, wall[2] * 0.42];
|
||||
local ceilC = [wall[0] * 1.08, wall[1] * 1.06, wall[2] * 1.05];
|
||||
local accent = [
|
||||
0.35 + 0.4 * ((id * 17) % 5).tofloat() / 4.0,
|
||||
0.25 + 0.35 * ((id * 9) % 7).tofloat() / 6.0,
|
||||
0.20 + 0.45 * ((id * 13) % 4).tofloat() / 3.0
|
||||
];
|
||||
local backDoor = (id % 3) == 0;
|
||||
local windowSide = (id % 5) == 2;
|
||||
local shelf = (id % 4) != 1;
|
||||
|
||||
for (local y = 0; y < CELL; ++y) {
|
||||
for (local x = 0; x < CELL; ++x) {
|
||||
local u = (x + 0.5) / CELL.tofloat();
|
||||
local v = (y + 0.5) / CELL.tofloat();
|
||||
// Orthographic "pre-projection": floor / ceiling / side walls / back.
|
||||
local col = wall;
|
||||
if (v < 0.18) {
|
||||
col = ceilC;
|
||||
} else if (v > 0.78) {
|
||||
// Floor with simple board lines.
|
||||
local board = ((u * 8.0).tointeger() % 2) == 0 ? 0.92 : 1.0;
|
||||
col = [floorC[0] * board, floorC[1] * board, floorC[2] * board];
|
||||
} else if (u < 0.12) {
|
||||
col = [wall[0] * 0.82, wall[1] * 0.82, wall[2] * 0.85];
|
||||
} else if (u > 0.88) {
|
||||
col = [wall[0] * 0.88, wall[1] * 0.86, wall[2] * 0.84];
|
||||
} else {
|
||||
// Back wall with optional door / art / window.
|
||||
col = wall;
|
||||
if (backDoor && u > 0.38 && u < 0.62 && v > 0.28 && v < 0.78) {
|
||||
col = [0.22, 0.18, 0.14];
|
||||
if (u > 0.56 && u < 0.60 && v > 0.48 && v < 0.54) {
|
||||
col = [0.75, 0.65, 0.35]; // knob
|
||||
}
|
||||
}
|
||||
if (windowSide && u > 0.70 && u < 0.86 && v > 0.32 && v < 0.62) {
|
||||
col = [0.45, 0.62, 0.82];
|
||||
}
|
||||
if (shelf && u > 0.16 && u < 0.34 && v > 0.40 && v < 0.70) {
|
||||
local onShelf = ((v * 20.0).tointeger() % 4) == 0;
|
||||
if (onShelf) col = [accent[0] * 0.5, accent[1] * 0.5, accent[2] * 0.5];
|
||||
else col = accent;
|
||||
}
|
||||
// Ceiling lamp hint.
|
||||
if (u > 0.46 && u < 0.54 && v > 0.20 && v < 0.28) {
|
||||
col = [0.95, 0.90, 0.70];
|
||||
}
|
||||
}
|
||||
// Soft vignette toward edges of the cell (helps perspective read).
|
||||
local edge = clampf(u * 4.0, 0.0, 1.0) * clampf((1.0 - u) * 4.0, 0.0, 1.0) *
|
||||
clampf(v * 3.0, 0.0, 1.0) * clampf((1.0 - v) * 3.0, 0.0, 1.0);
|
||||
edge = 0.75 + 0.25 * edge;
|
||||
img.setPixel(ox + x, oy + y, col[0] * edge, col[1] * edge, col[2] * edge, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildAtlas() {
|
||||
local imageModule = eve.Image();
|
||||
local w = ATLAS_COLS * CELL;
|
||||
local h = ATLAS_ROWS * CELL;
|
||||
local pixels = imageModule.newEmptyImageData(w, h, "RGBA8");
|
||||
local id = 0;
|
||||
for (local row = 0; row < ATLAS_ROWS; ++row) {
|
||||
for (local col = 0; col < ATLAS_COLS; ++col) {
|
||||
// Atlas V grows downward in ImageData; shader UV V grows up conventionally
|
||||
// for Mesh3D — paint so row 0 is the top of the image (v=1 in GL).
|
||||
local oy = (ATLAS_ROWS - 1 - row) * CELL;
|
||||
paintRoom(pixels, col * CELL, oy, id);
|
||||
id += 1;
|
||||
}
|
||||
}
|
||||
imAtlas = gfx.newTexture(pixels, false, false);
|
||||
gfx.setTextureSampler(imAtlas, "linear", "none", 1.0, 0.0);
|
||||
}
|
||||
|
||||
function makeFacadeMesh() {
|
||||
// Single wall in XY facing +Z, UVs tiled by window grid.
|
||||
local hw = WIN_X.tofloat() * 0.5;
|
||||
local hh = WIN_Y.tofloat() * 0.5;
|
||||
local pos = [
|
||||
-hw, -hh, 0.0,
|
||||
hw, -hh, 0.0,
|
||||
hw, hh, 0.0,
|
||||
-hw, hh, 0.0
|
||||
];
|
||||
local nrm = [
|
||||
0.0, 0.0, 1.0,
|
||||
0.0, 0.0, 1.0,
|
||||
0.0, 0.0, 1.0,
|
||||
0.0, 0.0, 1.0
|
||||
];
|
||||
local uv = [
|
||||
0.0, 0.0,
|
||||
WIN_X.tofloat(), 0.0,
|
||||
WIN_X.tofloat(), WIN_Y.tofloat(),
|
||||
0.0, WIN_Y.tofloat()
|
||||
];
|
||||
local idx = [0, 1, 2, 0, 2, 3];
|
||||
return gfx.newMeshFromArrays(pos, nrm, uv, 4, idx, 6);
|
||||
}
|
||||
|
||||
function updateCamera() {
|
||||
local dist = 7.2;
|
||||
local cx = dist * cos(imPitch) * sin(imYaw);
|
||||
local cy = 0.4 + dist * sin(imPitch);
|
||||
local cz = dist * cos(imPitch) * cos(imYaw);
|
||||
imCamera.setEye(cx, cy, cz);
|
||||
imCamera.setTarget(0.0, 0.15, 0.0);
|
||||
}
|
||||
|
||||
function applyPreset(which) {
|
||||
if (which == 1) {
|
||||
imRoomDepth = 0.35;
|
||||
imPerspective = 0.85;
|
||||
imStatus = "preset shallow";
|
||||
} else if (which == 2) {
|
||||
imRoomDepth = 0.55;
|
||||
imPerspective = 1.15;
|
||||
imStatus = "preset default";
|
||||
} else {
|
||||
imRoomDepth = 0.78;
|
||||
imPerspective = 1.6;
|
||||
imStatus = "preset deep";
|
||||
}
|
||||
}
|
||||
|
||||
eve_init = function() {
|
||||
gfx.setBackgroundColor(0.12, 0.14, 0.18, 1.0);
|
||||
if (imShader == null) {
|
||||
imShader = gfx.newMeshShader(fs.readText("shaders/interior_mapping.frag"));
|
||||
imShader.declareFloat("atlasColumns");
|
||||
imShader.declareFloat("atlasRows");
|
||||
imShader.declareFloat("perspective");
|
||||
imShader.declareFloat("roomDepth");
|
||||
imShader.declareFloat("randomRoom");
|
||||
imShader.declareFloat("randomSeed");
|
||||
imShader.declareFloat("fgSteps");
|
||||
imShader.declareFloat("time");
|
||||
imShader.declareFloat("showFg");
|
||||
imShader.declareFloat("frameWidth");
|
||||
}
|
||||
if (imAtlas == null) buildAtlas();
|
||||
if (imFacade == null) {
|
||||
imFacade = eve.Renderable3D();
|
||||
imFacade.setMesh(makeFacadeMesh());
|
||||
imFacade.setShader(imShader);
|
||||
imFacade.setTexture(imAtlas);
|
||||
imFacade.setTint(1.0, 1.0, 1.0, 1.0);
|
||||
imFacade.setCastShadow(false);
|
||||
imFacade.setReceiveShadow(false);
|
||||
imFacade.setPosition(0.0, 0.9, 0.0);
|
||||
}
|
||||
if (imGround == null) {
|
||||
imGround = eve.Renderable3D();
|
||||
imGround.setMesh(gfx.newMeshCube(1.0));
|
||||
imGround.setPosition(0.0, -0.12, 0.0);
|
||||
imGround.setScale(10.0, 0.2, 6.0);
|
||||
imGround.setTint(0.18, 0.19, 0.22, 1.0);
|
||||
imGround.setRoughness(0.95);
|
||||
imGround.setCastShadow(false);
|
||||
}
|
||||
if (imCamera == null) {
|
||||
imCamera = eve.Camera3D();
|
||||
imCamera.setFov(42.0);
|
||||
imCamera.setAmbient(0.22, 0.24, 0.28);
|
||||
imCamera.setActive(true);
|
||||
}
|
||||
gfx.setDirectionalLight(-0.35, 0.75, 0.55, 1.35, 1.25, 1.15);
|
||||
pushUniforms();
|
||||
updateCamera();
|
||||
print("Interior Mapping: O orbit | A/D yaw | [/] depth | -/= perspective | F furniture | R random\n");
|
||||
};
|
||||
|
||||
eve_asset_reload <- function(path) {
|
||||
if (path.find("interior_mapping.frag") != null) {
|
||||
local src = fs.readText("shaders/interior_mapping.frag");
|
||||
local result = gfx.replaceShaderFromGlsl(imShader, "", src);
|
||||
if (result.ok) {
|
||||
imStatus = "shader reloaded";
|
||||
print("[interior-mapping] shader reloaded\n");
|
||||
} else {
|
||||
imStatus = "compile failed";
|
||||
print("[interior-mapping] compile failed\n");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
eve_update = function(dt) {
|
||||
imTime += dt;
|
||||
if (imOrbit) imYaw += dt * 0.22;
|
||||
|
||||
if (key_just_pressed("o") || key_just_pressed("O")) {
|
||||
imOrbit = !imOrbit;
|
||||
imStatus = imOrbit ? "orbit on" : "orbit off";
|
||||
}
|
||||
if (key_just_pressed("a") || key_just_pressed("A")) imYaw -= 0.12;
|
||||
if (key_just_pressed("d") || key_just_pressed("D")) imYaw += 0.12;
|
||||
if (key_just_pressed("w") || key_just_pressed("W")) imPitch = clampf(imPitch + 0.06, -0.2, 0.55);
|
||||
if (key_just_pressed("s") || key_just_pressed("S")) imPitch = clampf(imPitch - 0.06, -0.2, 0.55);
|
||||
|
||||
if (key_just_pressed("[")) {
|
||||
imRoomDepth = clampf(imRoomDepth - 0.05, 0.1, 0.9);
|
||||
imStatus = "depth " + imRoomDepth;
|
||||
}
|
||||
if (key_just_pressed("]")) {
|
||||
imRoomDepth = clampf(imRoomDepth + 0.05, 0.1, 0.9);
|
||||
imStatus = "depth " + imRoomDepth;
|
||||
}
|
||||
if (key_just_pressed("-") || key_just_pressed("_")) {
|
||||
imPerspective = clampf(imPerspective - 0.1, 0.2, 3.0);
|
||||
imStatus = "perspective " + imPerspective;
|
||||
}
|
||||
if (key_just_pressed("=") || key_just_pressed("+")) {
|
||||
imPerspective = clampf(imPerspective + 0.1, 0.2, 3.0);
|
||||
imStatus = "perspective " + imPerspective;
|
||||
}
|
||||
if (key_just_pressed("f") || key_just_pressed("F")) {
|
||||
imShowFg = imShowFg > 0.5 ? 0.0 : 1.0;
|
||||
imStatus = imShowFg > 0.5 ? "furniture on" : "furniture off";
|
||||
}
|
||||
if (key_just_pressed("r") || key_just_pressed("R")) {
|
||||
imRandomRoom = imRandomRoom > 0.5 ? 0.0 : 1.0;
|
||||
imStatus = imRandomRoom > 0.5 ? "random rooms" : "tiled rooms";
|
||||
}
|
||||
if (key_just_pressed("1")) applyPreset(1);
|
||||
if (key_just_pressed("2")) applyPreset(2);
|
||||
if (key_just_pressed("3")) applyPreset(3);
|
||||
|
||||
pushUniforms();
|
||||
updateCamera();
|
||||
};
|
||||
|
||||
eve_render = function() {
|
||||
gfx.clear();
|
||||
gfx.render3D();
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
#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.
|
||||
|
||||
layout(location = 0) in vec3 vNormal;
|
||||
layout(location = 1) in vec2 vUV;
|
||||
layout(location = 2) in vec4 vTint;
|
||||
layout(location = 3) in vec3 vWorldPos;
|
||||
layout(location = 4) in vec3 vCameraPos;
|
||||
layout(location = 5) in vec3 vViewPos;
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
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;
|
||||
Light3D lights[8];
|
||||
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;
|
||||
|
||||
// pc layout (declareFloat order):
|
||||
// 0 atlasColumns 1 atlasRows 2 perspective 3 roomDepth
|
||||
// 4 randomRoom 5 randomSeed 6 fgSteps 7 time
|
||||
// 8 showFg 9 frameWidth
|
||||
|
||||
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(
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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));
|
||||
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 (insideAABB(pos, bmin, bmax)) {
|
||||
vec3 lo = prev;
|
||||
vec3 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;
|
||||
}
|
||||
pos = hi;
|
||||
return true;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
void main() {
|
||||
float atlasColumns = max(pc.data[0], 1.0);
|
||||
float atlasRows = max(pc.data[1], 1.0);
|
||||
float perspective = max(pc.data[2], 0.05);
|
||||
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 showFg = pc.data[8];
|
||||
float frameWidth = clamp(pc.data[9], 0.0, 0.25);
|
||||
|
||||
vec2 uv = vUV;
|
||||
vec2 tileIndex = floor(uv);
|
||||
vec2 localUV = fract(uv);
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Depth scale: remap roomDepth like the cubemap path (farFrac).
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// 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;
|
||||
|
||||
outColor = vec4(color * vTint.rgb, vTint.a);
|
||||
}
|
||||
Reference in new issue
Block a user