Processing(Java)で.objファイル読み込み

2010-05-12
.plyファイルはマテリアルとかなくて残念。てことで.objファイル。

Crytek Sponzaとか。

// .objファイルローダー
class OBJLoader {
class Material {
float[] ambient, diffuse, specular, emissive;
float shininess;
String diffusemap;
String bumpmap;
}

class Mesh {
ArrayList faces = new ArrayList();
Material material;
}

ArrayList m_vertices = new ArrayList();
ArrayList m_normals = new ArrayList();
ArrayList m_uvs = new ArrayList();
ArrayList m_mesh = new ArrayList();
HashMap m_mtllib = new HashMap();

boolean load(String fn) {
BufferedReader reader = createReader(fn);
try {
if (reader == null) return false;
if (!read_data(reader, fn)) return false;
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}

boolean read_data(BufferedReader reader, String fn) throws IOException {
String objname = null;
Mesh mesh = new Mesh();
for (;;) {
String line = reader.readLine();
if (line == null) break;

String[] m = line.trim().split("\\s+", 0);
if (m == null) continue;
if (m[0].equals("v")) { // 頂点座標
float[] vtx = str2floats(m);
m_vertices.add(vtx);
} else if (m[0].equals("vn")) { // 法線
float[] nrm = str2floats(m);
float rr = 0;
for (int i = 0; i < nrm.length; ++i) rr += sq(nrm[i]);
float invr = 1.0 / sqrt(rr);
for (int i = 0; i < nrm.length; ++i) nrm[i] *= invr;
m_normals.add(nrm);
} else if (m[0].equals("vt")) { // UV座標
float[] uv = str2floats(m);
m_uvs.add(uv);
} else if (m[0].equals("f")) { // フェース
int[][] face = new int[m.length - 1][];
for (int i = 1; i < m.length; ++i) {
String[] idxs = m[i].split("/", 0);
int[] iii = new int[idxs.length];
for (int j = 0; j < idxs.length; ++j) {
int x = int(idxs[j]);
if (x > 0) x = x - 1; // 絶対指定
else if (x < 0) x = m_vertices.size() + x; // 相対指定
else x = -1; // 0: 無効
iii[j] = x;
}
face[i - 1] = iii;
}
mesh.faces.add(face);
} else if (m[0].equals("mtllib")) { // マテリアルライブラリ
String dir = new File(fn).getParent();
for (int i = 1; i < m.length; ++i) {
String mfn = m[i];
if (!load_mtllib(dir + "/" + mfn)) {
return false;
}
}
} else if (m[0].equals("usemtl")) { // マテリアル
add_mesh(mesh);
mesh = new Mesh();
String name = m[1];
if (m_mtllib.containsKey(name)) {
mesh.material = (Material)m_mtllib.get(name);
}
} else {
// skip
}
}
add_mesh(mesh);
return true;
}

private boolean load_mtllib(String fn) throws IOException {
BufferedReader reader = createReader(fn);
if (reader == null) return false;

String mtlname = null;
Material material = null;
for (;;) {
String line = reader.readLine();
if (line == null) break;

String[] m = line.trim().split("\\s+", 0);
if (m[0].equals("newmtl")) { // 頂点座標
if (material != null) add_material(mtlname, material);
mtlname = m[1];
material = new Material();
} else if (m[0].equals("Ka")) { // アンビエント
material.ambient = str2floats(m);
} else if (m[0].equals("Kd")) { // ディフューズ
material.diffuse = str2floats(m);
} else if (m[0].equals("Ks")) { // スペキュラー
material.specular = str2floats(m);
} else if (m[0].equals("Ke")) { // エミッシブ
material.emissive = str2floats(m);
} else if (m[0].equals("map_Ka")) { // ディフューズマップ
material.diffusemap = m[1];
} else if (m[0].equals("map_bump")) { // バンプマップ
material.bumpmap = m[1];
} else {
// なし
}
}
if (material != null) add_material(mtlname, material);
return true;
}

private void add_material(String name, Material material) {
m_mtllib.put(name, material);
}

private void add_mesh(Mesh mesh) {
m_mesh.add(mesh);
}

private float[] str2floats(String words[]) {
float[] a = new float[words.length - 1];
for (int i = 1; i < words.length; ++i) a[i - 1] = float(words[i]);
return a;
}
}

これを使って、ProcessingのOpenGLで表示するテスト:

import processing.opengl.*;

class Model extends OBJLoader {
class Texture {
PImage img;
void bind() {
texture(img);
}
float s(float u) { return u * img.width; }
float t(float v) { return v * img.height; }
}

HashMap m_textures = new HashMap();

boolean load(String fn) {
if (!super.load(fn)) return false;
for (Iterator it = m_mtllib.values().iterator(); it.hasNext(); ) {
Material material = (Material)it.next();
if (material.diffusemap != null)
load_texture(material.diffusemap, fn);
}
return true;
}

void draw() {
for (Iterator it = m_mesh.iterator(); it.hasNext(); ) {
OBJLoader.Mesh mesh = (OBJLoader.Mesh)it.next();
beginShape(TRIANGLES);
Model.Texture tex = null;
if (mesh.material != null) {
int r = constrain(floor(mesh.material.diffuse[0] * 255), 0, 255);
int g = constrain(floor(mesh.material.diffuse[1] * 255), 0, 255);
int b = constrain(floor(mesh.material.diffuse[2] * 255), 0, 255);
fill(r, g, b);

tex = get_texture(mesh.material.diffusemap);
if (tex != null) {
tex.bind();
}
}
for (int i=0; i<mesh.faces.size(); ++i) {
int[][] face = (int[][])mesh.faces.get(i);
for (int j=2; j<face.length; ++j) {
drawvertex(face[0], tex);
drawvertex(face[j-1], tex);
drawvertex(face[j], tex);
}
}
endShape();
}
}

void drawvertex(int[] vinfo, Model.Texture tex) {
float[] vtx = (float[])m_vertices.get(vinfo[0]);
if (vinfo.length > 2 && vinfo[2] >= 0) { // 法線がある
float[] nrm = (float[])m_normals.get(vinfo[2]);
normal(nrm[0], nrm[1], nrm[2]);
}
if (vinfo.length > 1 && vinfo[1] >= 0 && tex != null) { // UVがある
float[] uv = (float[])m_uvs.get(vinfo[1]);
vertex(vtx[0], vtx[1], vtx[2], tex.s(uv[0]), tex.t(uv[1]));
} else {
vertex(vtx[0], vtx[1], vtx[2]);
}
}

Texture get_texture(String name) {
if (m_textures.containsKey(name)) {
return (Texture)m_textures.get(name);
} else return null;
}

private void load_texture(String mtlfn, String mdlfn) {
if (!m_textures.containsKey(mtlfn)) {
String dir = new File(mdlfn).getParent();
String name = new File(mtlfn).getName();
PImage img = loadImage(dir + "/" + name);
if (img != null) {
Texture tex = new Texture();
tex.img = img;
m_textures.put(mtlfn, tex);
}
}
}
}

Model model;

void setup() {
size(640, 480, OPENGL);

model = new Model();
boolean r = model.load("sponza/Sponza.obj");
if (r) {
println("model load succeeded");
} else {
println("model load failed");
}
}

float rotx = 0;
float roty = 0;
float dist = 100;

void mouseDragged() {
switch (mouseButton) {
case LEFT:
float rate = 0.01;
rotx += (pmouseY - mouseY) * rate;
roty += (pmouseX - mouseX) * rate;
break;
case RIGHT:
dist += dist * (pmouseY - mouseY) * 4 / height;
break;
}
}

void draw() {
lights();
background(color(0, 0, 128));

float cameraY = height/2.0;
float fov = radians(60);
float cameraZ = cameraY / tan(fov / 2.0);
float aspect = float(width)/float(height);
perspective(fov, aspect, cameraZ/10.0, cameraZ*10.0);

translate(width/2, height/2, cameraZ - dist);
rotateX(PI);
rotateX(rotx);
rotateY(roty);

noStroke();
model.draw();
stroke(255, 0, 0); line(0,0,0, 100,0,0);
stroke(0, 255, 0); line(0,0,0, 0,100,0);
stroke(0, 0, 255); line(0,0,0, 0,0,100);
}
  • objファイルフォーマットはこのへん、でもあまり見てない
  • ProcessingのloadImage()はjpegやpngだけじゃなく、tgaやbmpも対応してる、すげぇ!
  • CrytekのSponzaのスクリーンショットでは青や緑の布もかかってるのに、データでは赤しかない、クソッ!
  • 真ん中の布が邪魔
  • UVが狂ってるのかクランプされてるのかよくわからない
  • ノーマルマップは試してない
  • .objファイルフォーマットは行単位で、先頭の文字列で区別できるとか、シンプルでいいな
    • 簡潔なファイルフォーマットは生き残るね
    • 階層構造やモーションにもこういう、テキスト形式でシンプルなファイルフォーマットがあるといいな
  • これでメタセコで作られたデータとか出せるんじゃ…と思ったら、フリー版では.objに出力できなかった、残念…
  • Processingは、テクスチャがない状態で vertex() でUVを設定するとエラーが出て止まってしまう、うっとおしい
  • しかしJava、というかGCのある言語は後始末書かなくていいのは滅茶苦茶楽だな