Quick Start
이 페이지에서

Quick Start

설치

Terminal window
npm install https://github.com/typeshade/typeshade.git
npm install --save-dev tsx

Git 의존성은 TypeScript 소스를 포함합니다. Node.js에서 컴파일러와 CLI를 실행할 때는 tsx를 사용합니다.

TypeScript 파일의 첫 문장에 "use typeshade"를 선언합니다. 이 지시어가 TypeShade 셰이더 언어로 넘어가는 경계를 만들며, 아래 예제는 class와 스테이지 데코레이터가 붙은 함수로 작성한 완전한 첫 셰이더입니다.

hello.shade.ts
"use typeshade";
class Clip {
@builtin("position") pos: vec4;
}
class Color {
@location(0) color: vec4;
}
@vertex
export function vs(@builtin("vertex_index") i: u32): Clip {
const x = i === 1 ? 0.8 : i === 2 ? 0. : -0.8;
const y = i === 2 ? 0.8 : -0.8;
return { pos: vec4(x, y, 0., 1.) };
}
@fragment
export function fs(): Color {
return { color: vec4(1., 0., 0., 1.) };
}

라이브 예제

sin이 x 좌표를 파동으로 바꾸고 time이 그 파동을 흘려보냅니다. 아래 파일이 그게 전부이고, 여기서 바로 돕니다. 한 줄을 고치면 다음 입력에 캔버스가 따라옵니다.

stripes.shade.ts
"use typeshade"
const TAU: f32 = 6.2831853
class Uniforms {
time: f32
bands: f32
low: vec3
high: vec3
}
declare const u: uniform<Uniforms>
@fragment
export function main(@location(0) uv: vec2): vec4 {
const phase = (uv.x * u.bands + u.time) * TAU
const wave = sin(phase) * 0.5 + 0.5
return vec4(mix(u.low, u.high, wave), 1.)
}
Playground에서 열기
TypeScript로 쓰고 TypeShade가 컴파일한 사인 줄무늬 셰이더. 빌드할 때 렌더링함.
두 색 사이를 오가며 캔버스를 가로지르는 사인파.

bands는 파동이 왼쪽 끝에서 오른쪽 끝까지 몇 번 반복하는지를 정하고, time은 그 무늬를 옆으로 밀어냅니다. phase에 닿는 값이 uv.x뿐이라 세로 한 줄은 한 가지 색입니다.

페이지가 매 프레임 채우는 값이라 컨트롤이 없습니다: time.

컨트롤과 코드에 따른 변화

  • bands를 범위의 맨 아래까지 내려 파동 하나를 만듭니다. 맨 위까지 올리면 마루가 가는 선으로 좁아집니다.
  • phase 줄에서 uv.x를 uv.y로 바꿉니다. 줄무늬가 가로로 눕습니다. uv는 캔버스를 가로지르는 방향뿐 아니라 위쪽으로도 나아가기 때문입니다.
  • Uniforms에 speed: f32를 넣고, phase 줄에서 u.time에 u.speed를 곱합니다. 캔버스 아래에 speed 슬라이더가 생기고, 출력 결과의 struct Uniforms에도 필드가 하나 늘어납니다. 컨트롤도 WGSL도 컴파일러가 방금 만든 모듈에서 읽어 온 것입니다.
출력 결과

컴파일러가 본 모듈 전체입니다. 버텍스 진입점과 그것이 돌려주는 VsOut 구조체는 페이지가 붙였고, 나머지는 위 파일입니다.

