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() runs init(); while(update()){}; shutdown() in a GLFW window.

  • On the web, the wasm run path calls init() once, then drives update() from the browser’s animation frame (requestAnimationFrame), and the daslang Context persists 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.