model.rin

includes/learn/model.rin · 299 lines

// Loading models from Wavefront .obj files: what Assimp and LearnOpenGL's
// Model and Mesh classes are to LearnOpenGL. Only the part of the format the
// lessons' models use:
//
//   v  x y z        a position
//   vt u v          a texture coordinate
//   vn x y z        a normal
//   f  a/b/c ...    a face: position/uv/normal indices, counted from 1
//   mtllib x.mtl    the material file, which names the textures
//
// Faces with more than three corners are split into a fan. Every corner
// becomes its own vertex, so nothing is shared between triangles; that costs
// memory, not correctness.
//
// OBJ's v normally runs up the image and D3D's runs down, so v usually needs
// flipping. Whether it does depends on how the model's textures were saved:
// the backpack's already suit top-down v. Hence `flip_v`, checked per model.
//
// model_draw binds the textures to fixed slots: diffuse t0, specular t1,
// normal t2.

import "../win32.rin"
import "../math.rin"
import "../image.rin"
import "filesystem.rin"

strtof: proc[external](text: [*]const char, end: *[*]char) -> f32 = {}
strtol: proc[external](text: [*]const char, end: *[*]char, base: i32) -> long = {}

model_vertex: struct = {
    position: vec3;
    normal: vec3;
    uv: vec2;
}

model_vertex_layout: [3]D3D11_INPUT_ELEMENT_DESC = {
    { .SemanticName = "POSITION", .Format = DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT, .AlignedByteOffset = 0 },
    { .SemanticName = "NORMAL", .Format = DXGI_FORMAT.DXGI_FORMAT_R32G32B32_FLOAT, .AlignedByteOffset = 12 },
    { .SemanticName = "TEXCOORD", .Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, .AlignedByteOffset = 24 },
};

model: struct = {
    vertices: *ID3D11Buffer;
    vertex_count: UINT;
    diffuse: *ID3D11ShaderResourceView;    // map_Kd
    specular: *ID3D11ShaderResourceView;   // map_Ks
    normal: *ID3D11ShaderResourceView;     // map_Bump
}

obj_reader: struct = {
    text: [*]char;
    at: u64;
}

obj_skip_spaces: proc(r: *obj_reader) -> void = {
    while (r[0].text[r[0].at] == ' ' || r[0].text[r[0].at] == '\t') { r[0].at += 1; }
}

obj_next_line: proc(r: *obj_reader) -> void = {
    while (r[0].text[r[0].at] != 0 && r[0].text[r[0].at] != '\n') { r[0].at += 1; }
    if (r[0].text[r[0].at] == '\n') { r[0].at += 1; }
}

obj_float: proc(r: *obj_reader) -> f32 = {
    end: [*]char = null;
    value: f32 = strtof(r[0].text[r[0].at]&, end&);
    r[0].at = cast(cast(end, usize) - cast(r[0].text, usize), u64);
    return value;
}

obj_int: proc(r: *obj_reader) -> i32 = {
    end: [*]char = null;
    value: i32 = cast(strtol(r[0].text[r[0].at]&, end&, 10), i32);
    r[0].at = cast(cast(end, usize) - cast(r[0].text, usize), u64);
    return value;
}

// The word at the cursor, written into `out` with a terminator.
obj_word: proc(r: *obj_reader, out: []char) -> void = {
    obj_skip_spaces(r);
    n: u64 = 0;
    for (; n + 1 < out.length; n += 1) {
        c: char = r[0].text[r[0].at];
        if (c == 0 || c == '\n' || c == '\r' || c == ' ' || c == '\t') { break; }
        out[n] = c;
        r[0].at += 1;
    }
    out[n] = 0;
}

obj_starts: proc(r: *const obj_reader, word: [*]const char) -> b32 = {
    n: u64 = cstr_length(word);
    return cstr_equals_n(r[0].text[r[0].at]&, word, n) &&
           (r[0].text[r[0].at + n] == ' ' || r[0].text[r[0].at + n] == '\t');
}

// One `a/b/c` corner of a face.
obj_corner: struct = {
    position: i32;
    uv: i32;
    normal: i32;
}

