3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust + macroquad + tobjでobjファイルを読み込んで画面に表示してみるテスト

Posted at

はじめに

Rust + macroquad + tobjで.objファイルからメッシュ情報を抜き出して描写してみた備忘録。
macroquad: シングルスレッドベースのゲームライブラリクレート
tobj: objファイルを読み込むクレート

環境

cargo.toml
[dependencies]
macroquad = "0.4.14"
tobj = "4.0.3"

成果物

成果物先行提示主義
スクリーンショット 2025-11-12 164811.png

tobj->macroquadへ頂点情報の受け渡し

macroquadにはdraw_mesh()という頂点情報をもとに描写を行う関数があります。
.objファイルの中身は頂点座標などが詰め込まれてるだけなので取得できれば描写できるだろうという算段。

draw_mesh()に渡すMesh情報は以下のようになっています。

macroquadより引用
pub struct Mesh {
    pub vertices: Vec<Vertex>,
    pub indices: Vec<u16>,
    pub texture: Option<Texture2D>,
}

大切なのはVertexの情報で、中身は以下のようになっています。

  • position->メッシュの頂点の3次元座標
  • uv->テクスチャ座標
  • color->頂点色の情報
  • normal->頂点の法線方向の情報

これらはすべて.objファイルの中に記載できる(ない場合もある)情報で、
これらをtobjで取得して格納していきます。

macroquadより引用
pub struct Vertex {
    pub position: Vec3,
    pub uv: Vec2,
    pub color: [u8; 4],
    pub normal: Vec4,
}

賢い方法はありそうですが、愚直に変換していきます。

obj_to_mesh.rs
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からいい感じに頂点数の少ない机を借りてロードしてみます。

main.rs
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

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?