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);
}