WGSL
const TAU: f32 = 6.2831853;
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}
struct Uniforms {
time: f32,
bands: f32,
low: vec3<f32>,
high: vec3<f32>,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
@vertex
fn fullscreen(@builtin(vertex_index) i: u32) -> VsOut {
let x = select(-1.0, 3.0, (i == 1u));
let y = select(-1.0, 3.0, (i == 2u));
return VsOut(vec4<f32>(x, y, 0.0, 1.0), vec2<f32>(((x * 0.5) + 0.5), ((y * 0.5) + 0.5)));
}
@fragment
fn main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
let phase = (((uv.x * u.bands) + u.time) * TAU);
let wave = ((sin(phase) * 0.5) + 0.5);
return vec4<f32>(mix(u.low, u.high, wave), 1.0);
}
GLSL ES 3.00 버텍스
#version 300 es
precision highp float;
precision highp int;
const float TAU = 6.2831853;
out vec2 uv;
void main() {
uint i = uint(gl_VertexID);
float x = ((i == 1u) ? 3.0 : -1.0);
float y = ((i == 2u) ? 3.0 : -1.0);
gl_Position = vec4(x, y, 0.0, 1.0);
uv = vec2(((x * 0.5) + 0.5), ((y * 0.5) + 0.5));
}
GLSL ES 3.00 프래그먼트
#version 300 es
precision highp float;
precision highp int;
const float TAU = 6.2831853;
layout(std140) uniform Uniforms {
float time;
float bands;
vec3 low;
vec3 high;
} u;
in vec2 uv;
layout(location = 0) out vec4 _ret;
void main() {
float phase = (((uv.x * u.bands) + u.time) * TAU);
float wave = ((sin(phase) * 0.5) + 0.5);
_ret = vec4(mix(u.low, u.high, wave), 1.0);
}
유니폼 블록
필드타입오프셋
timef320
bandsf324
lowvec3<f32>16
highvec3<f32>32

호스트는 호출 하나로 파일을 컴파일합니다. 아래 gradient 패스를 gradient.shade.ts로 저장합니다. 유니폼 블록 하나와 버텍스 단계, 프래그먼트 단계가 든 32줄짜리 파일입니다. 그다음 compile()을 불러 WGSL을 출력하는 6줄짜리 호스트 파일로 컴파일합니다.

gradient.shade.ts
"use typeshade";
class Uniforms {
top: vec4;
bottom: vec4;
mix_bias: f32;
}
declare const u: uniform<Uniforms>;
class VsOut {
@builtin("position") pos: vec4;
@location(0) uv: vec2;
}
@vertex
export function vs_full(@builtin("vertex_index") idx: u32): VsOut {
let pos = vec2(-1., -1.);
if (idx === 1) {
pos = vec2(3., -1.);
} else if (idx === 2) {
pos = vec2(-1., 3.);
}
return { pos: vec4(pos, 0., 1.), uv: vec2((pos.x + 1.) * 0.5, (pos.y + 1.) * 0.5) };
}
@fragment
export function fs_gradient(vo: VsOut): vec4 {
const t = vo.uv.y + u.mix_bias;
const rgb = mix(u.bottom.rgb, u.top.rgb, t);
return vec4(rgb, f32(1.));
}
compile.ts
import { readFileSync } from 'node:fs'
import { compile } from 'typeshade'
const { diagnostics, wgsl } = compile(readFileSync('gradient.shade.ts', 'utf8'))
for (const d of diagnostics) console.error(`${d.line}:${d.character} ${d.message}`)
console.log(wgsl)
Terminal window
npx tsx compile.ts

실행하면 두 단계의 WGSL이 함께 출력됩니다. 프래그먼트 진입점은 여기 있습니다.

@fragment
fn fs_gradient(vo: VsOut) -> @location(0) vec4<f32> {
let t = (vo.uv.y + u.mix_bias);
let rgb = mix(u.bottom.rgb, u.top.rgb, t);
return vec4<f32>(rgb, 1.0);
}

같은 함수의 GLSL ES 3.00 단계와, reflect()가 복원한 유니폼 레이아웃은 예제 페이지에 있다. 나머지 API는 작성 가이드를 보면 된다.

TypeShade와 호스트의 경계

TypeShade 코드는 TypeScript처럼 작성하지만, "use typeshade"가 붙은 파일에서는 셰이더 언어 규칙이 적용됩니다. TypeScript의 타입과 문법으로 소스를 작성하며, GPU 타입, 리소스, 셰이더 스테이지에는 TypeShade의 언어 규칙이 적용됩니다.

  • "use typeshade"가 언어 경계를 선언합니다.
  • entry point의 GPU 입력은 @builtin(...) 같은 명시적인 매개변수로 표현합니다.
  • uniform<T>와 storage<T> 같은 리소스 타입은 GPU 리소스의 의미를 표현합니다.

호스트 애플리케이션에 연결하기