// Missing parts (`a//c`, `a/b`, `a`) come back as 0.
obj_read_corner: proc(r: *obj_reader) -> obj_corner = {
    c: obj_corner = {};
    obj_skip_spaces(r);
    c.position = obj_int(r);
    if (r[0].text[r[0].at] != '/') { return c; }
    r[0].at += 1;
    if (r[0].text[r[0].at] != '/') { c.uv = obj_int(r); }
    if (r[0].text[r[0].at] != '/') { return c; }
    r[0].at += 1;
    c.normal = obj_int(r);
    return c;
}

obj_at_corner: proc(r: *obj_reader) -> b32 = {
    obj_skip_spaces(r);
    c: char = r[0].text[r[0].at];
    return (c >= '0' && c <= '9') || c == '-';
}

// A texture with a full mip chain, from an image file.
model_texture: proc(device: *ID3D11Device, context: *ID3D11DeviceContext, path: [*]const char) -> *ID3D11ShaderResourceView = {
    img: image = image_load(path);
    if (img.pixels == null) {
        printf("model: cannot load %s\n", path);
        exit(1);
    }
    desc: D3D11_TEXTURE2D_DESC = {
        .Width = cast(img.width, UINT),
        .Height = cast(img.height, UINT),
        .ArraySize = 1,
        // Colours are drawn as stored, as LearnOpenGL's model loader does; the
        // normal map is data either way.
        .Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM,
        .SampleDesc = { .Count = 1 },
        .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET,
        .MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS,
    };
    texture: *ID3D11Texture2D = null;
    srv: *ID3D11ShaderResourceView = null;
    ID3D11Device_CreateTexture2D(device, desc&, null, texture&);
    ID3D11Device_CreateShaderResourceView(device, cast(texture, *ID3D11Resource), null, srv&);
    ID3D11DeviceContext_UpdateSubresource(context, cast(texture, *ID3D11Resource), 0, null, img.pixels, cast(img.width * 4, UINT), 0);
    ID3D11DeviceContext_GenerateMips(context, srv);
    return srv;
}

// Reads `name` from the .mtl next to the model, and loads its textures.
obj_load_material: proc(device: *ID3D11Device, context: *ID3D11DeviceContext, dir: [*]const char, name: [*]const char, out: *model) -> void = {
    path: [260]char = {};
    cstr_copy(path, dir);
    cstr_append(path, name);
    r: obj_reader = { .text = cast(filesystem_read(path[0]&, null), [*]char) };
    word: [128]char = {};
    while (r.text[r.at] != 0) {
        obj_skip_spaces(r&);
        is_diffuse: b32 = obj_starts(r&, "map_Kd");
        is_specular: b32 = obj_starts(r&, "map_Ks");
        is_normal: b32 = obj_starts(r&, "map_Bump");
        if (is_diffuse || is_specular || is_normal) {
            obj_word(r&, word);
            obj_word(r&, word);
            cstr_copy(path, dir);
            cstr_append(path, word[0]&);
            t: *ID3D11ShaderResourceView = model_texture(device, context, path[0]&);
            if (is_diffuse) { out[0].diffuse = t; }
            if (is_specular) { out[0].specular = t; }
            if (is_normal) { out[0].normal = t; }
        }
        obj_next_line(r&);
    }
}

model_arena: memops_arena = {};

