feat: initialize sunrise with 100-player shared rooms
@@ -0,0 +1,8 @@
|
||||
# Copy to .env. Never put MongoDB credentials in VITE_* variables.
|
||||
MONGODB_URI=mongodb://172.16.0.7:27017
|
||||
MONGODB_DATABASE=sunrise
|
||||
HOST=127.0.0.1
|
||||
PORT=3000
|
||||
ALLOWED_ORIGINS=http://127.0.0.1:3000,http://localhost:3000,http://127.0.0.1:5173,http://localhost:5173
|
||||
# Explicit temporary development mode only; visit records are not persisted:
|
||||
# VISIT_STORE=memory
|
||||
@@ -0,0 +1,21 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.local
|
||||
.env
|
||||
.tools/
|
||||
data/raw/
|
||||
__pycache__/
|
||||
|
||||
# Local authoring sources, references, and generated reports
|
||||
/assets/
|
||||
/data/references/
|
||||
/data/backups/
|
||||
/artifacts/
|
||||
/.tmp/
|
||||
/experiments/
|
||||
/*.png
|
||||
/*.jpg
|
||||
|
||||
# Environment secrets (keep the documented template)
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -0,0 +1,236 @@
|
||||
## 해안 높이 보정 제거 (2026-10-30)
|
||||
|
||||
하조대·기사문 높이 격자는 Copernicus 좌표 변환·보간값만 사용한다. OSM 해안선을 이용한 해안 높이 낮추기·최소 높이·바다 칸 0m 덮어쓰기를 제거했다. OSM 해안선·표면 마스크는 화면 표시와 이동 경계에 유지하며 관악산 지형은 변경하지 않았다. 아래 해안 보정 설명은 제거 전 개발 기록이다.
|
||||
|
||||
# 해보러가기
|
||||
|
||||
## 현재 과천 건물: VWorld 단독 (2026-10-30)
|
||||
|
||||
OSM 건물의 외곽선·높이·층수와 OSM 대조 기반 선별을 제거했다. VWorld 원본에서 독립적으로 재생성한 **6,743개**를 표시한다(높이 자료 1,756개 / 층수 환산 953개 / 임시 높이 4,034개). VWorld 내부 겹침 183개를 제외했다. 아래 OSM·VWorld 통합 설명은 이전 작업 기록이며 현재 구성에 적용되지 않는다. 해안선·정상 구조물 위치의 OSM 사용은 유지한다.
|
||||
|
||||
재생성은 `scripts/prepare-vworld-buildings.py` → `npm run prepare:buildings` → `npm run build`. 현재 출처·CC BY 2.0 KR 이용조건은 `/buildings/source.html`, 세부 기록은 `data/references/gwacheon/README.md`를 따른다.
|
||||
|
||||
|
||||
## 공동 관측 서버
|
||||
|
||||
관악산 채널 1은 최대 100명을 수용합니다. Node.js + Socket.IO로 이동·감정표현·우천 장비를 공유하고 MongoDB에는 접속 기록만 저장합니다. `npm run build` 후 `npm start`로 실행합니다. 기본 주소는 `http://127.0.0.1:3000`입니다.
|
||||
|
||||
환경설정·개발 실행·검증 방법과 운영 한계는 [서버 설명](docs/multiplayer-server.md)을 참고하세요. 기본 MongoDB는 `172.16.0.7:27017`의 `sunrise.visitSessions`이며 `.env.example`에서 변경할 수 있습니다.
|
||||
|
||||
## 모바일·웹 몰입형 UI (2026-10-30)
|
||||
|
||||
- 장면에는 장소·시간·메뉴와 인사·소원빌기·장비 버튼만 표시합니다. 날짜·시간, 관측 정보, 지도·시점, 접속, 설정, 출처는 버튼으로 여는 팝업에 모았습니다.
|
||||
- 팝업은 닫기·바깥 영역 클릭·Esc로 닫습니다. 열린 동안 배경 이동과 캐릭터 단축키를 차단하고, 닫으면 열었던 버튼으로 포커스가 돌아옵니다.
|
||||
- 미니맵은 기본 숨김입니다. `메뉴 → 지도·시점 → 미니맵을 화면에 계속 표시`로 켜고 지도 위 닫기 버튼으로 숨깁니다.
|
||||
- 함께 보기에서는 날짜·시간을 공유합니다. 시간 팝업의 `함께 보기·접속`에서 혼자 보기로 전환하면 직접 조절할 수 있습니다.
|
||||
- 소원빌기는 기존과 같이 일출 전후 5분에 활성화됩니다. 메뉴의 조작 방법에서 사용 조건과 단축키를 확인할 수 있습니다.
|
||||
- `메뉴 → 출처·오픈소스`에서 데이터·모델 출처와 `/licenses.txt`의 설치 버전별 라이브러리 저작권·라이선스 원문을 확인할 수 있습니다.
|
||||
|
||||
## 앱 진입 흐름 (2026-10-30)
|
||||
|
||||
시작 화면에는 직접 그린 일출 아이콘과 `보러가기`, `혼자보기 | 같이 보기`만 표시합니다. 달·별이 보이는 밤을 0.3초 보여준 뒤, 4.5초 동안 해가 능선 위로 충분히 떠오르며 별과 달이 희미해지고 마지막 장면을 유지합니다. 숨겨진 탭에서는 재생을 멈추고, 동작 줄이기 설정에서는 정지 장면을 표시합니다.
|
||||
|
||||
배경은 기존 관악산 DSM·정상 지형 보정과 `createSky`/`createSun`/`createNightSky`를 그대로 렌더링합니다. 기존 정상 관측점에서 방위각 110° 방향을 바라보며, 달이 같은 화면에 들어오는 2026-11-06 05:40~07:45 KST의 천체 계산을 4.5초로 압축한 연출입니다. 별은 분위기용 고정 배치이며 현재 날씨의 실시간 표시가 아닙니다. 별도의 능선 그림이나 CSS 태양은 사용하지 않습니다. 입장 시 배경 렌더러의 GPU 자원을 해제합니다.
|
||||
|
||||
- 혼자보기: 서버 목록 조회 없이 관악산 장면으로 바로 입장합니다.
|
||||
- 같이 보기: 채널 선택 → 지형·캐릭터 로딩 및 입장 승인 → 관악산 화면.
|
||||
|
||||
- 현재 관악산 공동 방을 `채널 1`로 사용한다.
|
||||
- `/api/lobby`에서 서버 목록, 채널 정원·인원·입장 가능 여부를 조회한다. 선택 화면에서는 방문 기록이나 3D 장면을 생성하지 않는다.
|
||||
- 개발 시 프런트엔드(`npm run dev`)와 백엔드(`npm run dev:server`)를 함께 실행한다. Vite가 `/api`와 `/socket.io`를 백엔드 3000번 포트로 전달한다.
|
||||
- 서버 조회 실패·입장 실패는 재시도할 수 있다. 명시적인 `?mode=solo` 비교 화면과 개발용 `?profile` 경로는 기존 직접 진입을 유지한다.
|
||||
|
||||
아래 내용은 각 기능의 개발 기록이다.
|
||||
|
||||
## Open-Meteo 기상 연동 (2026-10-30)
|
||||
|
||||
- 선택 날짜·시간에 맞춰 전체 운량(%), 시정(m), 강우량(mm/h), 강설량(cm/h)을 조회한다. 운량·시정은 Unix 시각 기준으로 보간하고 구름·안개·태양 감쇠를 부드럽게 전환한다. 비는 `rain + showers`(재분석은 `rain`), 눈은 `snowfall`을 사용한다. 누적량은 각 타임스탬프 직전 한 시간 구간에 적용하며 시간 사이를 보간해 가짜 진눈깨비를 만들지 않는다. 태양 위치와 천문 일출 계산은 바꾸지 않는다.
|
||||
- 최근 5일~최대 16일 예보 범위는 Forecast API, 그보다 과거인 2022년 이후는 Historical Forecast API를 사용한다. 1940~2021년은 Historical Weather API의 운량·비·눈을 반영하며 `시정 미제공`을 표시한다. 모델/변수별 결측이 있을 수 있다. 강우·강설 결측은 `미제공`으로 표시하며 해당 입자를 끈다. 범위 밖·연결 실패는 표시와 함께 기본 하늘로 복귀한다.
|
||||
- 강수량에 따라 카메라 주변의 빗줄기·눈송이 수가 달라지고 빗줄기 길이·불투명도도 변한다. 눈은 천천히 흔들리며 떨어지고, 두 자료가 모두 양수이면 함께 내린다. 렌더링 강도만 부드럽게 전환하며 정지된 CUSTOM 시각에도 입자는 움직인다. 지형/해수면 높이와 깊이 검사를 적용한다. 적설·젖은 표면·지붕 충돌·실측 바람은 구현하지 않는다. 맑으면 강수 애니메이션으로 인한 추가 렌더링을 중단하며 입자 수에는 성능 상한이 있다.
|
||||
- 요청은 관측 장소의 고정 좌표와 UTC 날짜별로 캐시한다(최대 8개, 30분 갱신). 슬라이더를 움직일 때 매번 요청하지 않고, 이전 날짜의 늦은 응답은 현재 장면에 적용하지 않는다. 연결 실패는 2분 후 재시도하며 CUSTOM 상태에서도 작동한다. 외부 통신은 15초 후 중단한다.
|
||||
- 수치예보/재분석에 따른 **시각적 근사**다. 실제 동쪽 수평선의 구름 모양·구름 차폐나 산 정상의 국지 시정을 재구성하지 않는다. 하늘과 태양에 같은 방향별 절차적 구름 분포를 적용하며, 운량 100%에서는 수평선까지 불투명한 구름층이 덮고 태양 원반·halo를 가린다. 구름의 음영은 태양 고도에 맞춰 낮·새벽·밤으로 변한다. 선형 안개와 운량·시정 기반 직사광 감쇠를 사용하며, 자료가 없는 시정은 짙은 안개로 해석하지 않는다.
|
||||
- 추가 의존성·API 키·백엔드는 없다. 무료 API는 비상업용이며 배포 용도에 맞는 이용 조건을 확인해야 한다. 화면에 출처를 표시한다: [Open-Meteo 문서](https://open-meteo.com/en/docs), [과거 예보](https://open-meteo.com/en/docs/historical-forecast-api), [재분석](https://open-meteo.com/en/docs/historical-weather-api), [이용 조건](https://open-meteo.com/en/pricing).
|
||||
- 구현: `src/weather/weather.ts`, `src/scene/precipitation.ts`, 조정 상수: `src/constants/weather.ts`, `src/constants/precipitation.ts`, 검증: `tests/weather.test.ts`, `tests/precipitation.test.ts`.
|
||||
|
||||
## 과천 건물 공식 자료 보완 (2026-10-30)
|
||||
|
||||
사용자가 제공한 VWorld `AL_D010_41_20260719.zip`을 기존 OSM과 공간 대조해 건물을 **1,640개 → 6,692개**로 보완했다. 기존 외곽선을 유지하고 겹치지 않는 공식 외곽선 5,052개를 추가했다. 기존 건물의 빈 높이 393건·층수 846건을 보완했으며 두 수치는 중복될 수 있다. 최종 높이 자료 1,771개 / 층수 환산 894개 / 임시 높이 4,027개. 충돌하거나 애매하게 겹치는 자료는 제외 내역으로 남긴다.
|
||||
|
||||
재생성은 `python scripts/prepare-vworld-buildings.py <원본 ZIP 경로>`. 상세 출처·조건은 `data/references/gwacheon/README.md`와 `/buildings/source.html`. 현재 지형은 여전히 30m급 DSM이므로 건물 기초의 실제 표고와 매몰 문제는 미해결이다. 아래 2026-10-29 건물 설명은 보완 전 기록이다.
|
||||
|
||||
## 기사문 해변 추가 (2026-10-29)
|
||||
|
||||
상단의 `기사문 해변` 링크 또는 `/?site=gisamun`으로 이동한다. 관측점은 위도 38.00555°, 경도 128.73195°의 해변 안쪽 임시 위치다. 기존 하조대 지형을 없애지 않고 기사문 중심 3×3km 데이터를 추가했다.
|
||||
|
||||
- 기존 N37/N38 DSM을 병합·추출한 높이 파일 362KB, 해안선·마스크·메타데이터 포함 약 416KB.
|
||||
- 바다·해안 렌더러, 이동 제한·시간·눈높이·조감 기능 재사용. 기사문항 방향 버튼 제공(항만 구조물 상세 모델은 없음).
|
||||
- 기사문 모래 영역은 OSM에 없어 해당 해안선을 서쪽으로 35m 확장해 시각적으로 근사했다. 위치·높이·모래 경계는 현장 측량값이 아니다.
|
||||
- 재생성: `python scripts/prepare-hajodae.py gisamun`. 자료 목록은 `data/references/gisamun/README.md`, 공개 설명은 `/terrain/gisamun/source.html`.
|
||||
- 2026-10-29 06:52/06:55/07:00의 태양 방향이 구현된 지형에 가리지 않는 것을 자동 검사했고, 06:55에는 브라우저에서 원반을 확인했다. 모든 날짜·실제 현장 조건에 대한 보장은 아니다.
|
||||
|
||||
## 하조대 해안 (2026-10-29)
|
||||
|
||||
기본 화면은 하조대 해변이다. `/?site=hajodae`와 `/?site=gwanaksan` 또는 좌측 상단 장소 링크로 전환한다. 아래의 관악산 설명은 기존 장면에 해당한다.
|
||||
|
||||
- 하조대 임시 관측점: 위도 38.02310°, 경도 128.72540°. 해변 내부 OSM 영역을 기준으로 선정했으며 현장 측량 좌표가 아니다.
|
||||
- Copernicus 원본 약 64MB 중 북쪽 타일에서 3×3km만 추출했다. 30m급 원본을 10m 간격 301×301로 보간한 높이 격자 **362,404 bytes**, 해안선 JSON **19,339 bytes**, PNG 마스크 **18,208 bytes**다. 메타데이터 포함 약 **403KB**만 하조대 장면이 요청한다(공통 JS 등 제외).
|
||||
- 실제 OSM 해안선과 모래사장 영역으로 육지/바다를 구분한다. 바다는 지구 곡률, 시각적 물결·거품·햇빛 반사를 표현한다. 날짜·시간·눈높이, WASD, 시작점 복귀를 지원하며 해안선 밖 바다로 이동하지 않는다. `해안 조감`은 관측용이 아닌 배치 확인용 카메라다.
|
||||
- 해안선 30m 이내 지면은 해수면에 접하도록 시각적으로 보정했다. 원본 DSM이 0m인 초기 해변 지점은 0.2m로 보정해 표시한다. 실제 지반 표고·조석·수심·파고를 의미하지 않는다. 작은 섬과 절벽의 세밀한 높이, 정자·등대·건물 모델은 미구현이다.
|
||||
- 해안선 마스크는 2m 간격, 지형 격자는 10m 간격이지만 원본 측량 정확도가 높아진 것은 아니다. 3×3km 밖은 육지 없이 바다를 연장한다. 실제 절벽이나 모래턱의 세밀한 높이는 재현하지 못한다.
|
||||
- 전처리: 기존 `.tools/terrain`의 rasterio/numpy/pyproj 환경에서 `python scripts/prepare-hajodae.py`. 상수는 `src/constants/coast.ts`, 출처·보정은 `public/terrain/hajodae/metadata.json`과 `/terrain/hajodae/source.html`에 기록했다. 원본·자료 목록은 `data/references/hajodae/README.md` 참조.
|
||||
- 검증: 기존 기능 포함 33개 테스트와 빌드 통과. 브라우저에서 낮/일출 전후, 해안 조감, 관악산 전환을 확인했다. 동쪽으로 약 52m 이동 시 해안선에서 멈추는 것도 확인했다. 내장 브라우저에서는 Pointer Lock 대신 기존 드래그/키보드 대체 조작을 사용한다.
|
||||
|
||||
## 기존 관악산 장면
|
||||
|
||||
고정 관측점에서 날짜·시각에 따른 태양의 실제 방위각과 겉보기 고도각을 관찰하는 PC 웹앱입니다. TypeScript + Vite + Three.js + SunCalc 2.0.1을 사용하며 백엔드는 없습니다.
|
||||
|
||||
Copernicus 30m급 DSM 두 타일에서 관측점 중심 **24×24km**를 추출한 실제 높이 격자를 적용했습니다. 브라우저는 원본 79.3MB 대신 약 **2.57MB**의 정적 격자를 불러옵니다. 건물 개별 모델과 항공사진 텍스처는 포함하지 않습니다.
|
||||
|
||||
## 설치 및 실행
|
||||
|
||||
Node.js 22.18 이상(검증 환경: 24.19), npm이 필요합니다.
|
||||
|
||||
```powershell
|
||||
cd D:\project\sunrise
|
||||
npm ci
|
||||
npm run dev
|
||||
```
|
||||
|
||||
터미널에 표시된 Local 주소를 **일반 Chrome 또는 Edge 탭**에서 엽니다. 기본 주소는 `http://127.0.0.1:5173/`이며, 포트가 사용 중이면 Vite가 다음 포트를 사용합니다. 내장 웹뷰에서는 Pointer Lock이 지원되지 않을 수 있습니다.
|
||||
|
||||
```powershell
|
||||
npm test # 좌표, 날짜, 시간 전환, 독립 천문 계산 대조
|
||||
npm run verify:solar # NOAA 대조 표 출력
|
||||
npm run build # TypeScript 검사 + dist/ 생성
|
||||
npm run preview # production build 로컬 확인
|
||||
```
|
||||
|
||||
## 사용법
|
||||
|
||||
- 기본은 LIVE입니다. 매초 시스템의 현재 `Date`를 새로 읽습니다. 탭 복귀/창 포커스 시 즉시 동기화합니다.
|
||||
- 하늘을 클릭하면 마우스 시점 조작이 시작됩니다. ESC로 잠금을 풀고 UI를 조작합니다.
|
||||
- 초기 시선은 동쪽(+X), 좌우 회전은 360°, 상하 회전은 ±85°입니다. 카메라 위치는 고정이며 WASD·휠 이동은 없습니다.
|
||||
- 날짜 또는 시간 슬라이더를 바꾸면 CUSTOM으로 전환되고 시간이 멈춥니다. 슬라이더 범위는 00:00~23:59, 간격은 1분입니다.
|
||||
- 실시간 복귀 버튼으로 현재 날짜와 시각으로 돌아옵니다.
|
||||
- 눈높이 슬라이더로 **160~195cm, 1cm 간격**으로 관측 높이를 조절합니다(기본 170cm). 선택값은 신장이 아닌 **지면에서 눈까지의 높이**로 직접 사용하며, 신장과 눈높이의 차이는 추정하지 않습니다. 카메라·기준선·태양 배치·일출/일몰 고도 보정이 즉시 갱신됩니다. 날짜·LIVE/CUSTOM 모드·시선 방향은 유지됩니다. 새로고침하면 기본값으로 돌아옵니다.
|
||||
- 일출 예시: **2026-03-20 06:50** 또는 **2026-10-29 07:00**, 일출 전 예시: **2026-03-20 06:00**.
|
||||
|
||||
## 좌표 및 계산 기준
|
||||
|
||||
`src/config/observer.ts`의 관측점은 **미확정 임시값**입니다.
|
||||
|
||||
| 설정 | 값 |
|
||||
|---|---:|
|
||||
| 위도 | 37.4450° N |
|
||||
| 경도 | 126.9640° E |
|
||||
| 설정 파일의 기존 임시 해발고도 | 629 m (지형 모드에서 사용하지 않음) |
|
||||
| 지형 모드의 관측점 DSM 표면고도 | 606.906 m, EGM2008 기준 |
|
||||
| 눈높이 | 기본 1.7m, UI에서 1.60~1.95m 조절 |
|
||||
|
||||
현장 측량이나 특정 전망대 위치로 검증된 좌표가 아닙니다. 브라우저 GPS는 사용하지 않습니다. 화면에도 임시값이라고 표시합니다.
|
||||
|
||||
지형 모드에서는 카메라를 DSM 표면+눈높이(약 608.606m)에 두고, 일출·일몰 계산에도 동일한 높이를 전달합니다. 30m급 DSM 표면은 수목·구조물을 포함할 수 있어 실제 전망대 바닥 높이와 다를 수 있습니다. 관측점 위도·경도를 바꾸면 아래 전처리를 다시 실행해야 합니다(정합 검사로 잘못된 데이터 조합을 거부).
|
||||
|
||||
- SunCalc **2.x**는 북쪽 0°·동쪽 90°·남쪽 180°·서쪽 270° 방위각과 대기 굴절이 보정된 고도각을 **도 단위**로 반환합니다. 1.x의 남쪽 기준/라디안 변환을 적용하지 않습니다.
|
||||
- 월드 좌표는 **+X 동쪽, +Y 위쪽, -Z 북쪽**입니다. 천문 계산은 Three.js와 독립이며 방향 벡터를 렌더러에 전달합니다.
|
||||
- 태양 원반 각지름은 0.53°입니다. 별도 halo와 광원 색상 효과를 사용하고 Sky의 기본 원반은 꺼서 중복 표시를 막습니다.
|
||||
- 모든 입력·표시는 `Asia/Seoul`을 사용합니다. 현재는 UTC+09:00이며, 과거 날짜는 IANA의 과거 시차/DST를 따릅니다. 선택 가능 범위는 1901~2099년입니다. 과거 DST 전환으로 존재하지 않는 현지 시각은 적용하지 않습니다.
|
||||
- 일출 계산은 선택한 서울 날짜의 **정오**를 기준으로 해당 날짜를 고정합니다. 자정에 전날의 solar day가 선택되는 것을 막습니다.
|
||||
|
||||
## 지형과 일출 시각의 차이
|
||||
|
||||
화면의 가는 선은 **고도각 0° 기준선**입니다. 실제 능선과 다릅니다. 기존 0° 아래 강제 숨김을 제거했고, 태양 원반·halo를 지형보다 먼 60km 거리에 두어 깊이 버퍼로 가립니다. 높은 곳에서는 0° 아래 태양도 보일 수 있고, 가까운 능선은 0° 위 태양도 가릴 수 있습니다.
|
||||
|
||||
정보 패널의 주 일출·일몰은 **DSM 표면+눈높이 약 608.606m**의 지평선 하강 보정을 적용한 천문 시각입니다. 보조 줄은 표준 0m 계산입니다. **능선 위로 태양이 처음 보이는 시각을 별도로 계산한 값은 아닙니다.** 지형 차폐는 렌더링에 반영됩니다. 영역 밖은 단순 해수면 기준 지평선이며 실제 먼 산은 추가 자료가 필요합니다.
|
||||
|
||||
## DEM 파일과 재현
|
||||
|
||||
- 원본: `data/raw/copernicus/Copernicus_DSM_COG_10_N37_00_E126_00_DEM.tif` (29,862,574 bytes), `...E127...tif` (49,403,795 bytes). 두 파일 모두 다운로드하여 보관했습니다.
|
||||
- 웹용: `public/terrain/gwanaksan.f32` (2,566,404 bytes), `gwanaksan.json` (격자·좌표·높이·원본 URL·SHA256), `source.html` (출처/제한).
|
||||
- 원본 격자 1 arc-second, WGS84 경위도. 두 타일을 먼저 병합해 경도 127° 경계의 보간을 연결합니다.
|
||||
- 관측점 중심 AEQD 투영, 30m 간격 bilinear 재표본화, 801×801. 표면고도 범위 1.314~618.475m, 결측 0개. 새 격자 간격은 원본보다 높아진 정확도를 의미하지 않습니다.
|
||||
- +X 동 / -Z 북, 지면의 높이는 DSM 표면고도에서 관측점 표면고도를 뺍니다. 지구 곡률은 평균 지구 반지름에 대한 `거리²/(2R)`로 근사합니다. 지상 대기굴절·지역 지오이드 기울기는 별도 반영하지 않습니다.
|
||||
- 1,280,000개 삼각형. 색상은 높이/경사 기반이며 실제 토지피복이나 항공사진이 아닙니다. 먼 영역은 시각적 안개로 완화합니다.
|
||||
|
||||
일반 실행에는 Python이 필요 없습니다. 전처리를 재현할 때만 Python 3.12와 아래 패키지가 필요합니다.
|
||||
|
||||
```powershell
|
||||
python -m pip install --target .tools/terrain -r scripts/terrain-requirements.txt
|
||||
python scripts/prepare-terrain.py
|
||||
npm run build
|
||||
```
|
||||
|
||||
전처리는 `src/config/observer.ts`의 좌표를 읽습니다. 원본 두 타일은 미리 다운로드되어 있어야 하며 URL은 `public/terrain/gwanaksan.json`에 기록되어 있습니다. 원본과 전처리 패키지는 `.gitignore` 대상이고, `public/terrain/` 가공 데이터는 앱 배포에 필요합니다.
|
||||
|
||||
[공식 배포](https://registry.opendata.aws/copernicus-dem/) · [사용 조건 및 가공 설명](public/terrain/source.html)
|
||||
|
||||
## 검증 결과
|
||||
|
||||
- `npm test`: **22개 통과**. 기존 18개 + 실제 격자 검증, 방위/높이/곡률/카메라 지면 정합, 합성 능선 및 실제 관악산 격자의 태양 차폐 검사.
|
||||
- `npm run build`: TypeScript 검사 및 production build 통과. Three.js를 포함한 단일 JS 번들이 약 548 kB(gzip 약 142 kB)로 Vite의 500 kB 권고 경고가 있습니다. 빌드 오류는 아닙니다.
|
||||
- NOAA 독립 수식 대조 최대 차이: 방위각 **0.0186°**, 0° 이상 태양의 고도각 **0.0659°**. 해당 구간 허용 오차 0.1° 이내입니다.
|
||||
- 0° 아래는 SunCalc와 NOAA 굴절 모델 차이로 최대 **0.4842°** 차이가 나며, 0.5° 기준으로 따로 검증합니다. 전체 구간이 0.1° 이내라는 의미는 아닙니다. 1단계에서는 음의 고도를 숨겼지만, 지형 모드에서는 그 일부가 보일 수 있으므로 이 모델 차이도 관측 일출의 오차 요인입니다.
|
||||
- 상세 결과: [태양 계산 검증표](docs/solar-validation.md). 검증용 NOAA 코드는 `tests/`에만 있고 앱에서 사용하지 않습니다.
|
||||
- 실제 내장 브라우저에서 DSM 메시, DSM 높이 표시, 날짜·슬라이더 변경, 일출/낮 장면을 확인했습니다. 별도 건물 데이터는 적용하지 않았습니다.
|
||||
- 실제 격자와 태양 방향의 교차 검사: 현재 임시 관측점 기준 2026-03-20 06:40 태양 중심 차폐, 06:50 통과. 현장 실측 일출 시각을 의미하지 않습니다.
|
||||
- **Pointer Lock 실제 회전/ESC 해제 검증은 미완료**입니다. 내장 브라우저가 Pointer Lock을 거부했고, 일반 Chrome 검증은 자동화 도구가 현재 URL을 확인하지 못해 중단되었습니다. 일반 PC 브라우저에서 하늘 클릭 → 좌우·상하 회전 → ESC 해제를 수동 확인해야 합니다. 앱에는 잠금 실패 안내가 있습니다.
|
||||
|
||||
## 모듈
|
||||
|
||||
```text
|
||||
src/
|
||||
main.ts 앱 연결
|
||||
config/observer.ts 임시 관측점
|
||||
constants/ 시간·장면 상수
|
||||
astronomy/sunCalculator.ts SunCalc 2.x 계산
|
||||
astronomy/direction.ts 월드 방향 변환
|
||||
scene/world.ts 장면·카메라·렌더 루프
|
||||
scene/sky.ts Sky 대기 효과
|
||||
scene/sun.ts 원반·halo·광원
|
||||
scene/horizon.ts 가상 지평선·방위 표시
|
||||
scene/terrain.ts 가공 높이 데이터 로딩·지형 재질
|
||||
scene/terrainGeometry.ts 격자 검증·좌표/곡률·메시 생성
|
||||
controls/fpsCameraController.ts 독립 FPS 시점 조작
|
||||
ui/time.ts 서울 날짜·시각 변환
|
||||
ui/timeController.ts LIVE/CUSTOM 제어
|
||||
ui/informationPanel.ts 정보 표시
|
||||
```
|
||||
|
||||
별도 건물 모델, 캐릭터 이동·충돌, 날씨·구름 데이터, 백엔드는 구현하지 않았습니다. 하늘 색과 빛 번짐은 시각적 근사이며 실제 날씨나 광도 예측이 아닙니다. 태양 위치 검증은 계산 모델 간 대조이며 현장 실측 검증이 아닙니다.
|
||||
# 구조물 1차 배치 (2026-10-29)
|
||||
|
||||
정상 마스트 1기, 남쪽 송신탑 4기, 레이돔·받침 건물, 북쪽/연주대 조망데크 2곳과 계단·난간을 추가했다. 하단 방향 버튼은 카메라 위치와 관측 시각을 유지하며 시선만 바꾼다. WASD 지형 탐색은 구현했으며 구조물 충돌·데크 위 보행은 아직 구현하지 않았다.
|
||||
|
||||
`src/constants/structures.ts`에서 위치·높이·크기·방향과 자료 근거를 보정할 수 있다. 모든 치수는 추정값이며 개별 송신탑과 데크 위치도 일부 추정이다. 기초와 지지대는 실제 렌더링되는 DSM 삼각형 표면을 샘플링해 배치한다. 지형은 편집하지 않았다. 현재 임시 관측점에서는 레이돔 하부가 능선에 가려질 수 있다. 구조물 위치·기초 표고와 근거리 지형은 추가 검증이 필요하다.
|
||||
|
||||
공개 안내: `/structures.html`. 수집 자료: `data/references/summit/README.md`. 사진은 모델링 참고로만 사용하며 앱 배포 자산에 포함하지 않는다. 구조물은 일반 불투명 메시의 깊이 판정으로 태양을 가리지만, 천문 일출·일몰 표시는 기존 계산을 유지한다.
|
||||
|
||||
# 과천 아파트·건물 (2026-10-29)
|
||||
|
||||
로그인 없는 OSM 공식 API 원본에서 추출한 1,640개 건물 외곽선을 3D로 배치했다(apartments 태그 242개). 높이 태그 223개, 층수 환산 28개, 임시 높이 1,389개를 색으로 구분한다. 공개 데이터와 근거는 `/buildings/source.html`, 원본과 수집 기록은 `data/raw/gwacheon/`에 있다.
|
||||
|
||||
`과천 방향`은 실제 관측 위치에서 시선만 돌린다. 현재 고정점에서는 가까운 능선에 도심이 가려져 `과천 건물 조감`을 별도로 제공한다. 조감은 기준 지면보다 180m 높은 별도 카메라로 배치를 확인하는 기능이며 실제 눈높이 관측이나 일출 검증용 시점이 아니다. 하늘 클릭 또는 다른 방향 버튼으로 실제 관측 시점에 복귀한다. 기존 관측 좌표·눈높이·천문 계산은 유지한다.
|
||||
|
||||
건물은 외곽선을 수직으로 올린 단순 모델이며 지붕·창문은 미재현. 주변 DSM에서 기초 표고를 추정하므로 일부 매몰·높이 오차가 남는다. 3개 병합 메시/약 3.3만 삼각형으로 렌더링하고 새로운 의존성은 추가하지 않았다.
|
||||
|
||||
2026-10-30 현재는 VWorld 보완 후 6,692개 건물, 3개 병합 메시, 148,488개 삼각형이다. 야간에는 창문 패턴 대신 기존 조명 맵으로 하부 벽면과 주변 지면을 밝히며, 태양 고도 -6°~5° 사이에서 서서히 소등한다. 실제 조명 위치나 차폐를 계산하는 방식은 아니다. 창문용 정점 속성을 높이 하나로 줄여 건물 기하 버퍼가 21,382,272바이트에서 16,036,704바이트로 감소했다. 이후 미사용 UV를 제거하고 위치·법선·조명 높이가 정확히 같은 정점만 공유하여 인덱스를 포함한 버퍼는 8,979,600바이트가 되었다(추가 약 44% 감소). 삼각형 순서와 각 꼭지점의 위치·법선·조명 높이, 조명 맵 및 시간별 밝기가 이전과 동일함을 대조했다. 태양의 투영 그림자는 미구현이다.
|
||||
|
||||
앱은 `/buildings/gwacheon.baked.json`과 `/buildings/gwacheon.baked.bin`을 읽어 메시와 조명 맵을 복원한다. 바이너리는 기하 버퍼 8,979,600바이트와 조명 맵 1,048,576바이트를 합친 10,028,176바이트다. GeoJSON 해석·기초 표고 계산·입체화·병합·정점 공유·조명 맵 계산은 앱 로딩 경로에서 제거했다. 재질과 시간별 조명 갱신 코드는 기존 생성기와 공유한다. 원본 GeoJSON은 출처 확인·재생성 및 실험 페이지용으로 보관한다.
|
||||
|
||||
건물/지형 데이터 또는 생성 규칙 변경 후 `npm run prepare:buildings`로 재생성한다. `npm run check:buildings`는 입력 및 출력 SHA-256을 검사하며, `npm run build` 전에도 자동 실행하여 오래된 파일의 배포를 막는다. LF/CRLF 차이는 입력 변경으로 취급하지 않는다. 파일이 없거나 기준 지형이 다르면 앱은 오류를 표시하며 실행 중 건물을 다시 생성하지 않는다.
|
||||
|
||||
로컬 Node 측정 3회에서 이미 읽은 데이터의 건물 생성은 572~786ms, 저장된 버퍼의 장면 연결은 0.14~0.40ms였다. 다운로드·JSON 파싱·GPU 업로드·나머지 장면 생성은 제외한 수치이며 전체 앱 로딩 시간이나 FPS 측정이 아니다. 바이너리는 원본 GeoJSON보다 크므로 실제 네트워크 전송 시간도 고려해야 한다(참고 gzip 압축 크기 약 2.37MB; 서버 압축 적용 여부에 따라 다름).
|
||||
|
||||
건물 전송 압축: `npm run prepare:buildings`는 `gwacheon.baked.bin.gz`도 함께 생성한다. gzip level 9 기준 2,358,179바이트로 원본 대비 약 76.5% 작으며, `check:buildings`는 압축을 풀어 원본과 일치하는지도 검사한다. 개발 서버와 `npm run preview`는 gzip을 지원하는 요청에 이 파일을 `Content-Encoding: gzip`, `Vary: Accept-Encoding`으로 전송한다. 앱의 요청 URL과 로딩 코드는 동일하며 브라우저가 자동으로 압축을 해제한다. gzip 비지원 요청과 Range 요청에는 기존 원본 파일을 제공한다. GPU 버퍼 크기는 변하지 않는다.
|
||||
|
||||
외부 정적 호스팅은 Vite 미들웨어를 실행하지 않으므로 해당 서버/CDN의 전송 압축 설정이 별도로 필요하다. Nginx에서 사전 압축 파일을 사용할 경우 기존 `server` 블록에 아래 위치 설정을 병합하고 `dist`를 정적 루트로 제공한다(`ngx_http_gzip_static_module` 필요). 압축 응답 여부는 `curl -I -H "Accept-Encoding: gzip" <주소>/buildings/gwacheon.baked.bin`의 `Content-Encoding: gzip`으로 확인한다. 외부 서버 배포는 수행하지 않았다.
|
||||
|
||||
```nginx
|
||||
location = /buildings/gwacheon.baked.bin {
|
||||
gzip_static on;
|
||||
gzip_vary on;
|
||||
default_type application/octet-stream;
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# 관악산 공동 관측 서버
|
||||
|
||||
Node.js 22.18 이상. 단일 프로세스에서 `gwanaksan` 장소의 `gwanaksan-001` 방을 운영한다. 입장한 참여자는 최대 100명이며 자신과 다른 탭도 각각 포함한다. Socket.IO 클라이언트는 HTTP long-polling으로 연결 후 WebSocket으로 업그레이드할 수 있다.
|
||||
|
||||
## 실행
|
||||
|
||||
```powershell
|
||||
npm ci
|
||||
# 인증이 필요하거나 주소/포트를 바꿀 때 .env.example을 .env로 복사해 편집
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
기본 접속: `http://127.0.0.1:3000`. 기본 DB: `mongodb://172.16.0.7:27017`, DB 이름 `sunrise`, 컬렉션 `visitSessions`. 인증이 필요하면 `MONGODB_URI`에 계정과 `authSource` 등 해당 서버 설정을 지정한다. URI를 브라우저용 `VITE_*` 변수에 넣지 않는다. `.env`는 버전 관리에서 제외한다.
|
||||
|
||||
개발 시 별도 터미널에서 `npm run dev:server`와 `npm run dev`를 실행한다. Vite의 `/socket.io`, `/health`, `/api` 요청은 3000 포트 서버로 전달된다. `npm run preview`는 프런트 미리보기이며 공동 방 서버를 대신하지 않는다.
|
||||
|
||||
`HOST` 기본값은 `127.0.0.1`이라 현재 컴퓨터에서만 접속할 수 있다. LAN/운영 배포 시 실제 바인딩 주소, `PORT`, `ALLOWED_ORIGINS`를 지정하고 TLS를 제공하는 프록시와 방화벽을 구성한다. 이번 구현은 외부 호스팅 배포까지 수행하지 않는다. 동일 방/정원을 여러 Node 프로세스에서 따로 실행하지 않는다. 프로세스별 메모리는 공유되지 않으며 현재는 단일 인스턴스 구성이다.
|
||||
|
||||
## 동작
|
||||
|
||||
- 공동 관측은 서버 UTC 시각을 기준으로 진행한다. 날짜 입력과 시간 슬라이더는 잠기며, 혼자 보기에서 기존 LIVE/CUSTOM 기능을 사용할 수 있다.
|
||||
- 위치·방향·인사/기도·우산·우비·후드를 서버 메모리에 저장한다. 초당 최대 10회 입력을 검증하고, 변경이 있으면 같은 방에 모아서 보낸다. 신규 입장에는 전체 참여자 상태를 전달한다. 5초마다 전체 상태도 다시 보낸다.
|
||||
- 현재는 100명 상한에서 단순성을 위해 변경이 있을 때 방 전체 스냅샷을 보낸다. 여러 방/대규모 확장 시 측정 결과에 따라 변경분 전송을 검토한다.
|
||||
- 보행 검증은 기존 관악산 경로·암반 영역과 이동 속도를 재사용한다. 위치 갱신 거리를 시간 기반 예산으로 제한하고 잘못된 좌표는 승인된 위치로 되돌린다. 완전한 서버 물리 시뮬레이션은 아니다.
|
||||
- 같은 장소의 날씨는 서버가 기존 조회·캐시 로직으로 읽어서 공유한다. 장비 착용 UI는 기존 비·눈 조건을 유지한다. 장비 자체는 영구 저장하지 않는다.
|
||||
- 같은 브라우저의 익명 `visitorId`를 localStorage에 보존하되, 로그인 인증이나 실제 사람의 신원으로 취급하지 않는다. 각 입장에는 별도의 서버 발급 방문 ID를 사용한다. 재접속 시 시작점과 장비 기본값으로 돌아온다. 시작점 복귀 버튼은 장비를 유지한다.
|
||||
- 연결이 끊기면 원격 캐릭터를 정리하고 재접속을 시도한다. heartbeat로 단절을 감지하기 전에는 자리가 잠시 유지될 수 있다. 혼자 보기로 전환하면 연결을 종료한다.
|
||||
- 101번째 참여자는 입장을 거절하고 재시도/혼자 보기를 제공한다. MongoDB 기록을 저장하는 동안에도 자리를 예약해 동시 입장으로 정원을 초과하지 않는다.
|
||||
|
||||
## 접속 기록
|
||||
|
||||
저장 필드: `_id`, `visitorId`, `nickname`, `locationId`, `roomId`, `joinedAt`, `lastSeenAt`, `leftAt`, `endReason`, `serverInstanceId`.
|
||||
|
||||
입장 기록 저장 성공 후 입장을 확정하고, 접속 중에는 60초마다 `lastSeenAt`을 갱신한다. 연결 종료와 정상 서버 종료에서 종료 정보를 기록한다. 위치·장비·감정표현은 DB에 쓰지 않는다.
|
||||
|
||||
서버 시작 시 과거 기록을 자동으로 변경하지 않는다. 강제 종료 또는 DB 장애로 퇴장 기록을 남기지 못하면 `leftAt`이 null로 남을 수 있다. 이는 현재 접속 중이라는 증거가 아니다. 실제 정원은 메모리에서 계산하며 `lastSeenAt`은 마지막 확인 시각일 뿐 정확한 퇴장 시각이 아니다. 로그 무손실 보장을 위한 디스크 큐와 자동 복구 작업은 포함하지 않는다.
|
||||
|
||||
DB 연결 실패 시 서버 시작을 중단한다. 실행 중 기록 저장 실패 시 `/health`는 503이고 신규 입장은 막으며 기존 참여자의 이동은 계속 처리한다. 원인을 해결한 후 서버를 재시작한다. 임의로 메모리 저장소로 전환하지 않는다. 개발·테스트에서만 `VISIT_STORE=memory`를 명시하면 기록을 영구 저장하지 않는 모드로 실행한다.
|
||||
|
||||
## 검증
|
||||
|
||||
```powershell
|
||||
npm test
|
||||
npm run build
|
||||
npm run verify:mongo
|
||||
npm run test:load
|
||||
```
|
||||
|
||||
`verify:mongo`는 매 실행마다 UUID가 붙은 임시 DB를 생성해 insert/heartbeat/finish/read 및 연결 시 기존 기록 보존을 확인한 뒤 그 DB만 삭제한다. `sunrise` 운영 데이터는 사용하지 않는다.
|
||||
|
||||
`test:load`는 메모리 저장소의 독립 로컬 서버와 100개 클라이언트를 한 프로세스에서 실행한다. 기본 300초이며 PowerShell에서 `$env:LOAD_SECONDS='10'`으로 짧은 실행도 가능하다. 보고서는 `artifacts/multiplayer/load-report.json`에 저장한다. 101번째 입장 거절·퇴장 후 자리 재사용·10Hz 이동을 검사한다.
|
||||
|
||||
2026-10-30 로컬 5분 결과: 오류 0, 이동 보정 0, 예상하지 않은 연결 종료 0. 방송 이후 수신 지연 p95 6ms, 최대 61ms. 이 값은 입력부터 방송까지의 대기 시간을 제외하며 실제 인터넷 지연·서버 단독 CPU 사용률·MongoDB 부하·모바일 GPU 성능을 보장하지 않는다. 두 브라우저에서 상호 캐릭터 표시/이동, 인원 표시, 혼자 보기와 재입장을 확인했다. 별도 메모리 검증 서버에서 99개 가상 참여자에 실제 브라우저 한 개가 입장해 100/100명 표시 및 양·우산·우비 렌더링을 확인했다. 해당 장면의 정량 프레임 시간과 모바일 성능은 측정하지 않았다.
|
||||
|
||||
## 주요 파일
|
||||
|
||||
- `server/app.ts`: HTTP/Socket.IO 서버, 정적 파일, 상태 확인
|
||||
- `server/room.ts`: 정원 예약, 입퇴장, 상태 검증과 방송
|
||||
- `server/visits.ts`: MongoDB 방문 기록과 명시적 메모리 테스트 저장소
|
||||
- `src/multiplayer/protocol.ts`: 클라이언트/서버 공통 이벤트 타입
|
||||
- `src/multiplayer/client.ts`: 입장·재접속·현재 상태·시각 동기화
|
||||
- `src/multiplayer/remotePlayers.ts`: 다른 참여자의 양 캐릭터 표시와 보간
|
||||
- `src/constants/multiplayer.ts`, `server/constants/multiplayer.ts`: 통신·정원·서버 제한
|
||||
@@ -0,0 +1,71 @@
|
||||
# 모바일 렌더링 최적화 검증 — 2026-10-30
|
||||
|
||||
## 측정 환경과 한계
|
||||
|
||||
Windows PC의 ChatGPT 내장 Chromium, CSS 412×915, 개발용 강제 DPR 설정으로 비교한다. 이는 모바일 **화면 크기 실험**이며 중급 안드로이드/Safari 실기기 검증이 아니다. 실제 모바일의 GPU, 발열, 메모리, 터치 입력 성능을 대체하지 않는다. 다른 데스크톱 프로그램도 실행 중인 환경이다.
|
||||
|
||||
앱 개발 서버에서 `/?profile&dpr=2`를 연 뒤 우측 DEV 패널의 시나리오 버튼을 누른다. 8초 준비 후 20초(지속 테스트는 300초)를 기록한다. 날짜 2026-10-30 07:00 KST, 기상은 맑음으로 고정된다. 낮 비교는 `&time=day`(12:00), 후처리 분리 실험은 `&post=off`를 사용한다. `dpr=1.5`, `dpr=1.25`는 내부 해상도만 바꾸며 CSS 크기는 같다.
|
||||
|
||||
일반 정지 장면은 기존과 같이 필요할 때만 렌더링한다. DEV 정지 시나리오는 GPU 비교를 위해 연속 렌더링을 강제한다. 보고된 FPS는 입력 콜백 빈도나 정지 시의 절전 효율이 아니라 **연속 렌더링 프레임 간격**의 역수다.
|
||||
|
||||
## 시나리오
|
||||
|
||||
- static: 초기 관측점과 시선을 유지한다.
|
||||
- rotation: 초기 관측점에서 20초마다 360° 회전한다.
|
||||
- movement: 관측점 동서 방향 ±3m 사인 왕복(8초 주기), 기존 지면 높이와 이동 가능 영역 검사를 사용한다.
|
||||
- dense: 같은 왕복 이동 중 과천 건물 방향을 바라본다. 현재 관측점에서는 능선이 건물을 가릴 수 있으므로 도시 전체가 화면에 보이는 고부하 테스트를 의미하지 않는다.
|
||||
- endurance: 위 왕복 이동과 360° 회전을 300초간 함께 실행한다.
|
||||
|
||||
스크립트 경로는 FPS와 독립된 위치를 입력한다. 일반 WASD 속도·터치·Pointer Lock의 수동 검증을 대신하지 않는다. 태양·좌표 갱신과 충돌·높이 처리는 기존 코드를 통과한다. 탭이 숨겨지면 측정을 취소한다. 초기/종료 카메라, 실제 이동거리, 충돌 거부 횟수와 렌더 크기를 JSON으로 남긴다.
|
||||
|
||||
## 계측 해석
|
||||
|
||||
- FPS = 1000 / 평균 프레임 간격. p95는 nearest-rank 방식이다. 50ms 초과 정지 프레임도 제거하지 않는다.
|
||||
- Draw Call/삼각형은 `renderer.info.autoReset=false`로 모든 후처리 패스를 합산한다. 프레임 시작 시 한 번만 초기화한다.
|
||||
- `renderer.info.memory`는 Three.js의 자원 개수이며 VRAM 바이트 사용량이 아니다. 시야 변화에 따른 최초 업로드 때문에 geometry 수가 증가할 수 있다.
|
||||
- CPU update는 이동/충돌/고도/태양/정보 UI 갱신을 포함한다. 미니맵과 WebGL 명령 제출은 별도다. 제출 시간은 GPU 실행 시간이 아니다.
|
||||
- 직접 참조된 재질 텍스처 목록은 셰이더 주입 텍스처 전체를 포함하지 않을 수 있다. 정상 표면은 color/normal/roughness 3종 × forest/path/rock 3표면을 사용하며 별도의 건물 조명 맵과 후처리 타깃이 있다.
|
||||
- 현재 그림자 맵은 비활성화되어 있다. 주광원 DirectionalLight와 환경광 HemisphereLight를 유지한다.
|
||||
|
||||
## 진행 기록
|
||||
|
||||
원본 변경 파일은 `.tmp/mobile-performance/`에 보존했다. 원본 렌더 설정의 시나리오별 수치는 `baseline-*.json`에 저장한다. 최종 비교와 미검증 항목은 작업 종료 시 아래에 추가한다.
|
||||
|
||||
## 현재 결과 — 계측 및 실험 준비 단계
|
||||
|
||||
사용자 요청에 따라 5분 테스트와 추가 연속 렌더링 부하를 중단했다. 측정 탭은 닫혔고 별도 측정 서버도 중지했다. **성능 최적화 완료 또는 모바일 30 FPS 달성을 주장하지 않는다.**
|
||||
|
||||
동시에 진행된 다른 작업에서 `summitRelief` 등 장면 소스가 변경된 사실을 발견했다. 아래 JSON은 각 실행에서 실제 관측된 수치지만, 실행 간 동일 소스 버전을 보장하지 못한다. 특히 texture/geometry 개수 변화에는 최초 GPU 업로드 외에 장면 변경 영향이 섞일 수 있다. 파일명 `baseline-*`은 초기 수집 파일명이며, 최종 전후 비교의 확정 기준값으로 재사용하면 안 된다. 고정 소스에서 기준값을 다시 수집해야 한다.
|
||||
|
||||
| 수집 시나리오 | FPS | 평균 ms | p95 ms | 평균 Draw Call | 평균 삼각형 | 텍스처(종료) | geometry(종료) | 50ms 초과 |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| 정지·강제 연속 렌더 | 119.85 | 8.34 | 8.40 | 49.00 | 1,802,718 | 28 | 117 | 0 |
|
||||
| 360° 회전 | 119.95 | 8.34 | 8.40 | 32.35 | 1,748,428 | 28 | 117 | 0 |
|
||||
| 왕복 이동 | 99.30 | 10.07 | 16.70 | 47.35 | 1,800,092 | 25 | 45 | 0 |
|
||||
| 과천 방향 왕복 | 112.35 | 8.90 | 16.60 | 22.00 | 1,744,875 | 28 | 117 | 0 |
|
||||
| 변경 후 동일 조건 | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 | 미측정 |
|
||||
| 5분 지속 | 중단 | — | — | — | — | — | — | — |
|
||||
|
||||
모든 표본은 CSS 412×915, 강제 DPR 2(내부 824×1830, 1,507,920픽셀), PC Chromium이다. 실기기 검증이 아니다. 단일 실행 결과이며 반복 측정 신뢰구간이 없다. 일반 장면의 원래 렌더링/재질/그림자/이동 설정은 이 작업에서 변경하지 않았다.
|
||||
|
||||
## 구현 파일과 실험 방법
|
||||
|
||||
- `src/dev/renderProfile.ts`: DEV 전용 패널·시나리오·원시 JSON. `stop` 버튼으로 중단 가능하며 탭이 숨겨지면 실행을 취소한다.
|
||||
- `src/scene/world.ts`, `src/main.ts`: DEV 계측 연결, 비교용 날짜·날씨 고정, 전체 후처리 카운트 합산. 일반 사용자의 시각·날씨 동작은 유지한다. 다른 작업의 지형 변경을 보존했다.
|
||||
- `src/scene/frameSummary.ts`: 평균/FPS/p95/지연 프레임 계산.
|
||||
- `src/scene/adaptiveResolution.ts`, `src/constants/renderQuality.ts`: 개발 실험용 자동 해상도. `/?profile&quality=adaptive`로 명시적으로 켠다. DPR 상한 1.5, 실제 픽셀 예산 150만, 하한 단계 0.75. 기기 native DPR도 넘지 않는다.
|
||||
- 고정 `dpr`를 지정하면 자동 조절보다 우선한다. 자동 조절은 5초 관측창, p95 34ms 초과 하향, p95 22ms 미만·평균 19ms 미만이 4개 창 동안 유지될 때 상향, 초기/변경/복귀/텍스처 로딩 후 10초 안정화 시간을 사용한다. 정지 렌더 생략 구간은 연속 프레임 근거로 사용하지 않는다.
|
||||
- 자동 조절은 렌더 해상도만 바꾸고 시뮬레이션 delta나 이동·회전·태양 계산을 변경하지 않는다. 실제 성능 효과와 화질 검증 전이므로 **production에서 활성화하지 않는다**.
|
||||
- `src/vite-env.d.ts`: Vite DEV 플래그 타입.
|
||||
- `tests/render-performance.test.ts`, `tests/adaptive-resolution.test.ts`: 통계와 조절 임계값/복귀/픽셀 상한 검증.
|
||||
|
||||
## 검증 및 남은 일
|
||||
|
||||
- `npm test`: 최종 69개 통과. 처음 발생한 로컬 HTTP EACCES는 권한이 허용된 실행에서 재검증하여 통과했다.
|
||||
- `npm run build`: 입력 자산 freshness 검사, TypeScript, production 빌드 통과. 번들 크기 경고는 남는다. production 번들에서 DEV 패널/결과 로그 문자열이 제외됨을 확인했다.
|
||||
- `npm run verify:solar`: 기존 SunCalc/NOAA 허용 오차 이내. 물리적 실측이 아닌 계산 대조다.
|
||||
- Android 중급 실기기, Safari, 터치 입력, 일반 브라우저 Pointer Lock, 전후 시각 비교 스크린샷: 미검증.
|
||||
- 5분 연속 테스트: 사용자 요청으로 중단, 통과 처리하지 않음.
|
||||
- 현재 과천 방향은 능선 때문에 실제 건물 밀집 부하를 대표하지 않을 수 있다. 재측정 때 구조물 밀집 시점이나 별도 도시 조감 시나리오를 추가 선정해야 한다.
|
||||
- 동일 소스 스냅샷에서 DPR 2/1.5/1.25와 후처리 분리 실험 → 화질 비교 → 효과 있는 설정만 production에 적용 → 실제 기기 검증 순서로 이어간다. 이 작업에서 LOD/Instancing/KTX2/그림자 변경은 도입하지 않았다.
|
||||
- 독립 코드 리뷰의 3건을 수정했다: 새 자동 화질 실험마다 초기 상한 복원, inventory 수집을 준비 시간으로 이동, 화면 resize 시 실행 취소. 이전 수집 JSON에는 첫 측정 프레임 inventory 비용도 일부 포함될 수 있으므로 최종 비교용이 아닌 임시 관찰 자료로만 보존한다. 수정 후 브라우저 재측정은 사용자 부하 제한에 따라 미실시했다.
|
||||
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"scenario": "dense",
|
||||
"url": "http://127.0.0.1:5173/?profile&dpr=2",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36",
|
||||
"measuredAt": "2026-10-30T01:25:49.069Z",
|
||||
"durationMs": 20000.100000023842,
|
||||
"frame": {
|
||||
"samples": 2247,
|
||||
"fps": 112.35168527527915,
|
||||
"meanMs": 8.9006230529595,
|
||||
"p95Ms": 16.60000000000582,
|
||||
"maxMs": 33.30000000000291,
|
||||
"overBudget": 0,
|
||||
"over50Ms": 0
|
||||
},
|
||||
"calls": {
|
||||
"mean": 22,
|
||||
"min": 22,
|
||||
"max": 22
|
||||
},
|
||||
"triangles": {
|
||||
"mean": 1744875,
|
||||
"min": 1744875,
|
||||
"max": 1744875
|
||||
},
|
||||
"cpuMs": {
|
||||
"update": {
|
||||
"mean": 0.25458185152014806,
|
||||
"min": 0.09999990463256836,
|
||||
"max": 0.8000000715255737
|
||||
},
|
||||
"minimap": {
|
||||
"mean": 0.5262900356508234,
|
||||
"min": 0.2999999523162842,
|
||||
"max": 1.7000000476837158
|
||||
},
|
||||
"renderSubmission": {
|
||||
"mean": 0.42971530084720283,
|
||||
"min": 0.19999992847442627,
|
||||
"max": 1.399999976158142
|
||||
}
|
||||
},
|
||||
"initial": {
|
||||
"css": [
|
||||
412,
|
||||
915
|
||||
],
|
||||
"pixels": [
|
||||
824,
|
||||
1830
|
||||
],
|
||||
"pixelCount": 1507920,
|
||||
"deviceDpr": 1,
|
||||
"renderDpr": 2,
|
||||
"lights": [
|
||||
{
|
||||
"type": "DirectionalLight",
|
||||
"intensity": 0.023818951740641622,
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"type": "HemisphereLight",
|
||||
"intensity": 0.6566541858247144,
|
||||
"castShadow": false
|
||||
}
|
||||
],
|
||||
"shadowsEnabled": false,
|
||||
"passes": [
|
||||
"RenderPass",
|
||||
"_UnrealBloomPass",
|
||||
"OutputPass"
|
||||
],
|
||||
"textureSources": [
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
}
|
||||
],
|
||||
"memory": {
|
||||
"geometries": 117,
|
||||
"textures": 28
|
||||
},
|
||||
"camera": {
|
||||
"position": [
|
||||
19.4687998205021,
|
||||
14.79398172597824,
|
||||
31.07608030252102
|
||||
],
|
||||
"quaternion": [
|
||||
-0.041274897227795544,
|
||||
-0.8886927344101164,
|
||||
-0.08164257516393467,
|
||||
0.4492839821161072
|
||||
]
|
||||
}
|
||||
},
|
||||
"final": {
|
||||
"css": [
|
||||
412,
|
||||
915
|
||||
],
|
||||
"pixels": [
|
||||
824,
|
||||
1830
|
||||
],
|
||||
"pixelCount": 1507920,
|
||||
"deviceDpr": 1,
|
||||
"renderDpr": 2,
|
||||
"lights": [
|
||||
{
|
||||
"type": "DirectionalLight",
|
||||
"intensity": 0.023818951497520877,
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"type": "HemisphereLight",
|
||||
"intensity": 0.656654184924715,
|
||||
"castShadow": false
|
||||
}
|
||||
],
|
||||
"shadowsEnabled": false,
|
||||
"passes": [
|
||||
"RenderPass",
|
||||
"_UnrealBloomPass",
|
||||
"OutputPass"
|
||||
],
|
||||
"textureSources": [
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
}
|
||||
],
|
||||
"memory": {
|
||||
"geometries": 117,
|
||||
"textures": 28
|
||||
},
|
||||
"camera": {
|
||||
"position": [
|
||||
19.46762172316039,
|
||||
14.794074630393776,
|
||||
31.07608030252102
|
||||
],
|
||||
"quaternion": [
|
||||
-0.041274897227795544,
|
||||
-0.8886927344101164,
|
||||
-0.08164257516393467,
|
||||
0.4492839821161072
|
||||
]
|
||||
}
|
||||
},
|
||||
"distanceMeters": 30.059211594469602,
|
||||
"rejectedMoves": 0,
|
||||
"note": "Forced continuous render; scripted path, not manual input validation. CPU submit time is not GPU time."
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"scenario": "movement",
|
||||
"url": "http://127.0.0.1:5173/?profile&dpr=2",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36",
|
||||
"measuredAt": "2026-10-30T01:24:18.782Z",
|
||||
"durationMs": 20006.899999952315,
|
||||
"frame": {
|
||||
"samples": 1986,
|
||||
"fps": 99.30148952234285,
|
||||
"meanMs": 10.070342396777441,
|
||||
"p95Ms": 16.700000000004366,
|
||||
"maxMs": 41.69999999999709,
|
||||
"overBudget": 1,
|
||||
"over50Ms": 0
|
||||
},
|
||||
"calls": {
|
||||
"mean": 47.354302969300456,
|
||||
"min": 42,
|
||||
"max": 51
|
||||
},
|
||||
"triangles": {
|
||||
"mean": 1800092.1026673377,
|
||||
"min": 1793631,
|
||||
"max": 1802850
|
||||
},
|
||||
"cpuMs": {
|
||||
"update": {
|
||||
"mean": 0.3373930544846011,
|
||||
"min": 0.09999990463256836,
|
||||
"max": 2.3000000715255737
|
||||
},
|
||||
"minimap": {
|
||||
"mean": 0.6290890787736099,
|
||||
"min": 0.2999999523162842,
|
||||
"max": 3.1999999284744263
|
||||
},
|
||||
"renderSubmission": {
|
||||
"mean": 0.6760946145655224,
|
||||
"min": 0.2999999523162842,
|
||||
"max": 4.299999952316284
|
||||
}
|
||||
},
|
||||
"initial": {
|
||||
"css": [
|
||||
412,
|
||||
915
|
||||
],
|
||||
"pixels": [
|
||||
824,
|
||||
1830
|
||||
],
|
||||
"pixelCount": 1507920,
|
||||
"deviceDpr": 1,
|
||||
"renderDpr": 2,
|
||||
"lights": [
|
||||
{
|
||||
"type": "DirectionalLight",
|
||||
"intensity": 0.02381895503796665,
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"type": "HemisphereLight",
|
||||
"intensity": 0.6566541980309549,
|
||||
"castShadow": false
|
||||
}
|
||||
],
|
||||
"shadowsEnabled": false,
|
||||
"passes": [
|
||||
"RenderPass",
|
||||
"_UnrealBloomPass",
|
||||
"OutputPass"
|
||||
],
|
||||
"textureSources": [
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
}
|
||||
],
|
||||
"memory": {
|
||||
"geometries": 36,
|
||||
"textures": 25
|
||||
},
|
||||
"camera": {
|
||||
"position": [
|
||||
19.484821852468222,
|
||||
14.792718233172673,
|
||||
31.07608030252102
|
||||
],
|
||||
"quaternion": [
|
||||
0.002282862348496262,
|
||||
0.9976790607156749,
|
||||
-0.052286144009298874,
|
||||
0.043559608510925475
|
||||
]
|
||||
}
|
||||
},
|
||||
"final": {
|
||||
"css": [
|
||||
412,
|
||||
915
|
||||
],
|
||||
"pixels": [
|
||||
824,
|
||||
1830
|
||||
],
|
||||
"pixelCount": 1507920,
|
||||
"deviceDpr": 1,
|
||||
"renderDpr": 2,
|
||||
"lights": [
|
||||
{
|
||||
"type": "DirectionalLight",
|
||||
"intensity": 0.023818948200196068,
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"type": "HemisphereLight",
|
||||
"intensity": 0.6566541727184744,
|
||||
"castShadow": false
|
||||
}
|
||||
],
|
||||
"shadowsEnabled": false,
|
||||
"passes": [
|
||||
"RenderPass",
|
||||
"_UnrealBloomPass",
|
||||
"OutputPass"
|
||||
],
|
||||
"textureSources": [
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
}
|
||||
],
|
||||
"memory": {
|
||||
"geometries": 45,
|
||||
"textures": 25
|
||||
},
|
||||
"camera": {
|
||||
"position": [
|
||||
19.45159968037214,
|
||||
14.795338124052773,
|
||||
31.07608030252102
|
||||
],
|
||||
"quaternion": [
|
||||
0.002282862348496262,
|
||||
0.9976790607156749,
|
||||
-0.052286144009298874,
|
||||
0.043559608510925475
|
||||
]
|
||||
}
|
||||
},
|
||||
"distanceMeters": 30.07526900052265,
|
||||
"rejectedMoves": 0,
|
||||
"note": "Forced continuous render; scripted path, not manual input validation. CPU submit time is not GPU time."
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"scenario": "rotation",
|
||||
"url": "http://127.0.0.1:5173/?profile&dpr=2",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36",
|
||||
"measuredAt": "2026-10-30T01:25:18.940Z",
|
||||
"durationMs": 20006.299999976152,
|
||||
"frame": {
|
||||
"samples": 2399,
|
||||
"fps": 119.95299882497062,
|
||||
"meanMs": 8.336598582742809,
|
||||
"p95Ms": 8.400000000008731,
|
||||
"maxMs": 16.59999999999127,
|
||||
"overBudget": 0,
|
||||
"over50Ms": 0
|
||||
},
|
||||
"calls": {
|
||||
"mean": 32.34625,
|
||||
"min": 20,
|
||||
"max": 75
|
||||
},
|
||||
"triangles": {
|
||||
"mean": 1748428.2675,
|
||||
"min": 1717845,
|
||||
"max": 1802878
|
||||
},
|
||||
"cpuMs": {
|
||||
"update": {
|
||||
"mean": 0.01099999984105428,
|
||||
"min": 0,
|
||||
"max": 0.2999999523162842
|
||||
},
|
||||
"minimap": {
|
||||
"mean": 0.04995833247900009,
|
||||
"min": 0,
|
||||
"max": 0.2999999523162842
|
||||
},
|
||||
"renderSubmission": {
|
||||
"mean": 0.47874999975164734,
|
||||
"min": 0.09999990463256836,
|
||||
"max": 2.5
|
||||
}
|
||||
},
|
||||
"initial": {
|
||||
"css": [
|
||||
412,
|
||||
915
|
||||
],
|
||||
"pixels": [
|
||||
824,
|
||||
1830
|
||||
],
|
||||
"pixelCount": 1507920,
|
||||
"deviceDpr": 1,
|
||||
"renderDpr": 2,
|
||||
"lights": [
|
||||
{
|
||||
"type": "DirectionalLight",
|
||||
"intensity": 0.023818951543106016,
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"type": "HemisphereLight",
|
||||
"intensity": 0.656654185093465,
|
||||
"castShadow": false
|
||||
}
|
||||
],
|
||||
"shadowsEnabled": false,
|
||||
"passes": [
|
||||
"RenderPass",
|
||||
"_UnrealBloomPass",
|
||||
"OutputPass"
|
||||
],
|
||||
"textureSources": [
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
}
|
||||
],
|
||||
"memory": {
|
||||
"geometries": 45,
|
||||
"textures": 25
|
||||
},
|
||||
"camera": {
|
||||
"position": [
|
||||
19.467857342665344,
|
||||
14.794056049507784,
|
||||
31.07608030252102
|
||||
],
|
||||
"quaternion": [
|
||||
0.002338710035799205,
|
||||
0.9976319637599487,
|
||||
-0.05228367576243833,
|
||||
0.04462524586604639
|
||||
]
|
||||
}
|
||||
},
|
||||
"final": {
|
||||
"css": [
|
||||
412,
|
||||
915
|
||||
],
|
||||
"pixels": [
|
||||
824,
|
||||
1830
|
||||
],
|
||||
"pixelCount": 1507920,
|
||||
"deviceDpr": 1,
|
||||
"renderDpr": 2,
|
||||
"lights": [
|
||||
{
|
||||
"type": "DirectionalLight",
|
||||
"intensity": 0.023818951543106016,
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"type": "HemisphereLight",
|
||||
"intensity": 0.656654185093465,
|
||||
"castShadow": false
|
||||
}
|
||||
],
|
||||
"shadowsEnabled": false,
|
||||
"passes": [
|
||||
"RenderPass",
|
||||
"_UnrealBloomPass",
|
||||
"OutputPass"
|
||||
],
|
||||
"textureSources": [
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
}
|
||||
],
|
||||
"memory": {
|
||||
"geometries": 117,
|
||||
"textures": 28
|
||||
},
|
||||
"camera": {
|
||||
"position": [
|
||||
19.467857342665344,
|
||||
14.794056049507784,
|
||||
31.07608030252102
|
||||
],
|
||||
"quaternion": [
|
||||
-0.0023346036782983066,
|
||||
-0.9976354655416118,
|
||||
0.052283859283038824,
|
||||
-0.04454689189728307
|
||||
]
|
||||
}
|
||||
},
|
||||
"distanceMeters": 0,
|
||||
"rejectedMoves": 0,
|
||||
"note": "Forced continuous render; scripted path, not manual input validation. CPU submit time is not GPU time."
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"scenario": "static",
|
||||
"url": "http://127.0.0.1:5173/?profile&dpr=2",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/156.0.0.0 Safari/537.36",
|
||||
"measuredAt": "2026-10-30T01:26:36.347Z",
|
||||
"durationMs": 20006.100000000006,
|
||||
"frame": {
|
||||
"samples": 2397,
|
||||
"fps": 119.85299632490812,
|
||||
"meanMs": 8.343554443053817,
|
||||
"p95Ms": 8.400000000023283,
|
||||
"maxMs": 25,
|
||||
"overBudget": 0,
|
||||
"over50Ms": 0
|
||||
},
|
||||
"calls": {
|
||||
"mean": 49,
|
||||
"min": 49,
|
||||
"max": 49
|
||||
},
|
||||
"triangles": {
|
||||
"mean": 1802718,
|
||||
"min": 1802718,
|
||||
"max": 1802718
|
||||
},
|
||||
"cpuMs": {
|
||||
"update": {
|
||||
"mean": 0.011050875083519282,
|
||||
"min": 0,
|
||||
"max": 0.6000000238418579
|
||||
},
|
||||
"minimap": {
|
||||
"mean": 0.004211843113187356,
|
||||
"min": 0,
|
||||
"max": 0.20000004768371582
|
||||
},
|
||||
"renderSubmission": {
|
||||
"mean": 0.5948707267977577,
|
||||
"min": 0.2999999523162842,
|
||||
"max": 1.8000000715255737
|
||||
}
|
||||
},
|
||||
"initial": {
|
||||
"css": [
|
||||
412,
|
||||
915
|
||||
],
|
||||
"pixels": [
|
||||
824,
|
||||
1830
|
||||
],
|
||||
"pixelCount": 1507920,
|
||||
"deviceDpr": 1,
|
||||
"renderDpr": 2,
|
||||
"lights": [
|
||||
{
|
||||
"type": "DirectionalLight",
|
||||
"intensity": 0.023818951543106016,
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"type": "HemisphereLight",
|
||||
"intensity": 0.656654185093465,
|
||||
"castShadow": false
|
||||
}
|
||||
],
|
||||
"shadowsEnabled": false,
|
||||
"passes": [
|
||||
"RenderPass",
|
||||
"_UnrealBloomPass",
|
||||
"OutputPass"
|
||||
],
|
||||
"textureSources": [
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
}
|
||||
],
|
||||
"memory": {
|
||||
"geometries": 117,
|
||||
"textures": 28
|
||||
},
|
||||
"camera": {
|
||||
"position": [
|
||||
19.467857342665344,
|
||||
14.794056049507784,
|
||||
31.07608030252102
|
||||
],
|
||||
"quaternion": [
|
||||
0.0022828623484962645,
|
||||
0.9976790607156749,
|
||||
-0.05228614400929893,
|
||||
0.043559608510925475
|
||||
]
|
||||
}
|
||||
},
|
||||
"final": {
|
||||
"css": [
|
||||
412,
|
||||
915
|
||||
],
|
||||
"pixels": [
|
||||
824,
|
||||
1830
|
||||
],
|
||||
"pixelCount": 1507920,
|
||||
"deviceDpr": 1,
|
||||
"renderDpr": 2,
|
||||
"lights": [
|
||||
{
|
||||
"type": "DirectionalLight",
|
||||
"intensity": 0.023818951543106016,
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"type": "HemisphereLight",
|
||||
"intensity": 0.656654185093465,
|
||||
"castShadow": false
|
||||
}
|
||||
],
|
||||
"shadowsEnabled": false,
|
||||
"passes": [
|
||||
"RenderPass",
|
||||
"_UnrealBloomPass",
|
||||
"OutputPass"
|
||||
],
|
||||
"textureSources": [
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"mipmaps": true
|
||||
}
|
||||
],
|
||||
"memory": {
|
||||
"geometries": 117,
|
||||
"textures": 28
|
||||
},
|
||||
"camera": {
|
||||
"position": [
|
||||
19.467857342665344,
|
||||
14.794056049507784,
|
||||
31.07608030252102
|
||||
],
|
||||
"quaternion": [
|
||||
0.0022828623484962645,
|
||||
0.9976790607156749,
|
||||
-0.05228614400929893,
|
||||
0.043559608510925475
|
||||
]
|
||||
}
|
||||
},
|
||||
"distanceMeters": 0,
|
||||
"rejectedMoves": 0,
|
||||
"note": "Forced continuous render; scripted path, not manual input validation. CPU submit time is not GPU time."
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# 붉은 양 캐릭터
|
||||
|
||||
## 현재 모델: 아이보리 아기 양 (v3)
|
||||
|
||||
아이보리 털·베이지 얼굴·갈색 뿔/발·붉은 목도리의 4색으로 변경했다. 뿔을 약 절반 크기로 줄여 위·뒤로 옮겼고, 눈을 키우고 얼굴 아래로 모았다. 팔다리는 짧고 통통하게 변경했다. 머리 깊이는 0.96m에서 1.40m, 몸통 깊이는 0.80m에서 1.10m로 늘려 측면의 납작한 인상을 줄였다(게임 내 배율 적용 전).
|
||||
|
||||
- 현재 원본: `assets/characters/ivory-lamb-v3.blend`
|
||||
- 재현 스크립트: `scripts/create-ivory-lamb.py`
|
||||
- 게임 모델 경로는 기존 `public/characters/red-sheep.glb`를 유지한다.
|
||||
- 정면 사선·측면·뒷면을 Blender에서 렌더링해 확인했고, 캐릭터 테스트 4개와 빌드가 통과했다.
|
||||
|
||||
아래는 초기 구현 기록이다.
|
||||
|
||||
승인된 4색 컨셉을 Blender MCP에서 제작했다. 모델은 약 3등신의 단순한 덩어리로 구성되며 정면은 glTF의 +Z 방향이다. 몸통·머리·양팔·양다리의 이름 있는 피벗을 사용한다. 현재 동작은 Three.js에서 관절을 회전시키는 방식이며, GLB 안에 애니메이션 클립을 저장하지 않는다.
|
||||
|
||||
- Blender 원본: `assets/characters/red-sheep.blend`
|
||||
- 웹 모델: `public/characters/red-sheep.glb` (약 415 KiB)
|
||||
- 재현 스크립트: `scripts/create-red-sheep.py` (Blender에서 실행; 별도 장면 생성)
|
||||
- 게임 표시: 네 가지 기본색의 unlit 재질과 어두운 역면 외곽선
|
||||
|
||||
## 사용
|
||||
|
||||
화면 클릭 후 WASD로 이동하고 마우스로 시선을 돌린다. 기본 카메라는 캐릭터 뒤를 따라간다. ESC로 마우스를 해제하면 화면 버튼을 사용할 수 있다.
|
||||
|
||||
- `1`: 3초 동안 손 흔들기
|
||||
- `2`: 선택한 관측 시각이 계산된 일출 전후 5분이면 해 방향을 보고 기도
|
||||
- WASD 이동 입력: 감정표현 종료
|
||||
- 시간 변경으로 기도 가능 구간을 벗어나면 기도 종료
|
||||
- 조감 모드: 감정표현 비활성화; 돌아오면 상태 복원
|
||||
|
||||
기도는 천문 일출 시각 기준이며 능선·구름에 가려졌는지와 무관하다. CUSTOM 시각으로도 확인할 수 있다. 카메라의 지형 높이 보정과 보행 위치는 분리되어 있고, 미니맵 및 태양 계산은 보행자의 위치를 사용한다. 건물·구조물에 대한 카메라 충돌은 추가하지 않았다.
|
||||
|
||||
## 검증 (2026-10-30)
|
||||
|
||||
- Blender MCP에서 모델 정면·뒷면·사선 확인.
|
||||
- 실제 GLB를 파싱하여 관절 동작 및 이동 종료 프레임을 테스트.
|
||||
- 캐릭터 관련 테스트 4개 통과; TypeScript 및 프로덕션 빌드 통과.
|
||||
- 전체 테스트 75개 중 74개 통과. 변경하지 않은 `tests/summit-walking.test.ts:40`의 정상 암반 경계 테스트 실패.
|
||||
- 브라우저 시각 검증은 도구가 Windows 현재 URL을 확인하지 못해 중단됨. 게임 안에서의 최종 화면·상호작용 검증은 미완료.
|
||||
# 현재 적용 모델
|
||||
|
||||
사용자 선택으로 둥근 v10을 복원했다. 원본은 `assets/characters/ivory-lamb-v10.blend`, 게임 파일은 `public/characters/red-sheep.glb`다. 로딩 URL의 버전 키로 이전 양털 모델의 브라우저 캐시를 구분한다. 부드러운 이동 방향 전환은 유지한다. `scripts/create-ivory-lamb.py`는 이후 양털 실험 버전이므로 현재 모델을 재생성하는 데 사용하지 않는다.
|
||||
|
||||
## 우산·우비·후드
|
||||
|
||||
- Blender MCP 생성 스크립트: `scripts/create-rain-gear.py`. v10 캐릭터 원본을 참조하며 캐릭터 GLB를 덮어쓰지 않는다.
|
||||
- 원본: `assets/characters/rain-gear.blend`. 게임 파일: `public/characters/umbrella.glb`, `public/characters/raincoat.glb`.
|
||||
- 비 또는 눈이 있을 때 3 우산, 4 우비를 각각 착용/해제한다. 우비를 입으면 5 후드를 올리거나 내릴 수 있다.
|
||||
- 장비 상태는 감정표현과 분리된다. 걷기·인사·기도 중 유지하며, 우산을 들면 빈손으로 인사한다. 우비를 벗으면 후드는 내려가고, 맑아지면 모든 우천 장비가 해제된다.
|
||||
- 우비 밑단은 캐릭터 좌표 높이 0.42에서 끝나 양털 다리와 발굽을 드러낸다. 후드는 머리 관절, 소매는 각 팔 관절을 따른다.
|
||||
- 개발 확인: `/?profile&time=day&weather=rain`. DEV 날씨 선택으로 비·눈·맑음 전환을 검증한다. 실제 기상자료가 아닌 명시적인 개발용 시나리오이며 프로덕션에서는 비활성화된다.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# 태양 위치 검증 결과
|
||||
|
||||
관측점 37.4450 N, 126.9640 E (임시). SunCalc 2.0.1과 독립 NOAA/Meeus 수식 구현 비교. 방위각과 0° 이상 고도각 허용 오차: 0.1°. 0° 아래 고도값은 굴절 모델 차이를 고려해 0.5°. 실측 검증이 아닌 계산 대조.
|
||||
|
||||
| KST | SunCalc 방위각 | NOAA 방위각 | 차이 | SunCalc 겉보기 고도 | NOAA 겉보기 고도 | 차이 |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| 2026-03-20 06:40:00 | 90.2539 | 90.2593 | 0.0054 | 0.3565 | 0.3788 | 0.0223 |
|
||||
| 2026-03-20 12:00:00 | 164.0032 | 164.0165 | 0.0133 | 51.2750 | 51.2759 | 0.0009 |
|
||||
| 2026-06-21 05:10:00 | 58.9743 | 58.9767 | 0.0024 | -0.5901 | -0.7629 | 0.1728 |
|
||||
| 2026-06-21 12:00:00 | 150.1821 | 150.1940 | 0.0119 | 74.2243 | 74.2256 | 0.0013 |
|
||||
| 2026-09-23 06:20:00 | 89.2445 | 89.2421 | 0.0023 | -0.4277 | -0.5524 | 0.1248 |
|
||||
| 2026-09-23 18:00:00 | 266.1434 | 266.1416 | 0.0018 | 4.9507 | 4.9519 | 0.0012 |
|
||||
| 2026-10-29 06:50:00 | 105.7296 | 105.7383 | 0.0087 | -1.0017 | -1.2499 | 0.2482 |
|
||||
| 2026-10-29 12:00:00 | 175.0242 | 175.0428 | 0.0186 | 39.0157 | 39.0161 | 0.0004 |
|
||||
| 2026-12-21 07:45:00 | 119.6824 | 119.6829 | 0.0005 | 0.0541 | 0.1201 | 0.0659 |
|
||||
| 2026-12-21 12:00:00 | 172.1418 | 172.1427 | 0.0009 | 28.7410 | 28.7398 | 0.0012 |
|
||||
| 2026-12-31 23:59:00 | 329.0559 | 329.0690 | 0.0132 | -73.1699 | -73.6540 | 0.4842 |
|
||||
| 2027-01-01 00:00:00 | 329.7923 | 329.8056 | 0.0133 | -73.2707 | -73.7549 | 0.4841 |
|
||||
|
||||
최대 차이: 방위각 0.0186°, 0° 이상 태양 고도 0.0659°, 지평선 아래 포함 고도 0.4842°.
|
||||
|
||||
SunCalc는 기하학적 고도가 음수이면 굴절 수식의 입력을 0°로 고정한다(약 0.484° 보정). NOAA는 음의 고도에도 별도 수식을 적용한다. 따라서 처음 시도한 전 구간 0.1° 기준은 5개 지평선 아래 사례에서 실패했다. 이 차이를 숨기지 않고 0° 아래 구간의 허용 차이를 분리했다. 지형 모드에서는 음의 고도 태양도 일부 보일 수 있으므로 이 모델 차이도 관측 일출의 오차 요인이다.
|
||||
|
||||
출처: [NOAA 계산 설명](https://gml.noaa.gov/grad/solcalc/calcdetails.html), [NOAA 계산 소스](https://gml.noaa.gov/grad/solcalc/main.js), [SunCalc 2.x](https://github.com/mourner/suncalc).
|
||||
|
||||
재현: npm run verify:solar. 앱 계산에는 NOAA 검증 코드를 사용하지 않는다. 대기 굴절 모델 차이와 실제 기압·기온 변동, 지형 차폐는 이 비교로 검증되지 않는다.
|
||||
@@ -0,0 +1,30 @@
|
||||
# 관악산 Copernicus 지형 적용 계획
|
||||
|
||||
**Goal:** 공개 30m급 두 타일을 보존하고 관악산 주변 DSM을 기존 고정 시점 시뮬레이터에 적용한다.
|
||||
**Architecture:** Python 오프라인 전처리 → 정적 float32 높이 격자·메타데이터 → 기존 Three.js 장면의 지형 메시. 브라우저 GIS 의존성/백엔드는 추가하지 않는다.
|
||||
|
||||
## 범위와 결정
|
||||
- 원본 N37 E126/E127을 data/raw/copernicus에 보관하고 SHA256/출처를 기록한다.
|
||||
- 원본 WGS84 경위도와 EGM2008 높이를 확인한 후 관측점 중심 AEQD(m) 좌표로 변환한다.
|
||||
- 초기 영역은 동서·남북 24km, 30m 간격(801×801). 원본보다 정밀하다는 의미가 아니다.
|
||||
- +X 동, +Y 위, -Z 북. 구면 지구 곡률 하강을 근사 반영한다. 지상 대기굴절은 미반영.
|
||||
- 임시 수동 해발고도 629m 대신 해당 임시 좌표의 DSM 표면+눈높이에 카메라를 둔다. 계산에도 같은 DSM 고도를 사용하고 화면에 명시한다. DSM은 수목·구조물을 포함하므로 실측 관측점이 아니다.
|
||||
- 가상 0° 지평선의 불투명 하반구는 해발고도에 따른 단순 해수면 지평선으로 낮추고 선/방위 표시는 유지한다. 영역 밖 실제 능선은 미반영임을 명시한다. 태양 원반은 지형보다 멀리 배치해 깊이 버퍼로 가린다. 0° 아래 일괄 숨김은 지형 모드에서 제거한다.
|
||||
- 일출/일몰 숫자는 계속 천문 계산값이며 능선 위 첫 관측 시각을 새로 계산하지 않는다.
|
||||
- 지형 다운로드 실패 시 빈 성공 화면 대신 오류를 표시한다.
|
||||
|
||||
## 작업 및 검증
|
||||
- [x] 다운로드·메타데이터·결측값·관측점 표면고도 검사, 재현 가능한 전처리 스크립트 및 출처 파일.
|
||||
- [x] 격자 정합/방위/보간/곡률/실데이터 범위 검증 테스트 후 지형 생성.
|
||||
- [x] 기존 world/sun/horizon/main/UI에 지형 통합, 로딩/실패 처리와 출처 표시.
|
||||
- [x] 전체 테스트·빌드, 일출 전후/낮 브라우저 검증, 독립 코드 리뷰, 문서 갱신.
|
||||
|
||||
## 검증 기록
|
||||
- 원본 합계 79,266,369 bytes. 가공 높이 격자 2,566,404 bytes / 801² 표본 / 결측 0.
|
||||
- 관측점 표면 606.906372m, 격자 범위 1.314315~618.474609m. 실측 고도가 아니며 원본의 표면 높이를 그대로 사용.
|
||||
- 새 테스트 4개 포함 전체 22개. 실제 03-20 06:40/06:50 태양 중심의 차폐/통과를 확인.
|
||||
- 독립 리뷰: important issue 없음. 지형 로딩/렌더링 오류 없음. 내장 브라우저의 Pointer Lock 거부는 재현되었으며 오류 안내가 표시됨.
|
||||
- 일반 브라우저 Pointer Lock 수동 조작은 이전 단계 미검증 상태 유지(컨트롤 코드 변경 없음).
|
||||
|
||||
## 검토 초점
|
||||
북쪽 뒤집힘, 경도127° 타일 경계, DSM/수동 표고 불일치, 지형보다 가까운 태양, 0° 강제 차폐, 큰 정적 파일 배포 누락.
|
||||
@@ -0,0 +1,13 @@
|
||||
# 기사문 해변 추가
|
||||
|
||||
사용자 요청: 하조대 곶의 차폐를 피해 기사문 해변에서 일출을 관찰한다.
|
||||
|
||||
기존 해안 렌더러·전처리를 장소 매개변수로 확장하고 새 장소의 데이터만 추가한다. 관악산·하조대는 유지하며 세 장소 링크를 제공한다. 기본 URL은 기존 하조대를 유지하고 기사문은 `?site=gisamun`으로 접근한다.
|
||||
|
||||
- [x] 기사문 OSM 원본·출처 기록, 관측점 선정, 3×3km DSM 추출. 위도 38° 경계를 넘으므로 보유한 N37/N38 타일을 병합한다.
|
||||
- [x] 바다 로더와 장소 전환을 장소별 경로로 연결한다. 공통 렌더러를 복제하지 않는다.
|
||||
- [x] 육지 원점·바다 방향·일출 태양 차폐를 테스트하고 기존 테스트·빌드·브라우저 검증.
|
||||
|
||||
모래 영역은 OSM에 누락되어 공식 관광 안내의 해변 위치와 실제 해안선 구간을 기준으로 35m 폭의 시각적 근사를 사용한다. 측량값이나 실제 해변 경계로 표시하지 않는다. 방파제 높이·구조물·작은 섬 상세 형상은 원본 해상도의 한계다.
|
||||
|
||||
검증: 기사문 자동 테스트 2개 통과, 전체 36개 테스트와 빌드 통과. 브라우저 2026-10-29 06:55 태양 원반 확인, 기사문→하조대 전환 확인. 독립 읽기 전용 리뷰에서 중요한 문제 없음. 동시 진행 중인 관악산 구조물 변경은 유지했다.
|
||||
@@ -0,0 +1,22 @@
|
||||
# 하조대 해안 구현 계획
|
||||
|
||||
## 설계
|
||||
사용자가 요청한 하조대 지형 추출, 해안선과 바다 구현을 수행한다. 기존 Three.js/SunCalc와 지형 전처리 패턴을 재사용한다. 기본 장면은 하조대이며 관악산은 링크로 유지한다. 실측하지 않은 관측점은 임시값으로 표시한다.
|
||||
|
||||
선택: 관측점 중심 3×3km DSM을 추출하고 OSM 해안선으로 육지/바다를 구분한다. 단순 바다 평면만 겹치는 방법은 높이 0인 해변을 침수시키므로 제외한다. 해안 근접 형상을 위해 10m로 재표본화하되 원본 정확도는 30m급으로 명시한다. 해안 마스크는 2m 간격이며 원본 OSM 정확도를 높이는 것이 아니다. 해안에서 30m 이내 표면은 시각적으로 해수면에 접속하며 정밀 지반/수심 데이터로 취급하지 않는다.
|
||||
|
||||
바다는 지구 곡률을 반영한 메시와 절차적 물결/태양 반사를 사용한다. 바다 이동은 차단한다. 건물·등대 모델링, 실제 날씨/조석, 정밀 암반은 범위 밖이다. 날짜·시간·눈높이·기존 관악산 기능을 유지한다. 데이터와 렌더링 상수는 기능별 constants 파일에 둔다.
|
||||
|
||||
## 작업
|
||||
- [x] `scripts/prepare-hajodae.py`: 기존 rasterio/pyproj로 DSM, OSM 마스크/해안선 추출; public/terrain/hajodae/에 원본 URL·해시·보정 범위 포함.
|
||||
- [x] `src/constants/locations.ts`, `src/constants/coast.ts`, `src/scene/coast.ts`: 장소 설정, 경계 판정, 바다/해안 재질. 기존 terrain/world/main/UI에 최소 통합.
|
||||
- [x] 테스트: 해변 원점이 육지, 동쪽 바다/남쪽 곶 구분, 바다 차단, 지형과 해수면 곡률, 위치 전환 데이터 정합.
|
||||
- [x] 전체 기존 테스트 및 빌드, 브라우저에서 하조대 낮/일출·관악산 검증. 출처 페이지와 README 갱신.
|
||||
|
||||
## 판단 기록
|
||||
- Git 저장소가 없는 기존 작업 폴더에서 직접 진행하며 다른 변경을 되돌리지 않는다.
|
||||
- 사용자 요청과 앞선 대화로 구현 범위가 정해져 있으므로 추가 승인 없이 진행한다.
|
||||
- 리뷰: 해안에 인접한 바다 칸 -2m가 육지 삼각형을 수면 아래로 당기는 문제 확인. 숨겨진 바다 칸을 0m로 변경하고 (52,0) 지점과 전체 격자 비음수 검증을 추가했다.
|
||||
- 브라우저: 해안 조감, 해변 눈높이 낮/일출 전후 표시 확인. 조감의 원거리 물결 앨리어싱은 화면 미분값으로 주파수를 감쇠해 수정했다.
|
||||
- 최종 검증: `npm test` 33/33, `npm run build` 통과. 기존 500KB 번들 크기 경고는 남아 있다. 관악산 건물 조감 정상, 링크로 하조대 복귀 정상. 하조대 동쪽 전진 15초 후 52m에서 멈추며 추가 3초 입력에도 위치 유지. 내장 브라우저 Pointer Lock은 지원되지 않지만 기존 드래그/키보드 대체 조작으로 이동 검증했다.
|
||||
- 캡처: `data/references/hajodae/implemented-overview.png`. 배포용 데이터 총 402,503 bytes(설명 HTML 제외).
|
||||
@@ -0,0 +1,37 @@
|
||||
# 관악산 Semi-MVP 1단계 Implementation Plan
|
||||
|
||||
**Goal:** 고정 관측점의 실제 태양 위치를 KST 시간 제어와 FPS 시점으로 관찰한다.
|
||||
**Architecture:** 제공된 모듈 구조를 따르는 프레임워크 없는 Vite 앱. 천문 계산은 Three.js와 독립. 설정은 config/, 기능 상수는 constants/에 둔다.
|
||||
**Tech Stack:** TypeScript, Vite, Three.js Sky / PointerLockControls, SunCalc 2.x.
|
||||
**Spec:** 사용자가 제공한 Semi-MVP 1단계 작업지시서.
|
||||
|
||||
## 결정 및 범위
|
||||
- 빈 폴더에 로컬 실행 프로젝트를 만든다. Git 저장소가 없어 worktree/커밋은 생성하지 않는다.
|
||||
- 임시 관측점: 37.4450 N, 126.9640 E, 해발 629 m, 눈높이 1.7 m. 측량값이 아님을 화면과 코드에 명시.
|
||||
- 표준 가상 지평선은 고도각 0°. 일출/일몰은 해발+눈높이로 보정하며 표준 0m 결과도 함께 표시한다.
|
||||
- KST 날짜의 정오를 getTimes에 전달해 자정에 전날 solar day가 선택되지 않게 한다.
|
||||
- 태양 원반은 0.53°, 광학 halo는 별도 셰이더. Sky 내장 원반은 비활성화해 중복 원반을 방지한다.
|
||||
- DEM, 지형, 건물, 이동, 백엔드, 호스팅은 범위 밖.
|
||||
|
||||
## 구현 및 검증
|
||||
- [x] 천문/KST/방향 테스트를 먼저 작성하고 실패 확인. SunCalc 2.x 기반 계산 및 시간 모듈 구현.
|
||||
- [x] Sky, 원반/halo, 0° 지평선, 방위 표시, 고정 FPS 카메라 구성.
|
||||
- [x] 최소 한국어 UI와 실시간 1초 갱신, 날짜/슬라이더, 탭 복귀 재동기화 연결.
|
||||
- [x] NOAA 공개 수식의 독립 구현과 여러 계절/시각 대조. 허용 오차를 명시하고 결과 저장.
|
||||
- [x] TypeScript/production build, 18개 테스트, 브라우저 화면·시간 UI 검증. README에 실행법/제한 기록.
|
||||
- [ ] 일반 브라우저 Pointer Lock 회전 및 ESC 실제 확인: 내장 브라우저 거부, 일반 Chrome 자동화 URL 확인 실패로 미검증.
|
||||
|
||||
## 검토 및 실행 기록
|
||||
- 독립 리뷰의 2건(과거 서울 시차 불일치, LIVE 중 날짜 편집 덮어쓰기)을 재현 테스트로 확인 후 수정. 입력과 출력 모두 IANA Asia/Seoul 사용.
|
||||
- NOAA 전 구간 0.1° 테스트는 지평선 아래 5개 사례에서 실패. SunCalc 음수 고도 굴절 입력 clamp를 확인해 표시 구간 0.1° / 비표시 고도 0.5°로 구분. 원본 차이를 검증표에 보존.
|
||||
- Vite 단일 번들 500 kB 권고 경고는 Three.js 포함으로 발생. 범위에 불필요한 지연 로딩은 추가하지 않음.
|
||||
|
||||
## 검토 초점
|
||||
KST 자정, 방위각 래핑, 지평선 아래 원반 숨김, 고도 보정 일출과 0° 표시 구분, Pointer Lock 실패 안내.
|
||||
|
||||
## 추후 TODO (2026-10-30)
|
||||
- [ ] 서버 운영 단계에서 LIVE 모드를 서버 시간 기준으로 동기화한다. 현재는 구현을 보류한다.
|
||||
- 목적: 사용자 기기 시계의 오차 및 실행 중 시계 변경으로 인한 LIVE 시각 차이를 줄인다.
|
||||
- 접속 시 서버 시각을 받아 기준을 맞추고, 이후에는 `performance.now()`의 경과 시간을 더해 브라우저에서 매초 SunCalc를 계산한다. 서버에 매초 요청하지 않는다.
|
||||
- 탭 복귀 및 주기적 재동기화를 적용한다. 통신 지연 보정과 동기화 실패 시 처리 방식은 구현 시 결정한다.
|
||||
- CUSTOM 모드는 사용자가 선택한 날짜·시간을 유지하고, 입력·표시는 기존 `Asia/Seoul` 기준을 유지한다.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Summit structure assets implementation plan
|
||||
|
||||
> **For agentic workers:** Execute inline with superpowers:executing-plans. Asset authoring is verified by export/import checks and visual inspection.
|
||||
|
||||
**Goal:** Refine the eight existing summit structures in Blender and deliver editable Blender source, individual GLBs, and previews.
|
||||
|
||||
**Architecture:** Reuse `src/constants/structures.ts` as the dimension and placement source. Build locally centered, material-batched meshes in a separate Blender scene; preserve the user's original scene. Store geographic placement and terrain support anchors in the asset manifest. Preserve the current application until asset integration is explicitly requested.
|
||||
|
||||
**Tech Stack:** Existing Node/TypeScript, Blender 5.2 through MCP, Python bpy, glTF 2.0.
|
||||
|
||||
**Spec:** User-approved conversation: refine existing structures using the collected photos and extract `.blend` / `.glb` assets; proceed now.
|
||||
|
||||
## Constraints and decisions
|
||||
|
||||
- Existing IDs, estimated dimensions and source attribution remain authoritative.
|
||||
- No new dependencies. Photographs are modeling references, never distributed textures.
|
||||
- Details visible in references inform geometry; hidden details remain approximations.
|
||||
- Deck origin is the upper walking surface; tower/radar origin is the base. Metres; GLB Y-up.
|
||||
- Terrain-dependent footings/support lengths stay outside fixed geometry; export their anchors.
|
||||
- Ruling: direct artifact authoring, not a runtime feature; no new regression tests. Validate the actual GLBs with Three.js and Blender re-import, inspect previews, run the existing build.
|
||||
- This directory has no Git repository; no commits or worktree operations.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] Snapshot existing constants and author `scripts/build-summit-assets.py`: shared material-batched geometry, five towers, radar, two decks.
|
||||
- [x] Export `public/models/summit/*.glb`, a manifest, and `assets/summit/summit-structures.blend` with a presentation scene.
|
||||
- [x] Render overview and detailed previews; inspect and correct visual defects.
|
||||
- [x] Load exported GLBs with Three.js and Blender, verify dimensions, materials, IDs, finite geometry, and no external resources. Write a verification report and usage notes.
|
||||
|
||||
## Review focus
|
||||
|
||||
Check north-axis conversion, origins, detached stair rails, closed deck ends, whole-scene export accidents, material loss, measured-versus-estimated wording, and inclusion of private/reference images.
|
||||
|
||||
## Completion evidence
|
||||
|
||||
- Three.js GLTFLoader: 8/8 passed; 87,523 triangles, 44 material batches, approximately 4.13 MiB total GLB payload.
|
||||
- Blender re-import: 8/8 passed; bounding boxes match Three.js within 1 mm.
|
||||
- `npm run build`: passed; existing bundle-size warning remains.
|
||||
- Overview plus radar, deck and tower renders inspected. Corrected overview framing and studio ground extent.
|
||||
- Export limited to active scene and selected hierarchy; initial default cube leakage corrected before delivery.
|
||||
- Independent review found stale geometry/spec metadata on reruns. Existing meshes now retain generation specs and changed dimensions fail explicitly; regeneration requires a fresh Blender file. Hand edits remain preserved during re-export.
|
||||
- Automatic approval rejected a destructive rebuild verification attempt. Removed that path and completed with non-destructive re-export. Original Cube, Camera and Light remain in the original scene.
|
||||
- Editable source saved, all assets visible in the presentation scene; GLBs and manifest also packaged in `assets/summit/summit-glb-pack.zip`.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Building exterior lighting
|
||||
|
||||
User choice: replace night window patterns with lighting on facades and surrounding ground, not whole-building emission. Reuse the existing static light-spill texture and solar-altitude fade. Keep building footprints, heights, terrain, and sun/shadow behavior unchanged.
|
||||
|
||||
Implementation: remove the four-component buildingWindow attribute and random window shader. Retain one scalar facade height per vertex; use normals to reject horizontal surfaces and fade illumination above the existing 24m limit. Unlit building types receive a fully faded value. Reuse the same texture on terrain and facades. Remove constants used only by windows. No new dependencies.
|
||||
|
||||
Verification: update the existing night/dawn/day/reverse-time test to verify spill intensity and non-emitting materials, assert the old window buffer is absent and total geometry bytes are reduced. Run tests/build and inspect daytime and nighttime in browser, including shader errors. Record actual geometry memory before/after; do not claim measured FPS improvement without a benchmark.
|
||||
|
||||
Completed: 43 tests passed and production build passed (existing bundle-size warning). Independent static review found no actionable issues. Browser checks at midnight and noon rendered successfully with no console errors; night lighting remains subtle at city-overview distance. Actual geometry buffers: 21,382,272 → 16,036,704 bytes (25% reduction); lighting attribute: 7,127,424 → 1,781,856 bytes. Mesh and triangle counts are unchanged. No FPS improvement claim or indexing changes.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Gwacheon building data improvement
|
||||
|
||||
Goal: improve agreement with real building footprints and heights using the user-supplied AL_D010_41_20260719.zip, without invented density.
|
||||
|
||||
Scope agreed in chat: prioritize actual map agreement; compare official data with OSM, fill missing heights, and inspect DSM placement. The supplied archive continues that authorized work.
|
||||
|
||||
Design: retain existing OSM geometry and explicit heights, supplement confidently matched missing height/levels from VWorld, and add non-overlapping official polygons within the existing AOI. Preserve holes and multipart geometry (split into separately identified Polygon features). Record ambiguous overlaps instead of duplicating buildings. Official zero heights are missing values, not zero-height buildings. Keep source and date per feature. Do not flatten terrain or raise buildings to fabricate survey accuracy.
|
||||
|
||||
Execution: in the existing workspace (no Git repository); Python preprocessing only adds pyshp/shapely to the existing local GIS tools. Browser dependencies remain unchanged.
|
||||
|
||||
- [x] Extract all three EPSG:5186 shapefiles with a streaming regional filter; verify height/levels against the official column workbook.
|
||||
- [x] Add focused matching tests: same building, missing height, non-overlap, ambiguous overlap and multipart/holes; run red then green.
|
||||
- [x] Generate merged GeoJSON and a source/audit manifest, preserving the OSM regeneration path.
|
||||
- [x] Check burial against rendered DSM, choose only a defensible placement correction, and document residual uncertainty.
|
||||
- [x] Update source labels and disclosures; run Python tests, npm test and build; inspect daytime city in browser and validate footprint distribution/overlaps computationally.
|
||||
|
||||
Acceptance: unique source IDs, finite valid polygons, no newly introduced overlapping buildings, provenance for numeric heights, counts reproducible from the supplied ZIP. Visual improvement alone does not establish survey accuracy. Footprint acquisition dates and unresolved conflicts remain visible in the source report.
|
||||
|
||||
Results: 6,692 polygons (5,052 new); height 1,771 / levels 894 / fallback 4,027. Six Python tests and 43 app tests pass; build passes with the existing large-bundle warning. Independent review found competing official matches and the missing-height/existing-level case; both reproduced and fixed with tests. Conflicting field values are logged, not selected by ID. Existing floor counts remain unchanged when a missing numeric height is added.
|
||||
|
||||
Decision: no ground-placement modification. The source contains object height, not surveyed foundation elevation. Moving buildings upward or flattening DSM would trade the user's accuracy priority for an unverified visual fix. Burial remains a documented limitation. Browser daytime overview confirms increased density; no claim of field validation or universal map completeness.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Immersive UI Implementation Plan
|
||||
|
||||
> **For agentic workers:** Use superpowers:executing-plans for this approved UI change.
|
||||
|
||||
**Goal:** Show only scene/action buttons by default on mobile and desktop; open all information and controls in popups; rename visible prayer copy to 소원빌기.
|
||||
|
||||
**Architecture:** Preserve existing element IDs and controllers. One native modal dialog switches between existing feature sections, providing browser focus containment and inert background. The minimap retains its renderer inside a separately toggled wrapper.
|
||||
|
||||
**Tech Stack:** Existing TypeScript, HTML, CSS, Vite; no new runtime dependencies.
|
||||
|
||||
**Spec:** User-approved immersive mockup and follow-up naming correction in this chat.
|
||||
|
||||
## Constraints and review focus
|
||||
- Preserve shared/solo viewing, weather, time and character logic; keep internal `pray` protocol/animation identifiers.
|
||||
- Default minimap hidden; optional pin through 지도·시점.
|
||||
- Cover 320px mobile, desktop and short landscape; safe-area spacing, touch targets, modal scrolling.
|
||||
- Opening dialogs stops background movement via canvas blur; modal keyboard input cannot trigger emotes.
|
||||
- Closing via button, backdrop or Escape restores opener focus; nested sections return to menu.
|
||||
- No Git metadata exists in this folder; edit in place, no worktree or commit.
|
||||
|
||||
## Tasks
|
||||
- [x] Browser acceptance check: default panels hidden, modal navigation/focus, time changes, map pinning, equipment and shared viewing accessible.
|
||||
- [x] Replace index.html HUD structure and src/style.css layout; retain functional IDs.
|
||||
- [x] Add src/ui/dialogs.ts initialization and cleanup in main.ts; rename visible world.ts strings.
|
||||
- [x] Populate sources with existing provenance links and bundled license notices.
|
||||
- [x] Run browser checks, existing tests and production build; inspect mobile/desktop screenshots.
|
||||
|
||||
## Verification log
|
||||
- Baseline npm test: local server tests hit sandbox EACCES on loopback; rerun with network-capable execution for valid results.
|
||||
- Final npm test: 102 passed, 1 skipped, 0 failed (103 total).
|
||||
- Final npm run build: passed; existing large-chunk advisory remains.
|
||||
- Chrome browser acceptance: passed at 390×844, 320×568, 844×390 and 1440×900. Inspected mobile, desktop and popup screenshots. Mobile checks are browser emulation, not physical iOS/Android devices.
|
||||
- Verified native modal background isolation, Tab containment, Escape/backdrop/button closing, focus restore, KST edits, map toggle, existing menu controls, wish animation and license URL.
|
||||
- Independent read-only review: no actionable regression. Browser testing caught Tab navigation leaving the popup; explicit boundary wrap added and rechecked.
|
||||
- Concurrent lobby/entry changes were retained. Tests use the supported solo entry path; full live backend admission remains covered by existing server tests, not this UI browser check.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Mobile Rendering Implementation Plan
|
||||
|
||||
> Execute inline with superpowers:executing-plans; use measured evidence before enabling optimizations.
|
||||
|
||||
**Goal:** Improve mobile movement and rotation smoothness while preserving astronomy, movement, terrain and materials.
|
||||
**Architecture:** Add development-only profiling around the existing loop, then measure resolution and postprocessing separately. Keep production adaptive resolution independent of simulation updates.
|
||||
**Tech Stack:** Existing TypeScript, Three.js, Vite, Node tests. No new dependencies.
|
||||
**Spec:** User's 2026-10-30 mobile rendering work order and approved measurement-first approach in this chat.
|
||||
|
||||
## Constraints and review focus
|
||||
- Preserve solar calculations, movement speed, camera direction and spatial geometry.
|
||||
- No unmeasured LOD, mesh batching, texture compression or shadow system changes.
|
||||
- Distinguish rendered frames from idle animation callbacks; aggregate all composer passes.
|
||||
- Exclude hidden/loading/resize transitions from quality decisions without filtering ordinary slow frames.
|
||||
- Test hysteresis, idle gaps and resolution bounds; do not label desktop viewport tests as device validation.
|
||||
- No Git repository exists here: preserve original touched source in `.tmp/mobile-performance/`; no worktree or commit possible.
|
||||
|
||||
## Tasks
|
||||
- [ ] 1. Add development profiling and fixed-condition replay. Save baseline FPS/mean/p95/slow frames, whole-frame draw calls, triangles, textures, geometries, viewport, lights and passes. Scenarios: static, rotation, movement, dense view, 300-second endurance. Tests verify percentile aggregation.
|
||||
- [ ] 2. Measure DPR 2, 1.5, 1.25 and postprocessing separately with identical scene/time/weather/path. Retain only justified changes, preserving near PBR and silhouette. Save screenshots and raw JSON.
|
||||
- [ ] 3. Add adaptive resolution with 5-second slow windows, 20-second fast observation, 10-second settling and asymmetric thresholds. Default touch/mobile starting cap 1.5, desktop cap 2; actual frame evidence drives changes. Test thresholds, idle/hidden reset, and bounds.
|
||||
- [ ] 4. Run existing tests/build/solar checks and browser interactions; obtain independent code review. Record real device availability, five-minute results and unmet targets honestly.
|
||||
|
||||
## Ledger
|
||||
- Initial inspection: DPR 2; RenderPass + optional night bloom + OutputPass; no enabled shadows; building geometry already merged. Movement triggers solar/UI refresh and minimap redraw. These are candidates, not proven bottlenecks.
|
||||
- Real Android device discovery failed because ADB daemon could not start; device availability not confirmed. Safari not available through current browser inventory.
|
||||
- Task 1: development instrumentation complete; four provisional raw runs saved. Concurrent terrain edits invalidate cross-run source equivalence; these are observations, not a final baseline.
|
||||
- Ruling: user requested stopping the five-minute workload while other work runs. Stop all continuous render benchmarking; no passed endurance result or screenshot comparison is claimed.
|
||||
- Ruling: because matched resolution experiments cannot yet be completed and shared scene sources are changing, keep automatic resolution DEV-only behind `?profile&quality=adaptive`. Production defaults stay unchanged. Task 2 and production activation in Task 3 are deferred pending matched evidence, not marked complete.
|
||||
- Task 3 prototype: statistics and adaptive controller tests pass. Slow/healthy hysteresis, idle and visibility/texture/resize resets are wired only through developer diagnostics.
|
||||
- Validation: full suite 68 pass, production build passes (size warning), solar numerical comparison passes. Initial localhost test EACCES resolved by rerunning with local networking permitted.
|
||||
- Final review fixes: reset adaptive quality at benchmark start, collect inventory before samples, abort external resize. Restart regression test failed before implementation and passes afterward. Final suite 69/69; build passes. No additional GPU workload; browser viewport override reset.
|
||||
@@ -0,0 +1,54 @@
|
||||
# 관악산 공동 방 구현 계획
|
||||
|
||||
> 실행: 현재 세션에서 구현하고 서버 작업은 독립 파일 범위로 병렬 진행한다. 사용자 요청에 따라 추가 승인 없이 구현한다.
|
||||
|
||||
**Goal:** 관악산 100명 방, 기존 양 캐릭터·장비 상태 공유, 서버 시각·날씨, MongoDB 접속 기록.
|
||||
**Architecture:** 단일 Node/Socket.IO 인스턴스가 `gwanaksan-001`을 관리한다. 장소와 방 ID를 분리하고 MongoDB에는 방문 세션만 저장한다.
|
||||
**Tech Stack:** 기존 TypeScript/Three.js + socket.io/socket.io-client + mongodb 공식 드라이버.
|
||||
**Spec:** `docs/superpowers/specs/2026-10-30-multiplayer-design.md`와 후속 사용자 합의. 현재 양 캐릭터/3인칭/우천 장비를 유지한다. DB는 `172.16.0.7:27017`, 인증은 환경변수로 설정한다.
|
||||
|
||||
## 제약 및 검토 항목
|
||||
|
||||
- 작업 폴더는 Git 저장소가 아니므로 worktree/commit 작업은 생략한다.
|
||||
- 장비는 메모리에만 저장하고 새 세션에서는 해제한다. 서버 기록에는 장비·이동을 저장하지 않는다.
|
||||
- DB 저장 대기 중 정원 예약, 중복 입장, 연결 종료 후 늦은 응답, DB 실패 시 정원 반환을 검증한다.
|
||||
- 재접속/혼자 보기 전환 시 이전 소켓 상태를 제거하고 장비·시각을 초기화한다.
|
||||
- 공통 형식은 `src/multiplayer/protocol.ts`, 상수는 `src/constants/multiplayer.ts`를 기준으로 한다.
|
||||
- 실제 호스팅 배포 및 1만 명 확장은 범위 밖이다.
|
||||
|
||||
## Task 1: 서버와 기록
|
||||
|
||||
- [x] `server/room.ts`, `server/app.ts`, `server/visits.ts`, `server/index.ts` 및 서버 테스트를 추가한다.
|
||||
- [x] 100명 정원/101번째 거절/동시 예약/비활성 장소/입력 검증 테스트를 먼저 실행해 실패 확인 후 구현한다.
|
||||
- [x] Node HTTP 정적 파일 제공, health, Socket.IO 및 DB 연결 종료를 구현한다.
|
||||
- [x] `join`, `player:update`, `player:reset`, `clock:sync` 요청과 `room:state`, `player:correction`, `weather:state` 응답을 공통 타입으로 연결한다.
|
||||
- [x] 기존 보행 검증과 날씨 조회를 재사용하고 DB 기록은 입장/마지막 확인/퇴장만 수행한다.
|
||||
|
||||
## Task 2: 클라이언트와 화면
|
||||
|
||||
- [x] `src/multiplayer/client.ts`, `remotePlayers.ts`, 시간 동기화를 추가한다.
|
||||
- [x] 서버 시각 고정과 원격 캐릭터 자원 공유·해제 테스트를 먼저 작성한다.
|
||||
- [x] `world.ts`에 로컬 상태 읽기/보정과 원격 표시를 추가하고 기존 캐릭터 모델을 복제한다.
|
||||
- [x] `main.ts`, `timeController.ts`, `index.html`에 공동/혼자 보기 전환과 연결/정원 상태를 표시한다.
|
||||
- [x] 공동 모드 날짜 조작 차단, 원본 지형/프로파일은 혼자 보기로 제한한다.
|
||||
|
||||
## Task 3: 통합과 실행
|
||||
|
||||
- [x] package scripts, Vite 프록시, 환경설정 예시, README 운영 명령을 추가한다.
|
||||
- [x] 실제 Socket.IO 100명/101번째 거절/퇴장/재입장/장비 스냅샷/시각을 검증한다.
|
||||
- [x] MongoDB는 테스트 전용 DB로 기록 확인하고 실제 서비스 데이터와 분리한다.
|
||||
- [x] 기존 테스트와 빌드, 5분 부하, 가능하면 브라우저 두 화면과 99 캐릭터 렌더링을 확인한다.
|
||||
- [x] 별도 코드 리뷰 후 중요한 문제를 수정하고 검증 한계를 보고한다.
|
||||
|
||||
## 진행 기록
|
||||
|
||||
- 계획 작성. 기존 타입과 공유 함수 재사용을 우선한다.
|
||||
- 서버/클라이언트 구현 완료. 기본 캐릭터 신규 제작 대신 기존 양 모델·장비를 재사용하고 GPU geometry/material을 공유한다.
|
||||
- 서버 테스트와 프런트 상태/시간/자원 테스트 RED→GREEN 확인. 전체 103개 중 102 통과, 기존 1개 skip.
|
||||
- MongoDB 임시 DB에서 입장/heartbeat/종료 기록 검증 통과. 시작 시 기존 기록 일괄 복구는 자동 승인 검토 거절로 제거했고, 임시 DB에서 기존 열린 기록 보존 RED→GREEN 확인.
|
||||
- 별도 리뷰의 DB 실패 응답 지연 문제 수정: 정리 작업을 기다리기 전에 오류 응답, 재현 테스트 RED→GREEN.
|
||||
- 100개 클라이언트 5분 10Hz 부하, 101번째 거절/자리 재사용 통과. 결과 artifacts/multiplayer/load-report.json.
|
||||
- 두 실제 브라우저 상호 표시/이동, 공동 시각 잠금, 혼자 보기/재입장 확인. 별도 시작 화면/HUD 변경이 반영된 최신 UI에서 서버/채널 선택 후 입장도 확인.
|
||||
- 99명 가상 참여자+실제 브라우저 1개로 100/100명 표시와 양·우산·비옷 렌더링 확인. 정량 GPU 프레임/모바일 성능은 미측정.
|
||||
- 최종 npm test: 102 pass, 1 skip, 0 fail. npm run build: 프런트/서버 타입 검사 및 운영 빌드 통과. 기존 대형 JS 청크 경고 유지.
|
||||
- 기존 미종료 기록의 자동 갱신을 제거한 결정: 강제 종료 시 leftAt은 미확정으로 남고 lastSeenAt으로 마지막 확인 시각만 알 수 있다. 장비 영구 저장/다중 인스턴스/외부 배포는 요청 범위 밖이다.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Prebuilt buildings implementation plan
|
||||
|
||||
**Goal:** Load precomputed building geometry and illumination without changing appearance.
|
||||
**Architecture:** Keep the existing generator for offline preparation and experiments. Share material/shader/update logic with a small binary loader. Store exact typed arrays, indices, light pixels, bounds, counts and target; reject stale artifacts at build time using source hashes. No new dependencies or geometry compression.
|
||||
|
||||
- [x] Add equivalence and loader tests, confirm failure before implementation.
|
||||
- [x] Extract shared scene and light-texture restoration; add binary loading and offline generation.
|
||||
- [x] Switch main/world to baked assets; preserve experiment generator API.
|
||||
- [x] Generate assets, check exact equivalence and stale-file rejection, run tests/build and preview.
|
||||
- [x] Document regeneration and measured sizes, review resource handling.
|
||||
|
||||
Verification: original and restored position/normal/light-height/index arrays, bounding volumes, counts, target, material colors, spill pixels/bounds and night/dawn/day intensity match exactly. Browser midnight/noon views render with no console errors. 44 tests pass; production build passes with the existing bundle-size warning. Independent review found source hash sensitivity to checkout line endings; normalized CRLF to LF for text inputs and verified both remain fresh while changed content is rejected.
|
||||
|
||||
Output: 10,028,176-byte binary (8,979,600 geometry + 1,048,576 light pixels) plus JSON metadata. Local Node assembly-only timings across 3 runs: generation 571.72–786.20ms, restoration 0.14–0.40ms; excludes download, parsing, GPU upload, other scene work. No total-load-time or FPS claim.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Rain and snow
|
||||
|
||||
User requested actual falling rain and snow with intensity driven by the selected time's weather. Extend the existing weather controller and Three.js world without dependencies. Use rain plus showers in forecast/history and rain in reanalysis; snowfall remains cm. Accumulations represent the preceding hour, unlike interpolated cloud/visibility. Missing amounts stay unknown and produce no particles.
|
||||
|
||||
Implementation: bounded camera-local rain line segments and soft snow points; animate only active precipitation, scale density and rain streak strength with amount, preserve scene depth, clip at the sampled terrain/sea surface. Reuse weather transitions and render invalidation. No accumulation, surface wetness, measured wind, or roof collisions.
|
||||
|
||||
- [x] Extend weather request/parser/sampling and focused tests.
|
||||
- [x] Add precipitation constants and renderer, connect active camera/update/disposal.
|
||||
- [x] Show hourly rain/snow amounts, update documentation.
|
||||
- [x] Run focused/full tests, build, and browser rain/snow/mixed/dry checks.
|
||||
|
||||
No Git metadata exists in this workspace. Work in place, preserving unrelated edits.
|
||||
|
||||
Verification: all 79 tests pass, including the localhost HTTP test when run with network access. Production build passes with the existing chunk-size warning. Actual forecast, historical forecast and reanalysis requests returned HTTP 200 with rain in mm and snow in cm. Review identified transient false mixed precipitation when switching types; absent types now stop immediately while positive intensities ease in.
|
||||
|
||||
Browser verification with controlled API responses: heavy rain (15 mm/h, 3,535 streaks), light rain (0.2 mm/h, 408 streaks), snow (2 cm/h, 1,555 flakes), mixed rain/snow, dry, and unavailable precipitation all passed without page or WebGL console errors. The rain-to-snow assertion reproduced the review finding before the fix and passed afterward. Screenshots and the reusable local QA runner are in `.tmp/`. UI dates were frozen at noon KST; API fixtures are test data, not actual weather claims.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Red sheep implementation plan
|
||||
|
||||
Goal: implement the approved four-color sheep using Blender MCP, with a rear third-person camera, waving, and sunrise prayer.
|
||||
|
||||
Architecture: keep the existing observer camera as the walking/astronomy anchor; render through a separate following camera. Export a hierarchical GLB from a dedicated Blender scene. Animate named limb pivots in Three.js without adding dependencies.
|
||||
|
||||
Design: approved concept in the conversation. Three-head proportions, vermilion wool, cream face, dark horns/outline and gold sunrise emblem. Existing terrain and walking constraints remain authoritative. Prayer window is an implementation default: inclusive five minutes before/after computed sunrise, based on the selected simulation time. Movement cancels emotes. Prayer faces the sun; wave lasts three seconds.
|
||||
|
||||
- [x] Create reproducible Blender model script, inspect front/back, save .blend and export GLB.
|
||||
- [x] Add focused tests for prayer timing, animation reset and following-camera behavior.
|
||||
- [x] Implement constants, character loader/poses and following camera; integrate world lifecycle and controls.
|
||||
- [x] Add accessible buttons and keyboard shortcuts (1 wave, 2 prayer), including disabled-state explanation.
|
||||
- [ ] Run targeted tests, full tests, build, and visual checks. Record limitations.
|
||||
|
||||
Review focus: movement cancels poses; time changes end unavailable prayer; overview does not move player; camera clears terrain; asset failures are visible; disposal releases resources.
|
||||
|
||||
Verification: build and four feature tests pass. Full suite 74/75, pre-existing summit walking boundary assertion fails. Blender views inspected; in-game browser verification blocked by Windows browser URL detection. See docs/red-sheep.md. Independent review found stale overview buttons and walking-to-idle render invalidation; both corrected.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Summit performance optimization
|
||||
1. Measure the current summit overlay construction and profile continuous rendering at the same camera.
|
||||
2. Extract pure geometry generation into an offline builder. Bake indexed geometry and material distances into a compressed binary asset, with source-hash freshness checks.
|
||||
3. Load the asset alongside existing assets; runtime creates typed-array views, without recursive geometry generation or distance queries.
|
||||
4. Reuse identical vertices without changing triangles. Evaluate single-surface regions without sampling unused material textures, keeping boundary blends unchanged.
|
||||
5. Verify attribute equivalence, decoded binary validity, stale checks, walking tests, production build, and before/after browser rendering. Report measured construction timings separately from full startup or GPU FPS.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Panorama-based summit terrain implementation
|
||||
|
||||
Goal: implement the user's authorized estimated panorama start and local 100–200m terrain correction. This is a visual reconstruction, not surveyed terrain.
|
||||
|
||||
Design: retain the original terrain origin, binaries and building assets. Estimate a camera on the summit-side rocks northeast of the radar at 37.44472, 126.96422. Photos show a saddle between observer and elevated radar rock, with the eastern valley to the left when looking south; the supplied contour screenshot only constrains broad ridge/valley layout. It does not provide control elevations. All local target elevations are artistic estimates, including the 620m observer ground. The blog coordinate is not used as a measured camera position.
|
||||
|
||||
Approaches: replacing the DSM needs unavailable high-resolution data; raising the entire area loses the saddle. Selected: sparse estimated elevation controls, smooth residual interpolation, full influence through 100m and zero at 200m. Keep a same-start original-terrain comparison. No new dependencies.
|
||||
|
||||
## Tasks
|
||||
- [x] Add failing behavioral tests: unchanged vertices outside 200m, radar visible above eye level across the saddle, walking ray matches corrected geometry, original data unchanged. Smooth boundary uses smoothstep falloff; not separately measured by a test.
|
||||
- [x] Add feature constants and a local geometry correction. Reuse coordinate conversion and triangle height sampling.
|
||||
- [x] Integrate estimated start/reset and initial southward view, corrected contours and structure foundations, original comparison and explicit labels/source notes.
|
||||
- [x] Inspect both modes, reset, first drag and contour zoom in browser. Original at same camera coordinate: 604.1m, reconstructed: 620.0m. Screenshot saved in data/backups/summit-terrain-20261030/corrected-preview.jpg.
|
||||
|
||||
Review: independent reviewer found first south-facing drag reinterpreted XYZ Euler angles as YXZ. Added failing real-handler test; changed rotation.order assignment to rotation.reorder, test passed. Native pointer lock unavailable in embedded browser; drag fallback checked. Initial heading adjusted from173° to185° to keep radar clear of the right information panel.
|
||||
|
||||
Verification: full suite 47/47 before camera fix, followed by targeted camera test RED→GREEN. Final full suite48/48, building asset check, TypeScript and Vite build passed. Existing Vite >500kB bundle warning remains.
|
||||
|
||||
Review focus: original assets preserved; raw mode at same start; only Gwanaksan patched; startup/reset/eye height use same surface; estimated data never presented as measured. No Git repository is present, so worktree/commits are unavailable. Existing files are backed up before editing.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Open-Meteo weather integration
|
||||
|
||||
Goal: Synchronize cloud cover and visibility to the selected instant, and smoothly attenuate sunlight in cloudy or hazy conditions.
|
||||
|
||||
Design: Extend the existing TypeScript/Three.js app without dependencies or a backend. Fetch two UTC days of hourly data and interpolate by Unix timestamp (the UI remains in Asia/Seoul). Use forecasts near today, historical forecasts from 2022, and cloud-only reanalysis from 1940. Display missing data explicitly; do not substitute today's weather for another date. Cache requests, bound memory, ignore obsolete responses, and clean up requests on disposal. Weather remains representative of the site's fixed coordinates, not every walking step.
|
||||
|
||||
Rendering: Reuse Sky cloud uniforms, terrain fog and the separate sun shader. Apply an explicitly approximate cloud/haze transmission to disc, halo and directional light. Smooth transitions over frames without changing solar geometry. This is weather-conditioned visualization, not directional cloud reconstruction.
|
||||
|
||||
## Tasks
|
||||
- [x] Add tests for date routing, UTC/KST selection, interpolation/missing data, cache/races, and attenuation.
|
||||
- [x] Add feature constants and a small weather data/controller module.
|
||||
- [x] Connect weather to the sky/sun/fog and add status/attribution in the location panel (avoids overlap with the bottom controls).
|
||||
- [x] Run focused tests, all tests, build, and browser checks where available. Document API/network limitations.
|
||||
|
||||
No Git metadata is present in this workspace; implementation remains in the current directory. The user's follow-up authorizes the Open-Meteo approach described in the preceding investigation.
|
||||
|
||||
Verification: seven focused weather tests and all 57 current tests pass; production build passes with the existing bundle-size warning. The localhost HTTP test needed network permission. Live requests to all three API endpoints returned HTTP 200 with CORS `*`. Browser verified live values, October 29 hourly scrubbing (100% cloud), 2000 reanalysis with missing visibility, and 2099 unavailable fallback. A review found missing CUSTOM-mode retries; reproduced with a failing test and fixed with a disposable expiry timer. Browser QA found native fetch receiver binding; reproduced, fixed and tested. Concurrent workspace changes to walking/vegetation were preserved.
|
||||
@@ -0,0 +1,80 @@
|
||||
# 관악산 100명 공동 해돋이 설계
|
||||
|
||||
상태: 초기 설계 기록. 후속 합의에 따라 Node.js/Socket.IO, 기존 양 캐릭터 및 우천 장비 공유, MongoDB 접속 기록으로 구현했다. 최신 동작과 검증 결과는 `docs/multiplayer-server.md`를 참고한다. 서버 시작 시 기존 기록 일괄 갱신은 수행하지 않는다.
|
||||
|
||||
## 목적과 합의
|
||||
|
||||
관악산 한 공간에서 최대 100명이 각자 이동하며 다른 참여자의 캐릭터를 본다. Socket.IO를 사용한다. 공동 모드는 서버 기준 실제 현재 시각으로 고정하고, 날짜·시간 변경은 혼자 보기에서만 제공한다. 하조대·기사문은 비활성 상태를 유지하되 장소 추가에 공통 통신 로직을 재사용한다.
|
||||
|
||||
초기 게스트 접속과 메모리 관리를 제안한 대화에 이어 작성한 설계다. 아래 구체적 기본값은 구현자 제안이며 사용자가 각각 선택한 사항은 아니다.
|
||||
|
||||
## Approaches
|
||||
|
||||
- 선택한 통신: Node.js 단일 프로세스 + Socket.IO. room과 연결 수명 관리 기능을 활용한다.
|
||||
- 대안: 직접 WebSocket 사용. 전송 계층은 단순하지만 입퇴장·재연결 프로토콜을 더 직접 구현해야 하므로 선택하지 않았다.
|
||||
- 저장 제안: 참여자 Map과 장소별 상태를 서버 메모리에 유지한다. 영구 프로필이 없는 초기 범위에는 PostgreSQL을 도입하지 않는다. Redis와 다중 서버는 확장 요구가 생길 때 검토한다.
|
||||
|
||||
## 현재 코드와 재사용
|
||||
|
||||
- `src/constants/locations.ts`: 세 장소 정의와 관악산만 활성인 설정이 이미 있다. 장소 ID와 활성 상태는 서버와 클라이언트의 단일 출처로 유지한다.
|
||||
- `src/ui/timeController.ts`: LIVE가 로컬 Date를 읽는다. 시각 공급 함수와 공동 모드 잠금을 추가해 재사용한다.
|
||||
- `src/scene/world.ts`: 카메라 이동, 지면 높이 보정, 렌더 무효화, 조감 카메라, 시작점 복귀를 관리한다. 네트워크에는 관측용 조감 카메라가 아닌 실제 보행자 상태만 노출한다.
|
||||
- `src/controls/summitWalking.ts`: 기존 보행 가능 영역 검증을 재사용한다. 순수 계산에 필요한 의존성만 최소한으로 분리하고 서버에서 렌더러를 로드하지 않는다.
|
||||
- 속도는 `src/constants/movement.ts`의 기존 4m/s, 빠른 이동 20m/s를 기준으로 한다.
|
||||
|
||||
## 구성과 저장 경계
|
||||
|
||||
`server/`에 서버 시작과 입퇴장·상태 처리를 둔다. `src/multiplayer/`에 연결과 원격 캐릭터 처리를 둔다. 공유 이벤트 타입은 브라우저 전용 의존성이 없는 모듈에 둔다. 통신 주기·정원·보간 등 기능 상수는 `constants/`에 둔다. 세부 파일 분할은 구현자가 기존 패턴에 맞춰 결정한다.
|
||||
|
||||
서버는 플레이어 ID, 닉네임, 기본 외형 색상, 장소 ID, x/z 위치, 방향, 마지막 갱신 시각을 메모리에 저장한다. ID와 색상은 서버가 발급하며 클라이언트가 다른 사람의 ID를 지정할 수 없다. 좌표는 기존 장면의 미터 좌표계를 사용한다. 지면 y는 각 브라우저가 같은 지형에서 계산한다.
|
||||
|
||||
닉네임은 기본 자동 이름을 제공하고 입장 전 짧은 일반 텍스트 입력을 허용한다. 길이와 자료형을 검증하며 HTML로 렌더링하지 않는다. 로그인, 계정, 영구 외형 저장, 방문 기록, 채팅·음성, 결제는 범위 밖이다.
|
||||
|
||||
## 입장과 연결 수명
|
||||
|
||||
기본은 공동 보기이며 명시적 혼자 보기 전환을 제공한다. 장면 로딩이 끝난 후 입장을 요청한다. 관악산 room의 정원은 참여자 100명이며 자기 자신을 포함한다. 초기에는 연결 한 개를 참여자 한 명으로 취급하므로 여러 탭은 각각 정원을 사용한다.
|
||||
|
||||
서버는 장소 활성 상태와 정원을 확인하고 같은 동기 처리 구간에서 자리를 배정한다. 중복 입장은 멱등적으로 처리한다. 정원 초과나 비활성 장소 요청은 명확한 오류 코드로 거절한다. 101번째 참여자는 자동으로 다른 방에 배정하지 않으며 혼자 보기를 선택할 수 있다.
|
||||
|
||||
입장 응답에는 자신의 상태, 현재 참여자 전체 목록, 서버 시각을 포함한다. 서버가 Socket.IO 연결 종료를 감지하면 참여자를 제거하고 다른 사람들에게 퇴장을 전달한다. heartbeat 감지 전까지는 정원을 유지할 수 있다. 복구 시 이전 연결을 그대로 복원하려 하지 않고 새 입장과 전체 상태를 받는다. 중복 캐릭터와 이전 연결의 늦은 이벤트는 세션 경계에서 정리한다.
|
||||
|
||||
서버 재시작이나 재입장 시 기본 시작 위치로 돌아간다. 서버 장애 중에는 연결 끊김을 표시하고 원격 캐릭터를 정리한다. 공동 연결을 성공한 것으로 표시하지 않는다. 혼자 보기 전환 시 소켓을 끊고 정원을 반환한다.
|
||||
|
||||
## 이동 및 표시
|
||||
|
||||
로컬 보행은 기존 즉각 반응을 유지한다. 이동 상태는 최대 초당 10회 전송하고 서버는 검증된 변경분을 초당 10회 모아서 같은 장소에 방송한다. 정지 시 불필요한 상태 전송을 줄인다. 초기 전체 상태 및 주기적 전체 동기화로 상태를 수렴시킨다. 서버 시퀀스 번호로 오래된 업데이트를 무시한다.
|
||||
|
||||
서버는 유한한 수, 좌표 범위, 이동 속도와 경과 시간, 보행 가능 경로, 전송 빈도와 패킷 크기를 검증한다. 긴 비활성 기간이 순간 이동 허용으로 이어지지 않도록 이동 허용 시간은 제한한다. 거절 시 승인된 위치로 보정한다. 시작점 복귀는 별도 서버 명령으로 처리한다. 완전한 서버 물리 시뮬레이션이나 경쟁 게임 수준의 부정행위 방지는 범위 밖이다.
|
||||
|
||||
기본 캐릭터는 외부 모델 없이 가벼운 머리·몸통 형태와 색상으로 구분한다. 1인칭 자신의 몸은 표시하지 않는다. 다른 캐릭터는 약 100~200ms 보간 지연으로 표시한다. 원격 이동 및 보간 중에는 기존 렌더 무효화 흐름을 갱신한다. 공유 geometry/material을 사용하며 종료 시 자원을 해제한다. 캐릭터끼리 충돌하지 않고 시작 위치 겹침을 허용한다.
|
||||
|
||||
## 시간·장면·날씨
|
||||
|
||||
서버의 UTC epoch 시각을 요청/응답으로 받고 왕복 시간의 절반을 추정해 기준을 보정한다. 수신 후에는 monotonic 경과 시간으로 진행하고 주기적으로 재동기화한다. 네트워크 지연 때문에 완전한 프레임 일치는 보장하지 않는다. 표시 시간대는 기존 Asia/Seoul을 유지한다.
|
||||
|
||||
공동 모드에서는 날짜·시간 입력과 CUSTOM 전환을 UI 및 제어 로직에서 잠근다. 혼자 보기에서는 기존 LIVE/CUSTOM 동작을 보존한다. 공동 보기 복귀 시 현재 서버 시각과 새 입장 상태를 적용한다.
|
||||
|
||||
공동 모드는 기존 사진 기반 보정 지형 하나로 고정한다. 원본 지형 비교와 개발 프로파일 모드는 혼자 보기로 한정해 서로 다른 지면과 시간이 같은 room에 섞이지 않도록 한다. 기존 조감 기능은 유지하되 보행자 위치를 바꾸지 않는다.
|
||||
|
||||
공동 날씨는 서버가 관악산의 기존 기상 조회·캐시 로직을 재사용해 같은 자료와 상태를 배포한다. 브라우저 UI 제어와 순수 조회/보간 로직만 필요한 만큼 분리한다. 실패 시 모두 같은 기본 하늘 상태와 안내를 받는다. 혼자 보기의 과거 날짜 기상 조회는 기존 동작을 유지한다.
|
||||
|
||||
## 장소 확장
|
||||
|
||||
room 이름은 장소 ID로 정한다. 추가 장소에는 활성화 여부, 좌표 원점, 시작점, 보행 검증 정책을 연결한다. 장소별 분기는 입장/이동 이벤트 전체를 복제하지 않고 장소 설정과 지형별 정책에 한정한다. 하조대·기사문 활성화 및 실제 멀티플레이 검증은 후속 작업이다.
|
||||
|
||||
## 실행과 배포 범위
|
||||
|
||||
개발에서는 Vite가 `/socket.io`를 Node 서버로 프록시한다. 운영 실행에서는 Node 서버가 빌드된 정적 파일과 Socket.IO를 함께 제공하는 단일 인스턴스 구성을 기본으로 한다. 정적 파일 경로는 배포 루트 안으로 제한하고 출처와 입력 제한을 적용한다. 인터넷 서비스에서는 TLS 지원 프록시 뒤에서 실행할 수 있게 환경변수로 포트와 허용 출처를 설정한다.
|
||||
|
||||
이번 범위는 실행 가능한 서버 코드, 운영 빌드 실행 명령과 설명, 로컬 검증이다. 호스팅 계정·도메인·서버 사양을 정하거나 외부에 실제 배포하는 것은 포함하지 않는다. Git 저장소가 아닌 현재 작업 폴더에서는 설계 파일만 저장하고 커밋하지 않는다.
|
||||
|
||||
## 성공 기준과 검증
|
||||
|
||||
1. 100개 Socket.IO 클라이언트를 관악산에 동시에 입장시키고 101번째 거절, 한 명 퇴장 후 새 입장을 검증한다. 중복 입장과 비활성 장소 요청도 확인한다.
|
||||
2. 100명이 10Hz 이동하는 5분 로컬 부하 시나리오에서 참여자 상태 일관성, 프로세스 안정성, 이벤트 지연, CPU·메모리 추이를 기록한다. 지연 p95 200ms 이하를 초기 로컬 목표로 삼되 결과와 하드웨어를 함께 보고하고 운영 성능 보장으로 표현하지 않는다.
|
||||
3. 잘못된 좌표·과도한 속도·빈도, 시작점 복귀, 연결 종료/재접속, 정원 해제를 집중 검사한다.
|
||||
4. 실제 브라우저 두 개에서 상호 입퇴장과 이동을 보고, 공동 시간 잠금·서버 시각 동기화 및 혼자 보기 복귀를 확인한다.
|
||||
5. 99개 원격 캐릭터를 표시하는 브라우저 시나리오에서 프레임 시간과 자원 사용을 측정한다. 서버 부하 테스트와 렌더링 검증을 구분해 보고한다.
|
||||
6. 기존 테스트와 TypeScript/운영 빌드를 수행한다. 기존 지형·천문 계산 동작은 유지한다.
|
||||
|
||||
세부 UI 배치, 색상 팔레트, 검증 허용 오차와 보간 조정은 구현자가 결정하고 측정으로 보완한다. 검증하지 못한 운영환경·모바일 성능은 명시한다.
|
||||
@@ -0,0 +1,15 @@
|
||||
# WASD 관측 위치 탐색
|
||||
|
||||
- 화면 클릭 후 W/S 전진·후진, A/D 좌우 이동. 시선의 수평 방향 기준.
|
||||
- 4m/s, Shift를 함께 누르면 20m/s. 대각선도 같은 속도.
|
||||
- 마우스 잠금 지원 시 자유 회전. 잠금이 불가능한 내장 브라우저에서는 왼쪽 버튼 드래그로 회전하고 WASD 이동 가능.
|
||||
- ESC, 창/탭 전환, 입력창으로 포커스 이동 시 눌린 이동 키를 해제한다. 날짜 입력 중에는 이동하지 않는다.
|
||||
- '과천 방향'으로 시선을 돌린 후 화면을 클릭하고 Shift+W로 약 7초 이동하면 현재 데이터 기준 약 140m 지점에서 아파트 군집을 볼 수 있다. 시간은 낮 12시로 설정하면 확인하기 쉽다.
|
||||
- 시작점 복귀는 좌표와 동쪽 시선을 복원하고 선택한 날짜·시각 및 눈높이를 유지한다.
|
||||
- 조감 화면에서는 WASD 정지. 화면 클릭 시 기존 관측 위치로 복귀하고 탐색을 시작한다.
|
||||
|
||||
지면은 렌더링되는 DSM 삼각형과 동일한 보간 및 지구 곡률 보정을 사용한다. 매 프레임 지면 높이 + 선택한 눈높이로 카메라를 배치한다. 단순 지형 탐색이므로 절벽·급경사 제한, 중력·점프, 건물/난간 충돌, 데크 위 보행은 미구현이다. DSM 외곽에서 이동을 제한한다.
|
||||
|
||||
이동한 위치의 경위도와 표면 표고를 천문 계산에 반영한다. CUSTOM 시간은 멈춘 채 유지하며 LIVE는 계속 현재 시각을 따른다. 경위도 환산은 기존 구조물 좌표계와 같은 WGS84 국소 접평면 근사다. 근거리 탐색용으로, 장거리 이동의 정밀 측량 좌표를 보장하지 않는다.
|
||||
|
||||
검증: 지형 삼각형과 발 위치 일치, 수평 이동·대각선 정규화, 프레임 지연 시 이동 제한, 영역 경계, 좌표 왕복 테스트. 브라우저에서 140m 이동·건물 조망·눈높이 변경·시작점 복귀와 CUSTOM 시간 유지 확인.
|
||||
@@ -0,0 +1,152 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#101823" />
|
||||
<meta name="description" content="함께 떠나는 온라인 해돋이, 해보러가기" />
|
||||
<title>해보러가기</title>
|
||||
<link rel="preload" href="/fonts/GowunDodum-Title.ttf" as="font" type="font/ttf" crossorigin />
|
||||
</head>
|
||||
<body class="in-lobby">
|
||||
<section id="lobby" aria-label="해보러가기 시작 화면">
|
||||
<canvas id="lobby-sunrise" aria-hidden="true"></canvas>
|
||||
<div class="lobby-content">
|
||||
<h1 class="lobby-brand" aria-label="해 보러가기"><img src="/sunrise-mark.svg" alt="" />보러가기</h1>
|
||||
<div class="lobby-panel">
|
||||
<h2 id="lobby-title" tabindex="-1" hidden></h2>
|
||||
<p id="lobby-status" role="status" aria-live="polite" hidden></p>
|
||||
<div id="lobby-options"></div>
|
||||
<button id="lobby-back" type="button" hidden>뒤로</button>
|
||||
<button id="lobby-retry" type="button" hidden>다시 시도</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<svg class="icon-definitions" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="icon-place" viewBox="0 0 24 24"><path d="M20 10c0 6-8 12-8 12S4 16 4 10a8 8 0 1 1 16 0Z"/><circle cx="12" cy="10" r="2.5"/></symbol>
|
||||
<symbol id="icon-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="icon-menu" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"/></symbol>
|
||||
<symbol id="icon-wave" viewBox="0 0 24 24"><path d="M8 12V5a1.5 1.5 0 0 1 3 0v6-8a1.5 1.5 0 0 1 3 0v8-6a1.5 1.5 0 0 1 3 0v7-3a1.5 1.5 0 0 1 3 0v6a7 7 0 0 1-12 5l-5-6a1.5 1.5 0 0 1 2-2l3 3M3 4 1 2M2 8H0"/></symbol>
|
||||
<symbol id="icon-sunrise" viewBox="0 0 24 24"><path d="M2 18h20M4 22h16M7 18a5 5 0 0 1 10 0M12 3v6M9 6l3-3 3 3M3 11l2 2M21 11l-2 2"/></symbol>
|
||||
<symbol id="icon-umbrella" viewBox="0 0 24 24"><path d="M2 12a10 10 0 0 1 20 0H2Zm10 0v7a2 2 0 0 0 4 0M12 2V1"/></symbol>
|
||||
<symbol id="icon-raincoat" viewBox="0 0 24 24"><path d="m8 3 4 3 4-3 5 5-3 4-2-2v11H8V10l-2 2-3-4 5-5ZM12 6v15M9 16h1M14 16h1"/></symbol>
|
||||
<symbol id="icon-hood" viewBox="0 0 24 24"><path d="M4 15V10a8 8 0 0 1 16 0v5l-3 6H7l-3-6Z"/><path d="M8 15V10a4 4 0 0 1 8 0v5l-4 3-4-3ZM10 18v4M14 18v4"/></symbol>
|
||||
</svg>
|
||||
<canvas id="world" aria-label="태양 관측 3D 풍경"></canvas>
|
||||
<main class="hud" aria-label="관측 조작">
|
||||
<div id="compass" role="img" aria-label="현재 시선 방위">
|
||||
<div class="compass-scale" aria-hidden="true"></div>
|
||||
<span class="compass-pointer" aria-hidden="true"></span>
|
||||
<span class="compass-bearing" aria-hidden="true"></span>
|
||||
</div>
|
||||
<nav class="scene-toolbar" aria-label="화면 메뉴">
|
||||
<div class="location-menu">
|
||||
<button type="button" id="location-toggle" aria-expanded="false" aria-controls="location-options"><svg class="icon" aria-hidden="true"><use href="#icon-place"/></svg><span id="current-location">관악산 정상</span></button>
|
||||
<div id="location-options" hidden>
|
||||
<span class="sr-only" id="site-label">GWANAKSAN</span><span class="sr-only" id="observer-name">관악산 정상</span>
|
||||
<nav class="site-navigation menu-list" aria-label="관측 장소">
|
||||
<a data-site="gwanaksan" href="?site=gwanaksan">관악산</a><a data-site="hajodae" href="?site=hajodae">하조대</a><a data-site="gisamun" href="?site=gisamun">기사문 해변</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-end">
|
||||
<button type="button" data-panel="menu" aria-label="메뉴" aria-haspopup="dialog" aria-controls="ui-dialog"><svg class="icon" aria-hidden="true"><use href="#icon-menu"/></svg></button>
|
||||
</div>
|
||||
</nav>
|
||||
<aside id="minimap-overlay" hidden>
|
||||
<section id="terrain-minimap" class="minimap" aria-label="등고선 미니맵" hidden>
|
||||
<button id="minimap-close" type="button" aria-label="미니맵 숨기기">×</button>
|
||||
<div class="minimap-map">
|
||||
<canvas aria-label="DSM 등고선 지도 · 화살표는 현재 위치와 방향" role="img"></canvas>
|
||||
<span class="minimap-direction north">N</span><span class="minimap-direction east">E</span><span class="minimap-direction south">S</span><span class="minimap-direction west">W</span>
|
||||
<div class="minimap-zoom"><button type="button" data-zoom="out" aria-label="미니맵 축소">−</button><button type="button" data-zoom="in" aria-label="미니맵 확대">+</button></div>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
<div id="solo-time-controls" aria-label="관측 시간">
|
||||
<div id="time-settings">
|
||||
<div class="date-row"><span id="mode" class="mode">LIVE</span><time id="selected-time"></time><span class="unit">KST</span></div>
|
||||
<fieldset id="time-controls" class="time-panel" aria-label="관측 시각 조절" disabled>
|
||||
<label class="date-field"><span class="date-label">관측 날짜 · KST</span><span class="date-picker"><svg class="icon" aria-hidden="true" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="16" rx="3"/><path d="M7 3v4M17 3v4M3 11h18"/></svg><span id="date-display" aria-hidden="true"></span><input id="date" type="date" min="1901-01-01" max="2099-12-31" aria-label="관측 날짜 · KST" /></span></label>
|
||||
<div class="slider-field"><div class="slider-label"><label for="time-slider">관측 시각</label><output id="time-output" for="time-slider">00:00:00</output></div>
|
||||
<input id="time-slider" type="range" min="0" max="86390" step="10" value="0" title="방향키로 10초씩 조절" />
|
||||
<div class="ticks" aria-hidden="true"><span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>23:59</span></div>
|
||||
</div>
|
||||
<button id="live-button" type="button" aria-pressed="true" title="현재 시각으로 돌아가기"><span class="live-dot" aria-hidden="true"></span>LIVE</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<div class="emote-controls" aria-label="캐릭터 액션">
|
||||
<button type="button" id="emote-wave" disabled aria-pressed="false" aria-describedby="emote-status" title="손 흔들기 · 단축키 1"><svg class="icon" aria-hidden="true"><use href="#icon-wave"/></svg>인사</button>
|
||||
<button type="button" id="emote-pray" disabled aria-pressed="false" aria-describedby="emote-status" title="소원빌기 · 일출 전후 5분 · 단축키 2"><svg class="icon" aria-hidden="true"><use href="#icon-sunrise"/></svg>소원빌기</button>
|
||||
<div class="equipment-menu">
|
||||
<button type="button" id="equipment-toggle" aria-expanded="false" aria-controls="equipment-options"><svg class="icon" aria-hidden="true"><use href="#icon-umbrella"/></svg>장비</button>
|
||||
<div id="equipment-options" hidden>
|
||||
<div class="equipment-controls" role="group" aria-label="우천 장비">
|
||||
<button type="button" id="gear-umbrella" disabled aria-pressed="false" aria-label="우산 쓰기" title="우산 쓰기 · 단축키 3" aria-describedby="gear-status"><svg class="icon" aria-hidden="true"><use href="#icon-umbrella"/></svg></button>
|
||||
<button type="button" id="gear-raincoat" disabled aria-pressed="false" aria-label="우비 입기" title="우비 입기 · 단축키 4" aria-describedby="gear-status"><svg class="icon" aria-hidden="true"><use href="#icon-raincoat"/></svg></button>
|
||||
<button type="button" id="gear-hood" disabled aria-pressed="false" aria-label="모자 쓰기" title="모자 쓰기 · 단축키 5" aria-describedby="gear-status"><svg class="icon" aria-hidden="true"><use href="#icon-hood"/></svg></button>
|
||||
</div>
|
||||
<p id="gear-status" class="sr-only" role="status">기상악화시 사용할 수 있습니다</p>
|
||||
</div>
|
||||
</div>
|
||||
<span id="emote-status" class="sr-only" role="status">양 캐릭터 불러오는 중…</span>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="equipment-notice" role="status" aria-live="polite" hidden><svg class="icon" aria-hidden="true"><use href="#icon-umbrella"/></svg><span></span></div>
|
||||
<dialog id="ui-dialog" aria-labelledby="dialog-title">
|
||||
<header class="dialog-header"><button type="button" id="dialog-back" aria-label="뒤로" hidden>←</button><h1 id="dialog-title">메뉴</h1><button type="button" data-close aria-label="닫기" autofocus>×</button></header>
|
||||
<div class="dialog-body">
|
||||
<section data-dialog-panel="menu" hidden>
|
||||
<p id="menu-current-mode" class="muted menu-current-mode">현재 함께 보기 중</p>
|
||||
<nav class="menu-list menu-primary" aria-label="주요 메뉴">
|
||||
<a href="./">메인으로 나가기 <span aria-hidden="true">↗</span></a>
|
||||
<button id="session-mode" type="button" data-scene-action>혼자 보기로 전환</button>
|
||||
</nav>
|
||||
<details class="menu-secondary"><summary>설정 및 정보</summary>
|
||||
<nav class="menu-list" aria-label="상세 메뉴">
|
||||
<button type="button" data-panel="information">관측 정보 <span aria-hidden="true">›</span></button>
|
||||
<button type="button" data-panel="view">지도·시점 <span aria-hidden="true">›</span></button>
|
||||
<button type="button" data-panel="session">닉네임·접속 상태 <span aria-hidden="true">›</span></button>
|
||||
<button type="button" data-panel="settings">설정·조작 방법 <span aria-hidden="true">›</span></button>
|
||||
<button type="button" data-panel="sources">출처·오픈소스 <span aria-hidden="true">›</span></button>
|
||||
</nav>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="information" data-dialog-panel="information" hidden>
|
||||
<div class="angles"><div><span class="label">방위각</span><strong id="azimuth">—</strong><span class="unit">북쪽 기준</span></div><div><span class="label">겉보기 고도각</span><strong id="altitude">—</strong><span id="sun-status" class="unit"></span></div></div>
|
||||
<div class="events"><span>일출 <b id="sunrise">—</b></span><span>일몰 <b id="sunset">—</b></span></div>
|
||||
<p class="muted">DSM 고도 보정 · 천문 시각 (능선 통과 시각 아님)<span id="standard-times"></span></p>
|
||||
<div class="weather-information"><h2>기상</h2><p id="weather-status" class="muted" role="status">선택 시각의 기상자료 불러오는 중</p><p class="muted">관측지점 대표값 · 구름·시정·비·눈은 시각적 근사</p><p class="muted"><a href="https://open-meteo.com/" target="_blank" rel="noopener">Weather data by Open-Meteo</a> · <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener">CC BY 4.0</a></p><p class="muted">운량·시정은 시간 보간하고, 강우량은 제공되는 비·소나기 강수량을 합산합니다. 하늘·강수 효과는 이 자료를 가공한 시각적 표현입니다.</p></div>
|
||||
<details><summary>관측점·지형 상태</summary><p id="observer-details" class="muted"></p><p id="terrain-status" class="muted" role="status">지형 불러오는 중…</p><p id="movement-status" class="muted">시작점에서 0m · 지형 따라 이동 · 구조물 충돌 미적용</p><p id="view-note" class="muted" role="status"></p></details>
|
||||
</section>
|
||||
<section data-dialog-panel="view" hidden>
|
||||
<label class="switch-row"><input id="minimap-toggle" type="checkbox" />미니맵을 화면에 계속 표시</label>
|
||||
<div class="structure-controls menu-list" aria-label="시점 선택">
|
||||
<button type="button" id="look-east" data-scene-action>동쪽 하늘</button><button type="button" id="look-city" data-scene-action>과천 방향</button><button type="button" id="reset-position" data-scene-action>시작점 복귀</button><button type="button" id="city-overview" data-scene-action>과천 건물 조감</button>
|
||||
<button type="button" data-look="summit-mast" data-scene-action>정상 통신탑</button><button type="button" data-look="summit-marker" data-scene-action>정상 표지석</button><button type="button" data-look="weather-radar" data-scene-action>레이돔</button><button type="button" data-look="north-deck" data-scene-action>북쪽 데크</button>
|
||||
</div>
|
||||
</section>
|
||||
<section data-dialog-panel="session" hidden>
|
||||
<div class="session-controls" aria-label="공동 관측 접속"><label for="nickname">닉네임</label><input id="nickname" type="text" maxlength="20" autocomplete="off" /><div class="button-row"><button id="retry-connection" type="button" hidden>다시 입장</button></div><p id="connection-status" class="muted" role="status" aria-live="polite">관악산 공동 방 준비 중…</p></div>
|
||||
</section>
|
||||
<section data-dialog-panel="settings" hidden>
|
||||
<div class="height-field"><div class="slider-label"><label for="eye-height">관측 기준 높이</label><output id="eye-height-output" for="eye-height"></output></div><input id="eye-height" type="range" aria-describedby="eye-height-note" /><p id="eye-height-note" class="muted">태양 계산 기준 · 카메라는 3인칭</p></div>
|
||||
<details open><summary>조작 방법</summary><p id="control-hint">터치 또는 우클릭한 곳으로 이동 · 드래그로 둘러보기</p><p class="muted">1 인사 · 2 소원빌기 · 3 우산 · 4 우비 · 5 모자</p><p class="muted">소원빌기는 선택 시각이 일출 전후 5분일 때 사용할 수 있습니다. 이동하면 감정표현이 종료됩니다.</p></details>
|
||||
<a id="terrain-comparison" class="terrain-source" href="?terrain=original">원본 지형 비교</a>
|
||||
</section>
|
||||
<section data-dialog-panel="sources" hidden>
|
||||
<details open><summary>데이터 출처</summary><a class="terrain-source" href="/data-sources.html" target="_blank" rel="noopener">자료별 출처·라이선스·가공 데이터 다운로드</a><a class="terrain-source" href="https://open-meteo.com/" target="_blank" rel="noopener">Weather data by Open-Meteo</a><a class="terrain-source" href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener">기상 데이터 이용조건 · CC BY 4.0</a><p class="muted">기상 자료의 시간 보간·강우량 합산 및 시각 효과 변환을 적용했습니다.</p><a id="osm-source" class="terrain-source" href="/buildings/source.html" target="_blank" rel="noopener">건물: 국토교통부/VWorld · CC BY 2.0 KR</a><a id="terrain-source" class="terrain-source" href="/terrain/source.html" target="_blank" rel="noopener">DSM: 수목·구조물 포함 · 데이터 출처/제한</a><a id="building-status" class="terrain-source" href="/buildings/source.html" target="_blank" rel="noopener">건물 자료 불러오는 중…</a></details>
|
||||
<details><summary>모델·참고 자료</summary><a id="structure-source" class="terrain-source" href="/structures.html" target="_blank" rel="noopener">구조물 모델 · 위치·치수 추정 · 근거 보기</a></details>
|
||||
<details><summary>오픈소스 라이브러리</summary><p class="muted">Three.js · SunCalc · Socket.IO Client 및 포함된 의존성</p><a class="terrain-source" href="/licenses.txt" target="_blank" rel="noopener">버전·저작권·라이선스 원문</a></details>
|
||||
<details><summary>글꼴</summary><p class="muted">고운돋움 · Pretendard — SIL Open Font License 1.1</p><a class="terrain-source" href="/fonts/GowunDodum-OFL.txt" target="_blank" rel="noopener">고운돋움 저작권·라이선스</a><a class="terrain-source" href="/fonts/Pretendard-LICENSE.txt" target="_blank" rel="noopener">Pretendard 저작권·라이선스</a></details>
|
||||
<p class="muted">자료별 원본·이용 조건과 보정 내역은 각 출처 페이지에서 확인할 수 있습니다.</p>
|
||||
</section>
|
||||
</div>
|
||||
</dialog>
|
||||
<p id="render-error" role="alert" hidden></p>
|
||||
<script type="module" src="/src/entry.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "gwanaksan-sunrise",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1",
|
||||
"prepare:buildings": "node scripts/prepare-building-assets.ts",
|
||||
"check:buildings": "node scripts/prepare-building-assets.ts --check",
|
||||
"prepare:summit": "node scripts/prepare-summit-assets.ts",
|
||||
"check:summit": "node scripts/prepare-summit-assets.ts --check",
|
||||
"prepare:data-notices": "node scripts/prepare-data-notices.ts",
|
||||
"prebuild": "npm run check:buildings && npm run check:summit && npm run prepare:data-notices",
|
||||
"build": "tsc --noEmit && tsc -p tsconfig.server.json && vite build",
|
||||
"dev:server": "node --env-file-if-exists=.env --watch server/index.ts",
|
||||
"start": "node --env-file-if-exists=.env server/index.ts",
|
||||
"check:server": "tsc -p tsconfig.server.json",
|
||||
"test:load": "node scripts/load-multiplayer.ts",
|
||||
"verify:mongo": "node --env-file-if-exists=.env scripts/verify-multiplayer-mongo.ts",
|
||||
"preview": "vite preview --host 127.0.0.1",
|
||||
"test": "node --test tests/*.test.ts",
|
||||
"verify:solar": "node tests/solar-report.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"mongodb": "^7.7.0",
|
||||
"socket.io": "^4.8.4",
|
||||
"socket.io-client": "^4.8.4",
|
||||
"suncalc": "2.0.1",
|
||||
"three": "^0.185.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.6.6",
|
||||
"@types/three": "^0.183.0",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sourceHash": "6f90fbd7927a723bbf3c46e68f66f22819666d374f069ae12087cc34a30af0c2",
|
||||
"bufferHash": "d8cf0a36a46572060189e2ca9e9a61e448fb3d0b441abc346b56b7c4603e0d65",
|
||||
"byteLength": 11036560,
|
||||
"terrain": {
|
||||
"origin": {
|
||||
"latitude": 37.445,
|
||||
"longitude": 126.964
|
||||
},
|
||||
"observerSurfaceElevation": 606.9063720703125
|
||||
},
|
||||
"counts": {
|
||||
"tagged": 1756,
|
||||
"levels": 953,
|
||||
"fallback": 4034
|
||||
},
|
||||
"target": [
|
||||
2389.237037522304,
|
||||
-530.4983918666275,
|
||||
1775.7760172638204
|
||||
],
|
||||
"meshes": [
|
||||
{
|
||||
"source": "tagged",
|
||||
"vertices": 113280,
|
||||
"position": 0,
|
||||
"normal": 1359360,
|
||||
"lightHeight": 2718720,
|
||||
"index": 3171840,
|
||||
"indexCount": 205500,
|
||||
"indexType": "uint32",
|
||||
"box": [
|
||||
968.7347412109375,
|
||||
-581.7264404296875,
|
||||
-1581.2288818359375,
|
||||
4514.0634765625,
|
||||
-475.2237243652344,
|
||||
3871.245361328125
|
||||
],
|
||||
"sphere": [
|
||||
2741.3991088867188,
|
||||
-528.4750823974609,
|
||||
1145.0082397460938,
|
||||
3153.873357261772
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "levels",
|
||||
"vertices": 59898,
|
||||
"position": 3993840,
|
||||
"normal": 4712616,
|
||||
"lightHeight": 5431392,
|
||||
"index": 5670984,
|
||||
"indexCount": 108384,
|
||||
"indexType": "uint16",
|
||||
"box": [
|
||||
972.2301025390625,
|
||||
-578.3482666015625,
|
||||
-1654.0888671875,
|
||||
4493.0498046875,
|
||||
-490.0949401855469,
|
||||
3798.428466796875
|
||||
],
|
||||
"sphere": [
|
||||
2732.6399536132812,
|
||||
-534.2216033935547,
|
||||
1072.1697998046875,
|
||||
2836.1249323113802
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "fallback",
|
||||
"vertices": 119274,
|
||||
"position": 5887752,
|
||||
"normal": 7319040,
|
||||
"lightHeight": 8750328,
|
||||
"index": 9227424,
|
||||
"indexCount": 190140,
|
||||
"indexType": "uint32",
|
||||
"box": [
|
||||
970.5235595703125,
|
||||
-583.4852294921875,
|
||||
-1658.1075439453125,
|
||||
4522.517578125,
|
||||
-376.1947937011719,
|
||||
3883.84912109375
|
||||
],
|
||||
"sphere": [
|
||||
2746.5205688476562,
|
||||
-479.8400115966797,
|
||||
1112.8707885742188,
|
||||
3129.6090105644394
|
||||
]
|
||||
}
|
||||
],
|
||||
"spill": {
|
||||
"offset": 9987984,
|
||||
"size": 1024,
|
||||
"bounds": [
|
||||
955.9872155081416,
|
||||
-1671.8254935390196,
|
||||
3580.3892490150624,
|
||||
5569.265821880235
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"bbox": [
|
||||
126.975,
|
||||
37.41,
|
||||
127.015,
|
||||
37.46
|
||||
],
|
||||
"count": 6743,
|
||||
"types": {
|
||||
"school": 36,
|
||||
"yes": 4345,
|
||||
"house": 1366,
|
||||
"commercial": 493,
|
||||
"church": 20,
|
||||
"retail": 3,
|
||||
"residential": 430,
|
||||
"warehouse": 48,
|
||||
"industrial": 1,
|
||||
"hospital": 1
|
||||
},
|
||||
"license": "CC-BY-2.0-KR",
|
||||
"licenses": {
|
||||
"VWorld": "CC-BY-2.0-KR"
|
||||
},
|
||||
"licenseUrls": {
|
||||
"VWorld": "https://creativecommons.org/licenses/by/2.0/kr/"
|
||||
},
|
||||
"distribution": {
|
||||
"notice": "source.html",
|
||||
"reviewedAt": "2026-10-30",
|
||||
"note": "Buildings derived exclusively from VWorld GIS building data. Retain source attribution, CC BY 2.0 KR terms and modification notices."
|
||||
},
|
||||
"heightTags": 1756,
|
||||
"levelTags": 2707,
|
||||
"heightSources": {
|
||||
"height": 1756,
|
||||
"levels": 953,
|
||||
"fallback": 4034
|
||||
},
|
||||
"selection": {
|
||||
"officialAdded": 6743,
|
||||
"officialOverlapsSkipped": [
|
||||
"vworld/2004200171814364088600000000/0",
|
||||
"vworld/2006199521724360085400000000/0",
|
||||
"vworld/2010199818314386251500000000/0",
|
||||
"vworld/2012200245534363150500000000/0",
|
||||
"vworld/2013199138114368963400000000/0",
|
||||
"vworld/2013199301334389900400000000/0",
|
||||
"vworld/2013199451814357330000000000/0",
|
||||
"vworld/2013199590864384344700000000/0",
|
||||
"vworld/2013199631994387415500000000/0",
|
||||
"vworld/2013200257034363366000000000/0",
|
||||
"vworld/2013200698154356969200000000/0",
|
||||
"vworld/2013200783624356557100000000/0",
|
||||
"vworld/2014199665444344625800000000/0",
|
||||
"vworld/2014200223604382287600000000/0",
|
||||
"vworld/2015200335254363151700000000/0",
|
||||
"vworld/2016198779184364087400000000/0",
|
||||
"vworld/2016200178494379485400000000/0",
|
||||
"vworld/2017199485094344337500000000/0",
|
||||
"vworld/2017199567794344698300000000/0",
|
||||
"vworld/2017200421314391944400000000/0",
|
||||
"vworld/2017200835224358449100000000/0",
|
||||
"vworld/2018199701784344736400000000/0",
|
||||
"vworld/2018200724084357596200000000/0",
|
||||
"vworld/2018200748444356685800000000/0",
|
||||
"vworld/2021199547984359315300000000/0",
|
||||
"vworld/2021199644984358812100000000/0",
|
||||
"vworld/2021199669674359468500000000/0",
|
||||
"vworld/1953200040624361546500000000/0",
|
||||
"vworld/1963198595404350511100000000/0",
|
||||
"vworld/1977200681434382098600000000/0",
|
||||
"vworld/1981200307244364150800000000/0",
|
||||
"vworld/1981200716724357928100000000/0",
|
||||
"vworld/1982200073724395779600000000/0",
|
||||
"vworld/1982200080414390527700000000/0",
|
||||
"vworld/1984198610034349994400000000/0",
|
||||
"vworld/1984200800414355499000000000/0",
|
||||
"vworld/1984200866674357003400000000/0",
|
||||
"vworld/1984200891194356084200000000/0",
|
||||
"vworld/1985199658084357385100000000/0",
|
||||
"vworld/1985199729734374194300000000/0",
|
||||
"vworld/1985199735064373656300000000/0",
|
||||
"vworld/1985199883654372860700000000/0",
|
||||
"vworld/1986199591064374049300000000/0",
|
||||
"vworld/1986199601554360651400000000/0",
|
||||
"vworld/1988199756264373164900000000/0",
|
||||
"vworld/1989199660774372923200000000/0",
|
||||
"vworld/1989199721734374334400000000/0",
|
||||
"vworld/1990199643224357691000000000/0",
|
||||
"vworld/1991199657404344707300000000/0",
|
||||
"vworld/0000197729374350448800000000/0",
|
||||
"vworld/0000198010484351177600000000/0",
|
||||
"vworld/0000198099184352399200000000/0",
|
||||
"vworld/0000198368944348908600000000/0",
|
||||
"vworld/0000198372574348879900000000/0",
|
||||
"vworld/0000198399504344098600000000/0",
|
||||
"vworld/0000198521154348630300000000/0",
|
||||
"vworld/0000198552814349641100000000/0",
|
||||
"vworld/0000198590874351082200000000/0",
|
||||
"vworld/0000198628594349645100000000/0",
|
||||
"vworld/0000198654504351275600000000/0",
|
||||
"vworld/0000199283204390016100000000/0",
|
||||
"vworld/0000199301924389747900000000/0",
|
||||
"vworld/0000199331864389641400000000/0",
|
||||
"vworld/0000199332504367872700000000/0",
|
||||
"vworld/0000199397484384272800000000/0",
|
||||
"vworld/0000199400864384286900000000/0",
|
||||
"vworld/0000199428694386752900000000/0",
|
||||
"vworld/0000199437344345537500000000/0",
|
||||
"vworld/0000199441924345552900000000/0",
|
||||
"vworld/0000199448834345556000000000/0",
|
||||
"vworld/0000199450314345501600000000/0",
|
||||
"vworld/0000199468894388220600000000/0",
|
||||
"vworld/0000199512774365846900000000/0",
|
||||
"vworld/0000199569864374373300000000/0",
|
||||
"vworld/0000199602144386588000000000/0",
|
||||
"vworld/0000199606254386556400000000/0",
|
||||
"vworld/0000199608634386574900000000/0",
|
||||
"vworld/0000199650954387734500000000/0",
|
||||
"vworld/0000199664274344505900000000/0",
|
||||
"vworld/0000199667464344640100000000/0",
|
||||
"vworld/0000199724414384357700000000/0",
|
||||
"vworld/0000199750564368776800000000/0",
|
||||
"vworld/0000199808484386197700000000/0",
|
||||
"vworld/0000199816764374245600000000/0",
|
||||
"vworld/0000199822754374071900000000/0",
|
||||
"vworld/0000199828244353312100000000/0",
|
||||
"vworld/0000199962124394087900000000/0",
|
||||
"vworld/0000199964494380722500000000/0",
|
||||
"vworld/0000199970454378684000000000/0",
|
||||
"vworld/0000199972844359027100000000/0",
|
||||
"vworld/0000199974064378744000000000/0",
|
||||
"vworld/0000199974694395709400000000/0",
|
||||
"vworld/0000199975154359051300000000/0",
|
||||
"vworld/0000199977794380667300000000/0",
|
||||
"vworld/0000199983414395809900000000/0",
|
||||
"vworld/0000199985354380678900000000/0",
|
||||
"vworld/0000199986354393567200000000/0",
|
||||
"vworld/0000199988244388649100000000/0",
|
||||
"vworld/0000199989474359098800000000/0",
|
||||
"vworld/0000199992794390389400000000/0",
|
||||
"vworld/0000200005044393480300000000/0",
|
||||
"vworld/0000200011654393479900000000/0",
|
||||
"vworld/0000200024044392489000000000/0",
|
||||
"vworld/0000200046374361615400000000/0",
|
||||
"vworld/0000200046834361452400000000/0",
|
||||
"vworld/0000200054224391693000000000/0",
|
||||
"vworld/0000200078614390589000000000/0",
|
||||
"vworld/0000200136914390204900000000/0",
|
||||
"vworld/0000200153384380355500000000/0",
|
||||
"vworld/0000200177294380365400000000/0",
|
||||
"vworld/0000200179874379543600000000/0",
|
||||
"vworld/0000200180734380578600000000/0",
|
||||
"vworld/0000200184624379487400000000/0",
|
||||
"vworld/0000200215244381437100000000/0",
|
||||
"vworld/0000200215934381366300000000/0",
|
||||
"vworld/0000200220244383099300000000/0",
|
||||
"vworld/0000200222924383162700000000/0",
|
||||
"vworld/0000200223374382178200000000/0",
|
||||
"vworld/0000200225674383227900000000/0",
|
||||
"vworld/0000200226684382262100000000/0",
|
||||
"vworld/0000200228374383292600000000/0",
|
||||
"vworld/0000200231024383356800000000/0",
|
||||
"vworld/0000200233884382404300000000/0",
|
||||
"vworld/0000200249904381500000000000/0",
|
||||
"vworld/0000200250964381436300000000/0",
|
||||
"vworld/0000200257314383513600000000/0",
|
||||
"vworld/0000200270954384559700000000/0",
|
||||
"vworld/0000200292304384768500000000/0",
|
||||
"vworld/0000200292624384690100000000/0",
|
||||
"vworld/0000200293724383954800000000/0",
|
||||
"vworld/0000200295034384033200000000/0",
|
||||
"vworld/0000200295154384615400000000/0",
|
||||
"vworld/0000200327724365028500000000/0",
|
||||
"vworld/0000200344264362922400000000/0",
|
||||
"vworld/0000200548734358107000000000/0",
|
||||
"vworld/0000200548754358014600000000/0",
|
||||
"vworld/0000200553304358141100000000/0",
|
||||
"vworld/0000200553344357928300000000/0",
|
||||
"vworld/0000200555384358051300000000/0",
|
||||
"vworld/0000200558924357979800000000/0",
|
||||
"vworld/0000200609294395359200000000/0",
|
||||
"vworld/0000200619544359246100000000/0",
|
||||
"vworld/0000200644214359210300000000/0",
|
||||
"vworld/0000200651584382384400000000/0",
|
||||
"vworld/0000200662824394260400000000/0",
|
||||
"vworld/0000200667144392648700000000/0",
|
||||
"vworld/0000200794014383339800000000/0",
|
||||
"vworld/0000200804464383101700000000/0",
|
||||
"vworld/0000200819744383548900000000/0",
|
||||
"vworld/0000200830154384590300000000/0",
|
||||
"vworld/0000200836084384275100000000/0",
|
||||
"vworld/0000200838954384330600000000/0",
|
||||
"vworld/0000200840414384162100000000/0",
|
||||
"vworld/0000200848214384226200000000/0",
|
||||
"vworld/0000200849234384145000000000/0",
|
||||
"vworld/0000200923424383813000000000/0",
|
||||
"vworld/0000200927704383870300000000/0",
|
||||
"vworld/1855200570064358187500000000/0",
|
||||
"vworld/1898199959784359239600000000/0",
|
||||
"vworld/1939198202944349372000000000/0",
|
||||
"vworld/1942200048044361522000000000/0",
|
||||
"vworld/1942200049984361567000000000/0",
|
||||
"vworld/1950200560504357940000000000/0",
|
||||
"vworld/1953199443234345601800000000/0",
|
||||
"vworld/1955198593804350255200000000/0",
|
||||
"vworld/1967200461234387878300000000/0",
|
||||
"vworld/1980199428524389091200000000/0",
|
||||
"vworld/1982200097844390512800000000/0",
|
||||
"vworld/1982200102004390499800000000/0",
|
||||
"vworld/1982200233594364216600000000/0",
|
||||
"vworld/1982200890684358639400000000/0",
|
||||
"vworld/1983199855174372766000000000/0",
|
||||
"vworld/1984198604644349927900000000/0",
|
||||
"vworld/1984198605844350051300000000/0",
|
||||
"vworld/1985199820014374225600000000/0",
|
||||
"vworld/1986200595694395192900000000/0",
|
||||
"vworld/1995200140784363713300000000/0",
|
||||
"vworld/1995200183704351617400000000/0",
|
||||
"vworld/1998200981194386109600000000/0",
|
||||
"vworld/1999198204514349658700000000/0",
|
||||
"vworld/2004199904224351989200000000/0",
|
||||
"vworld/2009199596284386844200000000/0",
|
||||
"vworld/2009199601324386892400000000/0"
|
||||
]
|
||||
},
|
||||
"official": {
|
||||
"archive": "AL_D010_41_20260719.zip",
|
||||
"sha256": "cec9fffa5627a6e27a25074e3d96f24fae6557453c14b45c623268ef4f35c4eb",
|
||||
"crs": "EPSG:5186",
|
||||
"url": "https://www.vworld.kr/dtmk/dtmk_ntads_s002.do?svcCde=NA&dsId=18",
|
||||
"license": "CC-BY-2.0-KR",
|
||||
"rawRegionalFeatures": 6986,
|
||||
"fieldDefinitions": "data/references/gwacheon/vworld-columns.xlsx"
|
||||
},
|
||||
"limitations": [
|
||||
"VWorld-only footprints and attributes. Snapshot can contain older footprints.",
|
||||
"Overlaps >0.5m² between VWorld records are excluded, preferring measured height or levels.",
|
||||
"Missing heights remain explicitly estimated; terrain is still Copernicus DSM, not surveyed ground."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html><html lang="ko"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>과천 건물 데이터</title><style>body{font:16px/1.8 sans-serif;max-width:850px;margin:40px auto;padding:20px;color:#24313d}td,th{padding:8px;border:1px solid #bbb}table{border-collapse:collapse}</style>
|
||||
<h1>과천 건물 — VWorld</h1>
|
||||
<p>2026-10-30. 국토교통부 GIS건물통합정보(VWorld)만으로 생성한 건물 6,743개입니다. 범위는 경도 126.975~127.015, 위도 37.410~37.460이며 과천시 전체 행정구역과 일치하지 않습니다. 외곽선·높이·층수의 선택과 가공에 OSM 건물 자료를 사용하지 않습니다.</p>
|
||||
<p>원본은 사용자 제공 AL_D010_41_20260719.zip입니다. 경기도 SHP 세 묶음에서 범위를 추출했습니다. 원본 좌표계는 EPSG:5186, 문자열은 CP949이며 A16(높이, m), A26(지상층수)를 사용합니다. 0·빈 값·숫자 오버플로는 높이 정보로 사용하지 않습니다. 배포일은 개별 건물의 측량·준공 확인일을 뜻하지 않습니다.</p>
|
||||
<p>좌표 변환, 영역 추출, 다중 폴리곤 분리 및 용도 분류를 적용했습니다. VWorld 건물끼리 면적 0.5㎡를 초과해 겹치는 경우 실높이·층수 보유 레코드를 우선하여 183개를 제외했습니다. 제외 목록은 메타데이터에 기록했습니다. 원본 자료에도 오래된 외곽선이나 누락이 있을 수 있습니다.</p>
|
||||
<table><tr><th>화면 색</th><th>높이 기준</th></tr><tr><td>밝은 회색 · 1,756개</td><td>VWorld 높이 자료. 현장 측량으로 검증한 값은 아닙니다.</td></tr><tr><td>청회색 · 953개</td><td>지상층수 × 3m. 층고·지붕 높이의 추정값입니다.</td></tr><tr><td>갈색 계열 · 4,034개</td><td>높이·층수 미상. 용도별 임시 높이(아파트 15m, 주거 9m, 단독주택 6m, 상업·학교 12m, 온실·지붕 3m, 기타 9m).</td></tr></table>
|
||||
<p>지반은 <a href="/terrain/source.html">Copernicus DSM</a> 주변 낮은 표면으로 추정합니다. 실제 지면 DEM이 아니므로 건물 매몰과 높이 오차가 남습니다. 야간 조명은 시각적 근사이며 실제 조명 위치·빛의 차폐를 계산하지 않습니다. 정밀 일출 차폐 검증용 모델이 아닙니다.</p>
|
||||
<h2>출처와 이용조건</h2>
|
||||
<p>출처: <a href="https://www.vworld.kr/dtmk/dtmk_ntads_s002.do?svcCde=NA&dsId=18">국토교통부 GIS건물통합정보 / VWorld</a> · <a href="https://creativecommons.org/licenses/by/2.0/kr/">CC BY 2.0 대한민국</a>. 2026-10-30 공식 자료 페이지의 CC BY 표시와 연결된 라이선스를 확인했습니다.</p>
|
||||
<p>가공 건물 자료를 이용·재배포할 때 원저작자, 자료명, 원본 링크, 라이선스 및 가공 사실을 유지해야 합니다. 원본의 저작권·면책 표시를 제거하거나 허용된 이용을 제한하는 조건·기술적 보호조치를 추가할 수 없습니다. 원본 자료는 있는 그대로 제공되며, 제공자는 라이선스에서 정한 범위에서 보증과 책임을 부담하지 않습니다. 상세 조건은 <a href="https://creativecommons.org/licenses/by/2.0/kr/legalcode.ko">이용허락규약</a>을 따릅니다.</p>
|
||||
<p>이 안내는 과천 건물 자료에 해당합니다. 다른 장면의 해안선과 정상 구조물 위치 등에 사용한 자료는 각 출처 안내를 따릅니다.</p>
|
||||
<p><a href="gwacheon.geojson" download>VWorld 가공 외곽선·속성 다운로드</a> · <a href="metadata.json">원본 SHA256·가공 및 제외 내역</a></p><p><a href="/data-sources.html">자료별 라이선스·가공 데이터 다운로드</a></p></html>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!doctype html><html lang="ko"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>데이터 출처와 이용조건</title>
|
||||
<style>body{max-width:850px;margin:40px auto;padding:0 24px;font:16px/1.8 system-ui;color:#24313d}a{color:#25654e}td,th{border:1px solid #ccc;padding:10px}table{border-collapse:collapse}</style>
|
||||
<h1>데이터 출처와 이용조건</h1>
|
||||
<p>자료별 이용조건은 서로 다릅니다. 원본은 출처별로 보관하며 웹에서는 좌표 변환·보간·선별한 자료를 사용합니다. 웹용 파일을 수정되지 않은 원본으로 표시하지 않습니다.</p>
|
||||
<h2>높이와 해안 표시</h2>
|
||||
<p>하조대·기사문 높이 격자는 Copernicus 자료만 좌표 변환·보간합니다. OSM 해안선으로 높이를 낮추거나 육지 최저 높이·바다 칸 높이를 덮어쓰지 않습니다. OSM 경계·표면 마스크는 별도 파일이며 화면에서 육지 표시, 모래 재질, 물결·거품 및 이동 경계를 결정합니다. 각 높이 파일과 해안 자료의 조건은 아래에 구분합니다.</p>
|
||||
<p>관악산은 원본 높이 격자와 기존 정상 보정 모델을 유지합니다. 정상 모델은 사진 해석·추정 높이와 일부 OSM 위치를 사용하며, 최적화를 위한 사전 생성 메시도 배포합니다. 이를 순수 Copernicus 원본 또는 화면 단계에서만 계산한 자료라고 표시하지 않습니다. <a href="/terrain/summit-reconstruction.html">정상 모델의 가공 내역</a>.</p>
|
||||
<h2>Copernicus GLO-30 높이</h2>
|
||||
<p>produced using Copernicus WorldDEM-30 © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all rights reserved</p>
|
||||
<p>The organisations in charge of the Copernicus programme by law or by delegation do not incur any liability for any use of the Copernicus WorldDEM-30.</p>
|
||||
<p><a href="https://dataspace.copernicus.eu/sites/default/files/media/files/2025-06/copernicus_contributing_mission_data_access_v2_cop_dem_licenses.pdf">공식 이용조건 GLO-30-F</a>. 재배포 시 제6조의 지정 출처·면책·비보증 및 후속 이용자에 관한 의무를 유지하고, 제공 기관의 보증·승인을 암시하지 않아야 합니다. <a href="/terrain/source.html">관악산 높이 출처</a>.</p>
|
||||
<h2>OpenStreetMap 경계·경로·시설 위치</h2>
|
||||
<p>© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>. 아래 OSM 기반 가공 지리 자료는 <a href="https://opendatacommons.org/licenses/odbl/1-0/">Open Database License 1.0 (ODbL)</a>로 제공합니다. 원본의 출처·라이선스 고지를 유지해야 하며, 파생 데이터베이스를 공개 이용하면 ODbL의 동일조건 및 기계 판독 가능한 자료 또는 변경 내역·방법 제공 조건을 따라야 합니다. 다른 출처의 자료 전체를 ODbL로 재허락한다는 뜻은 아닙니다.</p>
|
||||
<ul>
|
||||
<li>하조대: <a href="/terrain/hajodae/coast.json" download>해안선·해변</a> · <a href="/terrain/hajodae/surface.png" download>표면 마스크</a> · <a href="/terrain/hajodae/metadata.json">가공·원본 URL·해시·파일별 조건</a></li>
|
||||
<li>기사문: <a href="/terrain/gisamun/coast.json" download>해안선·추정 해변</a> · <a href="/terrain/gisamun/surface.png" download>표면 마스크</a> · <a href="/terrain/gisamun/metadata.json">가공·원본 URL·해시·파일별 조건</a></li>
|
||||
<li><a href="/osm-derived.json" download>정상 경로·시설 위치·추정 제어점 및 기사문항 입력 자료</a>. 길 폭·시설 치수·보정 높이·항만 안쪽 경계는 자체 추정입니다. OSM의 실측값으로 해석하면 안 됩니다.</li>
|
||||
</ul>
|
||||
<p>표면 PNG는 R=육지, G=모래, B=해안선 부호 거리이며 Copernicus 높이를 포함하지 않습니다. 투영·간격은 각 메타데이터를 따릅니다.</p>
|
||||
<h2>VWorld 건물</h2>
|
||||
<p>국토교통부 GIS건물통합정보 / VWorld, <a href="https://creativecommons.org/licenses/by/2.0/kr/">CC BY 2.0 KR</a>. OSM 건물을 병합하거나 높이를 보완하지 않습니다. <a href="/buildings/source.html">출처·가공 내역·다운로드·재배포 조건</a>.</p>
|
||||
<h2>기상·재질·글꼴</h2>
|
||||
<p><a href="https://open-meteo.com/">Weather data by Open-Meteo</a>, <a href="https://creativecommons.org/licenses/by/4.0/">CC BY 4.0</a>. 시간 보간·강우량 합산 및 시각 효과 변환을 적용했습니다. 무료 API는 <a href="https://open-meteo.com/en/terms">비상업 용도와 호출량 제한</a>을 따릅니다.</p>
|
||||
<p>Poly Haven 지면 재질: <a href="https://polyhaven.com/license">CC0</a> · <a href="/terrain/materials/sources.json">파일별 출처</a>. 참고사진과 거리뷰 원본은 앱에 포함하지 않습니다.</p>
|
||||
<p>Pretendard: <a href="/fonts/Pretendard-LICENSE.txt">저작권·SIL OFL 1.1</a>. Gowun Dodum: <a href="/fonts/GowunDodum-OFL.txt">저작권·SIL OFL 1.1</a>. <a href="/licenses.txt">프로그램 라이브러리 라이선스</a>.</p>
|
||||
</html>
|
||||
@@ -0,0 +1,94 @@
|
||||
Copyright 2021 The Gowun Dodum Project Authors (https://github.com/yangheeryu/Gowun-Dodum)
|
||||
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,94 @@
|
||||
Copyright (c) 2021, Kil Hyung-jin (https://github.com/orioncactus/pretendard),
|
||||
with Reserved Font Name Pretendard.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,257 @@
|
||||
SUNRISE — browser library notices
|
||||
|
||||
License texts copied from the installed packages.
|
||||
Data and model sources are listed separately in the application.
|
||||
|
||||
three 0.185.1
|
||||
License: MIT
|
||||
Source: https://github.com/mrdoob/three.js
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright © 2010-2026 three.js authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
========================================================================
|
||||
|
||||
suncalc 2.0.1
|
||||
License: BSD-2-Clause
|
||||
Source: git://github.com/mourner/suncalc.git
|
||||
|
||||
Copyright (c) 2026, Volodymyr Agafonkin
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are
|
||||
permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
of conditions and the following disclaimer in the documentation and/or other materials
|
||||
provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
||||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
========================================================================
|
||||
|
||||
socket.io-client 4.8.4
|
||||
License: MIT
|
||||
Source: git+https://github.com/socketio/socket.io.git
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-present Guillermo Rauch and Socket.IO contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
========================================================================
|
||||
|
||||
socket.io-parser 4.2.7
|
||||
License: MIT
|
||||
Source: git+https://github.com/socketio/socket.io.git
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014-present Guillermo Rauch and Socket.IO contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the 'Software'), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
========================================================================
|
||||
|
||||
engine.io-client 6.6.7
|
||||
License: MIT
|
||||
Source: git+https://github.com/socketio/socket.io.git
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014-present Guillermo Rauch and Socket.IO contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
========================================================================
|
||||
|
||||
engine.io-parser 5.2.3
|
||||
License: MIT
|
||||
Source: https://github.com/socketio/socket.io.git
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014-present Guillermo Rauch and Socket.IO contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
========================================================================
|
||||
|
||||
@socket.io/component-emitter 3.1.2
|
||||
License: MIT
|
||||
Source: https://github.com/socketio/emitter.git
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014 Component contributors <dev@component.io>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
========================================================================
|
||||
|
||||
debug 4.4.3
|
||||
License: MIT
|
||||
Source: git://github.com/debug-js/debug.git
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014-2017 TJ Holowaychuk <tj@vision-media.ca>
|
||||
Copyright (c) 2018-2021 Josh Junon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
and associated documentation files (the 'Software'), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
========================================================================
|
||||
|
||||
ms 2.1.3
|
||||
License: MIT
|
||||
Source: vercel/ms
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Vercel, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,457 @@
|
||||
{
|
||||
"version": 1,
|
||||
"units": "metres",
|
||||
"upAxis": "Y",
|
||||
"northAxis": "-Z",
|
||||
"dimensionsEstimated": true,
|
||||
"photographsEmbedded": false,
|
||||
"sourceConstants": "src/constants/structures.ts",
|
||||
"assets": [
|
||||
{
|
||||
"id": "summit-mast",
|
||||
"file": "summit-mast.glb",
|
||||
"kind": "tower",
|
||||
"latitude": 37.4450675,
|
||||
"longitude": 126.9638043,
|
||||
"heading": 0,
|
||||
"dimensionsEstimated": true,
|
||||
"positionEstimated": false,
|
||||
"source": "OSM node/3445797250",
|
||||
"dimensions": {
|
||||
"height": 24,
|
||||
"width": 2.4
|
||||
},
|
||||
"origin": "base contact plane",
|
||||
"terrainSupportAnchors": []
|
||||
},
|
||||
{
|
||||
"id": "south-tower-1",
|
||||
"file": "south-tower-1.glb",
|
||||
"kind": "tower",
|
||||
"latitude": 37.4401643,
|
||||
"longitude": 126.9642555,
|
||||
"heading": 0,
|
||||
"dimensionsEstimated": true,
|
||||
"positionEstimated": true,
|
||||
"source": "OSM node/4262612712 + recent-02.jpg",
|
||||
"dimensions": {
|
||||
"height": 48,
|
||||
"width": 5
|
||||
},
|
||||
"origin": "base contact plane",
|
||||
"terrainSupportAnchors": []
|
||||
},
|
||||
{
|
||||
"id": "south-tower-2",
|
||||
"file": "south-tower-2.glb",
|
||||
"kind": "tower",
|
||||
"latitude": 37.4400359,
|
||||
"longitude": 126.9640016,
|
||||
"heading": 0,
|
||||
"dimensionsEstimated": true,
|
||||
"positionEstimated": true,
|
||||
"source": "OSM node/4736107159 + recent-02.jpg",
|
||||
"dimensions": {
|
||||
"height": 36,
|
||||
"width": 3.2
|
||||
},
|
||||
"origin": "base contact plane",
|
||||
"terrainSupportAnchors": []
|
||||
},
|
||||
{
|
||||
"id": "south-tower-3",
|
||||
"file": "south-tower-3.glb",
|
||||
"kind": "tower",
|
||||
"latitude": 37.44012,
|
||||
"longitude": 126.96448,
|
||||
"heading": 0,
|
||||
"dimensionsEstimated": true,
|
||||
"positionEstimated": true,
|
||||
"source": "recent-02.jpg; approximate cluster offset",
|
||||
"dimensions": {
|
||||
"height": 40,
|
||||
"width": 3
|
||||
},
|
||||
"origin": "base contact plane",
|
||||
"terrainSupportAnchors": []
|
||||
},
|
||||
{
|
||||
"id": "south-tower-4",
|
||||
"file": "south-tower-4.glb",
|
||||
"kind": "tower",
|
||||
"latitude": 37.44003,
|
||||
"longitude": 126.96472,
|
||||
"heading": 0,
|
||||
"dimensionsEstimated": true,
|
||||
"positionEstimated": true,
|
||||
"source": "recent-02.jpg; approximate cluster offset",
|
||||
"dimensions": {
|
||||
"height": 34,
|
||||
"width": 2.4
|
||||
},
|
||||
"origin": "base contact plane",
|
||||
"terrainSupportAnchors": []
|
||||
},
|
||||
{
|
||||
"id": "weather-radar",
|
||||
"file": "weather-radar.glb",
|
||||
"kind": "radar",
|
||||
"latitude": 37.444028,
|
||||
"longitude": 126.963808,
|
||||
"heading": 0,
|
||||
"dimensionsEstimated": true,
|
||||
"positionEstimated": true,
|
||||
"source": "OSM way/337498781 (identity inferred), photo-01/09/10",
|
||||
"dimensions": {
|
||||
"radius": 4,
|
||||
"buildingHeight": 7
|
||||
},
|
||||
"origin": "base contact plane",
|
||||
"terrainSupportAnchors": []
|
||||
},
|
||||
{
|
||||
"id": "north-deck",
|
||||
"file": "north-deck.glb",
|
||||
"kind": "deck",
|
||||
"latitude": 37.44516,
|
||||
"longitude": 126.96405,
|
||||
"heading": -20,
|
||||
"dimensionsEstimated": true,
|
||||
"positionEstimated": true,
|
||||
"source": "deck-19/20/22; position and dimensions estimated",
|
||||
"dimensions": {
|
||||
"width": 3,
|
||||
"length": 7,
|
||||
"stairLength": 7,
|
||||
"stairDrop": 3
|
||||
},
|
||||
"origin": "upper deck surface",
|
||||
"terrainSupportAnchors": [
|
||||
{
|
||||
"position": [
|
||||
-1.5,
|
||||
-0.32,
|
||||
3.5
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-1.5,
|
||||
-0.32,
|
||||
2.0999999046325684
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-1.5,
|
||||
-0.32,
|
||||
0.7000002861022949
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-1.5,
|
||||
-0.32,
|
||||
-0.7000002861022949
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-1.5,
|
||||
-0.32,
|
||||
-2.0999999046325684
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-1.5,
|
||||
-0.32,
|
||||
-3.5
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-1.5,
|
||||
-0.8200000596046448,
|
||||
-4.6666669845581055
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-1.5,
|
||||
-1.3200001192092896,
|
||||
-5.833333969116211
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-1.5,
|
||||
-1.82,
|
||||
-7.0
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-1.5,
|
||||
-2.32,
|
||||
-8.166666984558105
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-1.5,
|
||||
-2.82,
|
||||
-9.333333015441895
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-1.5,
|
||||
-3.32,
|
||||
-10.5
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
1.5,
|
||||
-0.32,
|
||||
3.5
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
1.5,
|
||||
-0.32,
|
||||
2.0999999046325684
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
1.5,
|
||||
-0.32,
|
||||
0.7000002861022949
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
1.5,
|
||||
-0.32,
|
||||
-0.7000002861022949
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
1.5,
|
||||
-0.32,
|
||||
-2.0999999046325684
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
1.5,
|
||||
-0.32,
|
||||
-3.5
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
1.5,
|
||||
-0.8200000596046448,
|
||||
-4.6666669845581055
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
1.5,
|
||||
-1.3200001192092896,
|
||||
-5.833333969116211
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
1.5,
|
||||
-1.82,
|
||||
-7.0
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
1.5,
|
||||
-2.32,
|
||||
-8.166666984558105
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
1.5,
|
||||
-2.82,
|
||||
-9.333333015441895
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
1.5,
|
||||
-3.32,
|
||||
-10.5
|
||||
],
|
||||
"postWidth": 0.1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "yeonjudae-viewpoint",
|
||||
"file": "yeonjudae-viewpoint.glb",
|
||||
"kind": "deck",
|
||||
"latitude": 37.4441528,
|
||||
"longitude": 126.9644299,
|
||||
"heading": 10,
|
||||
"dimensionsEstimated": true,
|
||||
"positionEstimated": true,
|
||||
"source": "OSM node/5963311985; deck outline and dimensions estimated",
|
||||
"dimensions": {
|
||||
"width": 5,
|
||||
"length": 4,
|
||||
"stairLength": 4,
|
||||
"stairDrop": 1.5
|
||||
},
|
||||
"origin": "upper deck surface",
|
||||
"terrainSupportAnchors": [
|
||||
{
|
||||
"position": [
|
||||
-2.5,
|
||||
-0.32,
|
||||
2.0
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-2.5,
|
||||
-0.32,
|
||||
0.6666665077209473
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-2.5,
|
||||
-0.32,
|
||||
-0.6666667461395264
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-2.5,
|
||||
-0.32,
|
||||
-2.0
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-2.5,
|
||||
-0.8200000596046448,
|
||||
-3.3333334922790527
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-2.5,
|
||||
-1.32,
|
||||
-4.666666507720947
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
-2.5,
|
||||
-1.82,
|
||||
-6.0
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
2.5,
|
||||
-0.32,
|
||||
2.0
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
2.5,
|
||||
-0.32,
|
||||
0.6666665077209473
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
2.5,
|
||||
-0.32,
|
||||
-0.6666667461395264
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
2.5,
|
||||
-0.32,
|
||||
-2.0
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
2.5,
|
||||
-0.8200000596046448,
|
||||
-3.3333334922790527
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
2.5,
|
||||
-1.32,
|
||||
-4.666666507720947
|
||||
],
|
||||
"postWidth": 0.1
|
||||
},
|
||||
{
|
||||
"position": [
|
||||
2.5,
|
||||
-1.82,
|
||||
-6.0
|
||||
],
|
||||
"postWidth": 0.1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,894 @@
|
||||
{
|
||||
"attribution": "© OpenStreetMap contributors",
|
||||
"sourceUrl": "https://www.openstreetmap.org/copyright",
|
||||
"license": "ODbL-1.0",
|
||||
"licenseUrl": "https://opendatacommons.org/licenses/odbl/1-0/",
|
||||
"scope": "Geographic input collection below, including our estimated additions. Not a relicensing of Copernicus elevation grids, VWorld buildings, photographs, or model artwork.",
|
||||
"processing": "OSM route selection and facility positions; estimated widths, footprints, dimensions and terrain control elevations. These are not surveyed values.",
|
||||
"summit": {
|
||||
"routes": [
|
||||
{
|
||||
"id": "way/337498789",
|
||||
"label": "정상 암반길",
|
||||
"width": 3.2,
|
||||
"points": [
|
||||
[
|
||||
126.9642427,
|
||||
37.4452121
|
||||
],
|
||||
[
|
||||
126.9642346,
|
||||
37.4450861
|
||||
],
|
||||
[
|
||||
126.9641628,
|
||||
37.4449609
|
||||
],
|
||||
[
|
||||
126.9641194,
|
||||
37.4449084
|
||||
],
|
||||
[
|
||||
126.9641086,
|
||||
37.4448542
|
||||
],
|
||||
[
|
||||
126.9641235,
|
||||
37.4447614
|
||||
],
|
||||
[
|
||||
126.9641299,
|
||||
37.4446834
|
||||
],
|
||||
[
|
||||
126.9641627,
|
||||
37.4446443
|
||||
],
|
||||
[
|
||||
126.9641937,
|
||||
37.4446176
|
||||
],
|
||||
[
|
||||
126.9641853,
|
||||
37.4445946
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "way/37751526",
|
||||
"label": "남쪽 하산 계단길",
|
||||
"width": 2.4,
|
||||
"points": [
|
||||
[
|
||||
126.9652191,
|
||||
37.4426477
|
||||
],
|
||||
[
|
||||
126.965157,
|
||||
37.4427111
|
||||
],
|
||||
[
|
||||
126.9650832,
|
||||
37.4428437
|
||||
],
|
||||
[
|
||||
126.9649758,
|
||||
37.4428823
|
||||
],
|
||||
[
|
||||
126.9646903,
|
||||
37.4431718
|
||||
],
|
||||
[
|
||||
126.9644942,
|
||||
37.4433977
|
||||
],
|
||||
[
|
||||
126.9645363,
|
||||
37.4435795
|
||||
],
|
||||
[
|
||||
126.9644974,
|
||||
37.4437541
|
||||
],
|
||||
[
|
||||
126.9645055,
|
||||
37.4438755
|
||||
],
|
||||
[
|
||||
126.9644849,
|
||||
37.4439916
|
||||
],
|
||||
[
|
||||
126.9644634,
|
||||
37.4441563
|
||||
],
|
||||
[
|
||||
126.9644231,
|
||||
37.4442014
|
||||
],
|
||||
[
|
||||
126.9643909,
|
||||
37.4441957
|
||||
],
|
||||
[
|
||||
126.9643509,
|
||||
37.4442247
|
||||
],
|
||||
[
|
||||
126.9642731,
|
||||
37.4442455
|
||||
],
|
||||
[
|
||||
126.9642271,
|
||||
37.4442888
|
||||
],
|
||||
[
|
||||
126.9641796,
|
||||
37.4443334
|
||||
],
|
||||
[
|
||||
126.9641677,
|
||||
37.4443904
|
||||
],
|
||||
[
|
||||
126.9641998,
|
||||
37.4444216
|
||||
],
|
||||
[
|
||||
126.9642101,
|
||||
37.444477
|
||||
],
|
||||
[
|
||||
126.9641853,
|
||||
37.4445946
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "way/337498787",
|
||||
"label": "동쪽 계단길",
|
||||
"width": 2.2,
|
||||
"points": [
|
||||
[
|
||||
126.9646956,
|
||||
37.4448264
|
||||
],
|
||||
[
|
||||
126.9644011,
|
||||
37.4449652
|
||||
],
|
||||
[
|
||||
126.9641628,
|
||||
37.4449609
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "way/337498788",
|
||||
"label": "북쪽 능선길",
|
||||
"width": 2.4,
|
||||
"points": [
|
||||
[
|
||||
126.9652188,
|
||||
37.445905
|
||||
],
|
||||
[
|
||||
126.9651599,
|
||||
37.4458421
|
||||
],
|
||||
[
|
||||
126.9651204,
|
||||
37.445817
|
||||
],
|
||||
[
|
||||
126.9650739,
|
||||
37.4458262
|
||||
],
|
||||
[
|
||||
126.965047,
|
||||
37.4458008
|
||||
],
|
||||
[
|
||||
126.9650012,
|
||||
37.4458049
|
||||
],
|
||||
[
|
||||
126.9649836,
|
||||
37.4457585
|
||||
],
|
||||
[
|
||||
126.9649235,
|
||||
37.4457452
|
||||
],
|
||||
[
|
||||
126.9649129,
|
||||
37.4458003
|
||||
],
|
||||
[
|
||||
126.9648793,
|
||||
37.445814
|
||||
],
|
||||
[
|
||||
126.9648108,
|
||||
37.4457987
|
||||
],
|
||||
[
|
||||
126.9648084,
|
||||
37.4457327
|
||||
],
|
||||
[
|
||||
126.9647692,
|
||||
37.4457201
|
||||
],
|
||||
[
|
||||
126.9647305,
|
||||
37.4457475
|
||||
],
|
||||
[
|
||||
126.9646677,
|
||||
37.4457742
|
||||
],
|
||||
[
|
||||
126.9646443,
|
||||
37.4457812
|
||||
],
|
||||
[
|
||||
126.9646393,
|
||||
37.4457277
|
||||
],
|
||||
[
|
||||
126.9646373,
|
||||
37.4456519
|
||||
],
|
||||
[
|
||||
126.9646231,
|
||||
37.4456153
|
||||
],
|
||||
[
|
||||
126.9645478,
|
||||
37.4456006
|
||||
],
|
||||
[
|
||||
126.9645194,
|
||||
37.4455309
|
||||
],
|
||||
[
|
||||
126.9645211,
|
||||
37.4455051
|
||||
],
|
||||
[
|
||||
126.9644963,
|
||||
37.4454888
|
||||
],
|
||||
[
|
||||
126.9645078,
|
||||
37.4454665
|
||||
],
|
||||
[
|
||||
126.9644384,
|
||||
37.4454786
|
||||
],
|
||||
[
|
||||
126.9644443,
|
||||
37.4454544
|
||||
],
|
||||
[
|
||||
126.9644447,
|
||||
37.4454142
|
||||
],
|
||||
[
|
||||
126.9644298,
|
||||
37.4453984
|
||||
],
|
||||
[
|
||||
126.9644073,
|
||||
37.4454045
|
||||
],
|
||||
[
|
||||
126.9644102,
|
||||
37.4454574
|
||||
],
|
||||
[
|
||||
126.9643506,
|
||||
37.445427
|
||||
],
|
||||
[
|
||||
126.9642934,
|
||||
37.4454174
|
||||
],
|
||||
[
|
||||
126.9642857,
|
||||
37.4454008
|
||||
],
|
||||
[
|
||||
126.9643346,
|
||||
37.4453985
|
||||
],
|
||||
[
|
||||
126.9643315,
|
||||
37.4453643
|
||||
],
|
||||
[
|
||||
126.9642929,
|
||||
37.4453472
|
||||
],
|
||||
[
|
||||
126.9642727,
|
||||
37.4453499
|
||||
],
|
||||
[
|
||||
126.9642484,
|
||||
37.4453197
|
||||
],
|
||||
[
|
||||
126.9642584,
|
||||
37.4452722
|
||||
],
|
||||
[
|
||||
126.9642427,
|
||||
37.4452121
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"structures": {
|
||||
"towers": [
|
||||
{
|
||||
"id": "summit-mast",
|
||||
"latitude": 37.4450675,
|
||||
"longitude": 126.9638043,
|
||||
"height": 24,
|
||||
"width": 2.4,
|
||||
"source": "OSM node/3445797250",
|
||||
"estimatedPosition": false
|
||||
},
|
||||
{
|
||||
"id": "south-tower-1",
|
||||
"latitude": 37.4401643,
|
||||
"longitude": 126.9642555,
|
||||
"height": 48,
|
||||
"width": 5,
|
||||
"source": "OSM node/4262612712 + recent-02.jpg",
|
||||
"estimatedPosition": true
|
||||
},
|
||||
{
|
||||
"id": "south-tower-2",
|
||||
"latitude": 37.4400359,
|
||||
"longitude": 126.9640016,
|
||||
"height": 36,
|
||||
"width": 3.2,
|
||||
"source": "OSM node/4736107159 + recent-02.jpg",
|
||||
"estimatedPosition": true
|
||||
},
|
||||
{
|
||||
"id": "south-tower-3",
|
||||
"latitude": 37.44012,
|
||||
"longitude": 126.96448,
|
||||
"height": 40,
|
||||
"width": 3,
|
||||
"source": "recent-02.jpg; approximate cluster offset",
|
||||
"estimatedPosition": true
|
||||
},
|
||||
{
|
||||
"id": "south-tower-4",
|
||||
"latitude": 37.44003,
|
||||
"longitude": 126.96472,
|
||||
"height": 34,
|
||||
"width": 2.4,
|
||||
"source": "recent-02.jpg; approximate cluster offset",
|
||||
"estimatedPosition": true
|
||||
}
|
||||
],
|
||||
"radar": {
|
||||
"latitude": 37.444028,
|
||||
"longitude": 126.963808,
|
||||
"radius": 4,
|
||||
"buildingHeight": 7,
|
||||
"source": "OSM way/337498781 (identity inferred), photo-01/09/10"
|
||||
},
|
||||
"decks": [],
|
||||
"railing": {
|
||||
"height": 1.15,
|
||||
"postWidth": 0.1,
|
||||
"maxSpan": 1.5
|
||||
},
|
||||
"deck": {
|
||||
"thickness": 0.18,
|
||||
"clearance": 0.25,
|
||||
"plankWidth": 0.18
|
||||
}
|
||||
},
|
||||
"terrainControls": [
|
||||
{
|
||||
"label": "시작 위치 암반",
|
||||
"latitude": 37.44472,
|
||||
"longitude": 126.96422,
|
||||
"elevation": 620
|
||||
},
|
||||
{
|
||||
"label": "북쪽 정상 암반",
|
||||
"latitude": 37.44503,
|
||||
"longitude": 126.96394,
|
||||
"elevation": 629
|
||||
},
|
||||
{
|
||||
"label": "정상 표지석 일대 암반",
|
||||
"latitude": 37.4451398,
|
||||
"longitude": 126.9642379,
|
||||
"elevation": 629
|
||||
},
|
||||
{
|
||||
"label": "정상에서 내려오는 넓은 석판",
|
||||
"latitude": 37.44494,
|
||||
"longitude": 126.9642,
|
||||
"elevation": 626
|
||||
},
|
||||
{
|
||||
"label": "중간 안부",
|
||||
"latitude": 37.4444,
|
||||
"longitude": 126.96402,
|
||||
"elevation": 606
|
||||
},
|
||||
{
|
||||
"label": "레이돔 암봉",
|
||||
"latitude": 37.444028,
|
||||
"longitude": 126.963808,
|
||||
"elevation": 628
|
||||
},
|
||||
{
|
||||
"label": "레이돔 북쪽 받침",
|
||||
"latitude": 37.44408,
|
||||
"longitude": 126.963808,
|
||||
"elevation": 628
|
||||
},
|
||||
{
|
||||
"label": "레이돔 남쪽 받침",
|
||||
"latitude": 37.443975,
|
||||
"longitude": 126.963808,
|
||||
"elevation": 627
|
||||
},
|
||||
{
|
||||
"label": "레이돔 동쪽 받침",
|
||||
"latitude": 37.444028,
|
||||
"longitude": 126.963875,
|
||||
"elevation": 627
|
||||
},
|
||||
{
|
||||
"label": "레이돔 서쪽 받침",
|
||||
"latitude": 37.444028,
|
||||
"longitude": 126.96374,
|
||||
"elevation": 627
|
||||
},
|
||||
{
|
||||
"label": "북동 절벽 하단",
|
||||
"latitude": 37.444145,
|
||||
"longitude": 126.963945,
|
||||
"elevation": 603
|
||||
},
|
||||
{
|
||||
"label": "북쪽 절벽 하단",
|
||||
"latitude": 37.444185,
|
||||
"longitude": 126.963805,
|
||||
"elevation": 608
|
||||
},
|
||||
{
|
||||
"label": "서쪽 첨봉 받침",
|
||||
"latitude": 37.444105,
|
||||
"longitude": 126.96358,
|
||||
"elevation": 617
|
||||
},
|
||||
{
|
||||
"label": "첨봉 북쪽 암릉",
|
||||
"latitude": 37.44424,
|
||||
"longitude": 126.96357,
|
||||
"elevation": 608
|
||||
},
|
||||
{
|
||||
"label": "첨봉 아래 안부",
|
||||
"latitude": 37.44438,
|
||||
"longitude": 126.96368,
|
||||
"elevation": 603
|
||||
},
|
||||
{
|
||||
"label": "암봉 북사면",
|
||||
"latitude": 37.44425,
|
||||
"longitude": 126.9639,
|
||||
"elevation": 610
|
||||
},
|
||||
{
|
||||
"label": "암봉 동사면",
|
||||
"latitude": 37.44403,
|
||||
"longitude": 126.96408,
|
||||
"elevation": 605
|
||||
},
|
||||
{
|
||||
"label": "암봉 서사면",
|
||||
"latitude": 37.44403,
|
||||
"longitude": 126.96353,
|
||||
"elevation": 604
|
||||
},
|
||||
{
|
||||
"label": "암봉 남사면",
|
||||
"latitude": 37.44382,
|
||||
"longitude": 126.96382,
|
||||
"elevation": 610
|
||||
},
|
||||
{
|
||||
"label": "동쪽 골짜기",
|
||||
"latitude": 37.44435,
|
||||
"longitude": 126.96465,
|
||||
"elevation": 582
|
||||
},
|
||||
{
|
||||
"label": "서쪽 사면",
|
||||
"latitude": 37.44435,
|
||||
"longitude": 126.96335,
|
||||
"elevation": 582
|
||||
}
|
||||
],
|
||||
"rockBoundary": [
|
||||
[
|
||||
-17,
|
||||
-59
|
||||
],
|
||||
[
|
||||
-6,
|
||||
-61
|
||||
],
|
||||
[
|
||||
4,
|
||||
-61
|
||||
],
|
||||
[
|
||||
11,
|
||||
-56
|
||||
],
|
||||
[
|
||||
15,
|
||||
-49
|
||||
],
|
||||
[
|
||||
15,
|
||||
-40
|
||||
],
|
||||
[
|
||||
15,
|
||||
-33
|
||||
],
|
||||
[
|
||||
12,
|
||||
-23
|
||||
],
|
||||
[
|
||||
11,
|
||||
-16
|
||||
],
|
||||
[
|
||||
10,
|
||||
-5
|
||||
],
|
||||
[
|
||||
9,
|
||||
6
|
||||
],
|
||||
[
|
||||
2,
|
||||
14
|
||||
],
|
||||
[
|
||||
-7,
|
||||
14
|
||||
],
|
||||
[
|
||||
-12,
|
||||
12
|
||||
],
|
||||
[
|
||||
-18,
|
||||
4
|
||||
],
|
||||
[
|
||||
-22,
|
||||
-4
|
||||
],
|
||||
[
|
||||
-25,
|
||||
-13
|
||||
],
|
||||
[
|
||||
-27,
|
||||
-22
|
||||
],
|
||||
[
|
||||
-26,
|
||||
-31
|
||||
],
|
||||
[
|
||||
-25,
|
||||
-40
|
||||
],
|
||||
[
|
||||
-22,
|
||||
-50
|
||||
]
|
||||
],
|
||||
"rockOutcrops": [
|
||||
{
|
||||
"latitude": 37.444028,
|
||||
"longitude": 126.963808,
|
||||
"radius": 14
|
||||
},
|
||||
{
|
||||
"latitude": 37.444105,
|
||||
"longitude": 126.96358,
|
||||
"radius": 11
|
||||
},
|
||||
{
|
||||
"latitude": 37.44424,
|
||||
"longitude": 126.96357,
|
||||
"radius": 9
|
||||
}
|
||||
]
|
||||
},
|
||||
"gisamunHarbour": {
|
||||
"source": "OSM way/583161999; inland closure and dimensions estimated",
|
||||
"coordinateSystem": "Local east X, south Z, metres; origin 38.00555 N, 128.73195 E",
|
||||
"parameters": {
|
||||
"topElevation": 3,
|
||||
"bottomElevation": -2,
|
||||
"shelterWaveScale": 0.25,
|
||||
"entranceBlendMeters": 18,
|
||||
"parapetHeight": 0.7,
|
||||
"parapetWidth": 0.65,
|
||||
"topColor": "#a2a198",
|
||||
"wallColor": "#737a78",
|
||||
"land": [
|
||||
[
|
||||
-28.051,
|
||||
-155.739
|
||||
],
|
||||
[
|
||||
-4.057,
|
||||
-150.511
|
||||
],
|
||||
[
|
||||
13.586,
|
||||
-144.984
|
||||
],
|
||||
[
|
||||
25.908,
|
||||
-151.71
|
||||
],
|
||||
[
|
||||
47.118,
|
||||
-140.822
|
||||
],
|
||||
[
|
||||
58.307,
|
||||
-147.348
|
||||
],
|
||||
[
|
||||
36.956,
|
||||
-163.498
|
||||
],
|
||||
[
|
||||
25.943,
|
||||
-167.849
|
||||
],
|
||||
[
|
||||
-12.129,
|
||||
-159.935
|
||||
],
|
||||
[
|
||||
-31.678,
|
||||
-173.088
|
||||
],
|
||||
[
|
||||
-37.272,
|
||||
-178.882
|
||||
],
|
||||
[
|
||||
-114.628,
|
||||
-175.93
|
||||
],
|
||||
[
|
||||
-129.038,
|
||||
-274.496
|
||||
],
|
||||
[
|
||||
-122.205,
|
||||
-279.746
|
||||
],
|
||||
[
|
||||
-121.933,
|
||||
-316.042
|
||||
],
|
||||
[
|
||||
-115.995,
|
||||
-349.851
|
||||
],
|
||||
[
|
||||
-100.196,
|
||||
-362.937
|
||||
],
|
||||
[
|
||||
-42.848,
|
||||
-363.359
|
||||
],
|
||||
[
|
||||
-35.024,
|
||||
-332.835
|
||||
],
|
||||
[
|
||||
-22.272,
|
||||
-302.31
|
||||
],
|
||||
[
|
||||
-15.94,
|
||||
-303.764
|
||||
],
|
||||
[
|
||||
-3.047,
|
||||
-271.731
|
||||
],
|
||||
[
|
||||
16.019,
|
||||
-247.7
|
||||
],
|
||||
[
|
||||
95.386,
|
||||
-163.543
|
||||
],
|
||||
[
|
||||
90.485,
|
||||
-155.029
|
||||
],
|
||||
[
|
||||
98.916,
|
||||
-150.723
|
||||
],
|
||||
[
|
||||
111.818,
|
||||
-152.21
|
||||
],
|
||||
[
|
||||
118.158,
|
||||
-165.841
|
||||
],
|
||||
[
|
||||
56.804,
|
||||
-238.465
|
||||
],
|
||||
[
|
||||
21.244,
|
||||
-279.578
|
||||
],
|
||||
[
|
||||
-4.857,
|
||||
-330.848
|
||||
],
|
||||
[
|
||||
-17.424,
|
||||
-401.508
|
||||
],
|
||||
[
|
||||
-24.81,
|
||||
-423.552
|
||||
],
|
||||
[
|
||||
-180,
|
||||
-423.552
|
||||
],
|
||||
[
|
||||
-180,
|
||||
-155.739
|
||||
]
|
||||
],
|
||||
"basin": [
|
||||
[
|
||||
58.307,
|
||||
-147.348
|
||||
],
|
||||
[
|
||||
36.956,
|
||||
-163.498
|
||||
],
|
||||
[
|
||||
25.943,
|
||||
-167.849
|
||||
],
|
||||
[
|
||||
-12.129,
|
||||
-159.935
|
||||
],
|
||||
[
|
||||
-31.678,
|
||||
-173.088
|
||||
],
|
||||
[
|
||||
-37.272,
|
||||
-178.882
|
||||
],
|
||||
[
|
||||
-114.628,
|
||||
-175.93
|
||||
],
|
||||
[
|
||||
-129.038,
|
||||
-274.496
|
||||
],
|
||||
[
|
||||
-122.205,
|
||||
-279.746
|
||||
],
|
||||
[
|
||||
-121.933,
|
||||
-316.042
|
||||
],
|
||||
[
|
||||
-115.995,
|
||||
-349.851
|
||||
],
|
||||
[
|
||||
-100.196,
|
||||
-362.937
|
||||
],
|
||||
[
|
||||
-42.848,
|
||||
-363.359
|
||||
],
|
||||
[
|
||||
-35.024,
|
||||
-332.835
|
||||
],
|
||||
[
|
||||
-22.272,
|
||||
-302.31
|
||||
],
|
||||
[
|
||||
-15.94,
|
||||
-303.764
|
||||
],
|
||||
[
|
||||
-3.047,
|
||||
-271.731
|
||||
],
|
||||
[
|
||||
16.019,
|
||||
-247.7
|
||||
],
|
||||
[
|
||||
95.386,
|
||||
-163.543
|
||||
]
|
||||
],
|
||||
"parapet": [
|
||||
[
|
||||
118.158,
|
||||
-165.841
|
||||
],
|
||||
[
|
||||
56.804,
|
||||
-238.465
|
||||
],
|
||||
[
|
||||
21.244,
|
||||
-279.578
|
||||
],
|
||||
[
|
||||
-4.857,
|
||||
-330.848
|
||||
],
|
||||
[
|
||||
-17.424,
|
||||
-401.508
|
||||
],
|
||||
[
|
||||
-24.81,
|
||||
-423.552
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1200" viewBox="0 0 1600 1200" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="sky" x2="0" y2="1200" gradientUnits="userSpaceOnUse"><stop stop-color="#101e36"/><stop offset=".5" stop-color="#71575d"/><stop offset=".76" stop-color="#efad7b"/><stop offset="1" stop-color="#f8cf9d"/></linearGradient>
|
||||
<radialGradient id="glow"><stop stop-color="#ffdfaa" stop-opacity=".65"/><stop offset="1" stop-color="#ffdfaa" stop-opacity="0"/></radialGradient>
|
||||
</defs>
|
||||
<path fill="url(#sky)" d="M0 0h1600v1200H0z"/>
|
||||
<circle cx="1120" cy="570" r="360" fill="url(#glow)"/>
|
||||
<circle cx="1120" cy="570" r="81" fill="#ffe4af"/>
|
||||
<path d="M0 845 160 735 310 790 520 610 730 765 940 690 1130 820 1370 635 1600 785v415H0Z" fill="#6b646e"/>
|
||||
<path d="m0 880 230-100 180 105 240-160 200 150 205-85 160 100 190-110 195 70v350H0Z" fill="#404b5d"/>
|
||||
<path d="m0 970 190-80 230 105 260-110 240 100 210-90 220 100 250-80v285H0Z" fill="#253548"/>
|
||||
<path d="m0 1090 250-100 250 75 320-60 260 90 230-60 290 75v90H0Z" fill="#142638"/>
|
||||
<g fill="#fff3d9" opacity=".6"><circle cx="215" cy="180" r="2"/><circle cx="750" cy="130" r="1.5"/><circle cx="1300" cy="250" r="2"/><circle cx="440" cy="340" r="1.5"/></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,12 @@
|
||||
<!doctype html><html lang="ko"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>관악산 구조물 모델 근거</title>
|
||||
<style>body{font:16px/1.8 sans-serif;max-width:800px;margin:40px auto;padding:20px;color:#24313d}table{border-collapse:collapse}td,th{border:1px solid #bbb;padding:10px}a{color:#285e93}</style>
|
||||
<h1>구조물 정교화 모델 7종</h1><p>OSM 위치와 공개 현장 사진을 참고한 기존 구조물을 Blender에서 다듬어 적용했습니다. 철탑의 가새·발판·안테나, 레이돔 패널·난간, 데크의 판재·계단을 보강했습니다. 모든 구조물 치수는 추정값이며 측량·준공 도면에 따른 재현이 아닙니다. 구조물로 인한 태양 가림은 이 모델의 형상을 따릅니다.</p>
|
||||
<table><tr><th>대상</th><th>근거와 한계</th></tr>
|
||||
<tr><td>정상 마스트</td><td>OSM node/3445797250 위치. 몸체 24m와 안테나 4m는 추정.</td></tr>
|
||||
<tr><td>남쪽 송신탑 4기</td><td>OSM node/4262612712, node/4736107159의 시설 대표점과 2026년 사진. 개별 위치·높이 34~48m 및 안테나는 추정.</td></tr>
|
||||
<tr><td>레이돔</td><td>OSM way/337498781의 원형 건물과 사진을 대조한 위치 후보. 지름 8m·받침 건물 높이 7m는 추정.</td></tr>
|
||||
<tr><td>북쪽 데크·계단</td><td>현장 사진을 참고한 3×7m 데크. 위치·방향·계단 길이·난간 높이 모두 추정.</td></tr>
|
||||
</table>
|
||||
<p>기초와 데크 지지대는 현재 30m DSM 표면에 맞췄습니다. 이 표면은 수목·구조물까지 포함하므로 실제 지반과 바닥 높이 검증이 필요합니다. WASD 지형 탐색이 가능합니다. 구조물 충돌과 데크 위 보행은 후속 단계입니다.</p>
|
||||
<p>지도: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors, ODbL</a>. 사진 참고: <a href="https://commons.wikimedia.org/wiki/Category:Gwanaksan">Commons</a>, <a href="https://ydk5118.tistory.com/16906972">2026년 산행기</a>, <a href="https://newrunbusy.tistory.com/15">북쪽 데크계단 산행기</a>. 사진 원본은 앱에 포함하지 않았습니다.</p><p><a href="/data-sources.html">자료별 라이선스·가공 데이터 다운로드</a></p></html>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 56" fill="none">
|
||||
<defs><linearGradient id="gold" x1="32" y1="10" x2="32" y2="45" gradientUnits="userSpaceOnUse"><stop stop-color="#ffe8b6"/><stop offset="1" stop-color="#d99955"/></linearGradient></defs>
|
||||
<path d="M17 37a15 15 0 1 1 30 0" fill="url(#gold)"/>
|
||||
<g stroke="#eed0a0" stroke-width="2.5" stroke-linecap="round"><path d="M32 4v5M11 13l4 4M53 13l-4 4M3 32h6M55 32h6"/></g>
|
||||
<path d="m5 43 12-5 10 3 12-6 10 5 10-2M14 50h36" stroke="url(#gold)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 589 B |
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dataset": "Copernicus GLO-30 + OSM gisamun coast",
|
||||
"surfaceType": "Resampled Copernicus DSM; no OSM height correction",
|
||||
"sourceResolution": "1 arc-second (~30m); interpolated to 10m",
|
||||
"verticalDatum": "EGM2008 orthometric height (m); sea level approximated as 0m",
|
||||
"origin": {
|
||||
"latitude": 38.00555,
|
||||
"longitude": 128.73195
|
||||
},
|
||||
"projection": "+proj=aeqd +lat_0=38.00555 +lon_0=128.73195 +datum=WGS84 +units=m +no_defs",
|
||||
"width": 301,
|
||||
"height": 301,
|
||||
"spacingMeters": 10,
|
||||
"radiusMeters": 1500,
|
||||
"observerSurfaceElevation": 0.0,
|
||||
"minElevation": -0.37622588872909546,
|
||||
"maxElevation": 145.99964904785156,
|
||||
"missingSamples": 0,
|
||||
"geographicCorners": [
|
||||
[
|
||||
128.7148738783932,
|
||||
37.99203481488981
|
||||
],
|
||||
[
|
||||
128.7490261216068,
|
||||
37.99203481488981
|
||||
],
|
||||
[
|
||||
128.7490323912247,
|
||||
38.0190626735617
|
||||
],
|
||||
[
|
||||
128.71486760877534,
|
||||
38.0190626735617
|
||||
]
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"file": "data/raw/hajodae/Copernicus_DSM_COG_10_N37_00_E128_00_DEM.tif",
|
||||
"url": "https://copernicus-dem-30m.s3.amazonaws.com/Copernicus_DSM_COG_10_N37_00_E128_00_DEM/Copernicus_DSM_COG_10_N37_00_E128_00_DEM.tif",
|
||||
"bytes": 45141829,
|
||||
"sha256": "69036070795162395dcea060375abba2d448dc2a5b74949844784466191b707c",
|
||||
"crs": "EPSG:4326",
|
||||
"bounds": [
|
||||
127.99986111111112,
|
||||
37.00013888888889,
|
||||
128.9998611111111,
|
||||
38.00013888888889
|
||||
],
|
||||
"shape": [
|
||||
3600,
|
||||
3600
|
||||
],
|
||||
"resolutionDegrees": [
|
||||
0.0002777777777777778,
|
||||
0.0002777777777777778
|
||||
],
|
||||
"nodata": null,
|
||||
"min": -3.7442760467529297,
|
||||
"max": 1574.278564453125,
|
||||
"maskedSamples": 0,
|
||||
"nonfiniteSamples": 0
|
||||
},
|
||||
{
|
||||
"file": "data/raw/hajodae/Copernicus_DSM_COG_10_N38_00_E128_00_DEM.tif",
|
||||
"url": "https://copernicus-dem-30m.s3.amazonaws.com/Copernicus_DSM_COG_10_N38_00_E128_00_DEM/Copernicus_DSM_COG_10_N38_00_E128_00_DEM.tif",
|
||||
"bytes": 18904152,
|
||||
"sha256": "ef70790d1e4c889f6a0965b7f39fb8627e5682ddad7cc893cf61084af8c7c012",
|
||||
"crs": "EPSG:4326",
|
||||
"bounds": [
|
||||
127.99986111111112,
|
||||
38.00013888888889,
|
||||
128.9998611111111,
|
||||
39.00013888888889
|
||||
],
|
||||
"shape": [
|
||||
3600,
|
||||
3600
|
||||
],
|
||||
"resolutionDegrees": [
|
||||
0.0002777777777777778,
|
||||
0.0002777777777777778
|
||||
],
|
||||
"nodata": null,
|
||||
"min": -0.680739164352417,
|
||||
"max": 1703.6204833984375,
|
||||
"maskedSamples": 0,
|
||||
"nonfiniteSamples": 0
|
||||
},
|
||||
{
|
||||
"file": "data/raw/gisamun/coast.osm",
|
||||
"url": "https://www.openstreetmap.org/api/0.6/map?bbox=128.720,37.975,128.770,38.025",
|
||||
"bytes": 2680764,
|
||||
"sha256": "0b59c2383c6ebfa5e6bc95dfcd21d0a7bce2406fecbb7e2a8d245d35a45e1bb4"
|
||||
}
|
||||
],
|
||||
"attribution": "produced using Copernicus WorldDEM-30 © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all rights reserved; © OpenStreetMap contributors (ODbL-1.0)",
|
||||
"corrections": {
|
||||
"osmHeightCorrection": false,
|
||||
"note": "Heights use Copernicus reprojection and bilinear resampling only. OSM masks affect rendering and movement, not stored elevations. Offshore values are not bathymetry. No tide model."
|
||||
},
|
||||
"files": {
|
||||
"heights.f32": {
|
||||
"source": "Copernicus GLO-30",
|
||||
"processing": "AEQD projection and bilinear resampling only; no OSM height adjustment",
|
||||
"licenseUrl": "https://dataspace.copernicus.eu/sites/default/files/media/files/2025-06/copernicus_contributing_mission_data_access_v2_cop_dem_licenses.pdf",
|
||||
"licenseSection": "GLO-30-F, Articles 4 and 6",
|
||||
"notice": "source.html"
|
||||
},
|
||||
"coast.json": {
|
||||
"license": "ODbL-1.0",
|
||||
"licenseUrl": "https://opendatacommons.org/licenses/odbl/1-0/",
|
||||
"attribution": "© OpenStreetMap contributors",
|
||||
"sourceUrl": "https://www.openstreetmap.org/copyright",
|
||||
"processing": "Projected coastline and beach polygons; estimated beach strip for Gisamun"
|
||||
},
|
||||
"surface.png": {
|
||||
"license": "ODbL-1.0",
|
||||
"licenseUrl": "https://opendatacommons.org/licenses/odbl/1-0/",
|
||||
"attribution": "© OpenStreetMap contributors",
|
||||
"sourceUrl": "https://www.openstreetmap.org/copyright",
|
||||
"processing": "R: land mask; G: sand mask; B: signed coast distance; contains no Copernicus elevations"
|
||||
}
|
||||
},
|
||||
"sandApproximation": {
|
||||
"sandLatitudeMin": 38.0043299,
|
||||
"sandLatitudeMax": 38.0064958,
|
||||
"sandWidthMeters": 35,
|
||||
"note": "OSM beach polygon absent. Visual strip west of OSM shoreline; not surveyed beach limits."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<!doctype html>
|
||||
<html lang="ko"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>기사문 해변 — 데이터 출처와 표현 범위</title>
|
||||
<style>body{max-width:850px;margin:48px auto;padding:0 24px;font:16px/1.8 system-ui,sans-serif;color:#e3eaf0;background:#101b25}a{color:#9ed4ef}h1,h2{line-height:1.3}li{margin:10px 0}</style>
|
||||
<h1>기사문 해변의 데이터와 표현 범위</h1>
|
||||
<p>2026-10-29 가공. 관측점은 기사문항 남쪽 해변의 위도 38.00555°, 경도 128.73195° 임시값입니다. 현장 측량 좌표가 아닙니다.</p>
|
||||
<h2>확보 자료와 가공</h2>
|
||||
<ul>
|
||||
<li>Copernicus GLO-30 N37/E128, N38/E128 두 타일을 병합하고 관측점 중심 3×3km를 추출했습니다. 30m급 원본을 10m 간격으로 보간한 높이 격자는 362,404 bytes입니다. 원본 정확도가 높아지는 것은 아닙니다.</li>
|
||||
<li>OSM 해안선 두 구간을 공유 끝점으로 연결하고 주변 섬을 보존했습니다. 표면 마스크는 2m 간격이며 실제 측량 정확도를 의미하지 않습니다.</li>
|
||||
<li><strong>기사문 모래사장 영역은 OSM에 누락되어 있습니다.</strong> 위도 38.0043299–38.0064958의 해안선 구간을 서쪽으로 35m 넓혀 모래색 영역으로 근사했습니다. 실제 모래 경계나 폭으로 해석하면 안 됩니다.</li>
|
||||
<li>높이·해안선·표면 마스크·메타데이터 합계 약 416KB입니다. 약 64MB의 원본은 브라우저로 보내지 않습니다.</li>
|
||||
</ul>
|
||||
<h2>일출 관측과 한계</h2>
|
||||
<p>2026-10-30 기본 관측점을 북쪽 방파제 진입부의 임시 위치(데이터 원점에서 서쪽 30m, 북쪽 350m)로 옮기고 조도를 바라보도록 설정했습니다. 기존 해변 좌표는 지형 데이터 원점으로 유지합니다. 카메라는 추정 부두 상부 해발 3m에 눈높이를 더해 배치합니다. 날짜별 태양 위치는 그대로 계산하므로 항상 조도와 겹치는 것은 아닙니다.</p>
|
||||
<p>2026-10-29 06:52, 06:55, 07:00 KST의 태양 중심 방향이 이 지형 모델에 가리지 않는 것을 검사했습니다. 06:55에는 브라우저에서 원반을 확인했습니다. 이는 모델 검증이며 현장 일출의 실측 검증이나 모든 날짜의 무차폐 보장이 아닙니다.</p>
|
||||
<ul>
|
||||
<li>높이 격자는 Copernicus 자료를 좌표 변환·보간한 값입니다. OSM 해안선을 이용한 높이 낮추기, 최소 육지 높이 적용, 바다 칸의 0m 덮어쓰기를 하지 않습니다. OSM 경계와 DSM 높이가 맞지 않아 해안에 단차가 나타날 수 있습니다.</li>
|
||||
<li>2026-10-30 기사문항에 OSM way/583161999의 해안 윤곽을 따르는 콘크리트 방파제·안벽·포장면을 추가했습니다. 해당 구간은 DSM 지형 대신 별도 메시로 표현합니다. 상부 해발 3m, 수면 아래 2m까지의 벽, 외측 난간벽 0.7m와 내륙 포장 경계는 시각적 추정값이며 실측 설계가 아닙니다. 건물·등대·선박·소파블록은 포함하지 않습니다.</li>
|
||||
<li>방파제 안쪽 물결은 외해의 25% 강도로 근사하고 항구 입구에서 18m에 걸쳐 연결합니다. 파랑 해석이나 실제 항만 정온도 자료가 아닙니다. DSM은 수목·건물을 포함하며 작은 섬·절벽의 세부 높이까지 재현하지 못합니다.</li>
|
||||
<li>바다 칸도 Copernicus 보간값을 유지하며 수심이 아닙니다. OSM 마스크는 화면 표시·재질·이동 경계에 사용하며 높이 격자를 수정하지 않습니다. 물결·거품·반사는 시각 효과이고 실제 조석·파고·날씨는 반영하지 않습니다.</li>
|
||||
<li>3×3km 바깥은 육지를 표현하지 않고 바다 표면을 연장합니다. 먼 육지의 조망·차폐는 추가 자료가 필요합니다.</li>
|
||||
</ul>
|
||||
<h2>출처</h2>
|
||||
<p><a href="metadata.json">가공 설정·원본 URL·SHA256</a> · <a href="coast.json">가공 해안선</a></p>
|
||||
<p>© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>, ODbL-1.0. <a href="https://www.openstreetmap.org/api/0.6/map?bbox=128.720,37.975,128.770,38.025">OSM 원본 요청</a></p>
|
||||
<p><a href="https://copernicus-dem-30m.s3.amazonaws.com/readme.html">Copernicus 공개 배포·이용조건</a></p>
|
||||
<p>produced using Copernicus WorldDEM-30 © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all rights reserved</p>
|
||||
<p>The organisations in charge of the Copernicus programme by law or by delegation do not incur any liability for any use of the Copernicus WorldDEM-30.</p>
|
||||
<p><a href="https://dataspace.copernicus.eu/sites/default/files/media/files/2025-06/copernicus_contributing_mission_data_access_v2_cop_dem_licenses.pdf">Copernicus 원문 라이선스 (GLO-30-F, 22~24쪽)</a>. 가공 지형을 재배포할 때에도 지정 출처·면책 고지와 제6조의 의무를 유지해야 합니다.</p>
|
||||
<p><a href="https://tour.yangyang.go.kr/pub/yysea?mode=v&seq=151">양양군 38해변(기사문해변) 안내</a></p>
|
||||
<p><a href="/?site=gisamun">기사문 시뮬레이터로 돌아가기</a></p>
|
||||
<p><a href="/data-sources.html">자료별 라이선스·가공 데이터 다운로드</a></p></html>
|
||||
|
After Width: | Height: | Size: 57 KiB |
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dataset": "Copernicus DEM GLO-30 Public (2021 release)",
|
||||
"surfaceType": "DSM",
|
||||
"sourceResolution": "1 arc-second (~30m)",
|
||||
"verticalDatum": "EGM2008 orthometric height (m)",
|
||||
"origin": {
|
||||
"latitude": 37.445,
|
||||
"longitude": 126.964
|
||||
},
|
||||
"projection": "+proj=aeqd +lat_0=37.445 +lon_0=126.964 +datum=WGS84 +units=m +no_defs",
|
||||
"projectionWkt": "PROJCRS[\"unknown\",BASEGEOGCRS[\"unknown\",DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]],ID[\"EPSG\",6326]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],CONVERSION[\"unknown\",METHOD[\"Azimuthal Equidistant\",ID[\"EPSG\",1125]],PARAMETER[\"Latitude of natural origin\",37.445,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",126.964,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"False easting\",0,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",0,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]]",
|
||||
"width": 801,
|
||||
"height": 801,
|
||||
"spacingMeters": 30,
|
||||
"radiusMeters": 12000,
|
||||
"layout": "row-major north-to-south, west-to-east, float32 little-endian",
|
||||
"heightFile": "gwanaksan.f32",
|
||||
"geographicCorners": [
|
||||
[
|
||||
126.82858665147765,
|
||||
37.336799644387234
|
||||
],
|
||||
[
|
||||
127.09941334852236,
|
||||
37.336799644387234
|
||||
],
|
||||
[
|
||||
127.09980363427715,
|
||||
37.55304278295426
|
||||
],
|
||||
[
|
||||
126.82819636572286,
|
||||
37.55304278295426
|
||||
]
|
||||
],
|
||||
"observerSurfaceElevation": 606.9063720703125,
|
||||
"minElevation": 1.314314842224121,
|
||||
"maxElevation": 618.474609375,
|
||||
"missingSamples": 0,
|
||||
"sources": [
|
||||
{
|
||||
"file": "Copernicus_DSM_COG_10_N37_00_E126_00_DEM.tif",
|
||||
"url": "https://copernicus-dem-30m.s3.amazonaws.com/Copernicus_DSM_COG_10_N37_00_E126_00_DEM/Copernicus_DSM_COG_10_N37_00_E126_00_DEM.tif",
|
||||
"bytes": 29862574,
|
||||
"sha256": "f530dd28308ecd6c5ceb0405dc1ffb7113bcafdec30d023de50f16dc433a274f",
|
||||
"crs": "EPSG:4326",
|
||||
"bounds": [
|
||||
125.99986111111112,
|
||||
37.00013888888889,
|
||||
126.99986111111112,
|
||||
38.00013888888889
|
||||
],
|
||||
"shape": [
|
||||
3600,
|
||||
3600
|
||||
],
|
||||
"pixelSizeDegrees": [
|
||||
0.0002777777777777778,
|
||||
0.0002777777777777778
|
||||
],
|
||||
"nodata": null
|
||||
},
|
||||
{
|
||||
"file": "Copernicus_DSM_COG_10_N37_00_E127_00_DEM.tif",
|
||||
"url": "https://copernicus-dem-30m.s3.amazonaws.com/Copernicus_DSM_COG_10_N37_00_E127_00_DEM/Copernicus_DSM_COG_10_N37_00_E127_00_DEM.tif",
|
||||
"bytes": 49403795,
|
||||
"sha256": "4f8fef79da8c18ed9a9a0fb97b3d2695c4681181e013e039d87044e5479f7df6",
|
||||
"crs": "EPSG:4326",
|
||||
"bounds": [
|
||||
126.99986111111112,
|
||||
37.00013888888889,
|
||||
127.99986111111112,
|
||||
38.00013888888889
|
||||
],
|
||||
"shape": [
|
||||
3600,
|
||||
3600
|
||||
],
|
||||
"pixelSizeDegrees": [
|
||||
0.0002777777777777778,
|
||||
0.0002777777777777778
|
||||
],
|
||||
"nodata": null
|
||||
}
|
||||
],
|
||||
"attribution": "produced using Copernicus WorldDEM-30 © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all rights reserved"
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dataset": "Copernicus GLO-30 + OSM hajodae coast",
|
||||
"surfaceType": "Resampled Copernicus DSM; no OSM height correction",
|
||||
"sourceResolution": "1 arc-second (~30m); interpolated to 10m",
|
||||
"verticalDatum": "EGM2008 orthometric height (m); sea level approximated as 0m",
|
||||
"origin": {
|
||||
"latitude": 38.020813,
|
||||
"longitude": 128.7352564
|
||||
},
|
||||
"projection": "+proj=aeqd +lat_0=38.020813 +lon_0=128.7352564 +datum=WGS84 +units=m +no_defs",
|
||||
"width": 301,
|
||||
"height": 301,
|
||||
"spacingMeters": 10,
|
||||
"radiusMeters": 1500,
|
||||
"observerSurfaceElevation": 31.851652145385742,
|
||||
"minElevation": 0.0,
|
||||
"maxElevation": 123.29191589355469,
|
||||
"missingSamples": 0,
|
||||
"geographicCorners": [
|
||||
[
|
||||
128.71817673888106,
|
||||
38.00729784938007
|
||||
],
|
||||
[
|
||||
128.7523360611189,
|
||||
38.00729784938007
|
||||
],
|
||||
[
|
||||
128.75234233547536,
|
||||
38.03432563771381
|
||||
],
|
||||
[
|
||||
128.7181704645246,
|
||||
38.03432563771381
|
||||
]
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"file": "data/raw/hajodae/coast.osm",
|
||||
"url": "https://www.openstreetmap.org/api/0.6/map?bbox=128.700,38.005,128.750,38.045",
|
||||
"bytes": 2249397,
|
||||
"sha256": "b6bdbd87e466d5e726f755cd9f9bfc91a01c1a2f2f3fd9db06bcc609d7b22079"
|
||||
},
|
||||
{
|
||||
"file": "data/raw/hajodae/Copernicus_DSM_COG_10_N38_00_E128_00_DEM.tif",
|
||||
"url": "https://copernicus-dem-30m.s3.amazonaws.com/Copernicus_DSM_COG_10_N38_00_E128_00_DEM/Copernicus_DSM_COG_10_N38_00_E128_00_DEM.tif",
|
||||
"bytes": 18904152,
|
||||
"sha256": "ef70790d1e4c889f6a0965b7f39fb8627e5682ddad7cc893cf61084af8c7c012",
|
||||
"crs": "EPSG:4326",
|
||||
"bounds": [
|
||||
127.99986111111112,
|
||||
38.00013888888889,
|
||||
128.9998611111111,
|
||||
39.00013888888889
|
||||
],
|
||||
"shape": [
|
||||
3600,
|
||||
3600
|
||||
],
|
||||
"resolutionDegrees": [
|
||||
0.0002777777777777778,
|
||||
0.0002777777777777778
|
||||
],
|
||||
"nodata": null,
|
||||
"min": -0.680739164352417,
|
||||
"max": 1703.6204833984375,
|
||||
"maskedSamples": 0,
|
||||
"nonfiniteSamples": 0
|
||||
}
|
||||
],
|
||||
"attribution": "produced using Copernicus WorldDEM-30 © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all rights reserved; © OpenStreetMap contributors (ODbL-1.0)",
|
||||
"corrections": {
|
||||
"osmHeightCorrection": false,
|
||||
"note": "Heights use Copernicus reprojection and bilinear resampling only. OSM masks affect rendering and movement, not stored elevations. Offshore values are not bathymetry. No tide model."
|
||||
},
|
||||
"files": {
|
||||
"heights.f32": {
|
||||
"source": "Copernicus GLO-30",
|
||||
"processing": "AEQD projection and bilinear resampling only; no OSM height adjustment",
|
||||
"licenseUrl": "https://dataspace.copernicus.eu/sites/default/files/media/files/2025-06/copernicus_contributing_mission_data_access_v2_cop_dem_licenses.pdf",
|
||||
"licenseSection": "GLO-30-F, Articles 4 and 6",
|
||||
"notice": "source.html"
|
||||
},
|
||||
"coast.json": {
|
||||
"license": "ODbL-1.0",
|
||||
"licenseUrl": "https://opendatacommons.org/licenses/odbl/1-0/",
|
||||
"attribution": "© OpenStreetMap contributors",
|
||||
"sourceUrl": "https://www.openstreetmap.org/copyright",
|
||||
"processing": "Projected coastline and beach polygons; estimated beach strip for Gisamun"
|
||||
},
|
||||
"surface.png": {
|
||||
"license": "ODbL-1.0",
|
||||
"licenseUrl": "https://opendatacommons.org/licenses/odbl/1-0/",
|
||||
"attribution": "© OpenStreetMap contributors",
|
||||
"sourceUrl": "https://www.openstreetmap.org/copyright",
|
||||
"processing": "R: land mask; G: sand mask; B: signed coast distance; contains no Copernicus elevations"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html lang="ko"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>하조대 해안 — 데이터 출처와 표현 범위</title>
|
||||
<style>body{max-width:850px;margin:48px auto;padding:0 24px;font:16px/1.8 system-ui,sans-serif;color:#e3eaf0;background:#101b25}a{color:#9ed4ef}code{color:#edcda7}h1,h2{line-height:1.3}li{margin:10px 0}</style>
|
||||
<h1>하조대 해안의 데이터와 표현 범위</h1>
|
||||
<p>2026-10-30 정자 기준으로 재가공. 관측점은 OSM 하조대 정자(node/12019797394)의 위도 38.020813°, 경도 128.7352564°입니다. 시작·복귀 시 소나무 참고 위치(38°01.252′ N, 128°44.134′ E)를 향하는 약 78° 방위로 수평 시선을 맞춥니다. 좌표와 정자 바닥 높이는 현장 측량으로 검증하지 않았습니다.</p>
|
||||
<h2>웹용 자료</h2>
|
||||
<ul>
|
||||
<li>관측점 주변 3×3km. Copernicus 30m급 DSM을 10m 간격 301×301로 보간했습니다. 높이 파일 362,404 bytes. 보간은 실제 지형 정확도를 높이지 않습니다.</li>
|
||||
<li>OSM 해안선과 해변 영역을 AEQD 로컬 좌표로 변환했습니다. 원본 해안선을 보존한 육지 판정과 2m 간격 PNG 표면 마스크를 사용합니다. 마스크 간격은 측량 정확도가 아닙니다.</li>
|
||||
<li>표면 마스크 56,357 bytes, 해안선 JSON 20,003 bytes. 원본 GeoTIFF 약 64MB를 브라우저로 보내지 않습니다.</li>
|
||||
</ul>
|
||||
<h2>시각적 보정과 한계</h2>
|
||||
<ul>
|
||||
<li>DSM은 수목·건물을 포함하는 표면고도입니다. 높이 격자는 Copernicus 자료를 좌표 변환·보간한 값이며 OSM 경계로 수정하지 않습니다.</li>
|
||||
<li>해안선 주변 높이 낮추기, 최소 육지 높이 적용, 바다 칸의 0m 덮어쓰기를 하지 않습니다. 정자 좌표의 가공 DSM은 약 31.85m이며 실제 정자 바닥 표고가 아닙니다. 카메라는 렌더링 지면에 눈높이를 더해 배치합니다.</li>
|
||||
<li>높이 격자는 바다 칸도 Copernicus 보간값을 유지하며 수심으로 해석하지 않습니다. OSM 마스크는 화면에서 바다 영역의 지형을 숨기는 데 사용합니다. 육지·바다 재질은 동일한 마스크를 사용합니다. OSM 경계와 DSM 높이가 맞지 않아 해안에 단차가 나타날 수 있습니다. 해안선의 세부 높이는 10m 격자의 보간값으로 실제 절벽이나 모래턱을 정밀하게 표현하지 못합니다.</li>
|
||||
<li>바다는 EGM2008 기준 0m로 근사하고 지구 곡률을 적용합니다. 물결·거품·반사는 절차적 시각 효과입니다. 실제 조석·파고·해저 지형·날씨는 반영하지 않습니다.</li>
|
||||
<li>3×3km 바깥의 육지는 표현하지 않으며 바다 표면만 연장합니다. 먼 해안의 정확한 조망이나 실측 일출 검증용 장면은 아닙니다.</li>
|
||||
<li>OSM의 작은 섬도 육지 경계로 보존하지만 30m급 높이 원본에 없는 암반 형상·높이는 재현하지 못합니다. 등대·정자·소나무 모델은 포함하지 않습니다. 소나무 참고 좌표는 초기 시선의 방위를 정하는 용도이며 수목의 높이나 모양을 나타내지 않습니다.</li>
|
||||
<li>바다 진입을 막으며 육지에서만 이동합니다. 일출·일몰 표시는 근사 지면 높이를 사용한 천문 계산으로, 실제 지형 위 첫 태양 관측 시각과 다를 수 있습니다.</li>
|
||||
</ul>
|
||||
<h2>출처</h2>
|
||||
<p>관측점: <a href="https://www.openstreetmap.org/node/12019797394">OSM 하조대 정자</a>. 소나무 방향 참고: <a href="https://files-scs.pstatic.net/2025/10/19/jzjd280prT/%EC%9D%B4%EC%95%BC%EA%B8%B0%EA%B0%80%20%EC%9E%88%EB%8A%94%20%EB%B3%B4%ED%98%B8%EC%88%98_1_2013.pdf">이야기가 있는 보호수(2013) 자료집</a>. 소나무 위치는 참고 좌표이며 현장 재측량값이 아닙니다.</p>
|
||||
<p><a href="https://copernicus-dem-30m.s3.amazonaws.com/readme.html">Copernicus GLO-30 공개 배포</a> · <a href="https://dataspace.copernicus.eu/explore-data/data-collections/copernicus-contributing-missions/collections-description/COP-DEM">공식 데이터·이용조건</a></p>
|
||||
<p>produced using Copernicus WorldDEM-30 © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all rights reserved</p>
|
||||
<p>The organisations in charge of the Copernicus programme by law or by delegation do not incur any liability for any use of the Copernicus WorldDEM-30.</p>
|
||||
<p><a href="https://dataspace.copernicus.eu/sites/default/files/media/files/2025-06/copernicus_contributing_mission_data_access_v2_cop_dem_licenses.pdf">Copernicus 원문 라이선스 (GLO-30-F, 22~24쪽)</a>. 가공 지형을 재배포할 때에도 지정 출처·면책 고지와 제6조의 의무를 유지해야 합니다.</p>
|
||||
<p>© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>, ODbL-1.0. <a href="coast.json">가공 해안선</a> · <a href="metadata.json">원본 URL·SHA256·가공 설정</a> · <a href="https://www.openstreetmap.org/api/0.6/map?bbox=128.700,38.005,128.750,38.045">OSM 원본 요청</a></p>
|
||||
<p><a href="https://tour.yangyang.go.kr/pub/yy10view.do?mode=v&seq=120">양양군 하조대해변 안내</a> · <a href="https://tour.yangyang.go.kr/pub/yy10view.do?mode=v&seq=172">하조대 안내</a></p>
|
||||
<p><a href="/?site=hajodae">하조대 시뮬레이터로 돌아가기</a></p>
|
||||
<p><a href="/data-sources.html">자료별 라이선스·가공 데이터 다운로드</a></p></html>
|
||||
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 428 KiB |
|
After Width: | Height: | Size: 564 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 713 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 839 KiB |
|
After Width: | Height: | Size: 784 KiB |
|
After Width: | Height: | Size: 652 KiB |
@@ -0,0 +1,83 @@
|
||||
[
|
||||
{
|
||||
"file": "path-color.jpg",
|
||||
"asset": "dirt",
|
||||
"source": "https://polyhaven.com/a/dirt",
|
||||
"url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/dirt/dirt_diff_1k.jpg",
|
||||
"license": "CC0-1.0",
|
||||
"bytes": 730425,
|
||||
"md5": "cb37e24797597b1dd1ae3819fa80f076"
|
||||
},
|
||||
{
|
||||
"file": "path-normal.jpg",
|
||||
"asset": "dirt",
|
||||
"source": "https://polyhaven.com/a/dirt",
|
||||
"url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/dirt/dirt_nor_gl_1k.jpg",
|
||||
"license": "CC0-1.0",
|
||||
"bytes": 1202783,
|
||||
"md5": "362610edf09fd31735958eecec823a84"
|
||||
},
|
||||
{
|
||||
"file": "path-roughness.jpg",
|
||||
"asset": "dirt",
|
||||
"source": "https://polyhaven.com/a/dirt",
|
||||
"url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/dirt/dirt_rough_1k.jpg",
|
||||
"license": "CC0-1.0",
|
||||
"bytes": 253549,
|
||||
"md5": "d936d6760ce0f899b131d30626d52ce9"
|
||||
},
|
||||
{
|
||||
"file": "rock-color.jpg",
|
||||
"asset": "rock_boulder_dry",
|
||||
"source": "https://polyhaven.com/a/rock_boulder_dry",
|
||||
"url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/rock_boulder_dry/rock_boulder_dry_diff_1k.jpg",
|
||||
"license": "CC0-1.0",
|
||||
"bytes": 859211,
|
||||
"md5": "da794cd9be3db91e3160cf081109f322"
|
||||
},
|
||||
{
|
||||
"file": "rock-normal.jpg",
|
||||
"asset": "rock_boulder_dry",
|
||||
"source": "https://polyhaven.com/a/rock_boulder_dry",
|
||||
"url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/rock_boulder_dry/rock_boulder_dry_nor_gl_1k.jpg",
|
||||
"license": "CC0-1.0",
|
||||
"bytes": 802497,
|
||||
"md5": "6f1c575ceed560ae253f65518cf748e2"
|
||||
},
|
||||
{
|
||||
"file": "rock-roughness.jpg",
|
||||
"asset": "rock_boulder_dry",
|
||||
"source": "https://polyhaven.com/a/rock_boulder_dry",
|
||||
"url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/rock_boulder_dry/rock_boulder_dry_rough_1k.jpg",
|
||||
"license": "CC0-1.0",
|
||||
"bytes": 668127,
|
||||
"md5": "0c8b15df3e31c15256da5f8e1b0d0759"
|
||||
},
|
||||
{
|
||||
"file": "forest-color.jpg",
|
||||
"asset": "forest_leaves_02",
|
||||
"source": "https://polyhaven.com/a/forest_leaves_02",
|
||||
"url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/forest_leaves_02/forest_leaves_02_diffuse_1k.jpg",
|
||||
"license": "CC0-1.0",
|
||||
"bytes": 437891,
|
||||
"md5": "13e9bfac07cb72ca143e153cc2f589a8"
|
||||
},
|
||||
{
|
||||
"file": "forest-normal.jpg",
|
||||
"asset": "forest_leaves_02",
|
||||
"source": "https://polyhaven.com/a/forest_leaves_02",
|
||||
"url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/forest_leaves_02/forest_leaves_02_nor_gl_1k.jpg",
|
||||
"license": "CC0-1.0",
|
||||
"bytes": 577163,
|
||||
"md5": "72002432222467c74cd9980cbddb25a4"
|
||||
},
|
||||
{
|
||||
"file": "forest-roughness.jpg",
|
||||
"asset": "forest_leaves_02",
|
||||
"source": "https://polyhaven.com/a/forest_leaves_02",
|
||||
"url": "https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/forest_leaves_02/forest_leaves_02_rough_1k.jpg",
|
||||
"license": "CC0-1.0",
|
||||
"bytes": 192523,
|
||||
"md5": "578e637510d8fdd7acf97afc6f329e9d"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>관악산 지형 데이터 출처</title>
|
||||
<style>body{max-width:760px;margin:48px auto;padding:0 24px;background:#101823;color:#e4eaf0;font:16px/1.8 system-ui,sans-serif}a{color:#eec99d}h1{font-size:24px}small{color:#a8bac8}code{overflow-wrap:anywhere}</style></head>
|
||||
<body><h1>관악산 지형 데이터</h1>
|
||||
<p>Copernicus DEM GLO-30 Public, AWS 배포 2021 release. 원본 N37 E126 및 N37 E127 타일에서 임시 관측점 37.4450° N, 126.9640° E 중심 24×24km를 추출했습니다.</p>
|
||||
<ul><li>원본: WGS84 경위도 1 arc-second 격자(명목 30m급), 높이 EGM2008 기준(m).</li>
|
||||
<li>화면: 관측점 중심 AEQD 투영, 30m 간격으로 bilinear 재표본화한 801×801 격자. 색상은 높이·경사 기반 표현이며 항공사진이 아닙니다.</li>
|
||||
<li>DSM은 수목과 구조물 높이를 포함할 수 있습니다. 별도 VWorld 건물과 추정 정상 구조물은 각 출처 안내를 따릅니다.</li>
|
||||
<li>카메라: 이 격자의 관측점 표면 606.906m + 눈높이(기본 1.7m, 1.60~1.95m 조절). 현장 측량값이 아니며 기존 임시 629m와 다릅니다.</li>
|
||||
<li>지구 곡률은 평균 반지름 6,371,008.8m로 근사하고, 지상 대기굴절은 반영하지 않습니다.</li>
|
||||
<li>영역 밖은 단순 해수면 기준 지평선으로 표시합니다. 그 너머 실제 능선과 수목·건물의 세부 차폐를 검증한 결과가 아닙니다.</li>
|
||||
<li>일출·일몰 숫자는 고도 보정 천문 시각입니다. 지형 위로 태양이 처음 보이는 시각을 의미하지 않습니다.</li></ul>
|
||||
<p>produced using Copernicus WorldDEM-30 © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018 provided under COPERNICUS by the European Union and ESA; all rights reserved</p>
|
||||
<p>The organisations in charge of the Copernicus programme by law or by delegation do not incur any liability for any use of the Copernicus WorldDEM-30.</p>
|
||||
<p>가공 지형을 재배포할 때에도 지정 출처·면책 고지와 원문 라이선스 제6조의 의무를 유지해야 합니다.</p>
|
||||
<p><a href="https://registry.opendata.aws/copernicus-dem/">공식 배포 안내</a> · <a href="https://copernicus-dem-30m.s3.amazonaws.com/readme.html">COG 파일 명세</a> · <a href="https://dataspace.copernicus.eu/sites/default/files/media/files/2025-06/copernicus_contributing_mission_data_access_v2_cop_dem_licenses.pdf">원문 라이선스 (GLO-30-F, 22~24쪽)</a> · <a href="gwanaksan.json">메타데이터·원본 URL·SHA256</a></p>
|
||||
<small>다운로드·전처리: 2026-10-29. 원본 파일은 프로젝트 data/raw/copernicus에 보관하며 웹에는 가공 격자만 제공합니다.</small></body><p><a href="/data-sources.html">자료별 라이선스·가공 데이터 다운로드</a></p></html>
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sourceHash": "8f7306b64c88b4d706c8bd8ecce0385fe1f5f64502123735b13d7affc2cb8ba7",
|
||||
"bufferHash": "59f836d4ff44c1f047c4165c99672fc9e7a04f9981f15561f4277754577d6e5a",
|
||||
"byteLength": 15601904,
|
||||
"gzipBytes": 6138340,
|
||||
"originalVertices": 1180560,
|
||||
"vertices": 209224,
|
||||
"triangles": 393520,
|
||||
"originalAttributeBytes": 61389120,
|
||||
"generateMs": 3773.8809,
|
||||
"decodeMs": 13.8812999999991
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<!doctype html><html lang="ko"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>관악산 정상 주변 추정 지형</title>
|
||||
<style>body{max-width:850px;margin:40px auto;padding:0 24px;font:16px/1.8 system-ui;color:#27332e}a{color:#25654e}table{border-collapse:collapse}td,th{padding:8px;border:1px solid #ccc}</style>
|
||||
<h1>사진 기반 정상 주변 지형 보정</h1>
|
||||
<p>2026-10-30. 이것은 정밀 DEM이나 측량 결과가 아닌 시각적 추정 모델입니다. 원본 Copernicus 30m DSM과 원래 좌표 원점은 변경하지 않았습니다.</p>
|
||||
<h2>촬영 위치 추정</h2>
|
||||
<p>시작점은 북위 37.44472°, 동경 126.96422° 부근으로 추정했습니다. 레이돔 북동쪽 약 84m의 정상 쪽 암반을 후보로 삼았습니다. 사용자 제공 JINK의 2018년 6월 파노라마에는 레이돔 암봉이 오른쪽 위, 계곡과 시가지가 왼쪽으로 보입니다. 기존 참고사진 photo-09·photo-10에서도 안부를 건너 암봉 위 시설을 보는 관계가 확인됩니다. 이 배치와 남향 시야를 맞춘 후보이며, 실제 촬영 GPS를 복원하거나 오차를 측정한 결과가 아닙니다. 수십 미터 이상의 위치 차이가 남을 수 있습니다.</p>
|
||||
<p>블로그의 37.4430°, 126.9641°와 629m는 해당 파노라마의 촬영점·표고임이 확인되지 않아 그대로 사용하지 않았습니다. 사용자 등고선 스크린샷은 넓은 능선과 골짜기 배치의 정성적 참고이며, 등고선을 좌표화하거나 고해상도 DEM으로 추출한 것은 아닙니다.</p>
|
||||
<h2>높이와 범위</h2>
|
||||
<p>암릉은 Blender MCP에서 실제 앱 지형과 시점을 참조해 별도로 조형한 GLB 모델을 사용합니다. 기운 상부 바위, 오목한 세로 절리면, 돌출부와 하부 암반을 제작하고 풍화 홈을 메시로 표현했습니다. 기존의 반복 구형 암괴 생성은 제거했습니다. 사진의 형상을 참고한 근사 모델이며 사진측량·실측 복원이 아닙니다. 정상 석판 보행 경계는 앞서 확장한 경계를 유지합니다.</p>
|
||||
<p>추가 사용자 사진을 참고해 레이돔 하부를 좁은 받침과 급한 암벽으로 보정하고, 남향 시야 오른쪽의 바위 첨봉과 시설 연결 계단을 추가했습니다. 바위 형상·계단 치수는 시각적 추정이며 시설 계단은 보행 경로가 아닙니다. 남쪽 송신탑 군집과 북쪽 정상 통신탑은 기존 지도상의 별도 시설 위치를 유지합니다. 첫 사진의 레이돔 옆 철탑은 시기·정체가 확인되지 않아 북쪽 통신탑을 이동시키는 근거로 사용하지 않았습니다. 정상 표지석 사진은 넓은 암반 경사면의 참고이며 인파·적설·매점 천막은 재현하지 않았습니다.</p>
|
||||
<p>촬영점 지면 620m, 레이돔 아래 암봉 628m, 중간 안부 606m 등을 작업용 높이로 설정했습니다. 이 값들은 사진에서 측정한 수치가 아니라 상대 높이를 구성하기 위한 추정 제어점입니다. 모델 건물·돔의 치수와 위치도 기존 추정값을 사용합니다.</p>
|
||||
<p>제어점의 원본 대비 높이 차이를 거리 가중 보간해 지형에 더합니다. 시작점 반경 100m 안에서는 보정량을 그대로 적용하고 100~200m 구간에서 부드럽게 줄여 200m 바깥은 원본 메시와 같습니다. 메시의 근거리 세분화는 원본 해상도 향상을 의미하지 않습니다. 보행 지면·구조물 기초·미니맵은 같은 보정 메시를 사용합니다.</p>
|
||||
<p>‘원본 지형 비교’는 같은 추정 시작점과 시선으로 원본 DSM을 보여줍니다. 전환 시 이동 위치와 선택 시각은 초기화됩니다. 태양 방위 계산과 별개로 지형 차폐는 추정 지형의 영향을 받으며, 표시 일출 시각은 실제 능선 통과 시각이 아닙니다.</p>
|
||||
<h2>시작점 반경 200m 보행 구역</h2>
|
||||
<p>정상 석판 보행 범위를 추가 사진과 저장된 2013년 거리뷰로 재검토했습니다. 이전 경계는 시작점 주변에만 면적을 부여해 북쪽 정상 암반에서도 좁은 경로 밖으로 이동할 수 없었습니다. OSM 정상 후보점(node/442878166, 37.4451398°, 126.9642379°)까지 이어지는 넓은 암반 경사면을 하나의 보행 영역으로 연결했습니다. 지도상의 정상점이 표지석의 실측 위치라는 뜻은 아닙니다. 외곽선·약 40m 폭과 75m 길이의 범위는 사진 해석이며 공식 출입 경계가 아닙니다. 서쪽 통신시설과 동쪽 절벽 밖은 제외하고 미니맵·바위 재질·이동 판정에 같은 경계를 적용합니다. 공개 등산 안내도는 정상 접근 경로를 확인하는 용도이며 석판 외곽 치수를 제공하지 않습니다(<a href="https://seoulhiking.or.kr/data/mountain_pdf/note_20250910183805443_01.pdf">서울 등산관광 관악산 코스 지도</a>).</p>
|
||||
<p>네이버 거리뷰의 정상 암반·난간 옆 하산로·안내판 갈림길을 참고했습니다. 확인한 파노라마는 4GIINusjBpMhkE6lqWL10w와 dusWkUEvILaraw4XtD9-KA이며 화면 촬영일은 2013년 10월입니다. 200m 전 구간의 연속 촬영이나 현재 개방 여부를 확인한 것은 아닙니다.</p>
|
||||
<p>길 중심선은 OpenStreetMap way/337498789, way/37751526, way/337498787, way/337498788을 사용했습니다. 길 폭(2.2~3.2m), 시작점 암반 경계와 수목 배치는 시각적 추정입니다. 계단 구간도 현재는 기존 보정 지면을 따르는 보행면으로 단순화했습니다. 레이돔 시설 진입 가지길은 제외했습니다.</p>
|
||||
<p>흙·자갈 재질은 길, 밝은 바위 재질은 암반, 이끼·낙엽 바닥 재질은 길 밖 수목 구역을 표시합니다. 입체 나무는 제거한 상태입니다. 모든 암반이 이동 가능한 것은 아니며 정상 시작점 암반과 연결된 길만 이동할 수 있습니다. 반경 200m 경계와 시설 구역에서 멈춥니다. 미니맵의 금색 선은 경로, 점선 원은 200m 경계, 흰 점은 시작점입니다. 걸은 거리는 수평 이동 누계이며 복귀 버튼으로 초기화됩니다.</p>
|
||||
<p>이 자료는 시뮬레이터용 근사 모델이며 실측 등산로 또는 현장 길안내 자료가 아닙니다. 거리뷰 이미지는 앱 텍스처로 재배포하지 않습니다. 경로 출처: <a href="https://www.openstreetmap.org/copyright">© OpenStreetMap contributors, ODbL</a>. <a href="https://map.naver.com/p/search/연주대/place/19546172?p=4GIINusjBpMhkE6lqWL10w,178.8,-23.3,80,Float">참고한 네이버 거리뷰</a>.</p>
|
||||
<h2>지면 재질 (1단계)</h2>
|
||||
<p>반경 200m에 색상·OpenGL 노멀·거칠기 맵을 적용했습니다. Poly Haven의 <a href="https://polyhaven.com/a/dirt">Dirt</a>, <a href="https://polyhaven.com/a/rock_boulder_dry">Rock Boulder Dry</a>, <a href="https://polyhaven.com/a/forest_leaves_02">Forest Leaves 02</a>를 사용합니다. 모두 CC0이며 관악산 현장을 촬영한 재질은 아닙니다. 암반은 화강암 분위기의 대체 재질이며 실제 암종을 확인한 스캔은 아닙니다. 파일 출처와 해시는 <a href="/terrain/materials/sources.json">재질 목록</a>에 기록했습니다. Powered by Poly Haven.</p>
|
||||
<p>길·암반 구역과 보행 제한은 기존 모델을 사용합니다. 낙엽 무늬는 먼 거리에서 약해집니다. 흙길과 낙엽 바닥은 길 경계 안쪽 0.5m~바깥쪽 0.4m에서 색상·노멀·거칠기를 함께 혼합합니다. 작은 불규칙성을 더한 시각적 전환이며 보행 경계는 그대로입니다. 암반 경계도 안쪽 0.7m~바깥쪽 0.5m 구간에서 흙·낙엽과 색상·노멀·거칠기를 혼합합니다. 불규칙한 경계는 시각적 표현이며 실측 균열은 아닙니다. 높이 기반 혼합, 절벽용 트라이플래너, 입체 풀 포기는 아직 적용하지 않았습니다. 재질은 지형 실측 해상도나 산의 외곽선을 바꾸지 않습니다.</p>
|
||||
<h2>지면 로딩 최적화</h2>
|
||||
<p>정상 지면과 재질 경계 거리는 미리 계산한 파일을 읽습니다. 경계와 시작점 20m 안쪽은 촘촘한 메시를 유지하며, 경계 밖 단일 재질 영역만 최대 약 5.6m 변 길이로 줄였습니다. 기존 지형 삼각형의 평면을 그대로 따릅니다. 내부의 법선·색상 보간은 조금 달라질 수 있습니다. 세분화 수준이 달라지는 접합부는 이웃의 실제 꼭짓점과 속성을 재사용해 연결합니다.</p>
|
||||
<p>정상 재질 메시의 꼭짓점은 2,333,889개에서 58,513개, 삼각형은 777,963개에서 111,111개로 줄었습니다. 속성·인덱스 버퍼는 약 4.38MB, gzip 전송 파일은 약 1.77MB입니다. 텍스처 원본 9개(약 5.72MB)는 유지합니다. 단일 재질 내부에서는 이웃 재질 샘플을 생략하며 경계에서만 혼합합니다.</p>
|
||||
<p>지형·경로·생성 상수를 바꾼 뒤 <code>npm run prepare:summit</code>으로 다시 생성합니다. <code>npm run check:summit</code>은 입력 해시와 파일 무결성을 확인하며 정식 빌드 전에도 실행됩니다. <code>?profile=1</code>에서는 콘솔에 지면 구성 시간·첫 장면 시간·90프레임 중 마지막 60프레임 간격을 기록합니다. 첫 장면은 모든 텍스처 로드 완료를 뜻하지 않습니다.</p>
|
||||
<h2>참고자료</h2><ul>
|
||||
<li>사용자 제공 Google Maps 파노라마: JINK, 2018년 6월, 사진 ID CIHM0ogKEICAgIDErpDi7AE. 이미지 자체는 앱에 재배포하지 않습니다.</li>
|
||||
<li>사용자 제공 Google Maps 등고선 스크린샷: 정상·연주대·연주암 주변 능선 배치 참고, 격자 해상도 및 정확도 미확인.</li>
|
||||
<li>로컬 참고사진: data/references/summit/photos/photo-09.jpg, photo-10.jpg. 파일별 원출처는 photos/verified-index.json.</li>
|
||||
<li><a href="/terrain/source.html">Copernicus 원본 지형 출처</a> · <a href="/structures.html">구조물 근거·제한</a></li>
|
||||
</ul><p>이 추정 모델은 사진과 비슷한 공간 관계를 탐색하기 위한 것이며, 현장 GPS·정밀 표고 자료 확보 시 교체해야 합니다.</p><p><a href="/data-sources.html">자료별 라이선스·가공 데이터 다운로드</a></p></html>
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Hand-drawn, pressure-varied Hanja pigment on the existing stone atlas; no fonts."""
|
||||
import bpy,numpy as np,math
|
||||
s=bpy.data.scenes['Summit slab and marker'];bpy.context.window.scene=s
|
||||
body=bpy.data.objects['Photo matched summit boulder'];mat=body.data.materials[0];nodes=mat.node_tree.nodes;links=mat.node_tree.links
|
||||
N=2048;mask=np.zeros((N,N),dtype=np.float32)
|
||||
# Each control point is x, y (down), brush width. Sweeps and hooked ends are individually drawn.
|
||||
冠=[[(.13,.17,.045),(.10,.26,.078),(.08,.34,.018)],[(.12,.20,.035),(.48,.17,.062),(.87,.14,.085),(.83,.30,.015)],[(.22,.38,.025),(.39,.35,.055),(.54,.36,.018)],[(.16,.53,.022),(.35,.49,.065),(.56,.48,.027)],[(.29,.53,.072),(.26,.70,.052),(.18,.81,.032),(.11,.85,.007)],[(.45,.51,.073),(.42,.72,.062),(.46,.87,.047),(.66,.88,.066),(.87,.83,.076),(.90,.69,.010)],[(.57,.43,.025),(.74,.41,.055),(.91,.40,.012)],[(.77,.29,.032),(.78,.47,.069),(.76,.69,.048),(.69,.73,.014)],[(.59,.54,.024),(.65,.58,.070),(.67,.62,.012)]]
|
||||
岳=[[(.73,.10,.025),(.52,.16,.072),(.29,.21,.027)],[(.28,.22,.074),(.26,.40,.063),(.24,.56,.036)],[(.30,.35,.020),(.50,.34,.057),(.72,.31,.021)],[(.68,.34,.059),(.66,.46,.068),(.65,.56,.027)],[(.12,.58,.017),(.43,.56,.066),(.86,.54,.020)],[(.49,.64,.035),(.50,.77,.080),(.48,.91,.037)],[(.22,.72,.035),(.23,.91,.072),(.45,.92,.049),(.78,.88,.044)],[(.79,.68,.029),(.80,.77,.074),(.77,.93,.018)]]
|
||||
山=[[(.49,.12,.024),(.50,.32,.083),(.48,.56,.069),(.49,.82,.045)],[(.18,.39,.035),(.19,.62,.078),(.20,.86,.072),(.43,.85,.043),(.80,.81,.063)],[(.82,.34,.032),(.81,.56,.083),(.80,.78,.061),(.76,.87,.012)]]
|
||||
def brush(points,cx,top,w,h,lean,seed):
|
||||
rng=np.random.default_rng(seed)
|
||||
controls=np.array(points)
|
||||
# Catmull-Rom interpolation produces continuous hand motion rather than straight font stems.
|
||||
padded=np.vstack([controls[0],controls,controls[-1]])
|
||||
samples=[]
|
||||
for i in range(1,len(padded)-2):
|
||||
a,b,c,d=padded[i-1:i+3]
|
||||
for t in np.linspace(0,1,28,endpoint=False):
|
||||
samples.append(.5*((2*b)+(-a+c)*t+(2*a-5*b+4*c-d)*t*t+(-a+3*b-3*c+d)*t*t*t))
|
||||
samples.append(controls[-1])
|
||||
for k,(x,y,width) in enumerate(samples):
|
||||
px=cx+(x-.5)*w+lean*(.5-y);pz=top-y*h
|
||||
u=(.02+.46*(px+1.3)/2.6)*(N-1);v=(.02+.96*(pz+.2)/2.3)*(N-1)
|
||||
rx=max(.45,width*w*.46/2.6*N*.5);ry=max(.6,width*h*.96/2.3*N*.5)
|
||||
rx*=1+.08*math.sin(k*.73+seed)
|
||||
x0=max(0,int(u-rx-2));x1=min(N,int(u+rx+3));y0=max(0,int(v-ry-2));y1=min(N,int(v+ry+3))
|
||||
X,Y=np.meshgrid(np.arange(x0,x1)+.5,np.arange(y0,y1)+.5)
|
||||
radius=np.sqrt(((X-u)/rx)**2+((Y-v)/ry)**2)
|
||||
dab=np.clip((1.10-radius)*5,0,1)
|
||||
mask[y0:y1,x0:x1]=np.maximum(mask[y0:y1,x0:x1],dab)
|
||||
for char,cx,top,w,h,lean,seed in [(冠,.18,1.78,.48,.45,.025,13),(岳,.15,1.29,.43,.41,-.012,31),(山,.20,.83,.47,.40,.018,58)]:
|
||||
for i,stroke in enumerate(char):brush(stroke,cx,top,w,h,lean,seed+i*7)
|
||||
# Small mineral-shaped losses, sparse chips and uneven pigment opacity; retain legibility.
|
||||
rng=np.random.default_rng(194);Y,X=np.indices(mask.shape)
|
||||
wear=.89+.07*np.sin(X*.19+np.sin(Y*.27))+.035*rng.random(mask.shape)
|
||||
chips=rng.random(mask.shape)<.025
|
||||
mask*=np.where(chips,.12,wear)
|
||||
for _ in range(55):
|
||||
occupied=np.argwhere(mask>.7)
|
||||
y,x=occupied[rng.integers(len(occupied))];rx=rng.uniform(.6,1.7);ry=rng.uniform(.8,2.5)
|
||||
x0=max(0,x-4);x1=min(N,x+5);y0=max(0,y-5);y1=min(N,y+6)
|
||||
yy,xx=np.indices((y1-y0,x1-x0));patch=((xx+x0-x)/rx)**2+((yy+y0-y)/ry)**2<1
|
||||
mask[y0:y1,x0:x1]*=np.where(patch,.25,1)
|
||||
image_mask=bpy.data.images['Hanja pigment mask'];rgba=np.ones((N,N,4),dtype=np.float32);rgba[:,:,:3]=mask[:,:,None]
|
||||
image_mask.pixels.foreach_set(rgba.ravel());image_mask.update();image_mask.pack()
|
||||
bs=next(n for n in nodes if n.type=='BSDF_PRINCIPLED')
|
||||
paint=next(n for n in nodes if n.type=='MIX_RGB' and n.inputs[0].is_linked)
|
||||
target=next(n for n in nodes if n.type=='TEX_IMAGE' and n.image and n.image.name=='Painted Hanja on granite')
|
||||
links.new(paint.outputs[0],bs.inputs['Base Color']);nodes.active=target
|
||||
for o in s.objects:o.select_set(False)
|
||||
body.select_set(True);bpy.context.view_layer.objects.active=body
|
||||
s.render.bake.use_pass_direct=False;s.render.bake.use_pass_indirect=False;s.render.bake.use_pass_color=True
|
||||
bpy.ops.object.bake(type=next(i.identifier for i in bpy.ops.object.bake.get_rna_type().properties['type'].enum_items if i.identifier=='DIFFUSE'))
|
||||
target.image.save();target.image.pack();links.new(target.outputs['Color'],bs.inputs['Base Color'])
|
||||
body['inscriptionStyle']='Hand-drawn brush paths, pressure variation and worn pigment; no font rasterization'
|
||||
s.render.filepath='D:/project/sunrise/assets/summit/blender-plateau-study/marker-brush-painted.png'
|
||||
bpy.ops.render.render(write_still=True)
|
||||
print('Painted 20 hand-drawn brush paths with tapered hooks and sparse pigment wear.')
|
||||
@@ -0,0 +1,185 @@
|
||||
"""Photo-guided Blender study. Run through Blender MCP; export only the rock collection.
|
||||
|
||||
Blender: X east, Y north, Z up, origin at radar base. Geometry and sizes are estimates.
|
||||
The existing facility scene is retained. Source is saved to its own .blend file.
|
||||
"""
|
||||
import bpy, bmesh, math, json, random
|
||||
from pathlib import Path
|
||||
from mathutils import Vector, noise
|
||||
|
||||
ROOT = Path('D:/project/sunrise')
|
||||
OUT = ROOT / 'assets/summit/blender-rock-study'
|
||||
context = json.loads((OUT/'context.json').read_text(encoding='utf-8'))
|
||||
SCENE_NAME = 'Photo-guided summit rocks'
|
||||
if bpy.data.scenes.get(SCENE_NAME):
|
||||
raise RuntimeError('Study already exists; edit it in place rather than rebuilding.')
|
||||
scene = bpy.data.scenes.new(SCENE_NAME)
|
||||
bpy.context.window.scene = scene
|
||||
rocks = bpy.data.collections.new('Photo rocks - export')
|
||||
study = bpy.data.collections.new('Terrain and radar - context only')
|
||||
scene.collection.children.link(rocks)
|
||||
scene.collection.children.link(study)
|
||||
|
||||
def enum(owner, prop, wanted):
|
||||
return next(item.identifier for item in owner.bl_rna.properties[prop].enum_items if item.identifier == wanted)
|
||||
|
||||
def material(name, color):
|
||||
mat = bpy.data.materials.new(name)
|
||||
mat.use_nodes = True
|
||||
node = next(n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED')
|
||||
node.inputs['Base Color'].default_value = (*color,1)
|
||||
node.inputs['Roughness'].default_value = .94
|
||||
mat.diffuse_color = (*color,1)
|
||||
return mat
|
||||
|
||||
granite = material('Photo study / weathered granite', (.44,.43,.38))
|
||||
nodes=granite.node_tree.nodes
|
||||
bsdf=next(n for n in nodes if n.type=='BSDF_PRINCIPLED')
|
||||
tex=nodes.new('ShaderNodeTexImage')
|
||||
tex.image=bpy.data.images.load(str(ROOT/'public/terrain/materials/rock-color.jpg'),check_existing=True)
|
||||
granite.node_tree.links.new(tex.outputs['Color'],bsdf.inputs['Base Color'])
|
||||
|
||||
def mesh_object(name, vertices, faces, collection, mat):
|
||||
mesh=bpy.data.meshes.new(name)
|
||||
mesh.from_pydata(vertices,[],faces);mesh.update()
|
||||
obj=bpy.data.objects.new(name,mesh);collection.objects.link(obj)
|
||||
mesh.materials.append(mat)
|
||||
return obj
|
||||
|
||||
def sculpt_loft(name, outline, levels, seed, relief=.18):
|
||||
# Non-convex plan retains deep vertical joints instead of rounding them into potatoes.
|
||||
rng=random.Random(seed)
|
||||
count=len(outline)
|
||||
center=Vector((sum(p[0] for p in outline)/count,sum(p[1] for p in outline)/count))
|
||||
verts=[]
|
||||
offsets=[rng.uniform(-.75,.75) for _ in outline]
|
||||
for level,(z,scale,dx,dy) in enumerate(levels):
|
||||
for j,(x,y) in enumerate(outline):
|
||||
verts.append((center.x+(x-center.x)*scale+dx,
|
||||
center.y+(y-center.y)*scale+dy,z+offsets[j]*(.3 if level==0 else 1)))
|
||||
faces=[]
|
||||
for level in range(len(levels)-1):
|
||||
for j in range(count):
|
||||
k=(j+1)%count
|
||||
faces.append((level*count+j,level*count+k,(level+1)*count+k,(level+1)*count+j))
|
||||
faces.append(tuple(reversed(range(count))))
|
||||
faces.append(tuple((len(levels)-1)*count+j for j in range(count)))
|
||||
obj=mesh_object(name,verts,faces,rocks,granite)
|
||||
bm=bmesh.new();bm.from_mesh(obj.data)
|
||||
bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces))
|
||||
bmesh.ops.triangulate(bm,faces=list(bm.faces))
|
||||
bmesh.ops.subdivide_edges(bm,edges=list(bm.edges),cuts=3,use_grid_fill=True)
|
||||
bm.normal_update()
|
||||
for vertex in bm.verts:
|
||||
p=vertex.co.copy()
|
||||
# Small weathering preserves the authored fracture planes and crest silhouette.
|
||||
amount=noise.noise_vector(p*.72+Vector((seed,0,0))).x*relief
|
||||
vertex.co+=vertex.normal*amount
|
||||
bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces))
|
||||
bm.to_mesh(obj.data);bm.free();obj.data.update()
|
||||
uv=obj.data.uv_layers.new(name='Granite metre UV')
|
||||
colors=obj.data.color_attributes.new(name='Weathering',type=enum(bpy.types.Attribute,'data_type','FLOAT_COLOR'),domain=enum(bpy.types.Attribute,'domain','CORNER'))
|
||||
for face in obj.data.polygons:
|
||||
n=face.normal
|
||||
# Stable projection per face, at the same 3m texture scale as the app ground.
|
||||
axis=max(range(3),key=lambda a:abs(n[a]))
|
||||
for loop_index in face.loop_indices:
|
||||
p=obj.data.vertices[obj.data.loops[loop_index].vertex_index].co
|
||||
uv.data[loop_index].uv=((p.y,p.z) if axis==0 else (p.x,p.z) if axis==1 else (p.x,p.y))
|
||||
uv.data[loop_index].uv/=3
|
||||
shade=.86+.10*noise.noise_vector(p*.22+Vector((seed,0,0))).x
|
||||
colors.data[loop_index].color=(shade,shade*.99,shade*.95,1)
|
||||
face.use_smooth=True
|
||||
# Auto-smooth via sharp edges: small surface weathering smooth, major joints retain ridges.
|
||||
bm=bmesh.new();bm.from_mesh(obj.data)
|
||||
for edge in bm.edges:
|
||||
if edge.is_manifold and edge.calc_face_angle()>math.radians(35):edge.smooth=False
|
||||
bm.to_mesh(obj.data);bm.free()
|
||||
obj['reference']='User photos; authored fracture outline, estimated metres'
|
||||
return obj
|
||||
|
||||
# Radar buttress: a continuous narrow crown opening out into a broad fractured wall.
|
||||
sculpt_loft('Radar / north granite wall',
|
||||
[(-9,-4),(-5,-7),(4,-6),(9,-2),(10,5),(7,8),(4,7),(3,10),(-1,12),(-2,9),(-5,11),(-9,7)],
|
||||
[(-34,1.5,1,7),(-20,1.25,.8,3),(-10,1.08,0,1),(-3,.91,0,0),(-.7,.67,0,-1)],11,.15)
|
||||
# One connected western rock. Concave outline produces the vertical clefts in the photo.
|
||||
sculpt_loft('Western outcrop / rooted jointed wall',
|
||||
[(-25,0),(-20,-3),(-14,-2),(-11,2),(-12,6),(-14,7),(-13,10),(-16,12),(-17,9),(-20,13),(-23,11),(-22,7),(-25,6)],
|
||||
[(-33,1.38,0,7),(-20,1.2,0,3),(-10,1,0,1),(-2,.86,0,0),(2,.57,.2,-1)],23,.19)
|
||||
# The thin upper slab is rooted well inside the wall; a slanted crown, no symmetric apex.
|
||||
sculpt_loft('Western outcrop / leaning crown slab',
|
||||
[(-19,-1),(-16,-2),(-14,0),(-14,3),(-16,5),(-18,4),(-19,2)],
|
||||
[(-7,1.1,0,0),(0,1,0,0),(5,.69,-.7,-.4),(9,.31,-1.7,-.8)],31,.10)
|
||||
sculpt_loft('Western outcrop / broken lower shoulder',
|
||||
[(-26,1),(-22,-1),(-20,2),(-20,6),(-22,8),(-25,7)],
|
||||
[(-20,1.15,0,2),(-8,1,0,0),(0,.67,.4,-.4),(3,.27,1,-.7)],47,.16)
|
||||
# Sloping foot is a ridge, not a row of freestanding pillars.
|
||||
sculpt_loft('Western outcrop / descending rock apron',
|
||||
[(-28,12),(-23,10),(-18,13),(-15,20),(-15,27),(-20,34),(-25,33),(-29,24)],
|
||||
[(-38,1.15,0,0),(-27,1,0,0),(-20,.77,0,-2),(-13,.40,0,-5)],59,.25)
|
||||
|
||||
sculpt_loft('Western / fractured front ledge',[(-23,8),(-20,7),(-17,9),(-17,12),(-19,14),(-23,12)],[(-19,1.1,0,0),(-11,1,0,0),(-7,.72,0,-.3)],71,.22)
|
||||
sculpt_loft('Western / angular shoulder plates',[(-16,6),(-13,5),(-11,8),(-12,11),(-15,12),(-17,10)],[(-15,1.12,0,0),(-6,1,0,0),(-2,.6,-.6,-.7)],81,.2)
|
||||
sculpt_loft('Western / low bedding shelf',[(-26,16),(-22,14),(-19,17),(-20,21),(-24,23),(-27,20)],[(-29,1.12,0,0),(-20,1,0,0),(-16,.62,.3,-.4)],91,.23)
|
||||
sculpt_loft('Radar / split foreground ledge',[(2,9),(5,7),(8,10),(7,14),(4,15),(1,13)],[(-26,1.15,0,0),(-16,1,0,0),(-11,.63,-.6,-1)],101,.17)
|
||||
|
||||
# Match actual terrain contact and camera. This collection is excluded from the GLB.
|
||||
rows,cols=context['rows'],context['columns']
|
||||
faces=[]
|
||||
for row in range(rows-1):
|
||||
for col in range(cols-1):
|
||||
a=row*cols+col;faces.append((a,a+1,a+1+cols,a+cols))
|
||||
terrain=mesh_object('App terrain / contact reference',context['vertices'],faces,study,material('Study terrain',(.21,.26,.16)))
|
||||
root=bpy.data.objects.get('weather-radar')
|
||||
copies={}
|
||||
for source in [root,*root.children_recursive]:
|
||||
obj=source.copy()
|
||||
if source.data:obj.data=source.data.copy()
|
||||
study.objects.link(obj);copies[source]=obj
|
||||
for source,obj in copies.items():
|
||||
obj.parent=copies.get(source.parent)
|
||||
copies[root].location=(0,0,0)
|
||||
|
||||
camera_data=bpy.data.cameras.new('Photo comparison camera')
|
||||
camera=bpy.data.objects.new(camera_data.name,camera_data);study.objects.link(camera)
|
||||
camera.location=Vector(context['eye'])
|
||||
camera.rotation_euler=(Vector((-8,7,-6))-camera.location).to_track_quat('-Z','Y').to_euler()
|
||||
camera_data.lens=43;scene.camera=camera
|
||||
scene.render.resolution_x=1200;scene.render.resolution_y=850;scene.render.resolution_percentage=100
|
||||
scene.world=bpy.data.worlds.new('Photo study sky');scene.world.use_nodes=True
|
||||
bg=next(n for n in scene.world.node_tree.nodes if n.type=='BACKGROUND')
|
||||
bg.inputs['Color'].default_value=(.55,.64,.72,1);bg.inputs['Strength'].default_value=.65
|
||||
sun_data=bpy.data.lights.new('Photo study sun',enum(bpy.types.Light,'type','SUN'))
|
||||
sun_data.energy=2.2;sun_data.angle=.15
|
||||
sun=bpy.data.objects.new(sun_data.name,sun_data);study.objects.link(sun)
|
||||
sun.rotation_euler=(math.radians(32),math.radians(-25),math.radians(-45))
|
||||
try:scene.render.engine='CYCLES'
|
||||
except TypeError as exc:print(exc)
|
||||
scene.cycles.samples=24
|
||||
|
||||
# Reference images remain local and are not exported with the model.
|
||||
for i,path in enumerate([
|
||||
'C:/Users/horoli/AppData/Local/Temp/codex-clipboard-453c752b-f410-4d6b-97ad-8056e3dac06d.png',
|
||||
'C:/Users/horoli/AppData/Local/Temp/codex-clipboard-8cc46291-7018-4d33-a23e-553db9844d43.png']):
|
||||
if Path(path).exists():
|
||||
obj=bpy.data.objects.new('Photo reference '+str(i+1),None);study.objects.link(obj)
|
||||
obj.empty_display_type=enum(bpy.types.Object,'empty_display_type','IMAGE')
|
||||
obj.data=bpy.data.images.load(path,check_existing=True);obj.empty_display_size=30
|
||||
obj.location=(45+i*35,0,5);obj.rotation_euler=(math.pi/2,0,0)
|
||||
|
||||
def export_rocks():
|
||||
import io_scene_gltf2
|
||||
for obj in scene.objects:obj.select_set(False)
|
||||
for obj in rocks.objects:obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active=next(iter(rocks.objects))
|
||||
fmt=next(item[0] for item in io_scene_gltf2.get_format_items(None,bpy.context) if item[0]=='GLB')
|
||||
# App reuses its rock textures; UVs and vertex weathering travel with the geometry.
|
||||
mats=next(item.identifier for item in bpy.ops.export_scene.gltf.get_rna_type().properties['export_materials'].enum_items if item.identifier=='NONE')
|
||||
bpy.ops.export_scene.gltf(filepath=str(ROOT/'public/models/summit/radar-rocks.glb'),export_format=fmt,
|
||||
use_selection=True,use_active_scene=True,export_yup=True,export_apply=True,
|
||||
export_materials=mats,export_animations=False,export_cameras=False,export_lights=False,export_extras=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(OUT/'photo-guided-rocks.blend'))
|
||||
print('Exported photo-guided rocks:',sum(len(o.data.polygons) for o in rocks.objects),'faces')
|
||||
|
||||
bpy.app.driver_namespace['photo_rocks']=globals()
|
||||
print('Study ready:',len(rocks.objects),'rock meshes. Original scene retained.')
|
||||
@@ -0,0 +1,443 @@
|
||||
"""Run in Blender through MCP. Reuses the snapshot of src/constants/structures.ts.
|
||||
|
||||
Builds a separate scene, then exposes export_assets() and setup_studio().
|
||||
No source scene objects or external textures are changed.
|
||||
"""
|
||||
import bpy
|
||||
import bmesh
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from mathutils import Vector
|
||||
|
||||
ROOT = Path('D:/project/sunrise')
|
||||
SOURCE = ROOT / 'assets/summit'
|
||||
OUTPUT = ROOT / 'public/models/summit'
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
SPECS = json.loads((SOURCE / 'structure-specs.json').read_text(encoding='utf-8'))
|
||||
SCENE = bpy.data.scenes.get('Summit Assets - refined') or bpy.data.scenes.new('Summit Assets - refined')
|
||||
bpy.context.window.scene = SCENE
|
||||
SCENE.unit_settings.system = 'METRIC'
|
||||
SCENE.unit_settings.scale_length = 1
|
||||
ASSETS = []
|
||||
|
||||
|
||||
def enum_value(owner, prop, identifier):
|
||||
return next(i.identifier for i in owner.bl_rna.properties[prop].enum_items if i.identifier == identifier)
|
||||
|
||||
|
||||
def material(name, hex_color, roughness=0.65, metal=0):
|
||||
existing = bpy.data.materials.get('Summit / ' + name)
|
||||
if existing:
|
||||
return existing
|
||||
rgb = [int(hex_color[i:i+2], 16)/255 for i in (0, 2, 4)]
|
||||
rgba = [v/12.92 if v <= .04045 else ((v+.055)/1.055)**2.4 for v in rgb] + [1]
|
||||
mat = bpy.data.materials.new('Summit / ' + name)
|
||||
mat.diffuse_color = rgba
|
||||
mat.use_nodes = True
|
||||
node = next(n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED')
|
||||
node.inputs['Base Color'].default_value = rgba
|
||||
node.inputs['Roughness'].default_value = roughness
|
||||
node.inputs['Metallic'].default_value = metal
|
||||
return mat
|
||||
|
||||
|
||||
MATS = {
|
||||
'galvanized': material('Galvanized steel', 'a5b0b5', .4, .7),
|
||||
'red': material('Faded aviation red', 'a94f3d', .64, .25),
|
||||
'dark': material('Graphite metal', '37413e', .55, .45),
|
||||
'concrete': material('Warm concrete', 'b7b4a8', .92),
|
||||
'green': material('Muted green cladding', '6e8b7f', .7, .15),
|
||||
'seam': material('Panel joints', '8f9c98', .85),
|
||||
'white': material('Antenna ivory', 'e2e4dc', .52, .08),
|
||||
'panel0': material('Radome warm white', 'e8e9df', .65),
|
||||
'panel1': material('Radome pearl', 'dce2da', .65),
|
||||
'panel2': material('Radome light gray', 'd2dcd5', .68),
|
||||
'wood0': material('Deck weathered oak', '9b896c', .88),
|
||||
'wood1': material('Deck warm oak', 'aa9475', .88),
|
||||
'wood2': material('Deck gray oak', '918774', .9),
|
||||
'wood3': material('Deck dark oak', '84785f', .9),
|
||||
'groove': material('Deck grooves', '6e6654', .95),
|
||||
'rail': material('Dark timber railing', '454b42', .86),
|
||||
}
|
||||
|
||||
|
||||
class Meshes:
|
||||
"""Accumulate disconnected primitives by material instead of thousands of objects."""
|
||||
def __init__(self):
|
||||
self.parts = {}
|
||||
|
||||
def add(self, mat, verts, faces):
|
||||
vs, fs = self.parts.setdefault(mat, ([], []))
|
||||
offset = len(vs)
|
||||
vs.extend(tuple(v) for v in verts)
|
||||
fs.extend(tuple(offset+i for i in face) for face in faces)
|
||||
|
||||
def box(self, mat, center, size):
|
||||
x, y, z = center
|
||||
a, b, c = (v/2 for v in size)
|
||||
self.add(mat, [(x+dx*a, y+dy*b, z+dz*c) for dx, dy, dz in
|
||||
[(-1,-1,-1),(1,-1,-1),(1,1,-1),(-1,1,-1),
|
||||
(-1,-1,1),(1,-1,1),(1,1,1),(-1,1,1)]],
|
||||
[(0,3,2,1),(4,5,6,7),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7)])
|
||||
|
||||
def beam(self, mat, a, b, radius, sides=8, end_radius=None):
|
||||
a, b = Vector(a), Vector(b)
|
||||
direction = (b-a).normalized()
|
||||
helper = Vector((0,0,1)) if abs(direction.z) < .95 else Vector((1,0,0))
|
||||
u = direction.cross(helper).normalized()
|
||||
v = direction.cross(u)
|
||||
verts = []
|
||||
for point, r in [(a, radius), (b, radius if end_radius is None else end_radius)]:
|
||||
verts.extend(point + r*(math.cos(i*math.tau/sides)*u + math.sin(i*math.tau/sides)*v) for i in range(sides))
|
||||
faces = [tuple(reversed(range(sides))), tuple(range(sides, 2*sides))]
|
||||
faces.extend((i,(i+1)%sides,(i+1)%sides+sides,i+sides) for i in range(sides))
|
||||
self.add(mat, verts, faces)
|
||||
|
||||
def finish(self, root):
|
||||
for key, (verts, faces) in self.parts.items():
|
||||
mesh = bpy.data.meshes.new(root.name + '/' + key)
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
obj = bpy.data.objects.new(mesh.name, mesh)
|
||||
root.users_collection[0].objects.link(obj)
|
||||
obj.parent = root
|
||||
obj.data.materials.append(MATS[key])
|
||||
if key.startswith('wood') or key == 'rail':
|
||||
modifier = obj.modifiers.new('Subtle edge wear', enum_value(bpy.types.Modifier, 'type', 'BEVEL'))
|
||||
modifier.width = .006
|
||||
modifier.segments = 1
|
||||
obj['materialBatch'] = key
|
||||
|
||||
|
||||
def root_for(spec, kind, gallery):
|
||||
coll = bpy.data.collections.new(spec['id'])
|
||||
SCENE.collection.children.link(coll)
|
||||
root = bpy.data.objects.new(spec['id'], None)
|
||||
coll.objects.link(root)
|
||||
root.location = gallery
|
||||
root['assetId'] = spec['id']
|
||||
root['dimensionsEstimated'] = True
|
||||
root['source'] = spec['source']
|
||||
root['kind'] = kind
|
||||
root['units'] = 'metres'
|
||||
root['generationSpecJson'] = json.dumps(spec)
|
||||
root.asset_mark()
|
||||
root.asset_data.description = 'Gwanaksan ' + kind + '; estimated dimensions; photo-informed approximation.'
|
||||
ASSETS.append({'root': root, 'spec': spec, 'kind': kind, 'supports': []})
|
||||
return root
|
||||
|
||||
|
||||
def tower(spec, index):
|
||||
root = root_for(spec, 'tower', ((index-2)*12, 17, 0))
|
||||
m = Meshes()
|
||||
h, w = spec['height'], spec['width']
|
||||
levels = 12
|
||||
def half(t):
|
||||
return w/2 * (1-2.5*t if t < .28 else .3 - .1*(t-.28)/.72)
|
||||
def corner(t, j):
|
||||
r = half(t)
|
||||
return Vector(([-r,r,r,-r][j],[-r,-r,r,r][j],t*h))
|
||||
for i in range(levels):
|
||||
t, t1 = i/levels, (i+1)/levels
|
||||
color = 'galvanized' if index == 0 or index == 2 else ('red' if (i//2)%2 == 0 else 'galvanized')
|
||||
for j in range(4):
|
||||
k = (j+1)%4
|
||||
a,b,c,d = corner(t,j),corner(t,k),corner(t1,j),corner(t1,k)
|
||||
m.beam(color,a,c,.075 if index == 0 else .095)
|
||||
m.beam(color,a,b,.055)
|
||||
m.beam(color,a,d,.034)
|
||||
m.beam(color,b,c,.034)
|
||||
if i == levels-1:
|
||||
m.beam(color,c,d,.055)
|
||||
# Base plates and anchors; terrain foundation itself stays in the application.
|
||||
for j in range(4):
|
||||
x,y,_ = corner(0,j)
|
||||
m.box('dark',(x,y,.035),(.38,.38,.07))
|
||||
for dx in [-.135,.135]:
|
||||
for dy in [-.135,.135]:
|
||||
m.beam('galvanized',(x+dx,y+dy,.07),(x+dx,y+dy,.13),.026,6)
|
||||
# Maintenance platforms, open in the centre, with railings.
|
||||
for fraction in [.32,.64,.93]:
|
||||
z = h*fraction
|
||||
r = half(fraction)+.43
|
||||
for side in [-1,1]:
|
||||
m.box('galvanized',(0,side*(r-.13),z),(2*r,.26,.085))
|
||||
m.box('galvanized',(side*(r-.13),0,z),(.26,2*r-.52,.085))
|
||||
for j in range(4):
|
||||
a = Vector(([-r,r,r,-r][j],[-r,-r,r,r][j],z))
|
||||
b = Vector(([-r,r,r,-r][(j+1)%4],[-r,-r,r,r][(j+1)%4],z))
|
||||
for v in [a,b,(a+b)/2]:
|
||||
m.beam('galvanized',v,v+Vector((0,0,.95)),.022)
|
||||
for dz in [.48,.95]:
|
||||
m.beam('galvanized',a+Vector((0,0,dz)),b+Vector((0,0,dz)),.019)
|
||||
# Continuous service ladder and cable trunk.
|
||||
for x in [-.22,.22]:
|
||||
m.beam('dark',(x,0,.15),(x,0,h-.1),.025)
|
||||
for n in range(int(h/.32)):
|
||||
z = .25+n*.32
|
||||
m.beam('galvanized',(-.22,0,z),(.22,0,z),.017,6)
|
||||
m.beam('dark',(.32,.08,.1),(.32,.08,h),.045)
|
||||
# Dish covers and radial brackets, distributed by height and azimuth.
|
||||
dish_count = 3 if index == 0 else 8
|
||||
for n in range(dish_count):
|
||||
fraction = (.40+n*.16) if index == 0 else (.17+n*.065)
|
||||
z = h*fraction
|
||||
theta = (n*137.5 + index*31)*math.pi/180
|
||||
direction = Vector((math.cos(theta),math.sin(theta),0))
|
||||
base = direction*half(fraction)+Vector((0,0,z))
|
||||
center = base+direction*.65
|
||||
radius = .27 if index == 0 else .42+(n%3)*.13
|
||||
m.beam('galvanized',base,center,.05)
|
||||
m.beam('dark',center-direction*.08,center+direction*.03,radius,20)
|
||||
m.beam('white',center+direction*.03,center+direction*.16,radius*.98,24,radius*.9)
|
||||
for t in [.56,.75,.87]:
|
||||
x = half(t)+.18
|
||||
m.box('white',(x,0,t*h),(.22,.28,1.45 if index == 0 else 1.9))
|
||||
m.beam('galvanized',(half(t),0,t*h),(x,0,t*h),.04)
|
||||
# Keep the original four-metre antenna extension.
|
||||
for n in range(4):
|
||||
m.beam('red' if index and n%2 == 0 else 'white',(0,0,h+n),(0,0,h+n+1),.11 if index == 0 else .17,12)
|
||||
for z in [h+.4,h+1.4,h+2.4]:
|
||||
m.beam('dark',(-.25,0,z),(.25,0,z),.024)
|
||||
m.finish(root)
|
||||
|
||||
|
||||
def clipped(poly, floor):
|
||||
out = []
|
||||
for a,b in zip(poly, poly[1:]+poly[:1]):
|
||||
if a.z >= floor:
|
||||
out.append(a)
|
||||
if (a.z >= floor) != (b.z >= floor):
|
||||
out.append(a+(b-a)*((floor-a.z)/(b.z-a.z)))
|
||||
return out
|
||||
|
||||
|
||||
def radar():
|
||||
spec = {'id':'weather-radar', **SPECS['radar']}
|
||||
root = root_for(spec,'radar',(-17,-8,0))
|
||||
m = Meshes()
|
||||
h,r = spec['buildingHeight'],spec['radius']
|
||||
m.beam('concrete',(0,0,0),(0,0,h-.2),4,32,3.7)
|
||||
# Ribbed green cladding accents and concrete belt courses.
|
||||
for z in [.18,2.8,5.5]:
|
||||
m.beam('concrete',(0,0,z),(0,0,z+.18),4-.3*z/h+.055,32)
|
||||
for n in range(32):
|
||||
a = n*math.tau/32
|
||||
p = Vector((math.cos(a),math.sin(a),0))
|
||||
m.beam('green',p*3.97+Vector((0,0,.35)),p*3.73+Vector((0,0,h-.25)),.07,6)
|
||||
m.beam('green',(0,0,h-.22),(0,0,h+.02),4.4,48)
|
||||
m.beam('white',(0,0,h+.02),(0,0,h+.15),4.43,48)
|
||||
# Balcony ring rails.
|
||||
for n in range(48):
|
||||
a,b = n*math.tau/48,(n+1)*math.tau/48
|
||||
p,q = Vector((4.36*math.cos(a),4.36*math.sin(a),h+.15)),Vector((4.36*math.cos(b),4.36*math.sin(b),h+.15))
|
||||
m.beam('green',p,p+Vector((0,0,.85)),.022)
|
||||
for dz in [.43,.85]:
|
||||
m.beam('green',p+Vector((0,0,dz)),q+Vector((0,0,dz)),.019)
|
||||
m.box('dark',(0,-3.96,1.2),(1.25,.12,2.4))
|
||||
m.box('green',(0,-4.04,1.2),(1.08,.07,2.25))
|
||||
m.beam('galvanized',(.38,-4.09,1.03),(.38,-4.09,1.35),.024)
|
||||
m.box('concrete',(0,-4.18,.09),(1.5,.5,.18))
|
||||
for n in range(9):
|
||||
m.box('dark',(.95,-3.85,2.9+n*.075),(.65,.10,.035))
|
||||
# Dual of an icosphere creates hexagonal/pentagonal panels as in photo-09.
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_icosphere(bm,subdivisions=3,radius=1)
|
||||
bm.verts.ensure_lookup_table()
|
||||
center = Vector((0,0,h+r*.8))
|
||||
for index,v in enumerate(bm.verts):
|
||||
axis = v.co.normalized()
|
||||
u = axis.cross(Vector((0,0,1)) if abs(axis.z)<.95 else Vector((1,0,0))).normalized()
|
||||
w = axis.cross(u)
|
||||
points = [f.calc_center_median().normalized() for f in v.link_faces]
|
||||
points.sort(key=lambda p: math.atan2(p.dot(w),p.dot(u)))
|
||||
polygon = clipped([center+p*r for p in points], h+.17)
|
||||
if len(polygon)<3:
|
||||
continue
|
||||
centroid = sum(polygon,Vector())/len(polygon)
|
||||
# Slight inset leaves readable dark joints with no photographic texture.
|
||||
outer = [centroid+(p-centroid)*.985+axis*.018 for p in polygon]
|
||||
inner = [p-axis*.04 for p in polygon]
|
||||
n = len(polygon)
|
||||
m.add('seam',polygon+inner,[tuple(range(n)),tuple(reversed(range(n,2*n)))] + [(i,(i+1)%n,(i+1)%n+n,i+n) for i in range(n)])
|
||||
m.add('panel'+str(index%3),outer,[tuple(range(n))])
|
||||
bm.free()
|
||||
m.finish(root)
|
||||
|
||||
|
||||
def deck(spec,index):
|
||||
root = root_for(spec,'deck', (index*15,-9,4))
|
||||
m = Meshes()
|
||||
w,L,run,drop = (spec[k] for k in ['width','length','stairLength','stairDrop'])
|
||||
thickness = SPECS['deck']['thickness']
|
||||
rail_h = SPECS['railing']['height']
|
||||
supports = ASSETS[-1]['supports']
|
||||
# Blender +Y corresponds to Three.js -Z (the stair direction).
|
||||
def plank(y,z,depth,index):
|
||||
m.box('wood'+str(index%4),(0,y,z-thickness/2),(w,depth,thickness))
|
||||
# Shallow grooves and visible fasteners, all batched by material.
|
||||
for offset in [-.28,0,.28]:
|
||||
m.box('groove',(0,y+offset*depth,z+.0005),(w-.035,.004,.001))
|
||||
for x in [-w/2+.12,w/2-.12]:
|
||||
m.beam('galvanized',(x,y,z),(x,y,z+.006),.012,6)
|
||||
count = math.ceil(L/SPECS['deck']['plankWidth'])
|
||||
for i in range(count):
|
||||
plank(-L/2+(i+.5)*L/count,0,L/count-.009,i)
|
||||
steps = math.ceil(drop/.18)
|
||||
for i in range(1,steps+1):
|
||||
y,z = L/2+(i-.5)*run/steps,-i*drop/steps
|
||||
plank(y,z,run/steps+.015,i)
|
||||
m.box('dark',(0,y-run/(2*steps),z-.08),(w-.06,.025,.12))
|
||||
# Joists and stair stringers remain fixed geometry.
|
||||
for x in [-w*.37,0,w*.37]:
|
||||
m.box('dark',(x,0,-thickness-.085),(.11,L,.17))
|
||||
m.beam('dark',(x,L/2,-.27),(x,L/2+run,-drop-.27),.09,4)
|
||||
for i in range(math.ceil(L/.9)+1):
|
||||
y = -L/2+i*L/math.ceil(L/.9)
|
||||
m.box('dark',(0,y,-.27),(w,.08,.13))
|
||||
seen = set()
|
||||
def railing(a,b):
|
||||
a,b = Vector(a),Vector(b)
|
||||
count = math.ceil((b-a).length/SPECS['railing']['maxSpan'])
|
||||
for i in range(count+1):
|
||||
p = a.lerp(b,i/count)
|
||||
key = tuple(round(v,5) for v in p)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
m.beam('rail',p+Vector((0,0,-.32)),p+Vector((0,0,rail_h)),.072,8)
|
||||
m.beam('rail',p+Vector((0,0,rail_h)),p+Vector((0,0,rail_h+.025)),.078,8)
|
||||
m.box('dark',p+Vector((0,0,-.2)),(.19,.19,.05))
|
||||
supports.append({'position':[p.x,p.z-.32,-p.y],'postWidth':SPECS['railing']['postWidth']})
|
||||
for dz in [.5,rail_h-.035]:
|
||||
m.beam('rail',a+Vector((0,0,dz)),b+Vector((0,0,dz)),.045,4)
|
||||
for x in [-w/2,w/2]:
|
||||
railing((x,-L/2,0),(x,L/2,0))
|
||||
railing((x,L/2,0),(x,L/2+run,-drop))
|
||||
m.finish(root)
|
||||
root['terrainSupportAnchorsJson'] = json.dumps(supports)
|
||||
|
||||
|
||||
if not any(obj.get('assetId') for obj in SCENE.objects):
|
||||
for index,spec in enumerate(SPECS['towers']):
|
||||
tower(spec,index)
|
||||
radar()
|
||||
for index,spec in enumerate(SPECS['decks']):
|
||||
deck(spec,index)
|
||||
else:
|
||||
specs = {s['id']:s for s in SPECS['towers'] + SPECS['decks']}
|
||||
specs['weather-radar'] = {'id':'weather-radar', **SPECS['radar']}
|
||||
for obj in SCENE.objects:
|
||||
if obj.get('assetId'):
|
||||
spec = specs[obj['assetId']]
|
||||
if json.loads(obj['generationSpecJson']) != spec:
|
||||
raise ValueError('Dimension snapshot changed: regenerate in a new Blender file; existing geometry was preserved.')
|
||||
anchors = json.loads(obj.get('terrainSupportAnchorsJson', '[]'))
|
||||
ASSETS.append({'root':obj,'spec':spec,'kind':obj['kind'],'supports':anchors})
|
||||
bpy.context.view_layer.update()
|
||||
print('Created',len(ASSETS),'assets;',sum(len(a['root'].children) for a in ASSETS),'material batches')
|
||||
|
||||
|
||||
def export_assets():
|
||||
manifest = {'version':1,'units':'metres','upAxis':'Y','northAxis':'-Z',
|
||||
'dimensionsEstimated':True,'photographsEmbedded':False,
|
||||
'sourceConstants':'src/constants/structures.ts','assets':[]}
|
||||
# Blender 5.2 exposes these dynamic items through the exporter callback, not RNA.
|
||||
import io_scene_gltf2
|
||||
formats = io_scene_gltf2.get_format_items(None, bpy.context)
|
||||
fmt = next(item[0] for item in formats if item[0] == 'GLB')
|
||||
for asset in ASSETS:
|
||||
root,spec = asset['root'],asset['spec']
|
||||
original = root.location.copy()
|
||||
root.location = (0,0,0)
|
||||
bpy.context.view_layer.update()
|
||||
for obj in SCENE.objects:
|
||||
obj.select_set(False)
|
||||
root.select_set(True)
|
||||
for obj in root.children:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = root
|
||||
try:
|
||||
bpy.ops.export_scene.gltf(filepath=str(OUTPUT/(root.name+'.glb')),export_format=fmt,
|
||||
use_selection=True,use_active_scene=True,export_apply=True,export_yup=True,export_extras=True,
|
||||
export_animations=False,export_cameras=False,export_lights=False)
|
||||
finally:
|
||||
root.location = original
|
||||
manifest['assets'].append({'id':root.name,'file':root.name+'.glb','kind':asset['kind'],
|
||||
'latitude':spec['latitude'],'longitude':spec['longitude'],'heading':spec.get('heading',0),
|
||||
'dimensionsEstimated':True,'positionEstimated':spec.get('estimatedPosition',True),
|
||||
'source':spec['source'],'dimensions':{k:spec[k] for k in ['height','width','length','radius','buildingHeight','stairLength','stairDrop'] if k in spec},
|
||||
'origin':'upper deck surface' if asset['kind']=='deck' else 'base contact plane',
|
||||
'terrainSupportAnchors':asset['supports']})
|
||||
bpy.context.view_layer.update()
|
||||
(OUTPUT/'manifest.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
print('Exported',len(manifest['assets']),'GLBs and manifest')
|
||||
|
||||
|
||||
def setup_studio():
|
||||
if SCENE.camera:
|
||||
return SCENE.camera
|
||||
studio = bpy.data.collections.new('Presentation only - excluded from exports')
|
||||
SCENE.collection.children.link(studio)
|
||||
groundmat = material('Studio slate','23333d',.9)
|
||||
mesh = bpy.data.meshes.new('Studio ground')
|
||||
mesh.from_pydata([(-2000,-2000,-.45),(2000,-2000,-.45),(2000,2000,-.45),(-2000,2000,-.45)],[],[(0,1,2,3)])
|
||||
ground = bpy.data.objects.new('Studio ground',mesh)
|
||||
studio.objects.link(ground)
|
||||
ground.data.materials.append(groundmat)
|
||||
world = bpy.data.worlds.new('Summit studio world')
|
||||
SCENE.world = world
|
||||
world.use_nodes = True
|
||||
bg = next(n for n in world.node_tree.nodes if n.type=='BACKGROUND')
|
||||
bg.inputs['Color'].default_value = (.38,.46,.55,1)
|
||||
bg.inputs['Strength'].default_value = .55
|
||||
for name,loc,power,size in [('Key',(-25,-35,65),85000,35),('Fill',(40,-10,35),45000,25),('Rim',(0,45,55),110000,30)]:
|
||||
data = bpy.data.lights.new('Studio '+name,enum_value(bpy.types.Light,'type','AREA'))
|
||||
obj = bpy.data.objects.new(data.name,data)
|
||||
studio.objects.link(obj)
|
||||
obj.location = loc
|
||||
obj.rotation_euler = (Vector((0,5,15))-obj.location).to_track_quat('-Z','Y').to_euler()
|
||||
data.energy = power
|
||||
data.shape = enum_value(bpy.types.AreaLight,'shape','DISK')
|
||||
data.size = size
|
||||
data = bpy.data.cameras.new('Asset preview camera')
|
||||
camera = bpy.data.objects.new(data.name,data)
|
||||
studio.objects.link(camera)
|
||||
data.type = enum_value(bpy.types.Camera,'type','ORTHO')
|
||||
data.lens = 50
|
||||
SCENE.camera = camera
|
||||
SCENE.render.resolution_percentage = 100
|
||||
SCENE.render.image_settings.file_format = enum_value(bpy.types.ImageFormatSettings,'file_format','PNG')
|
||||
try:
|
||||
SCENE.render.engine = 'CYCLES'
|
||||
SCENE.cycles.samples = 24
|
||||
SCENE.cycles.use_denoising = True
|
||||
except TypeError:
|
||||
pass
|
||||
SCENE.render.film_transparent = False
|
||||
preview_camera((64,-92,64),(0,8,23),96,1600,1200)
|
||||
return camera
|
||||
|
||||
|
||||
def preview_camera(location,target,scale,width=1400,height=1100):
|
||||
camera = SCENE.camera
|
||||
camera.location = location
|
||||
camera.rotation_euler = (Vector(target)-camera.location).to_track_quat('-Z','Y').to_euler()
|
||||
camera.data.ortho_scale = scale
|
||||
SCENE.render.resolution_x = width
|
||||
SCENE.render.resolution_y = height
|
||||
|
||||
|
||||
def save_source():
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(SOURCE/'summit-structures.blend'))
|
||||
print('Saved editable Blender source')
|
||||
|
||||
|
||||
# MCP executes each request in a fresh namespace. Retain this task's helpers explicitly.
|
||||
bpy.app.driver_namespace['summit_assets'] = globals()
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Blender MCP study: shared summit slab, editable outline and summit stone."""
|
||||
import bpy,bmesh,json,math
|
||||
from pathlib import Path
|
||||
from mathutils import Vector
|
||||
ROOT=Path('D:/project/sunrise')
|
||||
OUT=ROOT/'assets/summit/blender-plateau-study'
|
||||
data=json.loads((OUT/'context.json').read_text(encoding='utf-8'))
|
||||
if bpy.data.scenes.get('Summit slab and marker'):
|
||||
raise RuntimeError('Edit the existing study; do not overwrite it by rebuilding.')
|
||||
scene=bpy.data.scenes.new('Summit slab and marker');bpy.context.window.scene=scene
|
||||
collection=bpy.data.collections.new('Summit slab authoring');scene.collection.children.link(collection)
|
||||
def enum(owner,prop,value):
|
||||
return next(i.identifier for i in owner.bl_rna.properties[prop].enum_items if i.identifier==value)
|
||||
def mat(name,color):
|
||||
m=bpy.data.materials.new(name);m.use_nodes=True
|
||||
n=next(n for n in m.node_tree.nodes if n.type=='BSDF_PRINCIPLED')
|
||||
n.inputs['Base Color'].default_value=(*color,1);n.inputs['Roughness'].default_value=.95
|
||||
m.diffuse_color=(*color,1);return m
|
||||
stone=mat('Summit pale weathered granite',(.58,.55,.47))
|
||||
texture=stone.node_tree.nodes.new('ShaderNodeTexImage')
|
||||
texture.image=bpy.data.images.load(str(ROOT/'public/terrain/materials/rock-color.jpg'),check_existing=True)
|
||||
stone.node_tree.links.new(texture.outputs['Color'],next(n for n in stone.node_tree.nodes if n.type=='BSDF_PRINCIPLED').inputs['Base Color'])
|
||||
earth=mat('Plateau transition context',(.19,.24,.14))
|
||||
ink=mat('Recessed summit inscription',(.025,.025,.022))
|
||||
def objmesh(name,verts,faces):
|
||||
mesh=bpy.data.meshes.new(name);mesh.from_pydata(verts,[],faces);mesh.update()
|
||||
obj=bpy.data.objects.new(name,mesh);collection.objects.link(obj);return obj
|
||||
|
||||
# X east, Y north, Z above 620m. Editable boundary is the single source of truth.
|
||||
boundary=[(-17,-59),(-6,-61),(4,-61),(11,-56),(15,-49),(15,-40),(15,-33),
|
||||
(12,-23),(11,-16),(10,-5),(9,6),(2,14),(-7,14),(-12,12),(-18,4),
|
||||
(-22,-4),(-25,-13),(-27,-22),(-26,-31),(-25,-40),(-22,-50)]
|
||||
outline=objmesh('WALKABLE_OUTLINE',[(x,-z,0) for x,z in boundary],[])
|
||||
edges=[(i,(i+1)%len(boundary)) for i in range(len(boundary))]
|
||||
outline.data.clear_geometry();outline.data.from_pydata([(x,-z,0) for x,z in boundary],edges,[])
|
||||
outline.hide_render=True;outline.display_type=enum(bpy.types.Object,'display_type','WIRE')
|
||||
def signed_distance(x,y):
|
||||
hit=False;distance=1e9
|
||||
points=[(a,-b) for a,b in boundary]
|
||||
for i,(ax,ay) in enumerate(points):
|
||||
bx,by=points[(i+1)%len(points)];dx=bx-ax;dy=by-ay
|
||||
t=max(0,min(1,((x-ax)*dx+(y-ay)*dy)/(dx*dx+dy*dy)))
|
||||
distance=min(distance,math.hypot(x-ax-t*dx,y-ay-t*dy))
|
||||
if (ay>y)!=(by>y) and x<(bx-ax)*(y-ay)/(by-ay)+ax:hit=not hit
|
||||
return -distance if hit else distance
|
||||
def slab_height(x,north):
|
||||
# Broad photograph-like shoulder and gentle exposed slope, with shallow joints.
|
||||
ramp=max(-14,min(49,north))*.185
|
||||
shoulder=.30*(math.tanh((north-27)/6)-math.tanh(-27/6))
|
||||
weather=.10*math.sin(x*.25)*math.sin(north*.19)
|
||||
joint=-.12*math.exp(-((x+.18*north+6)/1.1)**2)
|
||||
return ramp+shoulder+weather+joint
|
||||
vertices=[]
|
||||
for x,north,elevation in data['vertices']:
|
||||
d=signed_distance(x,north)
|
||||
t=max(0,min(1,d/7));blend=1-t*t*(3-2*t)
|
||||
height=(elevation-620)*(1-blend)+slab_height(x,north)*blend
|
||||
vertices.append((x,north,height))
|
||||
slab=objmesh('SLAB_SURFACE',vertices,data['faces']);slab.data.materials.append(stone);slab.data.materials.append(earth)
|
||||
uv=slab.data.uv_layers.new(name='Granite UV metres')
|
||||
for face in slab.data.polygons:
|
||||
center=sum((slab.data.vertices[i].co for i in face.vertices),Vector())/len(face.vertices)
|
||||
face.material_index=0 if signed_distance(center.x,center.y)<0 else 1
|
||||
face.use_smooth=True
|
||||
for li in face.loop_indices:
|
||||
p=slab.data.vertices[slab.data.loops[li].vertex_index].co;uv.data[li].uv=(p.x/3,p.y/3)
|
||||
slab['heightDatum']=620
|
||||
slab['note']='App reuses these exact terrain vertices for rendered surface and walking height.'
|
||||
|
||||
root=bpy.data.objects.new('summit-marker',None);collection.objects.link(root)
|
||||
root.location=(-12,22,slab_height(-12,22))
|
||||
root['blockedRadius']=1.1
|
||||
# Irregular broad stone with a cut face for the vertical inscription; no pedestal.
|
||||
front=[(-.82,-.43,-.10),(.76,-.43,-.10),(.65,-.43,1.72),(.35,-.43,2.12),(-.50,-.43,1.94)]
|
||||
back=[(-.65,.40,-.12),(.61,.36,-.12),(.53,.26,1.68),(.28,.17,2.05),(-.40,.28,1.86)]
|
||||
faces=[tuple(reversed(range(5))),tuple(range(5,10))]+[(i,(i+1)%5,(i+1)%5+5,i+5) for i in range(5)]
|
||||
body=objmesh('Marker granite body',front+back,faces);body.parent=root;body.data.materials.append(stone)
|
||||
bpy.context.view_layer.objects.active=body;body.select_set(True)
|
||||
bevel=body.modifiers.new('Weathered stone edges',enum(bpy.types.Modifier,'type','BEVEL'));bevel.width=.07;bevel.segments=2
|
||||
bpy.ops.object.modifier_apply(modifier=bevel.name)
|
||||
bm=bmesh.new();bm.from_mesh(body.data);bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces));bm.to_mesh(body.data);bm.free()
|
||||
uv=body.data.uv_layers.new(name='Stone UV')
|
||||
for face in body.data.polygons:
|
||||
for li in face.loop_indices:
|
||||
p=body.data.vertices[body.data.loops[li].vertex_index].co;uv.data[li].uv=(p.x/1.3,p.z/1.3)
|
||||
font=bpy.data.fonts.load('C:/Windows/Fonts/malgunbd.ttf')
|
||||
for i,letter in enumerate('관악산'):
|
||||
curve=bpy.data.curves.new('Inscription '+letter,next(t.identifier for t in bpy.data.curves.bl_rna.functions['new'].parameters['type'].enum_items if t.identifier=='FONT'))
|
||||
curve.body=letter;curve.font=font;curve.size=.42;curve.extrude=.005
|
||||
curve.align_x=enum(bpy.types.TextCurve,'align_x','CENTER')
|
||||
o=bpy.data.objects.new('Inscription '+letter,curve);collection.objects.link(o);o.parent=root
|
||||
o.location=(0,-.435,1.48-i*.47);o.rotation_euler=(math.pi/2,0,0);curve.materials.append(ink)
|
||||
for other in scene.objects:other.select_set(False)
|
||||
o.select_set(True);bpy.context.view_layer.objects.active=o
|
||||
bpy.ops.object.convert(target=next(t.identifier for t in bpy.ops.object.convert.get_rna_type().properties['target'].enum_items if t.identifier=='MESH'))
|
||||
|
||||
camdata=bpy.data.cameras.new('Plateau inspection camera');cam=bpy.data.objects.new(camdata.name,camdata);collection.objects.link(cam)
|
||||
cam.location=(12,-18,17);cam.rotation_euler=(Vector((-9,23,4))-cam.location).to_track_quat('-Z','Y').to_euler();camdata.lens=38;scene.camera=cam
|
||||
scene.render.resolution_x=1200;scene.render.resolution_y=850;scene.render.resolution_percentage=80
|
||||
scene.world=bpy.data.worlds.new('Plateau daylight');scene.world.use_nodes=True
|
||||
background=next(n for n in scene.world.node_tree.nodes if n.type=='BACKGROUND');background.inputs['Color'].default_value=(.58,.66,.76,1);background.inputs['Strength'].default_value=.7
|
||||
light=bpy.data.lights.new('Slab sun',enum(bpy.types.Light,'type','SUN'));light.energy=2;light.angle=.2
|
||||
sun=bpy.data.objects.new(light.name,light);collection.objects.link(sun);sun.rotation_euler=(.6,-.4,-.5)
|
||||
scene.render.engine='CYCLES';scene.cycles.samples=24
|
||||
bpy.app.driver_namespace['summit_plateau']=globals()
|
||||
print('Blender plateau:',len(vertices),'shared height vertices; boundary:',len(boundary),'points')
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createReadStream, statSync } from 'node:fs';
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { pipeline } from 'node:stream';
|
||||
import { BUILDINGS } from '../src/constants/buildings.ts';
|
||||
|
||||
// Only serves the precomputed building buffer; other assets use Vite's defaults.
|
||||
export function buildingCompressionMiddleware(publicDirectory: URL) {
|
||||
const path = new URL(BUILDINGS.assetBufferUrl.slice(1) + '.gz', publicDirectory);
|
||||
return (request: IncomingMessage, response: ServerResponse, next: () => void) => {
|
||||
if (request.url?.split('?')[0] !== BUILDINGS.assetBufferUrl || !['GET', 'HEAD'].includes(request.method ?? '')) return next();
|
||||
const vary = response.getHeader('Vary');
|
||||
response.setHeader('Vary', vary ? `${vary}, Accept-Encoding` : 'Accept-Encoding');
|
||||
const encodings = (request.headers['accept-encoding'] ?? '').toLowerCase().split(',').map(entry => {
|
||||
const [name, ...parameters] = entry.trim().split(';');
|
||||
const quality = parameters.find(value => value.trim().startsWith('q='));
|
||||
return { name, quality: quality ? Number(quality.trim().slice(2)) : 1 };
|
||||
});
|
||||
const accepted = encodings.find(entry => entry.name === 'gzip') ?? encodings.find(entry => entry.name === '*');
|
||||
if (!accepted || !(accepted.quality > 0) || request.headers.range) return next();
|
||||
let size: number;
|
||||
try { size = statSync(path).size; } catch { return next(); }
|
||||
response.setHeader('Content-Encoding', 'gzip');
|
||||
response.setHeader('Content-Type', 'application/octet-stream');
|
||||
response.setHeader('Content-Length', size);
|
||||
response.setHeader('Cache-Control', 'no-cache');
|
||||
if (request.method === 'HEAD') { response.end(); return; }
|
||||
pipeline(createReadStream(path), response, error => { if (error) response.destroy(error); });
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pyshp==2.3.1
|
||||
shapely==2.1.2
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Save a small set of publicly displayed, recent structure reference photos."""
|
||||
import html
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
root=Path(__file__).resolve().parents[1]
|
||||
page=(root/'.tools/sunrise-photo-page.html').read_text(encoding='utf-8')
|
||||
records=[]
|
||||
patterns=['송신소를 배경으로','송신소의 철탑 4개','안전목책이 설치된','연주대 전망데크에서 드디어','기상레이더 관측소 좌측 뒤로','관악산정상을 출발해 북쪽','중간의 전망데크에서 진행 방향']
|
||||
for index,pattern in enumerate(patterns,1):
|
||||
at=page.find(pattern)
|
||||
figures=list(re.finditer(r'<figure\b.*?</figure>',page[:at],re.S))
|
||||
if at<0 or not figures:
|
||||
continue
|
||||
figure=figures[-1].group()
|
||||
url=html.unescape(re.search(r'<img[^>]+src="([^"]+)"',figure).group(1))
|
||||
name=f'recent-{index:02d}.jpg'
|
||||
target=root/'data/references/summit/photos'/name
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=30) as response:
|
||||
data=response.read()
|
||||
if not data.startswith(b'\xff\xd8'):
|
||||
raise ValueError('Not JPEG')
|
||||
target.write_bytes(data)
|
||||
records.append({'file':name,'title':f'2026-06-03 structure reference {index}','status':'downloaded','source':'https://ydk5118.tistory.com/16906972','download_url':url,'metadata':{'Artist':{'value':'칠갑산 사랑'},'DateTimeOriginal':{'value':'2026-06-03 (게시글 산행일 기준)'},'LicenseShortName':{'value':'공개 재사용 허락 미확인 — 로컬 참고용, 앱 배포 자산으로 사용하지 않음'}}})
|
||||
print(name,'downloaded',flush=True)
|
||||
except Exception as error:
|
||||
print(name,str(error),flush=True)
|
||||
time.sleep(1)
|
||||
(root/'data/references/summit/photos/recent-downloads.json').write_text(json.dumps(records,ensure_ascii=False,indent=2),encoding='utf-8')
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Collect Commons reference thumbnails; preserve attribution and download status."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
root = Path(__file__).resolve().parents[1] / 'data/references/summit/photos'
|
||||
pages = json.loads((root / 'commons-metadata.json').read_text(encoding='utf-8-sig'))['query']['pages']
|
||||
old = json.loads((root / 'manifest.json').read_text(encoding='utf-8-sig'))
|
||||
by_title = {p['title']: p for p in pages.values()}
|
||||
result = []
|
||||
for index, entry in enumerate(old, 1):
|
||||
info = by_title[entry['title']].get('imageinfo', [])
|
||||
if not info:
|
||||
result.append({'title': entry['title'], 'status': 'missing metadata'})
|
||||
continue
|
||||
info = info[0]
|
||||
name = f'photo-{index:02d}.jpg'
|
||||
original = root / entry['file']
|
||||
target = root / name
|
||||
record = dict(title=entry['title'], file=name, source=info['descriptionurl'], metadata=info['extmetadata'])
|
||||
try:
|
||||
if original.exists():
|
||||
original.rename(target)
|
||||
record['download_url'] = info['url']
|
||||
elif not target.exists():
|
||||
time.sleep(3)
|
||||
url = info.get('thumburl', info['url'])
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'SunriseReferenceCollector/1.0 (local research)'})
|
||||
with urllib.request.urlopen(request, timeout=25) as response:
|
||||
data = response.read()
|
||||
if not data.startswith(b'\xff\xd8'):
|
||||
raise ValueError('Not JPEG')
|
||||
target.write_bytes(data)
|
||||
record['download_url'] = url
|
||||
record['status'] = 'downloaded'
|
||||
except Exception as error:
|
||||
record['status'] = str(error)
|
||||
result.append(record)
|
||||
print(name, record['status'], flush=True)
|
||||
(root / 'downloads.json').write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||