Skip to content

Commit 00a7689

Browse files
Hyeoncheol Kimclaude
andcommitted
look: solve the hero backlight against the terrain, stop AA erasing the eave
The entry settle frame had almost no rim. Attribution, measured: 1. The landing pinned the sun to the hero's back (rotY + pi + 55 deg), but baesan-imsu puts a mountain there. Measured on vseed 20260716: sun elevation 9.51 deg against a 16.59 deg terrain horizon on that bearing — the sun sat 7.08 deg *below* the ridge, so no direct light reached the compound at all and rim.js `_directGate` started pinned at its 0.45 shadow floor. Ridge profiles differ per seed, so no constant survives; src/env/hero-sun.js now solves azimuth and elevation together against the site, scoring the two gates that actually make the rim (altitude x backlit) and raising the sun only as far as clearing the ridge needs. Flat sites keep the profile elevation unchanged (identity path). 2. The Fresnel AA damp deleted the silhouette it exists to draw — a silhouette is by definition where ndv changes fastest. Measured at the hero settle (7 deg lens, 146 m): mean _aa 0.491 with 42.9% of silhouette triangles fully dead, readable fraction 0.803 -> 0.459. The two geometric cutoffs now attenuate to a floor instead of zeroing; the energy-conserving Toksvig term still does the real anti-aliasing. 3. Tile fields carry no rim by design, so the eave gold lives on the eave band alone — 2-3 px in that telephoto frame. It gets its own multiplier through the existing per-material uniform (no new program family). The discrete eave-end ornaments are excluded: at the same multiplier the ridge read as a dotted gold line, which is the stipple the AA layer exists to prevent. The sun solve also runs after the framing is known, so its backlit term uses the real settle azimuth (13.8 deg off rotY) instead of assuming the camera stands on the house axis. Gate: npm run check:hero-sun (pure, carries a FAIL-first fixture that reproduces the occluded legacy constant). check:rim confirms the program family plateau is unchanged (6 -> 6, one canonical LOD program). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 49347e8 commit 00a7689

10 files changed

Lines changed: 498 additions & 56 deletions

File tree

app/src/engine/engine.js

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
isVillageMjaHouseProductContext,
5252
} from '../../../src/api/village-options.js';
5353
import { terrainMeshHeightAt } from '../../../src/api/village-plan.js';
54+
import { solveHeroBacklight } from '../../../src/api/lighting.js';
5455
import { configFromSeed, paramsFor, newSeed } from '../lib/seed.js';
5556
import { buildingNavigationTargetFromProxy } from '../lib/building-navigation.js';
5657
import { normalizeStandaloneParamPatch } from '../lib/standalone-param-spec.js';
@@ -227,9 +228,17 @@ export function createEngine({ container, perf = false, compact = false } = {})
227228
let legacyHeroPoll = null;
228229
let legacyHeroFadeFrame = null;
229230
// 히어로 역광 방위(#98 사용자 지시). 종가 랜딩 시 종가 배면(frontDir+180±) 쪽으로 태양 방위를 고정해,
230-
// 정측면에서 멈춘 카메라가 집을 역광으로 보게 한다(처마 실루엣 골든 림). 태양 고도·색은 시간대(석양)
231-
// 그대로 두고 방위만 매 프레임 회전(env sky 가 sun.position 을 세팅한 뒤 덮어씀). null=미적용(비히어로).
231+
// 정측면에서 멈춘 카메라가 집을 역광으로 보게 한다(처마 실루엣 골든 림). 방위는 매 프레임 회전
232+
// (env sky 가 sun.position 을 세팅한 뒤 덮어씀). null=미적용(비히어로).
233+
//
234+
// 2026-08-07: 방위와 **고도**를 함께 지형에서 푼다(src/env/hero-sun.js). 종전엔 배면 +55° 상수에
235+
// 고도는 시간대 프로필 그대로였는데, 배산임수 규약상 종가 배면은 곧 배산이라 고도 9.51° 석양이
236+
// 능선(그 방위 16.59°) 뒤 7.08° 아래에 묻혔다 — 종가에 직사광이 0 이고 rim.js `_directGate` 가
237+
// 그림자 바닥값 0.45 로 눌린 채 시작했다(사용자 보고 "조립 후 림라이트가 거의 없다"). 능선 윤곽은
238+
// 시드마다 다르므로 상수로는 다른 부지에서 재발한다. heroSunElev=null 이면 프로필 고도 유지.
232239
let heroSunAz = null;
240+
let heroSunElev = null;
241+
let heroSunSolve = null; // 검증 판독(__hero.sunSolve)
233242

