はじめに
Rust + macroquad + tobjで.objファイルからメッシュ情報を抜き出して描写してみた備忘録。
macroquad: シングルスレッドベースのゲームライブラリクレート
tobj: objファイルを読み込むクレート
環境
[dependencies]
macroquad = "0.4.14"
tobj = "4.0.3"
成果物
tobj->macroquadへ頂点情報の受け渡し
macroquadにはdraw_mesh()という頂点情報をもとに描写を行う関数があります。
.objファイルの中身は頂点座標などが詰め込まれてるだけなので取得できれば描写できるだろうという算段。
draw_mesh()に渡すMesh情報は以下のようになっています。
pub struct Mesh {
pub vertices: Vec<Vertex>,
pub indices: Vec<u16>,
pub texture: Option<Texture2D>,
}
大切なのはVertexの情報で、中身は以下のようになっています。
-
position->メッシュの頂点の3次元座標 -
uv->テクスチャ座標 -
color->頂点色の情報 -
normal->頂点の法線方向の情報
これらはすべて.objファイルの中に記載できる(ない場合もある)情報で、
これらをtobjで取得して格納していきます。
pub struct Vertex {
pub position: Vec3,
pub uv: Vec2,
pub color: [u8; 4],
pub normal: Vec4,
}
賢い方法はありそうですが、愚直に変換していきます。
use tobj;
use macroquad::prelude::*;
//tobjのMeshと名前が被るので型同義語を作成
type MacroMesh = macroquad::models::Mesh;
pub fn obj_to_vec_mesh(path: &str) -> Vec<MacroMesh> {
//tobjにpathを指定してObjファイルのメッシュ情報を読み込む
let cornell_box = tobj::load_obj(
path,
&tobj::LoadOptions {
single_index: true,
triangulate: true, //必須
..Default::default()
},
);
//モデル情報を読み込み。マテリアルは今回は不使用
let (models, _materials) = cornell_box.expect("Failed to load OBJ file");
//モデル毎にmacroquad対応のメッシュ情報を作成 -> 配列に格納
let mut result: Vec<MacroMesh> = vec![];
for model in models.iter(){
//tobj -> 対象のメッシュ
let mesh: &tobj::Mesh = &model.mesh;
//頂点座標
assert!(mesh.positions.len() % 3 == 0);
let mut v_positions : Vec<Vec3> = vec![];
for v in 0..mesh.positions.len() / 3 {
let mut v_position: Vec3 = Vec3::ZERO;
v_position.x = mesh.positions[3*v];
v_position.y = mesh.positions[3*v+1];
v_position.z = mesh.positions[3*v+2];
v_positions.push(v_position);
}
//テクスチャ座標
let mut v_uvs : Vec<Vec2> = vec![];
if !mesh.texcoords.is_empty(){
assert!(mesh.texcoords.len() % 2 == 0);
for uv in 0..mesh.texcoords.len() / 2{
let mut v_uv: Vec2 = Vec2::ZERO;
v_uv.x = mesh.texcoords[2*uv];
v_uv.y = mesh.texcoords[2*uv+1];
v_uvs.push(v_uv);
}
}else{
for _ in 0..v_positions.len() {
let v_uv = Vec2 {x:0.0, y:0.0};
v_uvs.push(v_uv);
}
}
assert!(v_positions.len() == v_uvs.len());
//頂点色情報
//macroquadのカラー情報は[u8; 4]より( f32 0.0~1.0 -> u8 0~255 )へinto()で変換
let mut v_colors : Vec<[u8; 4]> = vec![];
if !mesh.vertex_color.is_empty() {
assert!(mesh.vertex_color.len() % 3 == 0);
for c in 0..mesh.vertex_color.len() / 3 {
let mut color : Color = Color { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
color.r = mesh.vertex_color[3 * c];
color.g = mesh.vertex_color[3 * c+1];
color.b = mesh.vertex_color[3 * c+2];
v_colors.push(color.into());
}
}else{
for _ in 0..v_positions.len() {
//色が無い場合はピンクで埋める
v_colors.push(PINK.into());
}
}
assert!(v_positions.len() == v_colors.len());
//法線 -> macroquadの標準シェーダーでは法線情報は使わないので省略しても良い
let mut v_normal : Vec<Vec4> = vec![];
if !mesh.normals.is_empty() {
assert!(mesh.normals.len() % 3 == 0);
for n in 0..mesh.normals.len() / 3{
let mut normal: Vec4 = Vec4{x:0.0, y:0.0, z:0.0, w:0.0};
normal.x = mesh.normals[3 * n];
normal.y = mesh.normals[3 * n+1];
normal.z = mesh.normals[3 * n+2];
v_normal.push(normal);
}
}else{
for _ in 0..v_positions.len() {
//法線が設定されていない場合はmacroquadの他の例と同じく0にしておく
v_normal.push(Vec4{x:0.0, y:0.0, z:0.0, w:0.0});
}
}
assert!(v_positions.len() == v_normal.len());
//macroquadのVertexリストを作成
let mut macro_vertices: Vec<Vertex> = vec![];
for i in 0..v_positions.len() {
let vertex = Vertex {
position: v_positions[i],
uv: v_uvs[i],
color: v_colors[i],
normal: v_normal[i],
};
macro_vertices.push(vertex);
}
//頂点インデックス
let mut macro_indices: Vec<u16> = vec![];
for i in mesh.indices.iter() {
/*
tobjのIndicesはVec<u32>定義だが、
macroquadのMeshのIndicesはVec<u16>なのでコンバートする。0~65535
※macroquadの描写制限より基本的にはモデルあたりメッシュ数1000程度しか使えないので問題はないと思われる
*/
let index = u16::try_from(*i).expect("Index too large for u16");
macro_indices.push(index);
}
//macroquadのMeshを作成
let macro_mesh = MacroMesh {
vertices: macro_vertices,
indices: macro_indices,
texture: None,
};
//格納
result.push(macro_mesh);
}
return result;
}
macroquadで呼び出して描写
以下mainコード。
free3Dからいい感じに頂点数の少ない机を借りてロードしてみます。
mod obj_to_mesh;
use macroquad::prelude::*;
use crate::obj_to_mesh::obj_to_vec_mesh;
#[macroquad::main("obj test")]
async fn main() {
//机はfree3dから借用 https://free3d.com/ja/3d-model/tablle-396579.html
let meshes: Vec<Mesh> = obj_to_vec_mesh("models/table_obj/table.obj");
loop {
//カメラ設定
set_camera(&Camera3D{
position: vec3(-1.0, 1.0, -1.0),
target: Vec3::ZERO,
up: Vec3::Y,
fovy: 90.0_f32.to_radians(),
..Default::default()
});
//背景色を指定
clear_background(SKYBLUE);
//地面を描写
draw_plane(Vec3::ZERO, vec2(20.0, 20.0),None, DARKBROWN);
draw_grid(20, 1.0, BLACK, DARKGRAY);
//読み込んだメッシュから描写
for mesh in meshes.iter(){
draw_mesh(mesh);
}
set_default_camera();
next_frame().await;
}
}
おわりに
tobjとmacroquadで狙い通りobjファイルから3Dモデルを読み込ませることができました。
しかしmacroquad側の描写制限でメッシュあたりの頂点数に制約があったり、
そもそも3D対応はメインでないのでシェーダーを自作する場合にバグがあったりまだ実用的とは言い難い感じ……。
とはいえobjファイルがあれば自分の好きなモデルでゲーム制作ができるので、球体や立方体に飽きてきたら使ってみるのも面白いかもしれません。
お借りしたもの
ローポリの机: https://free3d.com/ja/3d-model/tablle-396579.html
参考
macroquad: https://docs.rs/macroquad/latest/macroquad/index.html
tobj: https://docs.rs/tobj/latest/tobj/index.html
