01 - The Rotating Triangle
The canonical “hello triangle” – three vertices, per-vertex black/yellow/red,
interpolated across the face – but with every line of the shader written in
daslang and lowered to GLSL (GLSL ES 3.00 on the web) at compile time by
dasGlsl. No hand-written GLSL, no committed shader strings. A per-frame
u_angle uniform spins it, so the loop has a real GPU parameter, and
u_aspect keeps it square at any window or canvas size.
This is the first rung of an OpenGL / WebGL2 tutorial ladder that mirrors the dasVulkan series shader-for-shader: the same daslang shader language, the same modern builtins, lowered to a third backend.
One homogeneous program
Unlike the Vulkan tutorials – which need a separate headless render and a
windowed viewer – an OpenGL tutorial is one file that runs on both the
desktop and the web. It exposes the three lifecycle functions init /
update / shutdown plus a main driver:
On the desktop,
main()runsinit(); while(update()){}; shutdown()in a GLFW window.On the web, the wasm run path calls
init()once, then drivesupdate()from the browser’s animation frame (requestAnimationFrame), and the daslangContextpersists across frames.main()is never called.
The same .das, byte-for-byte, both places.
var @in @location v_position : float2
var @in @location v_color : float3
var @uniform u_angle : float
var @uniform u_aspect : float
var @inout f_color : float3
var @out f_FragColor : float4
[vertex_program]
def vs_main {
f_color = v_color
let c = cos(u_angle)
let s = sin(u_angle)
let p = v_position
let r = float2(p.x * c - p.y * s, p.x * s + p.y * c)
gl_Position = float4(r.x / u_aspect, r.y, 0.0, 1.0)
}
[fragment_program]
def fs_main {
f_FragColor = float4(f_color, 1.0)
}
var program : uint
var vao : uint
var vbo : uint
var window : GLFWwindow?
var angle : float = 0.0
[vertex_buffer]
struct Vertex {
xy : float2
rgb : float3
}
// An equilateral triangle centered on the origin, so its centroid IS the
// rotation center and it spins in place (apex up in GL's y-up clip space).
// Offsets: apex at (0, r); base corners at (±r·√3/2, -r/2) with r = 0.6 → the
// centroid averages to (0, 0). u_aspect (below) keeps it from stretching on a
// non-square window/canvas.
let vertices = [Vertex(
xy=float2(0.0, 0.6), rgb=float3(0.0, 0.0, 0.0)), Vertex(
xy=float2(-0.5196, -0.3), rgb=float3(1.0, 1.0, 0.0)), Vertex(
xy=float2(0.5196, -0.3), rgb=float3(1.0, 0.0, 0.0)
)];
def create_gl_objects {
program = create_shader_program(@@vs_main, @@fs_main)
glGenVertexArrays(1, safe_addr(vao))
glBindVertexArray(vao)
glGenBuffers(1, safe_addr(vbo))
glBindBuffer(GL_ARRAY_BUFFER, vbo)
glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW)
bind_vertex_buffer(null, type<Vertex>)
}
[export]
def init {
if (glfwInit() == 0) {
panic("can't init glfw")
}
glfwInitOpenGL(3, 3)
window = glfwCreateWindow(640, 480, "OpenGL - 01 rotating triangle", null, null)
if (window == null) {
panic("can't create window")
}
glfwMakeContextCurrent(window)
create_gl_objects()
}
[export]
def update : bool {
angle += 0.02
var display_w, display_h : int
glfwGetFramebufferSize(window, display_w, display_h)
let h = max(display_h, 1)
u_angle = angle
u_aspect = float(display_w) / float(h)
glViewport(0, 0, display_w, display_h)
glClearColor(0.1, 0.1, 0.12, 1.0)
glClear(GL_COLOR_BUFFER_BIT)
glUseProgram(program)
vs_main_bind_uniform(program)
fs_main_bind_uniform(program)
glBindVertexArray(vao)
glDrawArrays(GL_TRIANGLES, 0, 3)
glfwPollEvents()
glfwSwapBuffers(window)
return glfwWindowShouldClose(window) == 0
}
[export]
def shutdown {
glfwDestroyWindow(window)
glfwTerminate()
}
// Desktop driver. On the web this is never called -- the run path drives the
// three lifecycle functions directly and persists the Context across frames.
[export]
def main {
init()
while (update()) {
}
shutdown()
}
The shader
Both stages are plain daslang functions tagged [vertex_program] /
[fragment_program]. u_angle and u_aspect are @uniform globals;
the annotations synthesise a per-shader <shader>_bind_uniform(program) helper
that uploads them for the host each frame – you set the global, the generated
bind does the glUniform call. The vertex stage reads u_angle, builds a 2D
rotation, spins the hardcoded clip-space positions, and divides x by
u_aspect so the triangle stays square; the fragment stage just writes the
interpolated varying.
The loop
update() advances the angle, reads the live framebuffer size (so the aspect
correction follows window/canvas resizes), clears, binds the program and its
uniforms, and draws the three vertices. It returns true to keep going;
false (the window was asked to close) ends the loop on the desktop. On the
web a void or true-returning update simply runs until the page is
closed.
Run it
Locally, in a window:
daslang tutorials/opengl/01_triangle/01_triangle.das
In the browser, it runs live in the daslang playground (the canvas-backed
graphics samples) – the same .das, lowered to WebGL2.