234243
// ---------- 그림자 정적 캐시(#140-A) ----------
235244
// sun.shadow.camera 는 ±22 ortho 크기를 유지하되, 선택/전환 중에는 controls.target 을 texel 단위로
@@ -1391,8 +1400,14 @@ export function createEngine({ container, perf = false, compact = false } = {})
13911400
// position 회전만으로 그림자·rim(post uSunViewDir)·flare 가 일관되게 역광으로 정렬된다.
13921401
if (village.active && heroSunAz != null) {
13931402
const sp = sun.position;
1394-
const hmag = Math.hypot(sp.x, sp.z);
1395-
if (hmag > 1e-4) { sp.x = Math.sin(heroSunAz) * hmag; sp.z = Math.cos(heroSunAz) * hmag; }
1403+
const r = sp.length();
1404+
if (r > 1e-4) {
1405+
// 고도는 해가 능선을 넘기려고 올려 둔 값이 있으면 그것을, 없으면 프로필 그대로.
1406+
// 반지름을 보존하므로 sun.target=원점 규약과 그림자 카메라 거리는 불변이다.
1407+
const el = heroSunElev != null ? heroSunElev : Math.asin(THREE.MathUtils.clamp(sp.y / r, -1, 1));
1408+
const ch = Math.cos(el);
1409+
sp.set(Math.sin(heroSunAz) * ch * r, Math.sin(el) * r, Math.cos(heroSunAz) * ch * r);
1410+
}
13961411
}
13971412
// Product sun.position remains a direction vector for clouds/motes/grass/rim/flare.
13981413
// Only the shadow camera follows the viewed architectural target. During focus
@@ -3139,15 +3154,6 @@ export function createEngine({ container, perf = false, compact = false } = {})
31393154
const bbSpan = pr.bbox ? { x: pr.bbox.max.x - pr.bbox.min.x, y: pr.bbox.max.y - pr.bbox.min.y, z: pr.bbox.max.z - pr.bbox.min.z } : null;
31403155
const rotY = Number.isFinite(pr.rotY) ? pr.rotY : 0;
31413156
const maxDim = Number.isFinite(pr.maxDim) ? pr.maxDim : (bbSpan ? Math.max(bbSpan.x, bbSpan.y, bbSpan.z) : 14);
3142-
// 역광 무대(#98): 태양을 종가 배면(frontDir≈rotY, +180°+25° 사선)에 고정한다.
3143-
// 카메라 XZ 방향은 일반 focus와 같은 남측 개방부 계약을 쓰므로 고정 방위로 앞집을 끌어들이지 않는다.
3144-
// 배면 사선 역광. +25° 는 태양이 시선축에서 11° 밖에 안 벗어나 정배면과 다름없었다(측정:
3145-
// sunAz -155° vs 카메라 시선 -166°). 그러면 카메라를 향한 모든 면 — 남측 지붕면·벽·배산 사면 —
3146-
// 이 전부 음영측이 되어 프레임이 실루엣 하나로 붕괴한다(정착 프레임 피사체 밴드 중값 14/255,
3147-
// 룩 계약 "크러시드 블랙 실루엣 금지" 위반). +55° 는 3/4 역광이다: 림 게이트의 backlit 항
3148-
// (-dot(표면→카메라, 표면→태양) ≥ 0.45)은 0.63~0.70 으로 여전히 만점이고, 서측 지붕면과
3149-
// 배산 사면이 골든 그레이징을 받아 처마선이 기댈 밝은 면이 생긴다.
3150-
heroSunAz = rotY + Math.PI + 55 * DEG;
31513157
village.heroRotY = rotY; // 검증용(카메라·태양 방위 vs frontDir 단언)
31523158
const heroFraming = pr.heroCameraFraming;
31533159
let finalPosition;
@@ -3171,6 +3177,55 @@ export function createEngine({ container, perf = false, compact = false } = {})
31713177
finalReferenceFov = fittedFocus.referenceFov;
31723178
}
31733179