// `relative_dir` ends in a slash, `file` is the .obj inside it.
model_load: proc(device: *ID3D11Device, context: *ID3D11DeviceContext, relative_dir: [*]const char, file: [*]const char, flip_v: b32) -> model = {
    if (model_arena.base == null) { memops_arena_initialize(model_arena&); }
    dir: [*]const char = filesystem_path(relative_dir);
    path: [260]char = {};
    cstr_copy(path, dir);
    cstr_append(path, file);
    r: obj_reader = { .text = cast(filesystem_read(path[0]&, null), [*]char) };
    if (r.text == null) {
        printf("model: cannot read %s\n", path[0]&);
        exit(1);
    }

    // First pass: count, so every array is allocated once at its final size.
    positions: u64 = 0;
    uvs: u64 = 0;
    normals: u64 = 0;
    corners: u64 = 0;
    while (r.text[r.at] != 0) {
        if (obj_starts(r&, "v")) { positions += 1; }
        if (obj_starts(r&, "vt")) { uvs += 1; }
        if (obj_starts(r&, "vn")) { normals += 1; }
        if (obj_starts(r&, "f")) {
            r.at += 1;
            n: u64 = 0;
            while (obj_at_corner(r&)) {
                obj_read_corner(r&);
                n += 1;
            }
            if (n >= 3) { corners += (n - 2) * 3; }
        }
        obj_next_line(r&);
    }

    p: [*]vec3 = cast(memops_arena_push(model_arena&, (positions + 1) * sizeof(vec3), 16), [*]vec3);
    t: [*]vec2 = cast(memops_arena_push(model_arena&, (uvs + 1) * sizeof(vec2), 16), [*]vec2);
    n: [*]vec3 = cast(memops_arena_push(model_arena&, (normals + 1) * sizeof(vec3), 16), [*]vec3);
    vertices: [*]model_vertex = cast(memops_arena_push(model_arena&, corners * sizeof(model_vertex), 16), [*]model_vertex);
    // Index 0 is the "missing" entry, so 1-based indices need no adjusting.
    p[0] = v3(0.0, 0.0, 0.0);
    t[0] = v2(0.0, 0.0);
    n[0] = v3(0.0, 1.0, 0.0);

    result: model = {};
    word: [260]char = {};
    np: u64 = 1;
    nt: u64 = 1;
    nn: u64 = 1;
    nv: u64 = 0;
    r.at = 0;
    while (r.text[r.at] != 0) {
        if (obj_starts(r&, "v")) {
            r.at += 1;
            x: f32 = obj_float(r&);
            y: f32 = obj_float(r&);
            z: f32 = obj_float(r&);
            p[np] = v3(x, y, z);
            np += 1;
        } else if (obj_starts(r&, "vt")) {
            r.at += 2;
            u: f32 = obj_float(r&);
            v: f32 = obj_float(r&);
            t[nt] = v2(u, flip_v ? 1.0 - v : v);
            nt += 1;
        } else if (obj_starts(r&, "vn")) {
            r.at += 2;
            x: f32 = obj_float(r&);
            y: f32 = obj_float(r&);
            z: f32 = obj_float(r&);
            n[nn] = v3(x, y, z);
            nn += 1;
        } else if (obj_starts(r&, "f")) {
            r.at += 1;
            first: obj_corner = {};
            previous: obj_corner = {};
            count: u64 = 0;
            while (obj_at_corner(r&)) {
                corner: obj_corner = obj_read_corner(r&);
                if (count == 0) { first = corner; }
                if (count >= 2) {
                    fan: [3]obj_corner = { first, previous, corner };
                    for (k: u64 = 0; k < 3; k += 1) {
                        c: obj_corner = fan[k];
                        vertices[nv] = { .position = p[c.position], .normal = n[c.normal], .uv = t[c.uv] };
                        nv += 1;
                    }
                }
                previous = corner;
                count += 1;
            }
        } else if (obj_starts(r&, "mtllib")) {
            obj_word(r&, word);
            obj_word(r&, word);
            obj_load_material(device, context, dir, word[0]&, result&);
        }
        obj_next_line(r&);
    }
    desc: D3D11_BUFFER_DESC = {
        .ByteWidth = cast(nv * sizeof(model_vertex), UINT),
        .Usage = D3D11_USAGE.D3D11_USAGE_IMMUTABLE,
        .BindFlags = D3D11_BIND_VERTEX_BUFFER,
    };
    init: D3D11_SUBRESOURCE_DATA = { .pSysMem = vertices };
    ID3D11Device_CreateBuffer(device, desc&, init&, result.vertices&);
    result.vertex_count = cast(nv, UINT);
    return result;
}

model_draw_instanced: proc(context: *ID3D11DeviceContext, m: *const model, instances: UINT) -> void = {
    stride: UINT = sizeof(model_vertex);
    offset: UINT = 0;
    views: [3]*ID3D11ShaderResourceView = { m[0].diffuse, m[0].specular, m[0].normal };
    ID3D11DeviceContext_PSSetShaderResources(context, 0, 3, views[0]&);
    ID3D11DeviceContext_IASetPrimitiveTopology(context, D3D11_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    ID3D11DeviceContext_IASetVertexBuffers(context, 0, 1, m[0].vertices&, stride&, offset&);
    ID3D11DeviceContext_DrawInstanced(context, m[0].vertex_count, instances, 0, 0);
}

model_draw: proc(context: *ID3D11DeviceContext, m: *const model) -> void = {
    model_draw_instanced(context, m, 1);
}
Part of rin-dx11, LearnOpenGL's lessons rewritten for Direct3D 11 and rin. Licensed CC BY-NC 4.0, after LearnOpenGL by Joey de Vries.