# 복셀 야구장 v3.8 — 실제 표면 질감을 담은 GLB

현재 승인된 인트로는 [ocean-intro-v1.0.0](./versions/ocean-intro-v1.0.0/README.md)에 코드·재생 에셋·검증 결과와 함께 보관했습니다. 이후 인트로 수정은 이 버전을 기준으로 진행하며 보관 압축 파일은 덮어쓰지 않습니다.

타격 페이지는 이제 인트로를 자동 반복하며 참여를 기다립니다. 홈런 챌린지 제목과 하단 시작 안내, 왼쪽 위 압정 카드의 참여 QR은 인트로에서만 표시합니다. QR은 기존 `/controller/?room=…` 주소와 LAN 주소 탐색을 재사용합니다. 연습·휴대폰 게임 시작 시 타석으로 이동하고, 연습 종료·연결 해제·최종 결과창 닫기 후 대기 상태에서는 인트로로 돌아옵니다. 플레이 중에는 큰 제목과 시작 안내를 숨기고 점수·판정·연습 조작 버튼을 표시합니다. `?view=batting&intro=0`으로 타석에 바로 진입할 수 있습니다. 동작 줄이기 설정에서는 인트로를 정지 화면으로 보여줍니다.

[야수가 배치된 타석](https://localhost:3451/stadium-comparison/ocean.html?view=batting) · [수비 배치 전체 보기](https://localhost:3451/stadium-comparison/ocean.html?view=fielders)

화면 상단의 **결과 · 확률 시연**에서 타격 결과 8종과 수비 동작 5종을 한 구씩 자동 재생합니다. 수비는 일반 포구·장거리 달려서 포구·전방·좌측(글러브)·우측(백핸드) 슬라이딩입니다. 좌·중·우 타구 방향과 느림/빠름을 고를 수 있고 동작별 시연은 해당 동작에 맞는 궤적을 사용합니다. `?view=batting&intro=0&preview=1`은 버튼 모음을 엽니다. 시연은 같은 공·타격·포구·추적 카메라를 사용하며 한 구 뒤 멈추고 서버 기록을 전송하지 않습니다. 다시 보기·일시정지·종료를 지원하고 일반 연습·휴대폰 참여 중에는 시연을 막습니다. `window.oceanStudy.game.playDemo({ kind: "flyout", catchStyle: "RunningCatch", direction: "center" })`, `playDemo({ kind: "flyout", pace: "slow" })`, `stopDemo()`도 제공합니다.

`stadium-fielders.js`가 승인된 야수 GLB 네 종으로 내야수 4명(1루수·2루수·유격수·3루수)과 외야수 3명(좌익수·중견수·우익수)을 세웁니다. 모델의 미터 크기와 왼손 글러브를 유지하고 홈플레이트를 바라보게 합니다. 대기 `Ready`는 각 선수의 독립 스켈레톤·믹서에서 서로 다른 시작 시점으로 반복됩니다. 4개 파일을 한 번씩 받고 7개의 독립 GLB 그래프로 파싱하며 원본 GLB는 변경하지 않습니다.

배치는 월드 XZ 기준 1루수 `[23,-30]`, 2루수 `[15,-38]`, 유격수 `[-15,-38]`, 3루수 `[-23,-30]`, 좌익수 `[-40,-74]`, 중견수 `[3,-91]`, 우익수 `[40,-74]`입니다. 모두 페어 지역에 있고 높이는 `groundHeight()`를 따릅니다. 중견수는 타석에서 투수와 완전히 겹치지 않도록 중심축 오른쪽 3m에 둡니다. 지면 투영 그림자는 대기 자세를 따라 움직이며 구장의 4K 그림자 캐시는 유지합니다.

야수 대기는 움직임 정지·게임 일시정지·인트로 일시정지·탭 숨김·OS 동작 줄이기를 따릅니다. 기존 타석 카메라를 유지했으며 메뉴(☰)의 ‘수비 배치’ 버튼으로 일곱 명을 내려다볼 수 있습니다. `window.oceanStudy.fielders.getState()`에서 포지션·모델·좌표·회전·대기 시간과 진행 중인 수비를 확인합니다. [배치 검증 결과](./fielders-validation.json).

`fielding-play.js`는 땅볼·파울·뜬공을 예측해 도착 가능한 야수를 선택합니다. 최고 12m/s로 질주하고 Run 배율은 2.15배로 제한합니다. 장거리 포구와 슬라이딩은 공을 읽는 Ready 대기 이후 빠른 달리기에서 캐치로 이어집니다. 캐치 진입은 일반 포구 0.32초·땅볼 0.42초·슬라이딩 약 0.473초입니다. 슬라이딩 세 종류의 재생 속도는 1.2배에서 1.32배로 10% 올렸고 일반 포구는 1.2배를 유지합니다. 글러브 포켓과 Hips 이동은 실제 GLB에서 샘플링하며 게임 공 하나를 포구 후 왼손에 연결합니다. 고정 자세를 혼합하고 발·발끝 접지를 보정합니다. 장거리 시연은 외야수가 22~29m를 달려오며 좌우 슬라이딩 시연은 중견수가 약 20m를 질주합니다.

수비와 공은 같은 게임 시계로 진행합니다. 게임 정지·인트로·탭 숨김에서 함께 멈추고 환경 움직임만 꺼도 수비는 계속됩니다. 포구·복귀 및 타구 카메라·결과 표시가 끝난 뒤 다음 투구를 시작합니다. 안타·2루타는 `fielding-pursuit.js`가 예상 낙하지점에 가장 가까운 야수 한 명을 선택해 최고 12m/s로 추격시킵니다. 실제 바운드·담장 반사·구르는 공을 따라가고 경기장 안에 머뭅니다. 공을 글러브로 끌어오거나 아웃으로 바꾸지 않으며 공이 멈추면 원래 위치로 복귀합니다. 홈런(장외 포함)·헛스윙·스트라이크에는 추격을 생성하지 않습니다. 땅볼·파울 수비 연결 전 소스는 `versions/fielders-ready-v1/`, 뜬공 추가 전 소스는 `versions/fielding-v1/`에 보관했습니다. [땅볼·파울 검증](./fielding-validation.json) · [뜬공 검증](./flyout-validation.json).

수비 동작은 무작위 확률 대신 접점의 수평 타구 속도와 야수 상대 위치로 결정합니다. 14m/s(50.4km/h) 미만은 일반 포구이며, 빠른 공이 야수 왼쪽으로 벗어나면 GloveSideCatch, 오른쪽이면 BackhandCatch, 앞쪽이면 ForwardCatch를 사용합니다. 로컬 +X가 야수 왼쪽, -X가 오른쪽입니다. 실제 스윙 세기 0.65 이상은 빠른 궤적(뜬공 30m/s·26.5°), 그 미만은 느린 고각 궤적(29m/s·67°)을 사용합니다. 뜬공은 첫 바운드 전에 실제로 잡아야 아웃으로 기록합니다. GLB와 FlyCatch JSON은 유지하고 발부터 SlideCatch는 게임·시연 선택에서 제외합니다. 이전 소스는 `versions/fielding-before-directional-v1/`에 보관했습니다.

현재 확률은 전체 스윙에 대한 고정 비율이 아니라 **타이밍 구간 안의 조건부 확률**입니다. 배트를 너무 늦게 휘둘러 공이 이미 지나간 경우에는 헛스윙으로 처리합니다. `/wall`의 기존 타이밍 분류는 유지하며 이 분기는 바다 야구장에 적용합니다.

| 타격 타이밍 오차(절댓값) | 결과                            |
| ------------------------ | ------------------------------- |
| 0~55ms                   | 홈런 50% · 2루타 50%            |
| 55ms 초과~115ms          | 홈런 30% · 2루타 30% · 안타 40% |
| 115ms 초과~175ms         | 땅볼 50% · 뜬공 아웃 50%        |
| 175ms 초과~240ms         | 파울 100%                       |
| 240ms 초과               | 헛스윙 100%                     |
| 스윙 없음                | 스트라이크                      |

| 수비 동작          | 조건                                       |
| ------------------ | ------------------------------------------ |
| 일반 / 달려서 포구 | 느린 타구 또는 여유 있게 받을 수 있는 타구 |
| 전방 슬라이딩      | 빠른 공이 야수 앞쪽으로 벗어남             |
| 좌측 슬라이딩      | 빠른 공이 야수 왼쪽으로 벗어남             |
| 우측 슬라이딩      | 빠른 공이 야수 오른쪽으로 벗어남           |

타구 방향은 페어 ±44도 안에서 균등하게 선택하며 파울 방향은 좌우 각 50%입니다. 장외홈런은 홈런 궤적에 포함하고 별도 확률을 두지 않습니다. 뜬공 아웃은 타수에 포함하고 안타에는 포함하지 않습니다. 소스는 `shared/batting-rules.js`의 `chooseOceanContact`와 `stadium-comparison/fielding-rules.js`에 있습니다.

[실제 타석의 타자 보기](https://localhost:3451/stadium-comparison/ocean.html?view=batter): `stadium-batter.js`가 클래식 타자의 실제 GLB를 미터 단위 원본 크기로 불러와 오른손 타자 박스의 `[-0.95, 0, -0.30]`에 세웁니다. Y축으로 90도 돌려 투수를 향하게 했으며, 임팩트 시 배럴 접촉점은 약 `[-0.007, 1.008, -0.439]`로 홈플레이트 중앙 바로 앞입니다. 초기 상태는 준비 자세이고, 타자/타석 시점의 패널에서 스윙·일시정지·0.1~2.5배속을 조절합니다. 가까운 타자 카메라는 최소 거리 2m를 사용합니다. 타자만 지면에 투영하는 가벼운 스킨 그림자로 움직임을 따라가며, 기존 구장 그림자 캐시는 유지합니다. 인트로 중에는 준비 자세로 멈추고 패널을 숨깁니다. 기존 야구장·타자 GLB 파일과 승인된 인트로 보관본은 그대로입니다.

바다 환경 시안은 [ocean.html](https://localhost:3451/stadium-comparison/ocean.html)에서 확인합니다. 승인된 v3.8 야구장의 위치·형상과 보관 GLB를 유지하며, `ocean-environment.js`가 해수면(Y=-4.2m), 방파석·기단, 동쪽 선착장, 요트 5척(범선 3 / 모터 요트 2), 갈매기 10마리를 별도 root로 생성합니다. 갈매기는 필드·외야·관중석 상공의 세 높이에서 서로 다른 반경과 속도로 순환합니다. 카메라 아홉 시점과 주변 움직임 재생/정지는 `ocean-viewer.js`에 있습니다. 갈매기와 요트는 외부 에셋 없이 코드로 만든 입체 모델입니다.

[홈플레이트 정면 보기](https://localhost:3451/stadium-comparison/ocean.html?view=batting)가 기본 시점입니다. `batting-camera.js`의 공통 프리셋은 홈 뒤 `(0,1.2994,3.49607)`에서 `(0,1.699167,-3.801539)`를 바라보며 시야각은 45도입니다. 사용자가 G로 캡처한 위치·시선의 Y/Z와 시야각을 유지하고, 두 X를 0으로 맞춰 홈플레이트–투수판 중심축에 정렬했습니다. 가까운 타석에서 약간 위를 바라보는 구도이며, 구장의 좌우 건물 배치는 그대로입니다. 기본 진입·‘홈 정면’ 버튼·인트로 완료/건너뛰기에서 이 구도를 사용합니다. 드래그로 시선을 바꾸고 휠로 시야각을 조절할 수 있으며, 모바일에서도 카메라 위치는 같습니다.

`ocean-sky.js`는 양궁 프로젝트의 `scene.js / buildClouds`를 참고한 선명한 파란 하늘과 흰색 복셀 구름 24개를 만듭니다. 근거리 6개·중거리 9개·원거리 9개를 구장 전체 둘레로 분산하고, 높이는 220~900m로 달리해 푸른 하늘 사이에 간격을 두었습니다. 실제 입체 형상이므로 멀리 있는 구름은 작고 연하게 보이며, 같은 월드 이동량도 화면에서 작게 움직입니다. 해수면과 하늘은 같은 수평선 색을 사용합니다.

[잔디·흙 확대 보기](https://localhost:3451/stadium-comparison/ocean.html?view=surface)는 새 필드 질감을 확인하는 시점입니다. 바다 시안의 `tiled-field-materials.js`가 Turf·Mound에만 반복 PNG 재질을 적용합니다. [ambientCG Grass004](https://ambientcg.com/a/Grass004)와 [Ground054](https://ambientcg.com/a/Ground054)의 원본 2K PNG 색상·OpenGL normal·roughness 6장을 사용하며, 잔디는 1.4m, 흙은 3.5m마다 반복합니다. 출처·CC0 라이선스·원본 SHA256은 `assets/field/sources.json`, 다운로드 재현은 `python scripts/stadium/fetch-field-assets.py`입니다.

기존 4K atlas에서 흙 마스크만 추출해 승인된 흙 경계를 보존하고, 잔디 3.15m 체크와 미세 질감은 각각 화면 픽셀 크기에 맞게 필터링합니다. 거칠기는 0.88~1.0으로 제한해 흰 반사를 줄였으며, 미터 좌표에서 노멀의 접선 방향을 다시 계산해 마운드에도 같은 크기로 이어집니다. 이 혼합은 Three.js 실행 시 적용하는 셰이더이며 기존 v3.8 GLB에는 포함되지 않습니다. 현재 `applyTiledFieldMaterials(stadium)`은 `createTexturedStadium()`의 Canvas atlas 입력을 사용하며, 반환된 `dispose()`를 장면 종료 시 호출합니다. 향후 GLB 입력 지원에는 로드된 이미지의 마스크와 UV 방향을 처리하는 별도 연결 작업이 필요합니다.

[시네마 인트로 재생](https://localhost:3451/stadium-comparison/ocean.html?intro=1)은 로딩 후 35초짜리 6컷을 자동 재생합니다. 일반 바다 시안에서는 상단 ‘인트로 재생’ 버튼으로 시작합니다. 요트 옆 수면 이동(6초) → 곡선을 따라 구장 공개(7초) → 옥상 수영장(5초) → 실제 갈매기 동행(6초) → 그라운드 저공 이동(5초) → 타석 도착(6초) 순서입니다. `ocean-intro.js`가 원래 양궁 인트로의 카메라/환경 분리 구조를 참고하되, 자체 베지어 경로·시선·렌즈·컷 길이를 사용합니다. 구장 공개 컷에는 타이틀이 나타나며, 컷 전환과 종료에는 짧은 블랙 전환을 넣었습니다.

`ocean-intro.css`는 상하 레터박스와 재생 UI를 담당합니다. 가로 화면은 약 2.39:1의 화면 구성을 기준으로 하고, 세로 화면은 높이의 9%씩만 가립니다. 가로 모바일에서는 캔버스 최소 높이를 해제해 하단 버튼이 실제 화면 안에 들어오게 합니다. 일반 컷의 렌즈는 레터박스 내부 시야각을 기준으로 계산하며 외부 컷에는 세로 화면 거리 보정을 적용합니다. 마지막 타석 컷은 전체 캔버스 기준 45도로 도착하여 레터박스가 사라져도 구도가 바뀌지 않습니다. 그라운드·타석 컷에는 거리 보정을 적용하지 않습니다.

인트로 중 자유 시점 UI·드래그·휠은 잠기고, ‘일시정지’는 카메라와 바다·요트·갈매기·구름을 함께 멈춥니다. ‘타석으로’ 또는 Esc는 홈 정면 시점으로 전환합니다. 끝까지 재생하면 공통 프리셋의 타석 좌표 `(0,1.2994,3.49607)`에 도착해 둘러보기로 이어집니다. 숨겨진 탭에서는 시간이 멈추고, OS의 모션 줄이기 설정에서는 자동 재생하지 않습니다. 다시 재생 버튼은 계속 사용할 수 있습니다. 제작 검증용으로 `window.oceanStudy.intro`의 `getState()`, `seek(seconds)`, `pause(boolean)`, `play()`, `stop()`을 제공합니다.

현재 인트로의 도착지는 제작 시안의 타석 뷰이며 실제 `/wall/` 게임 시작이나 소켓 접속에는 아직 연결하지 않았습니다. 수면은 Three.js 셰이더로 잔물결·하늘 반사·햇빛·연안 포말을 표현하며 물리적 유체나 선체 반사 시뮬레이션은 아닙니다. 현재 페이지는 제작 시안으로, 제품용 성능 최적화 이전입니다.

승인된 v3.7 형상에 벽돌·석재·콘크리트·목재·잔디·흙의 PBR 표면을 적용했습니다. 위치·법선·인덱스·정점 색과 플레이 좌표는 유지하고 UV 및 재질만 추가합니다. 벽돌 줄눈과 나뭇결, 미세 요철은 2K 색상·OpenGL normal·roughness 지도이며, 필드와 광장은 기존 잔디 체크와 흙 경계를 보존한 4K atlas입니다. 물에는 잔물결 normal과 IOR 1.333, clearcoat를, 유리·금속·좌석에는 서로 다른 반사와 거칠기를 적용했습니다. 텍스처는 전부 GLB 안에 포함됩니다.

기존 v3.7 HQ GLB는 `versions/v3.7.0/models/voxel-ballpark-v3.7-hq.glb`에, 당시 내보내기 소스는 `versions/v3.7.0/export-source/`에 보관했습니다. 최신 파일은 `models/voxel-ballpark.glb`와 `versions/v3.8.0/models/voxel-ballpark-v3.8-pbr.glb`입니다. 기본 화면과 `?compare=baseline`은 실제 재질 / 보관 v3.7 GLB를 비교합니다. `?compare=glb`는 Three.js PBR 원본 / 실제 v3.8 GLB를 비교하고, `?compare=reference`는 컨펌한 이미지를 표시합니다. 두 패널은 같은 카메라·조명·반사 환경을 사용합니다.

- [실제 재질 GLB 다운로드](./versions/v3.8.0/models/voxel-ballpark-v3.8-pbr.glb)
- [질감 적용 전후](https://localhost:3451/stadium-comparison/?compare=baseline&view=facade)
- [Three.js와 실제 GLB 비교](https://localhost:3451/stadium-comparison/?compare=glb&view=pool-detail)
- [원본 질감·해상도·해시·라이선스](./assets/pbr/sources.json)

재질 원본은 [ambientCG](https://ambientcg.com/)의 Bricks059, Concrete034, Wood049, Grass004, Ground054이며 [CC0-1.0](https://docs.ambientcg.com/license/)으로 제공됩니다. 물결·직물·잎 표면의 미세 질감은 결정적 절차식으로 생성합니다. 재다운로드는 `python scripts/stadium/fetch-pbr-assets.py`로 수행합니다. Water는 표면 반사를 표현하며 유체 시뮬레이션이나 움직이는 물결은 포함하지 않습니다.

## 타자 대기 동작 비교

[세 가지 대기 동작](https://localhost:3451/character-study/idle.html)은 같은 클래식 타자의 호흡형·체중 이동형·배트 리듬형을 나란히 보여줍니다. 사용자가 선택한 **3번 배트 리듬형(`waggle`)**이 기본 대기 동작이며 스윙 후에도 같은 동작으로 돌아옵니다. 공통 기본값은 `idle-animation.js`의 `DEFAULT_IDLE`에 있습니다. 바다 시안의 타자 패널에서도 ‘대기 동작’ 버튼으로 전환하고 멈출 수 있습니다. `ocean.html?idle=breath`, `?idle=shift`, `?idle=waggle`로 다른 시안도 확인할 수 있습니다. 승인된 기본 카메라는 그대로입니다.

이 동작들은 기존 GLB 본에 적용하는 Three.js 런타임 레이어입니다. 발 고정과 양손 배트 그립을 유지하고, 스윙 시작 후 0.25초 이내에 대기 영향이 사라집니다. 스윙 종료 후에는 선택한 대기로 돌아옵니다. 인트로 중에는 동작을 멈추고, 모션 감소 설정에서는 대기를 자동 재생하지 않습니다. 기존 GLB와 인트로 보관본은 재출력하지 않았습니다. 진단 API `window.oceanStudy.batter`에 `setIdle(id)`, `setIdlePaused(boolean)`, `seekIdle(seconds)`를 추가했으며 `getState().idle`로 시간·속도·가중치와 손/발 IK 오차를 확인합니다.

## 바다 시안의 카메라 이동과 앵글 출력

`ocean.html`에서 양궁 프로젝트의 자유 카메라와 같은 키를 사용합니다. `W/S`는 수평 앞뒤, `A/D`는 좌우, `R/F`는 높이 이동이며 `Shift`를 함께 누르면 3배 빨라집니다. 기본 속도는 9m/s이고 대각선도 같은 속도입니다. 홈 정면에서는 드래그로 제자리에서 시선을 돌리고 휠로 시야각을 바꿉니다. 다른 시점에서는 기존 회전·확대 조작을 사용합니다.

원하는 구도에서 `G` 또는 하단 ‘앵글 출력’을 누르면 개발자 도구 Console에 `[Ocean camera preset]`과 `{ position: [x,y,z], target: [x,y,z], fov }` JSON이 출력됩니다. 카메라 위치와 바라보는 지점을 같은 거리만큼 옮기며, 출력값은 소수점 여섯 자리의 복사본입니다. `window.oceanStudy.captureCamera()`도 같은 값을 출력하고 반환합니다. 이동하거나 시선을 조절한 뒤에는 DevTools를 열거나 화면 크기를 바꿔도 그 좌표를 유지합니다. ‘홈 정면’ 버튼으로 기본 위치를 복원합니다.

`camera-navigation.js`가 키 상태와 출력을 담당합니다. 인트로 중에는 이동과 출력을 잠그며, 인트로 시작·프리셋 전환·입력 필드 포커스·창 전환 시 누른 키를 비웁니다. Ctrl/Alt/Meta 단축키와 텍스트 입력에는 반응하지 않습니다. 물리 키 코드 기준이므로 한글 입력 상태에서도 같은 위치의 키를 사용합니다. 이 이동은 구도를 찾기 위한 자유 카메라이며 보행 충돌 처리는 적용하지 않습니다.

## 보관된 형상 작업 내역

옥상의 나무는 수관을 넓히고 높이가 다른 군집으로 바꿨습니다. 서관 좌석 양끝과 전광판의 측면·후면에 큰 나무를 두고, 앞쪽은 화면을 가리지 않는 낮은 화단으로 연결했습니다. 중앙 옥상과 전광판 뒤에는 목재 데크와 테이블, 벤치, 작은 조명을 배치했습니다. 수영장은 15.6×13.2m로 넓히고 모서리 나무 4그루, 선베드 4개와 화단을 추가했습니다. 분수는 금색 수반을 약 8.5m로 키우고 물줄기, 모서리 화분, 벤치 2개로 테라스를 채웠습니다. 소품은 `roof-props.js`에서 지붕의 로컬 좌표를 기준으로 생성합니다. 승인된 삼각석과 건물 구조, 필드, U자 관람석은 v3.6 그대로입니다.

- [옥상 정원 수정 전후](https://localhost:3451/stadium-comparison/?view=roof-gardens&compare=baseline)
- [수영장 확대 비교](https://localhost:3451/stadium-comparison/?view=pool-detail&compare=baseline)
- [분수 테라스 확대 비교](https://localhost:3451/stadium-comparison/?view=fountain-detail&compare=baseline)

최신 이미지의 빨간 윤곽에 맞춰 삼각석을 더 완만하게 눕혔습니다. 오른쪽 밑변 끝 XZ `[-22,-123.3]`은 유지하고, 왼쪽 끝은 `[-49.45,-106.83]`로 옮겨 밑변을 10% 줄였습니다. 시작 높이는 3.5m, 최고 단 높이는 23.5m에서 17.5m로 낮췄습니다. 경사진 관중석 면의 세 꼭짓점 사이 거리가 같은 정삼각형이 되도록, 밑변의 수직 이등분선 위에서 꼭짓점 위치를 계산합니다. 밑변 길이를 L, 높이 차를 H라 하면 평면 깊이는 `sqrt(3*L²/4-H²)`입니다. 세 변은 약 32.012m, 경사는 약 30.33도입니다. 좌석·계단·기초도 같은 월드 좌표로 함께 생성합니다. 관중석 코핑과 건물 기둥의 돌출까지 포함해도 서측 연결동과 약 0.80m, 전면 파빌리온과 약 0.69m 떨어집니다.

파란 표시 영역인 기존 삼각석 자리에는 폭 40m, 깊이 11m, 높이 18m의 3층 전면 건물을 추가했습니다. 층별 열린 갤러리, 안쪽 창, 초록 차양과 옥상 화단이 있으며 타워·동측 연결동과 이어집니다. 필드와 승인된 U자 관람석, 수영장 타워와 기존 동관의 위치는 v3.2 그대로입니다.

외야 건물 앞뒤에는 최대 4.8m 깊이의 열린 갤러리를 만들었습니다. 바닥·천장 보·난간·방 칸막이가 실제 입체이고, 창은 안쪽 벽에 있습니다. 좁은 연결동은 건물 깊이의 30% 이내로 갤러리 깊이를 제한합니다. 필드·펜스·베이스·마운드와 승인된 v3.1 U자 관람석은 유지합니다.

- [수정안과 원본 이미지](https://localhost:3451/stadium-comparison/)
- [수정 전후 Three.js 비교](https://localhost:3451/stadium-comparison/?compare=baseline)
- [외야 상세 확대 비교](https://localhost:3451/stadium-comparison/?view=outfield-detail&compare=baseline)
- [삼각석 평면 확대 비교](https://localhost:3451/stadium-comparison/?view=stand-top&compare=baseline)
- [표시 이미지와 비슷한 정면 시점](https://localhost:3451/stadium-comparison/?view=stand-front&compare=baseline)
- [외벽 디테일 확대 비교](https://localhost:3451/stadium-comparison/?view=facade&compare=baseline)

외야 배치는 `field-layout.js`의 `ARCHITECTURE`, 건물은 `world-model.js`, 회전 가능한 삼각석은 `outfield-stand.js`에 있습니다. 승인된 U자 관람석은 `bowl-detail.js`에 별도로 유지합니다. 아래 좌표·통합 설명은 현재 v3.7 GLB에도 포함된 공통 필드 설계입니다.

건물·필드·관람석을 같은 월드 좌표로 구성한 야구장입니다. 건물의 앞·뒤·옆면과 지붕, 계단형 관람석을 실제 입체로 연결했습니다. 한 카메라에서만 모양을 맞추던 v2의 이미지 역투영 코드는 현재 모델에서 사용하지 않습니다.

- [Three.js 시안 비교](https://localhost:3451/stadium-comparison/)
- [뒷면과 원본 이미지 비교](https://localhost:3451/stadium-comparison/?view=rear-left&compare=reference)
- [타석 눈높이에서 둘러보기](https://localhost:3451/stadium-comparison/?view=batting)
- [GLB 다운로드](./models/voxel-ballpark.glb)
- [좌표·치수·충돌 경계 manifest](./models/manifest.json)

## 조작

외부 시점은 드래그로 회전, 휠/핀치로 확대합니다. 앞·뒤·위 시점 버튼과 자동 회전을 사용할 수 있습니다. 기준 이미지 비교는 네 방향의 해당 PNG를 표시합니다.

타석·마운드·외야 시점에서는 원근 카메라로 전환합니다. 캔버스를 누르고 WASD로 이동하며 드래그로 시선을 돌립니다. Shift는 빠른 이동입니다. 모바일에서는 화살표 버튼을 누르는 동안 이동하고, 캔버스를 드래그해 둘러봅니다. 눈높이는 지면 위 1.7m이며 마운드 높이를 따라갑니다. 외야 펜스와 관람석 앞 경계 안쪽으로만 이동합니다. 동기화를 켜면 두 구현의 카메라가 함께 움직입니다.

## 좌표와 크기

모든 부위는 **1 unit = 1m**, root scale `(1,1,1)`입니다. 홈플레이트 뒤쪽 꼭짓점이 `(0,0,0)`, 위는 `+Y`, 외야는 `-Z`, 1루 방향은 `+X`입니다.

- 루간 거리: 27.432m
- 홈에서 투수판 앞쪽까지: 18.4404m
- 마운드 높이: 0.254m
- 좌우 파울 폴 거리: 약 103.24m, 중앙 펜스: 119m
- 외야 패딩 높이: 3.1m, 상단 마감 포함: 3.3m

루간 거리와 투수판 거리는 [MLB 필드 치수](https://www.mlb.com/glossary/rules/field-dimensions)를 기준으로 삼았습니다. 외야·관람석·건물은 컨셉을 바탕으로 만든 이 구장의 설계값입니다. 서로 다른 원본 이미지의 세부 표현은 하나의 일관된 구조로 정리했습니다.

## 소스

`outfield-stand.js`는 삼각석 앞변을 기준으로 좌석·계단·기초를 함께 회전해 생성합니다.

| 파일                                             | 역할                                                        |
| ------------------------------------------------ | ----------------------------------------------------------- |
| `field-layout.js`                                | 필드 기준 좌표, 건물 배치, 관람석 평면, 이동 영역, metadata |
| `world-geometry.js`                              | 실제 XYZ 박스·기둥·닫힌 프리즘과 로컬 좌표 변환             |
| `world-model.js`                                 | 건물·관람석·필드·장식·표면 텍스처 생성                      |
| `roof-props.js`                                  | 옥상 나무·연속 화단·데크·가구·조명 생성                     |
| `stadium.js`                                     | `createStadium()`, `getModelStats()`, 버전                  |
| `viewer.js`                                      | 두 모델 로드, 외부/내부 카메라, 이동, SSAO, 동기화          |
| `models/voxel-ballpark.glb`                      | 현재 v3.8 전체 형상과 PBR 텍스처를 내장한 glTF 2.0 binary   |
| `models/manifest.json`                           | 버전·용량·통계·앵커·펜스 및 전체 경계                       |
| `vendor/`                                        | 로컬 Three.js r160와 addons, MIT 라이선스                   |
| `scripts/stadium/export-glb.cjs` (프로젝트 루트) | GLB와 manifest 재생성                                       |

실제 재질 GLB는 `await createTexturedStadium({ textureSize: 4096 })`의 결과입니다. 기존 동기 함수 `createStadium()`도 기본 색상 재질과 같은 형상으로 유지합니다. `realistic-materials.js`는 물리적 크기의 UV와 표준 glTF 재질을 적용하고, `daylight-environment.js`는 뷰어의 반사 환경을 생성합니다. 조명·환경 맵·SSAO는 GLB에 굽지 않으므로 다른 앱에서 사용할 때도 환경 맵과 조명을 설정해야 물·유리·금속의 반사가 보입니다. 커스텀 셰이더 없이 표준 metallic-roughness와 `KHR_materials_ior`, `KHR_materials_clearcoat`를 사용합니다.

Normal map에 사용하는 tangent를 명시적으로 계산해 GLB에 포함합니다. 재질이 다른 분수 수반/물 및 가구 금속/목재는 material group으로 구분하므로, GLTFLoader의 mesh 수는 254개이고 삼각형 수는 원본과 같은 624,240개입니다. `vertexColors:false` 재질에는 `COLOR_0`를 생략해 기존 색이 질감 위에 중복으로 곱해지지 않게 했습니다. 원본 Three.js geometry의 색 배열은 그대로 보관합니다. 내보내기는 임시 브라우저의 바이너리 다운로드로 저장해 큰 파일을 base64 문자열로 전송하지 않습니다.

## 프로젝트에 적용

```js
import { createTexturedStadium } from '/stadium-comparison/stadium.js';
const stadium = await createTexturedStadium();
scene.add(stadium);
const metadata = stadium.userData;
```

현재 외야 조경과 관중석 수정이 포함된 GLB를 다음과 같이 불러올 수 있습니다.

```js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const gltf = await new GLTFLoader().loadAsync(
  '/stadium-comparison/models/voxel-ballpark.glb',
);
scene.add(gltf.scene);
const stadium = gltf.scene.getObjectByName('World_space_voxel_ballpark');
const metadata = stadium.userData;
```

필요한 메타데이터는 root의 glTF extras와 manifest에 함께 있습니다.

- `anchors`: 홈·베이스·투수판·타자 및 투수 시작 좌표. `Gameplay_anchors` 아래 실제 Object3D도 포함합니다.
- `field.playablePolygonXZ`: 필드 이동 가능 영역의 XZ 다각형.
- `fenceSegments`: 페어 외야 펜스 중심선, 지면 높이, 마감 포함 전체 높이, 두께, 안쪽 법선입니다.
- `boundarySegments`: 파울 지역 낮은 벽과 양쪽 연결 게이트까지 포함한 전체 폐곡선. 실제 벽 표면은 중심선에서 두께/2만큼 떨어져 있으므로 충돌체 반경을 더해 판정합니다.
- `meshRoles`: 실제 mesh 이름으로 된 표면·벽·장식 목록.
- `cameras`: 타석·마운드·외야의 초기 원근 카메라 좌표.

이 시안은 필드 내부 이동과 경계 제한을 제공합니다. 기존 `/wall/` 게임의 투구·타격·공 물리에는 아직 연결하지 않았습니다. 기존 게임의 `PARK_SIZE=4`는 외야에만 적용되므로 이 모델을 그 그룹 아래 넣으면 안 됩니다. 실제 게임 연결 시에는 홈을 원점으로 맞추고 베이스·공 위치·벽 충돌을 위 metadata의 미터 단위로 함께 전환해야 합니다. 건물 내부와 관람석 통로의 보행은 제공하지 않습니다.

## GLB 재생성

아래 스크립트는 뷰어와 분리된 브라우저에서 현재 형상과 4K 텍스처를 생성해 내보냅니다. 표준 경로와 현재 버전의 보관 경로, 두 manifest를 함께 갱신합니다.

개발 서버가 실행 중인 상태에서 프로젝트 루트에서 실행합니다.

```powershell
node scripts/stadium/export-glb.cjs
```

기본 주소는 `https://localhost:3451`입니다. 다른 서버 주소는 `STADIUM_PREVIEW_ORIGIN`, 브라우저 실행 파일은 `CHROME_PATH`로 지정합니다. `models/manifest.json`에 형상 통계, 텍스처 크기, SHA256, 생성 시각과 좌표를 자동 기록합니다.