3180+
// ── 역광 무대(#98 · 2026-08-07 지형 인지 해) ────────────────────────────────────────
3181+
// 종전: `heroSunAz = rotY + π + 55°` — 배면 사선 고정 상수(2026-07-31 판정이 고른 값).
3182+
// 그 판정은 옳은 축(방위)을 조정했지만 프레임을 죽이던 축은 **고도**였다. 배산임수 규약상
3183+
// 종가 배면은 곧 배산이고, 그 방위의 능선 수평선은 16.59° 인데 석양 고도는 9.51° 다 —
3184+
// 태양이 능선 뒤 7.08° 아래에 묻혀 종가에 직사광이 0 이었다(scratch/rim-entry/occlusion.json).
3185+
// 그래서 rim.js `_directGate` 가 그림자 바닥값 0.45 로 눌린 채 시작했고, 무엇보다 씬 전체가
3186+
// 앰비언트만 받아 골든아워로 읽히지 않았다. 능선 윤곽은 시드마다 다르므로 상수 재조정으로는
3187+
// 다른 부지에서 재발한다 → 방위·고도를 부지에서 함께 푼다(src/env/hero-sun.js).
3188+
//
3189+
// 해가 정착 프레이밍 **뒤에** 도는 이유: 목적함수의 역광 항이 실제 정착 카메라 방위를 쓴다.
3190+
// 종전 상수는 "카메라가 집 정면에 선다"를 가정했는데 실측 카메라 방위는 rotY 에서 13.8° 벗어나
3191+
// 있었고, 그만큼 역광 분리각이 어긋났다.
3192+
{
3193+
const site = village.handle?.plan?.site;
3194+
const camDir = finalPosition.clone().sub(finalTarget);
3195+
const camAzimuth = Math.atan2(camDir.x, camDir.z);
3196+
const camElevation = Math.atan2(camDir.y, Math.hypot(camDir.x, camDir.z));
3197+
const sunElevation = Math.asin(THREE.MathUtils.clamp(
3198+
sun.position.y / Math.max(1e-6, sun.position.length()), -1, 1));
3199+
const solved = typeof site?.heightAt === 'function' ? solveHeroBacklight({
3200+
backAzimuth: rotY + Math.PI,
3201+
cameraAzimuth: camAzimuth,
3202+
cameraElevation: camElevation,
3203+
sunElevation,
3204+
heightAt: (x, z) => site.heightAt(x, z),
3205+
// 원점은 종가 상단(용마루 부근) — "태양이 이 점에 닿는가"가 직사광 판정이다.
3206+
origin: { x: finalTarget.x, y: finalTarget.y + maxDim * 0.25, z: finalTarget.z },
3207+
}) : null;
3208+
if (solved) {
3209+
heroSunAz = solved.azimuth;
3210+
// 프로필 고도로 이미 능선을 넘으면 해가 sunElevation 을 그대로 돌려주므로 여기서 override 를
3211+
// 달지 않는다 — 골든아워 저고도를 불필요하게 잃지 않기 위한 항등 경로다.
3212+
heroSunElev = solved.elevation > sunElevation + 1e-4 ? solved.elevation : null;
3213+
} else {
3214+
heroSunAz = rotY + Math.PI + 55 * DEG; // 지형 미노출 핸들(구/커스텀) 폴백 — 종전 상수
3215+
heroSunElev = null;
3216+
}
3217+
heroSunSolve = solved && {
3218+
offsetDeg: +(solved.offset * 180 / Math.PI).toFixed(1),
3219+
azimuthDeg: +(solved.azimuth * 180 / Math.PI).toFixed(1),
3220+
elevationDeg: +(solved.elevation * 180 / Math.PI).toFixed(2),
3221+
horizonDeg: +(solved.horizon * 180 / Math.PI).toFixed(2),
3222+
clearanceDeg: +(solved.clearance * 180 / Math.PI).toFixed(2),
3223+
occluded: solved.occluded,
3224+
score: +solved.score.toFixed(4),
3225+
camAzimuthDeg: +(camAzimuth * 180 / Math.PI).toFixed(1),
3226+
};
3227+
}
3228+
31743229
// 순수 건축 리빌 경로(#22): 넓은 establishing 화각에서 종가 둘레를 완만히 돌아 공유 24°의
31753230
// 마당·문높이 망원 프레임으로 내려앉는다. seed는 선회 방향만 정하고 생성 RNG를 소비하지 않는다.
31763231
// 모바일은 호를 줄이며 reduced-motion은 즉시 endpoint를 적용한다.
@@ -4759,6 +4814,13 @@ export function createEngine({ container, perf = false, compact = false } = {})
47594814
get dofAperture() { return !disposed && bokehPass ? bokehPass.uniforms.aperture.value : null; },
47604815
// #98 역광: 태양 방위(sun.position 실측)·히어로 종가 frontDir(rotY)·카메라 방위 — 역광 구도 단언.
47614816
get sunAz() { return disposed ? 0 : Math.atan2(sun.position.x, sun.position.z); },
4817+
// 태양 고도(라디안) + 지형 인지 해의 판독. 게이트가 "능선을 넘겼는가"를 직접 단언한다.
4818+
get sunElev() {
4819+
if (disposed) return 0;
4820+
const r = sun.position.length();
4821+
return r > 1e-6 ? Math.asin(THREE.MathUtils.clamp(sun.position.y / r, -1, 1)) : 0;
4822+
},
4823+
get sunSolve() { return disposed ? null : heroSunSolve; },
47624824
get heroRotY() { return !disposed && village.heroRotY != null ? village.heroRotY : null; },
47634825
get timeState() { return state.time; },
47644826
};