호스트가 이 파일을 쓰는 방법은 세 가지입니다. 파일을 컴파일해 셰이더 소스를 자신의 WebGPU나 WebGL2 코드에 넘기거나, 파일을 가져와 그 파일이 내보내는 것을 부릅니다. 프레임을 직접 그리는 엔진이라면 세 번째 방법을 씁니다. 컴파일된 프로그램을 프로그램 런타임에 불러오는 방법이며, WebGPU와 WebGL2에서 설명합니다.

파일 컴파일하기

지금 쓸 수 있는 방법입니다. 파일을 컴파일한 뒤, 호스트 애플리케이션이 생성된 WGSL 또는 GLSL ES 3.00 문자열을 WebGPU나 WebGL2에 넘깁니다.

webgpu.ts
const shaderCode = /* WGSL emitted from the TypeShade module */
const shaderModule = device.createShaderModule({ code: shaderCode })
const pipeline = device.createComputePipeline({
layout: 'auto',
compute: {
module: shaderModule,
entryPoint: 'paint',
},
})

TypeShade가 담당하는 것은 언어 의미와 셰이더 코드 생성입니다. device, pipeline, bind group, buffer, texture, command encoder 같은 GPU 런타임 객체의 생성과 수명 관리는 호스트가 담당합니다.

파일 가져오기

Vite 플러그인은 Git에서 설치한 소스 패키지를 지원합니다. 호스트 파일이 .shade.ts를 가져와 그 파일이 내보내는 것을 부르면, 호출에 필요한 디바이스와 파이프라인은 TypeShade의 런타임이 만듭니다. 헬퍼는 CPU에서 실행됩니다. @compute 진입점은 await entry(bindings, workgroups)로 부릅니다. 지원하는 환경에서는 WebGPU나 WebGL2로 실행하고, 진입점이 쓴 값을 다시 읽어 옵니다. 전체 화면 @fragment 진입점은 entry(canvas, bindings)로 캔버스에 그리는데, WebGPU가 먼저이고 다음이 WebGL2, 마지막이 CPU입니다.

설치 후에는 아래의 Vite 플러그인과 호스트 tsconfig.json 설정을 추가합니다. 스크립트는 Vite와 설치한 CLI를 tsx로 실행합니다. prepare는 tsc가 읽는 호스트 뷰를 만듭니다. .gitignore에 *.shade.typeshade.ts를 추가합니다.

Terminal window
npm install --save-dev vite
vite.config.ts
import { defineConfig } from 'vite';
import { typeshade } from 'typeshade/vite';
export default defineConfig({ plugins: [typeshade()] });
tsconfig.json
{
"compilerOptions": {
// ...
"moduleSuffixes": [".typeshade", ""]
},
"exclude": ["src/**/*.shade.ts"]
}
package.json
{
"scripts": {
"dev": "tsx node_modules/vite/bin/vite.js",
"build": "tsx node_modules/vite/bin/vite.js build",
"prepare": "tsx node_modules/typeshade/src/cli/bin.ts sync"
}
}

그러면 호스트 파일이 이 페이지 맨 위의 hello.shade.ts를 호출 하나로 그립니다. 버텍스 진입점은 아직 호스트 코드에서 부를 수 없어서 뷰가 vs를 never로 선언하고, fs는 캔버스 전체를 빨간색으로 채웁니다.

app.ts
import { fs } from './hello.shade.ts';
const canvas = document.querySelector('canvas')!;
fs(canvas, {});
Terminal window
npm run dev

WebGPU와 WebGL2는 컴파일러, 호스트 호출, 프로그램 런타임의 소유권 경계를 설명합니다.

상태

저장소 패키지는 0.1.0 버전입니다. 0.2.0의 레지스트리 배포는 아직 나오지 않았지만, npm으로 Git 소스를 설치할 수 있습니다. 호스트는 설치한 패키지를 typeshade로 import합니다. lockfile을 커밋하면 설치한 Git 리비전이 기록됩니다. 배포된 버전은 릴리스에 있습니다.

다음 학습

언어 가이드는 TypeScript의 개념과 TypeShade의 GPU 실행 규칙을 연결해 설명합니다.

전체 Language Guide 보기

VS Code 확장을 설치하면 입력하는 동안 진단과 파일이 컴파일되는 WGSL을 볼 수 있습니다.

이 페이지 편집 문제 보고