slow.monster

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.
7record 벡터 { x: float, y: float, z: float }
8record 구 { 중심: 벡터, 반지름: float }
9// 명중 거리를 태그 안에 함께 싣습니다.
10enum 명중 { 없음, 구(float), 바닥(float) }
11record 장면설정 { 해상도: int, 광원각: int }
13let 엡실론 = 0.0001
14let 최대해상도 = 64
15let 칸크기 = 6
16let 바닥법선 = 벡터 { x: 0.0, y: 1.0, z: 0.0 }
17let 장면구 = 구 { 중심: 벡터 { x: 0.0, y: 1.0, z: 0.0 }, 반지름: 1.0 }
18let 시점 = 벡터 { x: 0.0, y: 1.6, z: 5.0 }
19let 화면거리 = 4.0
20let 반화면 = 1.7
22// 공용 도우미 시작 (이스케이프 숫자인가 모두숫자인가 오류알림 정수오류 정수인가 정수읽기 결과풀기 오류바꾸기)
23// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.
25// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.
26function 이스케이프(원문: string) -> string {
27 원문.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
30function 숫자인가(글자: string) -> bool {
31 let 코드 = 글자.codePointAt(0) ?? 0
32 코드 >= 48 && 코드 <= 57
35// 빈 문자열은 참입니다. 부호와 소수점을 받지 않으려고 toInt 앞에서 씁니다.
36function 모두숫자인가(글: string) -> bool {
37 글.scalars().reduce(true, (모두, 글자) => 모두 && 숫자인가(글자))
40// 문구는 고정 문장이거나 이미 이스케이프한 값이어야 합니다.
41function 오류알림(문구: string) -> string {
42 "<div class=\"err\">{문구}</div>"
45// 정수 읽기가 실패한 까닭입니다. 보여 줄 문구는 앱이 정합니다.
46type 정수오류 = "숫자아님" | "너무큼"
48// 맨 앞의 빼기 부호 하나만 받습니다. 더하기 부호와 소수점은 받지 않습니다.
49function 정수인가(토큰: string) -> bool {
50 let 글자들 = 토큰.scalars()
51 let 시작 = if 토큰.startsWith("-") { 1 } else { 0 }
52 글자들.length > 시작 && 모두숫자인가(글자들.slice(시작, 글자들.length).join(""))
55// 부호까지 센 글자 수를 최대자릿수로 막아 toInt 와 뒤의 계산이 넘치지 않게 합니다.
56function 정수읽기(토큰: string, 최대자릿수: int) -> Result<int, 정수오류> {
57 if !정수인가(토큰) { return Err("숫자아님") }
58 if 토큰.scalars().length > 최대자릿수 { return Err("너무큼") }
59 Ok(toInt(토큰) ?? 0)
62// 성공 값과 안내가 모두 바로 보여 줄 HTML 조각일 때 진입점에서 한 번 풉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.
63function 결과풀기(값: Result<string, string>) -> string {
64 match 값 {
65 case Ok(결과) => 결과
66 case Err(안내) => 안내
67 }
70// 성공 값은 그대로 두고 오류만 문구로 바꿉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.
71function 오류바꾸기<T, E>(결과: Result<T, E>, 바꾸기: (E) -> string) -> Result<T, string> {
72 match 결과 {
73 case Ok(값) => Ok(값)
74 case Err(오류) => Err(바꾸기(오류))
75 }
78// 공용 도우미 끝
80// Math.sqrt 대신 앱에서 계산하는 것이 이 도구의 주제이고, 반복 횟수가 출력 소수를 정하므로 고정합니다.
81function 제곱근(값: 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.0
86 }
87 추정
90function 더하기(가: 벡터, 나: 벡터) -> 벡터 { 벡터 { x: 가.x + 나.x, y: 가.y + 나.y, z: 가.z + 나.z } }
92function 빼기(가: 벡터, 나: 벡터) -> 벡터 { 벡터 { x: 가.x - 나.x, y: 가.y - 나.y, z: 가.z - 나.z } }
94function 곱하기(가: 벡터, 배율: float) -> 벡터 { 벡터 { x: 가.x * 배율, y: 가.y * 배율, z: 가.z * 배율 } }
96function 내적(가: 벡터, 나: 벡터) -> float { 가.x * 나.x + 가.y * 나.y + 가.z * 나.z }
98function 길이(가: 벡터) -> float { 제곱근(내적(가, 가)) }
100function 정규화(가: 벡터) -> 벡터 {
101 let 크기 = 길이(가)
102 if 크기 < 엡실론 { 가 } else { 곱하기(가, 1.0 / 크기) }
105// 방향은 단위 벡터여야 합니다. 시점 바로 앞의 교차는 자기 표면이므로 엡실론보다 먼 것만 셉니다.
106function 구교차(시점: 벡터, 방향: 벡터, 공: 구) -> 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 }
119// 바닥은 y = 0 평면입니다.
120function 바닥교차(시점: 벡터, 방향: 벡터) -> Option<float> {
121 if Math.abs(방향.y) < 엡실론 { return None }
122 let t = -시점.y / 방향.y
123 if t > 엡실론 { Some(t) } else { None }
126// 거리가 같으면 구를 고릅니다.
127function 가까운명중(구까지: 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 }
140// 한 변 8 인 정사각형을 네 칸으로 나눈 체커무늬입니다.
141function 바닥밝기(점: 벡터) -> 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 }
147// 그림자 광선은 표면에서 법선 쪽으로 조금 띄워 쏴야 자기 표면에 걸리지 않습니다.
148function 비추기(점: 벡터, 법선: 벡터, 기본밝기: 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.0
156 }
157 let 명암 = 0.15 * 기본밝기 + 0.85 * 기본밝기 * 확산 * 그림자
158 Math.min(1.0, Math.max(0.0, 명암))
161// 0 에서 1 사이 명암을 돌려줍니다. 아무것도 맞지 않으면 위로 갈수록 밝은 하늘입니다.
162function 추적(시점: 벡터, 방향: 벡터, 공: 구, 광원: 벡터) -> 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 }
176// float 를 int 로 바꾸는 표준 기능이 없어 0 쪽으로 자른 값을 이분 탐색으로 찾습니다. 절댓값 2^30 근처에서 멈춥니다. 토파즈 과제 '실수를 정수로 자르는 변환' 의 우회입니다.
177function 정수로자르기(값: float) -> int {
178 let 크기 = Math.abs(값)
179 if 크기 < 1.0 { return 0 }
180 let mut 상한 = 1
181 while toFloat(상한) <= 크기 && 상한 < 1073741824 { 상한 = 상한 * 2 }
182 let mut 하한 = 상한 / 2
183 while 상한 - 하한 > 1 {
184 let 가운데 = (하한 + 상한) / 2
185 if toFloat(가운데) <= 크기 { 하한 = 가운데 } else { 상한 = 가운데 }
186 }
187 if 값 < 0.0 { -하한 } else { 하한 }
190function 십육진두자리(값: int) -> string {
191 let 숫자표 = "0123456789abcdef".scalars()
192 let 채널 = 값.atLeast(0).atMost(255)
193 숫자표[채널 / 16] + 숫자표[채널 % 16]
196function 정수문구(종류: 정수오류, 토큰: string) -> string {
197 match 종류 {
198 case "숫자아님" => 오류알림("숫자만 입력하세요: {이스케이프(토큰)}")
199 case "너무큼" => 오류알림("숫자가 너무 큽니다: {이스케이프(토큰)}")
200 }
203// 아홉 자리까지만 받아 toInt 가 넘치지 않게 합니다.
204function 인자읽기(토큰들: Array<string>, 자리: int, 기본값: int) -> Result<int, string> {
205 if 자리 >= 토큰들.length { return Ok(기본값) }
206 let 토큰 = 토큰들[자리]
207 오류바꾸기(정수읽기(토큰, 9), 종류 => 정수문구(종류, 토큰))
210// 인자 개수보다 각 인자의 오류를 먼저 알립니다.
211function 설정읽기(입력: 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) })
219// 화면은 시점 앞 [-1, 1] 정사각형이고 y 는 위가 양수입니다.
220function 칸그리기(줄: int, 열: int, 실수해상도: float, 광원: 벡터) -> string {
221 let u = ((toFloat(열) + 0.5) / 실수해상도) * 2.0 - 1.0
222 let v = 1.0 - ((toFloat(줄) + 0.5) / 실수해상도) * 2.0
223 let 화면점 = 벡터 { x: u * 반화면, y: 시점.y + v * 반화면, z: 시점.z - 화면거리 }
224 let 방향 = 빼기(화면점, 시점) |> 정규화
225 let 회색 = 십육진두자리(정수로자르기(추적(시점, 방향, 장면구, 광원) * 255.0 + 0.5))
226 "<rect x=\"{열 * 칸크기}\" y=\"{줄 * 칸크기}\" width=\"{칸크기}\" height=\"{칸크기}\" fill=\"#{회색}{회색}{회색}\"/>"
229// 광원은 광원각 35 일 때 가운데이고 각이 커지면 오른쪽으로 갑니다.
230function 그리기(설정값: 장면설정) -> 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 요약 + 그림
241function 처리(입력: string) -> string {
242 결과풀기(설정읽기(입력).map(그리기))
245input() |> 처리 |> print

SHA256 a2815c601c0abb45c79721d485411983736ba8f99b74506b7a913f37c208f70e