docs/look-grammar.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,42 @@ cheoma의 장르는 카툰도, 로우폴리 쇼케이스도, 리얼리스틱도
3535
카툰 악센트가 되어 장르를 오염시킨다. 뇌록·주홍·수목 녹·하늘은 서로 구분되는 색상을 유지하되
3636
하나의 대기 안에서 만난다(단일 주황 워시 금지).
3737

38+
### 2.1 역광은 기하학적으로 성립해야 한다 (2026-08-07)
39+
40+
시그니처 역광은 "태양을 피사체 뒤에 둔다"로 끝나지 않는다. **그 뒤에 무엇이 서 있는지**까지
41+
계산해야 성립한다.
42+
43+
히어로 랜딩은 종가 배면(`rotY + π + 55°`)에 태양을 고정해 왔는데, 배산임수 규약상 종가 배면은
44+
곧 배산이다. 실측(vseed 20260716): 태양 고도 **9.51°**, 그 방위의 지형 수평선 **16.59°**
45+
태양이 능선 뒤 **7.08° 아래**에 묻혀 종가에 직사광이 한 줄기도 닿지 않았다. `rim.js`
46+
`_directGate` 는 그림자 항이 0 이면 바닥값 0.45 로 눌리고, 무엇보다 씬 전체가 앰비언트만 받아
47+
골든아워로 읽히지 않는다. 사용자 보고 "조립 후 림라이트가 거의 없다"의 원인이 이것이었다.
48+
49+
규율:
50+
51+
- **역광 방위를 상수로 두지 않는다.** 능선 윤곽은 시드마다 다르므로 어떤 상수를 골라도 다른
52+
부지에서 재발한다. 방위와 고도를 지형에서 함께 푼다(`src/env/hero-sun.js`). 목적함수는 실제로
53+
림을 만드는 두 게이트의 곱이다 — 고도 게이트(낮을수록 강함)와 역광 게이트(마주볼수록 강함).
54+
태양을 올리면 능선은 넘지만 골든아워를 잃고, 옆으로 돌리면 능선은 낮아지지만 역광을 잃는다.
55+
- **넘길 능선이 없으면 아무것도 바꾸지 않는다.** 평지 부지는 프로필 고도 그대로 — 저고도
56+
골든아워를 불필요하게 소비하지 않는 항등 경로가 계약이다.
57+
- **판정 축을 혼동하지 말 것.** 2026-07-31 라운드는 "정배면이 프레임을 실루엣으로 붕괴시킨다"를
58+
옳게 잡고 **방위**를 조정했지만, 프레임을 죽이던 축은 **고도**였다. 역광이 안 나올 때 물어야
59+
할 첫 질문은 "태양이 피사체 뒤인가"가 아니라 "태양이 지평선 위에 실제로 떠 있는가"다.
60+
61+
게이트: `npm run check:hero-sun`(순수, FAIL-first 픽스처 포함).
62+
63+
### 2.2 안티에일리어싱은 실루엣을 지우면 안 된다 (2026-08-07)
64+
65+
`rim.js` 의 프레넬 AA 는 화면 미분이 크면 림을 지웠다. 그런데 실루엣은 정의상 ndv 가 가장
66+
급변하는 곳이라, 그 삭제는 림이 그려야 할 바로 그 선을 정조준한다. 히어로 정착(7° 렌즈·146 m)의
67+
실측: `_aa` 평균 0.491, **실루엣 삼각형의 42.9% 가 완전 소멸**, 읽히는 비율 0.803 → 0.459.
68+
69+
규율: 고주파 실루엣은 **약하게** 만들되 지우지 않는다(`RIM_FRESNEL_AA.floor`). 진짜
70+
안티에일리어싱은 에너지 보존형 Toksvig 항이 맡는다. 그리고 킥은 **선에만** 붙인다 — 낱개 장식
71+
(막새·와구토·적새)에 같은 배수를 걸면 용마루가 금빛 점선이 되어, 그 AA 가 막으려던 스티플을
72+
정확히 되살린다.
73+
3874
## 3. 요소별 규율
3975

