Created
April 24, 2024 23:40
-
-
Save AskingQuestions/85c84e5a178b85e8e7b41c1800e2f01c to your computer and use it in GitHub Desktop.
Shadeup lang prompt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The following is a specification for a language. This language is called Shadeup | |
Shadeup is a language that makes writing shaders and computer graphics code easier, it's built on Typescript and looks similar to hlsl. | |
Core values: | |
- Easy to read, boilerplate is abstracted away | |
- CPU/GPU Sharing of functions and data-structures. Write code once and reuse on the cpu or gpu. | |
- Runs on webgpu | |
Some basic syntax: | |
While similar to hlsl you can only define variables with let: | |
let a = 1; // inferred as int | |
let b: int = 2; | |
const c = 3.0; // inferred as float | |
const d: float = 4; | |
## Vectors: | |
(1, 2, 3); // int3 vector | |
(1.0, 2, 3); // float3 vector | |
float3(1.0, 2, 3); // float3 vector | |
int3(1, 2, 3); // int3 vector | |
float3x3( | |
1, 0, 0, | |
0, 1, 0, | |
0, 0, 1, | |
); // float3x3 matrix | |
let vec = (1, 2, 3); | |
vec.x; // 1 | |
vec.xy; // (1, 2) | |
vec.xyzx; // (1, 2, 3, 1) | |
1.xyz // (1, 1, 1) | |
1.0.xyz // (1.0, 1.0, 1.0) | |
1.xyzw.xw // (1, 1) | |
Let's start with an example: | |
fn main() { | |
draw(shader { | |
out.color = (in.uv, 0.5, 1); | |
}); | |
} | |
Explainer: | |
- `fn main()`: This is the frame loop, it runs every frame on the CPU. | |
- `draw(shader { ... })`: This is the draw call, it takes a shader as an argument and runs it on the GPU. This has a few overloads but passing a single shader argument will dispatch a fullscreen quad draw. | |
- `out.color`: Every shader has an `in` and an `out` struct. Here we're just setting the color of the fragment shader to a vector. | |
- `in.uv`: As you can guess, this is the UV coordinate of the fragment. In this case it's spanning the screen | |
- `(in.uv, 0.5, 1)`: Shadeup lets you define vectors by just listing their components, this is equivalent to `float4(in.uv, 0.5, 1)`. If you pass all `int`s `(1, 0, 0)` it'll be an `int3` and so on. | |
- `draw()` is queued up automatically and dispatched at the end of the frame, shadeup will automatically flush any queued up draw calls or compute calls when needed. | |
You can use uniforms like so: | |
fn main() { | |
let myCpuValue = sin(env.time / 1000.0); | |
draw(shader { | |
out.color = myCpuValue.xyzw; | |
}); | |
} | |
You'll notice we can define a variable on the CPU and then pull that into our shader by simply referencing it. | |
This is called a closure and allows you to pass data from the CPU to the GPU. | |
A lot of data-types are supported, including: | |
- Any numeric primitive e.g. `int`, `float`, `uint`, `float3`, `float4x4` etc. | |
- Arrays | |
- Structs | |
- `buffer<T>` | |
- `texture2d<T>` | |
Things like `map<K, T>` and `string` are not supported among others. | |
Finally, we introduced the `env` global, this is a special struct that contains data about the current frame. Its contents are: | |
- `time`: The time in seconds since the start of the program | |
- `deltaTime`: The time in seconds since the last frame | |
- `frame`: The current frame number | |
- `screenSize`: The resolution of the screen | |
- `mouse`: Special mouse data (like `env.mouse.screen`) | |
- `keyboard`: Special keyboard data (like `env.keyboard.keySpace`) | |
- `camera`: User controllable camera with a `position` `rotation` `fov` `near` and `far` properties | |
Here's another example: | |
struct Circle { | |
pub position: float2, | |
pub radius: float, | |
pub color: float4, | |
} | |
let arr: Circle[] = []; | |
for (let i = 0; i < 10; i++) { | |
arr.push(Circle { | |
position: (rand(), rand()) * env.screenSize, | |
radius: rand() * 40, | |
color: (rand(), rand(), rand(), 1), | |
}); | |
} | |
fn main() { | |
draw(shader { | |
for (let i = 0; i < arr.len(); i++) { | |
if (dist(in.screen, arr[i].position) < arr[i].radius) { | |
out.color = arr[i].color; | |
} | |
} | |
}); | |
} | |
We can define structs and arrays of structs on the CPU and pass them into the GPU. This is a very powerful feature that lets you define complex data structures on the CPU and then use them in your shaders. | |
Note: | |
- Non-cpu data is stripped away when you pass a struct into a shader. So if you have a `pub` field on a struct, it'll be stripped away when you pass it into a shader. | |
- Dynamic arrays inside structs are not supported. These will be stripped away. | |
- You can use structured buffers for more effecient data passing. Arrays are uploaded each dispatch, while buffers are uploaded once and can be read/written to on the GPU. | |
- `let arr = buffer<Circle>(1000)` | |
Here's how we can draw a mesh: | |
fn main() { | |
// Create a cube mesh at the origin | |
// with a size of 100 | |
let cube = mesh::box(0.xyz, 100.xyz); | |
let mat = env.camera.getCombinedMatrix(); | |
draw(cube, shader { | |
// Vertex shader | |
out.position = mat * (in.position, 1); | |
}, shader { | |
// Fragment shader | |
out.color = ( | |
dot(in.normal, normalize((1, 2, 1))).xyz, | |
1 | |
); | |
}); | |
} | |
Here's an example of render targets: | |
let tex = texture2d<float4>(env.screenSize); | |
fn main() { | |
let cube = mesh::box(0.xyz, 100.xyz); | |
let mat = env.camera.getCombinedMatrix(); | |
tex.clear(); | |
tex.drawAdvanced({ | |
mesh: cube, | |
vertex: shader { | |
out.position = mat * (in.position + (in.instanceIndex * 110.0, 0, 0), 1); | |
}, | |
fragment: shader { | |
out.color = ( | |
dot(in.normal, normalize((1, 2, 1))).xyz, | |
1 | |
); | |
}, | |
instances: 10, | |
); | |
draw(shader { | |
out.color = tex.sample((in.uv * 4) % 1).xyzw; | |
}); | |
} | |
Here's how buffers work: | |
let buf = buffer<float>(64); | |
buf[0] = 1; | |
buf.upload(); | |
fn main() { | |
compute((2, 1, 1), shader<32, 1, 1> { | |
buf[in.globalId.x] = buf[in.globalId.x] + 1; | |
}); | |
buf.download(); | |
print(buf[0]); | |
} | |
Shadeup inherits a number of types from HLSL. The following types are available: | |
- `int` | |
- `int2`, `int3`, `int4` | |
- `float` | |
- `float2`, `float3`, `float4` | |
- `float2x2`, `float3x3`, `float4x4` | |
- `bool` | |
- `string` *(non-GPU)* | |
- `.split(sep: string) -> string[]` | |
- `.includes(substr: string) -> bool` | |
- `.startsWith(str: string) -> bool` | |
- `.endsWith(str: string) -> bool` | |
- `.replace(from: string, to: string)` | |
- `.trim(chars: string = ' \t | |
') -> string` | |
- `.lower() -> string` | |
- `.upper() -> string` | |
- `.substr(start: int, end: int) -> string` | |
- `.len() -> int` | |
- `[index: int] -> string` | |
- `T[]` *Variable length arrays* (partial GPU support) | |
- `.join(sep: string) -> string` | |
- `.push(val: T) -> string` | |
- `.len() -> int` | |
- `.first() -> T` | |
- `.last() -> T` | |
- `.append(vals: T[])` | |
- `.remove(index: int)` | |
- `[index: int] -> T` | |
- `T[3]` *Fixed length arrays* same methods as above | |
- `map<K extends Hashable, V>` *(non-GPU)* | |
- `.has(key: K) -> bool` | |
- `.set(key: K, value: V)` | |
- `.get(key: K) -> V` | |
- `.delete(key: K)` | |
- `.keys() -> K[]` | |
- `.values() -> V[]` | |
- `[index: K] -> V` | |
- `shader` *Instantiated shader* | |
Math: | |
abs((-1, 1)); // (1, 1) | |
2 * (3, 4) // (6, 8) | |
(1, 2) + (3, 4) // (4, 6) | |
dot((1, 2), (3, 4)) | |
dist((0, 0, 0), (1, 2, 3)) | |
reflect((1, 0, 0), (0, 1, 0)) | |
## Type casting: | |
let a = 1.0; | |
let b = int(a); // b is now 1 | |
## Conditionals: | |
let a = 1; | |
let b = 2; | |
if (a == b) { | |
// do something | |
} else if (a > b) { | |
// do something else | |
} else { | |
// do something else | |
} | |
let c = a == b ? 1 : 2; | |
## Structs | |
struct Ball { | |
position: float3; | |
velocity: float3; | |
radius: float; | |
} | |
let myBall = Ball { | |
position: float3(0.0, 0.0, 0.0), | |
// velocity and radius will default to 0.0 | |
}; | |
## Methods: | |
impl Ball { | |
fn update(self) { | |
self.position += self.velocity; | |
} | |
fn new() -> Ball { | |
return Ball { | |
position: float3(0.0, 0.0, 0.0), | |
velocity: float3(0.0, 0.0, 0.0), | |
radius: 0.0, | |
}; | |
} | |
} | |
let myNewBall = Ball::new(); | |
myNewBall.update(); | |
Shadeup supports `for` and `while` loops. `break` and `continue` are also supported. | |
for (let i = 0; i < 10; i++) { continue; } | |
while (true) { break; } | |
for (let (x, y) of (10, 20)) { /* 2d for loop (left-right, top-down) */ } | |
for (let (x, y, z) of (10, 20, 30)) { /* 3d for loop (left-right, top-down, front-back) */ } | |
## Atomics example: | |
let buf = buffer<atomic<uint>>(2); | |
let inCircle = 0u; | |
let numSamples = 0u; | |
fn main() { | |
buf[0].store(0u); | |
buf[1].store(0u); | |
buf.upload(); | |
compute((100, 100, 1), shader<16, 16, 1> { | |
let randomSeed = float2(in.globalId.xy) + (env.frame * 1000.0).xy; | |
let randomX = rand2((randomSeed.x, randomSeed.y)); | |
let randomY = rand2((randomSeed.y + 251, randomSeed.y * 24.0)); | |
if (length((randomX, randomY)) < 1.0) { | |
buf[0].add(1u); // This point falls inside the circle | |
} | |
buf[1].add(1u); | |
}); | |
buf.download(); | |
inCircle += buf[0].load(); | |
numSamples += buf[1].load(); | |
stat("Apprx", (float(inCircle) / float(numSamples)) * 4.0); | |
stat("Real", PI); | |
stat("Total Samples", numSamples); | |
} | |
## Workgroup example: | |
fn main() { | |
let buf = buffer<uint>(1); | |
compute((1, 1, 1), shader<16, 16, 1> { | |
workgroup { | |
count: atomic<uint> | |
} | |
count.add(1u); | |
workgroupBarrier(); | |
if (in.globalId.x == 0 && in.globalId.y == 0) | |
buf[0] = count.load(); | |
}); | |
buf.download(); | |
print(buf[0]); | |
} | |
API Reference: | |
## buffer<T> | |
methods: | |
len( ) -> int | |
Returns the underlying cpu buffer as a typed array. | |
[!NOTE] This is considerably faster than using the raw index [] operator. | |
[!NOTE] If the buffer contents are structured (atomic, or a struct), this will return a normal array | |
123456 | |
let buf = buffer<uint>(); | |
let data = buf.getData(); | |
for (let i = 0; i < data.length; i += 4) { | |
// Do something with data[i] | |
} | |
getData( ) -> T[] | Float32Array | Int32Array | Uint32Array | Uint8Array | |
write( other: buffer_internal<T> ) -> void | |
download( ) -> Promise<void> | |
downloadAsync( ) -> Promise<void> | |
upload( ) -> void | |
## texture2d<T> | |
properties: | |
size: float2 = [0, 0] | |
paint: PaintingContext = null as any | |
methods: | |
draw: { (geometry: Mesh, vertexShader: shader<ShaderInput, ShaderOutput, 0>, pixelShader: shader<ShaderInput, ShaderOutput, 0>): void; (fullScreenPixelShader: shader<...>): void; } | |
drawIndexed: (indexBuffer: buffer<uint>, vertexShader: shader<ShaderInput, ShaderOutput, 0>, pixelShader: shader<ShaderInput, ShaderOutput, 0>) => void | |
drawAdvanced: { <A0, A1, A2, A3, A4, A5, A6, A7>(descriptor: DrawDescriptorBase & AttachmentBindings8<A0, A1, A2, A3, A4, A5, A6, A7>): void; <A0, A1, A2, A3, A4, A5, A6>(descriptor: DrawDescriptorBase & AttachmentBindings7<...>): void; <A0, A1, A2, A3, A4, A5>(descriptor: DrawDescriptorBase & AttachmentBindings6<...>): void; <A0... | |
Methods | |
__index( index: int2 | uint2 ) -> T | |
__index_assign( index: int2 | uint2,value: T ) -> void | |
getFast( index: int2 | uint2 ) -> T | |
setFast( index: int2 | uint2,value: T ) -> void | |
download( ) -> void | |
downloadAsync( ) -> Promise<void> | |
Returns the underlying cpu buffer as a typed array. | |
Note that this is considerably faster than using the raw index [] operator. | |
1234567891011 | |
let tex = texture2d<float4>(); | |
let data = tex.getData(); | |
for (let i = 0; i < data.length; i += 4) { | |
let r = data[i]; | |
let g = data[i + 1]; | |
let b = data[i + 2]; | |
let a = data[i + 3]; | |
// Do something with the pixel | |
getData( ) -> Float32Array | Int32Array | Uint32Array | Uint8Array | |
upload( ) -> void | |
sample( position: float2 ) -> float4 | |
clear( ) -> void | |
flush( ) -> void | |
Release the texture | |
destroy( ) -> void | |
## ShaderOutput (aka out): | |
Properties | |
Vertex output position | |
position: float4 = float4(0, 0, 0, 0) | |
Vertex output normal | |
normal: float3 = float4(0, 0, 0, 0) | |
UV channel 0 output | |
uv: float2 = float2(0, 0) | |
UV channel 1 output | |
uv1: float2 = float2(0, 0) | |
UV channel 2 output | |
uv2: float2 = float2(0, 0) | |
UV channel 3 output | |
uv3: float2 = float2(0, 0) | |
UV channel 4 output | |
uv4: float2 = float2(0, 0) | |
UV channel 5 output | |
uv5: float2 = float2(0, 0) | |
UV channel 6 output | |
uv6: float2 = float2(0, 0) | |
UV channel 7 output | |
uv7: float2 = float2(0, 0) | |
Pixel color output | |
color: float4 = float4(0, 0, 0, 0) | |
Methods | |
attr< T > ( index: int,value: T,interpolation: "flat" | "linear" | "perspective" | undefined ) -> void | |
## ShaderInput (aka in): | |
Properties | |
Interpolated world position (available in fragment, and vertex) | |
position: float3 = float3(0, 0, 0) | |
Interpolated normal (fragment), Source mesh normal (vertex) | |
normal: float3 = float3(0, 0, 0) | |
Vertex shader output position | |
clipPosition: float4 = float4(0, 0, 0, 0) | |
realPosition: float4 = float4(0, 0, 0, 0) | |
UV channel 0 input (available in fragment, and vertex) | |
uv: float2 = float2(0, 0) | |
UV channel 1 input | |
uv1: float2 = float2(0, 0) | |
UV channel 2 input | |
uv2: float2 = float2(0, 0) | |
UV channel 3 input | |
uv3: float2 = float2(0, 0) | |
UV channel 4 input | |
uv4: float2 = float2(0, 0) | |
UV channel 5 input | |
uv5: float2 = float2(0, 0) | |
UV channel 6 input | |
uv6: float2 = float2(0, 0) | |
UV channel 7 output | |
uv7: float2 = float2(0, 0) | |
Screen position in pixels (available in fragment, and vertex) | |
screen: float2 = float2(0, 0) | |
Interpolated vertex color (available in fragment, and vertex) | |
color: float4 = float4(0, 0, 0, 0) | |
Group ID (available in compute) | |
groupId: int3 = int3(0, 0, 0) | |
Group size (available in compute) | |
groupSize: int3 = int3(0, 0, 0) | |
Global id (groupId * groupSize + localId) (available in compute) | |
globalId: int3 = int3(0, 0, 0) | |
Local id (available in compute) | |
localId: int3 = int3(0, 0, 0) | |
Instance index (available in fragment, and vertex) | |
instanceIndex: int = 0 | |
Vertex index (available in vertex) | |
vertexIndex: int = 0 | |
Methods | |
attr< T > ( index: int,interpolation: "flat" | "linear" | "perspective" | undefined ) -> T | |
## Camera: | |
Properties | |
position: float3 | |
rotation: float4 | |
width: float | |
height: float | |
fov: float | |
near: float | |
far: float | |
Methods | |
getRay( screen: float2 ) -> float3 | |
getTransformToViewMatrix( position: float3,scale: float3,rotation: float4 ) -> float4x4 | |
getCombinedMatrix( ) -> float4x4 | |
getWorldToViewMatrix( ) -> float4x4 | |
getPerspectiveMatrix( ) -> float4x4 | |
getOrthographicMatrix( ) -> float4x4 | |
clone( ) -> Camera | |
Generally you should scale everything to 100 units in size as 1 unit = 1cm. The camera is zoomed such that 1m looks good by default | |
You also have access to a default camera under `env.camera` you should use this as much as possible as it gives the user orbit controls | |
## Quat: | |
can be accessed like `quat::...` | |
module quat | |
Creates a quaternion from an angle and axis. | |
fromAngleAxis( angle: float,axis: float3 ) -> float4 | |
Rotates a vector by a quaternion and returns the rotated vector. | |
rotate( quaternion: float4,vector: float3 ) -> float3 | |
Returns the conjugate of the input quaternion. | |
The conjugate of a quaternion number is a quaternion with the same magnitudes but with the sign of the imaginary parts changed | |
conjugate( quaternion: float4 ) -> float4 | |
Returns the inverse of the input quaternion. | |
inverse( quaternion: float4 ) -> float4 | |
Generates a quaternion that rotates from one direction to another via the shortest path. | |
fromToRotation( from: float3,to: float3 ) -> float4 | |
diff( a: float4,b: float4 ) -> float4 | |
Generates lookAt quaternion. | |
lookAt( forward: float3,up: float3 ) -> float4 | |
Smooth interpolation between two quaternions. | |
slerp( a: float4,b: float4,t: float ) -> float4 | |
Converts quaternion to matrix. | |
toMatrix( quaternion: float4 ) -> float4x4 | |
clone( ) -> quat | |
module matrix | |
lookAt( from: float3,to: float3,up: float3 ) -> float4x4 | |
perspective( fov: float,aspect: float,near: float,far: float ) -> float4x4 | |
clone( ) -> matrix | |
some global functions: | |
draw( geometry: Mesh,vertexShader: shader<ShaderInput, ShaderOutput, 0>,pixelShader: shader<ShaderInput, ShaderOutput, 0> ) -> void | |
draw( fullScreenPixelShader: shader<ShaderInput, ShaderOutput, 0> ) -> void | |
drawAdvanced(...): | |
drawAdvanced({ | |
mesh: mesh::box(0.xyz, 100.xyz), | |
vertex: shader { | |
// ... | |
}, | |
fragment: shader { | |
// ... | |
}, | |
}); | |
drawAdvanced({ | |
mesh: mesh::box(0.xyz, 100.xyz), | |
vertex: shader { | |
in.instanceIndex; | |
}, | |
fragment: shader { | |
// ... | |
}, | |
instances: 100, | |
}); | |
let mesh = mesh::box(0.xyz, 100.xyz); | |
let indirectBuffer = buffer(5); indirectBuffer[0] = uint(m.getTriangles().len()); // indexCount indirectBuffer[1] = 1; // instanceCount indirectBuffer[2] = 0; // firstIndex indirectBuffer[3] = 0; // vertexOffset indirectBuffer[4] = 0; // firstInstance | |
drawAdvanced({ | |
mesh: mesh::box(0.xyz, 100.xyz), | |
vertex: shader { | |
// ... | |
}, | |
fragment: shader { | |
// ... | |
}, | |
indirect: indirectBuffer, | |
}); | |
type DrawAdvancedBaseInput = ( | |
| { | |
mesh: Mesh; | |
} | |
| { | |
indexBuffer: buffer<uint>; | |
} | |
) & { | |
indirect?: buffer<uint> | buffer<atomic<uint>>; | |
indirectOffset?: int | uint; | |
depth?: texture2d<float>; | |
depthOnly?: boolean; | |
instances?: int; | |
}; | |
--- end of tutorial --- | |
With that please write a scene with a spinning cube |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment