STUDIO HAZE · jeniapp
Topaz source
Open by house policy. Topaz is our own language.
Topaz 5.20.2 · 63 files
apps/render/render.tpz
Topaz · 11802 B · Original file
1
// 레이트레이서. 바닥 평면과 구 하나와 점광원으로 된 고정 장면을 화소마다 광선으로 추적해 그립니다.2
// 입력은 해상도와 생략할 수 있는 광원각 두 정수이고, 출력은 요약 한 줄과 회색조 칸으로 된 SVG 를 담은 HTML 조각입니다.3
// 같은 입력이면 인터프리터와 웹 빌드에서 바이트까지 같은 출력을 냅니다.4
// 메시, 가속 구조, 반사, 굴절, 안티에일리어싱, 고해상도, 실제 이미지 파일은 다루지 않는 저해상 미리보기입니다.5
// Raytracer: traces a fixed floor, sphere and point light scene into a grayscale SVG grid.7
record 벡터 { x: float, y: float, z: float }8
record 구 { 중심: 벡터, 반지름: float }9
// 명중 거리를 태그 안에 함께 싣습니다.10
enum 명중 { 없음, 구(float), 바닥(float) }11
record 장면설정 { 해상도: int, 광원각: int }13
let 엡실론 = 0.000114
let 최대해상도 = 6415
let 칸크기 = 616
let 바닥법선 = 벡터 { x: 0.0, y: 1.0, z: 0.0 }17
let 장면구 = 구 { 중심: 벡터 { x: 0.0, y: 1.0, z: 0.0 }, 반지름: 1.0 }18
let 시점 = 벡터 { x: 0.0, y: 1.6, z: 5.0 }19
let 화면거리 = 4.020
let 반화면 = 1.722
// 공용 도우미 시작 (이스케이프 숫자인가 모두숫자인가 오류알림 정수오류 정수인가 정수읽기 결과풀기 오류바꾸기)23
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.25
// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.26
function 이스케이프(원문: string) -> string {27
원문.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)28
}30
function 숫자인가(글자: string) -> bool {31
let 코드 = 글자.codePointAt(0) ?? 032
코드 >= 48 && 코드 <= 5733
}35
// 빈 문자열은 참입니다. 부호와 소수점을 받지 않으려고 toInt 앞에서 씁니다.36
function 모두숫자인가(글: string) -> bool {37
글.scalars().reduce(true, (모두, 글자) => 모두 && 숫자인가(글자))38
}40
// 문구는 고정 문장이거나 이미 이스케이프한 값이어야 합니다.41
function 오류알림(문구: string) -> string {42
"<div class=\"err\">{문구}</div>"43
}45
// 정수 읽기가 실패한 까닭입니다. 보여 줄 문구는 앱이 정합니다.46
type 정수오류 = "숫자아님" | "너무큼"48
// 맨 앞의 빼기 부호 하나만 받습니다. 더하기 부호와 소수점은 받지 않습니다.49
function 정수인가(토큰: string) -> bool {50
let 글자들 = 토큰.scalars()51
let 시작 = if 토큰.startsWith("-") { 1 } else { 0 }52
글자들.length > 시작 && 모두숫자인가(글자들.slice(시작, 글자들.length).join(""))53
}55
// 부호까지 센 글자 수를 최대자릿수로 막아 toInt 와 뒤의 계산이 넘치지 않게 합니다.56
function 정수읽기(토큰: string, 최대자릿수: int) -> Result<int, 정수오류> {57
if !정수인가(토큰) { return Err("숫자아님") }58
if 토큰.scalars().length > 최대자릿수 { return Err("너무큼") }59
Ok(toInt(토큰) ?? 0)60
}62
// 성공 값과 안내가 모두 바로 보여 줄 HTML 조각일 때 진입점에서 한 번 풉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.63
function 결과풀기(값: Result<string, string>) -> string {64
match 값 {65
case Ok(결과) => 결과66
case Err(안내) => 안내67
}68
}70
// 성공 값은 그대로 두고 오류만 문구로 바꿉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.71
function 오류바꾸기<T, E>(결과: Result<T, E>, 바꾸기: (E) -> string) -> Result<T, string> {72
match 결과 {73
case Ok(값) => Ok(값)74
case Err(오류) => Err(바꾸기(오류))75
}76
}78
// 공용 도우미 끝80
// Math.sqrt 대신 앱에서 계산하는 것이 이 도구의 주제이고, 반복 횟수가 출력 소수를 정하므로 고정합니다.81
function 제곱근(값: float) -> float {82
if 값 <= 0.0 { return 0.0 }83
let mut 추정 = if 값 > 1.0 { 값 / 2.0 } else { 1.0 }84
for _ in 0..<22 {85
추정 = (추정 + 값 / 추정) / 2.086
}87
추정88
}90
function 더하기(가: 벡터, 나: 벡터) -> 벡터 { 벡터 { x: 가.x + 나.x, y: 가.y + 나.y, z: 가.z + 나.z } }92
function 빼기(가: 벡터, 나: 벡터) -> 벡터 { 벡터 { x: 가.x - 나.x, y: 가.y - 나.y, z: 가.z - 나.z } }94
function 곱하기(가: 벡터, 배율: float) -> 벡터 { 벡터 { x: 가.x * 배율, y: 가.y * 배율, z: 가.z * 배율 } }96
function 내적(가: 벡터, 나: 벡터) -> float { 가.x * 나.x + 가.y * 나.y + 가.z * 나.z }98
function 길이(가: 벡터) -> float { 제곱근(내적(가, 가)) }100
function 정규화(가: 벡터) -> 벡터 {101
let 크기 = 길이(가)102
if 크기 < 엡실론 { 가 } else { 곱하기(가, 1.0 / 크기) }103
}105
// 방향은 단위 벡터여야 합니다. 시점 바로 앞의 교차는 자기 표면이므로 엡실론보다 먼 것만 셉니다.106
function 구교차(시점: 벡터, 방향: 벡터, 공: 구) -> Option<float> {107
let 상대 = 빼기(시점, 공.중심)108
let 투영 = 내적(상대, 방향)109
let 바깥정도 = 내적(상대, 상대) - 공.반지름 * 공.반지름110
let 판별식 = 투영 * 투영 - 바깥정도111
if 판별식 < 0.0 { return None }112
let 뿌리 = 제곱근(판별식)113
let 가까운거리 = -투영 - 뿌리114
if 가까운거리 > 엡실론 { return Some(가까운거리) }115
let 먼거리 = -투영 + 뿌리116
if 먼거리 > 엡실론 { Some(먼거리) } else { None }117
}119
// 바닥은 y = 0 평면입니다.120
function 바닥교차(시점: 벡터, 방향: 벡터) -> Option<float> {121
if Math.abs(방향.y) < 엡실론 { return None }122
let t = -시점.y / 방향.y123
if t > 엡실론 { Some(t) } else { None }124
}126
// 거리가 같으면 구를 고릅니다.127
function 가까운명중(구까지: Option<float>, 바닥까지: Option<float>) -> 명중 {128
match 구까지 {129
case Some(구거리) => match 바닥까지 {130
case Some(바닥거리) => if 바닥거리 < 구거리 { 명중.바닥(바닥거리) } else { 명중.구(구거리) }131
case None => 명중.구(구거리)132
}133
case None => match 바닥까지 {134
case Some(바닥거리) => 명중.바닥(바닥거리)135
case None => 명중.없음136
}137
}138
}140
// 한 변 8 인 정사각형을 네 칸으로 나눈 체커무늬입니다.141
function 바닥밝기(점: 벡터) -> float {142
let 칸x = 점.x - 8.0 * Math.floor(점.x / 8.0)143
let 칸z = 점.z - 8.0 * Math.floor(점.z / 8.0)144
if (칸x < 4.0) != (칸z < 4.0) { 0.35 } else { 0.6 }145
}147
// 그림자 광선은 표면에서 법선 쪽으로 조금 띄워 쏴야 자기 표면에 걸리지 않습니다.148
function 비추기(점: 벡터, 법선: 벡터, 기본밝기: float, 공: 구, 광원: 벡터) -> float {149
let 빛방향 = 빼기(광원, 점) |> 정규화150
let 확산 = Math.max(0.0, 내적(법선, 빛방향))151
let 그늘시점 = 더하기(점, 곱하기(법선, 엡실론 * 10.0))152
let 광원거리 = 길이(빼기(광원, 점))153
let 그림자 = match 구교차(그늘시점, 빛방향, 공) {154
case Some(막힌거리) => if 막힌거리 < 광원거리 { 0.3 } else { 1.0 }155
case None => 1.0156
}157
let 명암 = 0.15 * 기본밝기 + 0.85 * 기본밝기 * 확산 * 그림자158
Math.min(1.0, Math.max(0.0, 명암))159
}161
// 0 에서 1 사이 명암을 돌려줍니다. 아무것도 맞지 않으면 위로 갈수록 밝은 하늘입니다.162
function 추적(시점: 벡터, 방향: 벡터, 공: 구, 광원: 벡터) -> float {163
match 가까운명중(구교차(시점, 방향, 공), 바닥교차(시점, 방향)) {164
case 없음 => 0.75 + 0.2 * Math.max(0.0, 방향.y)165
case 구(거리) => {166
let 점 = 더하기(시점, 곱하기(방향, 거리))167
비추기(점, 빼기(점, 공.중심) |> 정규화, 0.85, 공, 광원)168
}169
case 바닥(거리) => {170
let 점 = 더하기(시점, 곱하기(방향, 거리))171
비추기(점, 바닥법선, 바닥밝기(점), 공, 광원)172
}173
}174
}176
// float 를 int 로 바꾸는 표준 기능이 없어 0 쪽으로 자른 값을 이분 탐색으로 찾습니다. 절댓값 2^30 근처에서 멈춥니다. 토파즈 과제 '실수를 정수로 자르는 변환' 의 우회입니다.177
function 정수로자르기(값: float) -> int {178
let 크기 = Math.abs(값)179
if 크기 < 1.0 { return 0 }180
let mut 상한 = 1181
while toFloat(상한) <= 크기 && 상한 < 1073741824 { 상한 = 상한 * 2 }182
let mut 하한 = 상한 / 2183
while 상한 - 하한 > 1 {184
let 가운데 = (하한 + 상한) / 2185
if toFloat(가운데) <= 크기 { 하한 = 가운데 } else { 상한 = 가운데 }186
}187
if 값 < 0.0 { -하한 } else { 하한 }188
}190
function 십육진두자리(값: int) -> string {191
let 숫자표 = "0123456789abcdef".scalars()192
let 채널 = 값.atLeast(0).atMost(255)193
숫자표[채널 / 16] + 숫자표[채널 % 16]194
}196
function 정수문구(종류: 정수오류, 토큰: string) -> string {197
match 종류 {198
case "숫자아님" => 오류알림("숫자만 입력하세요: {이스케이프(토큰)}")199
case "너무큼" => 오류알림("숫자가 너무 큽니다: {이스케이프(토큰)}")200
}201
}203
// 아홉 자리까지만 받아 toInt 가 넘치지 않게 합니다.204
function 인자읽기(토큰들: Array<string>, 자리: int, 기본값: int) -> Result<int, string> {205
if 자리 >= 토큰들.length { return Ok(기본값) }206
let 토큰 = 토큰들[자리]207
오류바꾸기(정수읽기(토큰, 9), 종류 => 정수문구(종류, 토큰))208
}210
// 인자 개수보다 각 인자의 오류를 먼저 알립니다.211
function 설정읽기(입력: string) -> Result<장면설정, string> {212
let 토큰들 = 입력.trim().split(" ").filter(토큰 => 토큰 != "")213
let 해상도 = 인자읽기(토큰들, 0, 40)?214
let 광원각 = 인자읽기(토큰들, 1, 35)?215
if 토큰들.length > 2 { return Err(오류알림("입력은 해상도와 선택 광원각 두 개입니다. 예: 44 35")) }216
Ok(장면설정 { 해상도: 해상도.atLeast(8).atMost(최대해상도), 광원각: 광원각.atLeast(0).atMost(80) })217
}219
// 화면은 시점 앞 [-1, 1] 정사각형이고 y 는 위가 양수입니다.220
function 칸그리기(줄: int, 열: int, 실수해상도: float, 광원: 벡터) -> string {221
let u = ((toFloat(열) + 0.5) / 실수해상도) * 2.0 - 1.0222
let v = 1.0 - ((toFloat(줄) + 0.5) / 실수해상도) * 2.0223
let 화면점 = 벡터 { x: u * 반화면, y: 시점.y + v * 반화면, z: 시점.z - 화면거리 }224
let 방향 = 빼기(화면점, 시점) |> 정규화225
let 회색 = 십육진두자리(정수로자르기(추적(시점, 방향, 장면구, 광원) * 255.0 + 0.5))226
"<rect x=\"{열 * 칸크기}\" y=\"{줄 * 칸크기}\" width=\"{칸크기}\" height=\"{칸크기}\" fill=\"#{회색}{회색}{회색}\"/>"227
}229
// 광원은 광원각 35 일 때 가운데이고 각이 커지면 오른쪽으로 갑니다.230
function 그리기(설정값: 장면설정) -> string {231
let 해상도 = 설정값.해상도232
let 광원 = 벡터 { x: -2.5 + (toFloat(설정값.광원각) - 35.0) / 12.0, y: 5.0, z: 3.0 }233
let 실수해상도 = toFloat(해상도)234
let 칸들 = [for 줄 in 0..<해상도 for 열 in 0..<해상도 => 칸그리기(줄, 열, 실수해상도, 광원)]235
let 변 = 해상도 * 칸크기236
let 요약 = "<div class=\"meta\">해상도 {해상도}x{해상도}, 광원각 {설정값.광원각}. 앱 레벨 제곱근으로 추적했습니다.</div>"237
let 그림 = "<svg viewBox=\"0 0 {변} {변}\" width=\"{변}\" height=\"{변}\" style=\"image-rendering:pixelated;border:1px solid #000\" xmlns=\"http://www.w3.org/2000/svg\">{칸들.join("")}</svg>"238
요약 + 그림239
}241
function 처리(입력: string) -> string {242
결과풀기(설정읽기(입력).map(그리기))243
}245
input() |> 처리 |> printSHA256 a2815c601c0abb45c79721d485411983736ba8f99b74506b7a913f37c208f70e