4076
| 요소 | 규율 |

docs/verification.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,7 @@ npx esbuild src/api/index.js --bundle --format=esm \
11311131
| `tools/verify-gltf.mjs` | FULL/FAR 카메라 독립 export, scenery·transient focus 제외, commit된 일반집 overlay 포함과 대체 base LOD 제외, GLB 라운드트립·예산 | 기본 출력은 OS 임시 디렉터리이며 `CHEOMA_GLTF_OUT`으로 재지정할 수 있다. |
11321132
| `tools/check-rim-facing.mjs` | 실제 WebGL에서 N·V 실루엣, 실제 태양 N·L, V·L 역광, directDiffuse=0·그림자·제품형 비그림자 fill·`receiveShadow=false` 반례, 50% penumbra 비율, 실제 cloud-shadow callback/cache-key 합성, plain/LOD cloud+snow+rim 프로그램 공유(R8 diet)와 동차 `worldPosition` 복원, cloud/snow 단독 install이 rim 전에도 screen-door token을 갖는지, 추가 shadow fetch 0, HDR 에너지 상한, instanceColor·program plateau; `--app`은 시간을 멈춘 최종 24° 석양 focus에서 실제 마을 cloud+rim 지붕과 OFF/ON A/B | 고정 fixture와 대표 실앱 한 장이 모든 seed·재질의 미학을 대신하지 않는다. |
11331133
| `tools/check-program-diet.mjs` | R8(#220) 순수 계약: known token 어휘, order-independent cacheKey 합성, `patchLodScreenDoorMaterial` 멱등, rim/cloud/snow/impostor install-site가 항상 screen-door path를 다는지, hanyang programs 상한(aerial 144 / focus·mid·focusOut 192 / aerial→focusOut Δ≤64), 숲 간벌 레버 부재 | WebGL 실측 예산은 `check:lod:app` + `render-budget-contract`가 소유. |
1134+
| `tools/check-hero-sun.mjs` | 히어로 랜딩 역광 태양 해(`src/env/hero-sun.js`) 순수 계약. 배산임수 픽스처(배면 능선 25.3°·+55° 15.5°·+90° 10.1°)에서 (1) **FAIL-first**: 종전 상수 `rotY + π + 55°` 가 같은 픽스처에서 실제로 가려짐(지형 수평선 15.48° > 석양 9.51°)을 재현하고, (2) 해가 능선을 넘긴다(clearance ≥ ridgeMargin), (3) 오프셋이 배면 대역 [35°,115°] 안이고 고도가 프로필 이상·22° 이하, (4) rim.js 역광 게이트 ≥0.60 이며 점수가 종전 상수(그림자 바닥값 0.45 포함)보다 높다, (5) **평지 항등 경로** — 넘길 능선이 없으면 고도를 올리지 않는다, (6) 수평선 산출이 해석해와 일치, (7) 고도 게이트가 post.js 저고도 램프와 같다 | 순수 노드(브라우저 없음). `npm run check:hero-sun`. 실제 프레임의 처마 금빛은 `check:rim`(프로그램 계열·계수)과 진입 캡처가 맡는다. |
11341135
| `tools/check-rim-master.mjs` | #35-1 순수 계약: (1) 실제 `createFocusPolicyRuntime` 에 `setPostFocus(false)` 를 걸어 부감·시네마틱 정책이 `setRimEnabled(false)` 를 부르지 않고 `RIM_CONTEXT_MASTER.aerial > 0` 만 다이얼하는지(flare OFF·DoF 0 은 유지, focus 는 1.0 불변, dofAmount 단독 호출이 마스터를 흐트리지 않음), (2) `rimDistanceGate` 가 실측 부감 프레이밍(한양 crane-in 464m·pullback 432m·capital·village)에서 피사체 거리 페이드 ≥0.60 을 남기고 렌즈 밴드를 **확장만** 하는지(근경 60m·히어로 170m 무회귀, 비정상 입력은 렌즈 밴드로 폴백), (3) sunset 역광 처마 프래그먼트의 가산이 authored 피크의 ≥6% 이고 정오(altGate 0)에서는 정확히 0, 부감 수관(organic)이 건물 아래인지. post.js·engine.js 접합 2줄은 소스 계약으로 확인 | GL 없이는 실제 `uRimScale` 값을 못 읽으므로 살아 있는 유니폼·프로그램 수는 `tools/shoot-rim-aerial.mjs` 가 확인한다. `CHEOMA_RIM_ROOT` 로 다른 트리(예: `git archive HEAD`)를 가리켜 FAIL-first 를 재현할 수 있다. |
11351136
| `tools/check-fog-wash.mjs` | #35-R2 순수 계약(네 노을 프로필 gold·crimson·violet·dawn 전부): (1) 저작 축 — `fog` 의 HSL 채도 ≤0.25 와 선형 휘도 ≤ 그 프로필 표면 휘도 중앙값의 4배, (2) 워시 — 마을 부감 최대 대기 혼합비 `fogFactor 0.456` 에서 대표 알베도 12종의 채도가중 색상 표준편차 ≥25°, hue 320~30° 대역 점유 ≤6/12, (3) 암부 중립 — 중성 알베도(화강암·회벽)가 대기에서 얻는 채도 증가 ≤+0.06, (4) 부감 돔 구배 — `sky.js` `DOME_HAZE` 를 소스에서 읽어 pos 0.44~0.52 밴드가 제품 부감 렌즈(46°/1080행) 기준 100행당 **+**1.5 이상 단조 증가(무구배 밝은 평면 금지). 마을 조명 리그는 미러 + 드리프트 단언 | `<fog_fragment>` 선형 mix·캔버스 그라디언트·GradePass·ACES 를 해석적으로 재현하므로 GL 이 필요 없다. 실제 프레임의 대기 원근·능선 겹침은 `shoot:cine`·`shoot:village-light` 가 맡는다. FAIL-first 재현: 소스 사본에서 fog 를 `be6c74`/`a8788f`/`91859c`/`e4cfbd` 로, `DOME_HAZE` 를 0.28/0.74/0.96 으로 되돌리면 20건이 실패한다. |
11361137
| `tools/check-celestial.mjs` | #53 천체 순수 계약: 태양 원반 림 다크닝·저고도 편평화/확대, 달 위상각·조명 방향·가시 조명 면적·터미네이터·지구조, 별 시드 재현성·등급 멱법칙·반구·시간대 페이드, 은하수 페이드, `SUN_BAND` 밤 배율, `sky.js` 배선 정합 | 순수 노드(브라우저 없음). `npm run check:celestial`. 픽셀·미감은 `shoot:sky`가 맡는다. |

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"check:drainage": "node tools/check-drainage-plan.mjs",
112112
"check:dangsan": "node tools/check-dangsan-plan.mjs",
113113
"check:rim": "node tools/run-browser-locked.mjs -- node tools/check-rim-facing.mjs",
114+
"check:hero-sun": "node tools/check-hero-sun.mjs",
114115
"check:rim-gate": "node tools/check-rim-gate.mjs",
115116
"check:rim-master": "node tools/check-rim-master.mjs",
116117
"check:fog-wash": "node tools/check-fog-wash.mjs",

src/api/lighting.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
11
// Reusable physical HDR light-source geometry. Product lighting policy and
22
// village owner selection remain outside this low-level factory.
33
export { createPhysicalNightlightBatch } from '../village/nightlight-physical-geometry.js';
4+
5+
// 히어로 랜딩 역광 태양 해(2026-08-07). 종가 배면은 배산임수 규약상 곧 배산이라, 배면에 고정된
6+
// 상수 방위는 저고도 석양을 능선 뒤에 묻는다(실측 −7.08°, 직사광 0). 지형을 읽어 방위·고도를
7+
// 함께 고르는 순수 수치 해다 — 렌더러 없이 게이트가 전 부지를 훑을 수 있어야 한다.
8+
export {
9+
solveHeroBacklight,
10+
terrainHorizonAngle,
11+
altitudeGate,
12+
backlitGate,
13+
HERO_SUN_SOLVE,
14+
} from '../env/hero-sun.js';

0 commit comments

Comments
 (0)