OpenGLによるCOLLADAなはちゅねの描画
前回の記事では、モダンなOpenGLでティーポットを描画するための基礎知識について整理し、ティーポットが描画できたよ、ということまで書きました。
実は、前回のティーポットの描画では、ジオメトリ部分だけを読み込んで描画しています。シンプルなモデルではこれでも上手くいくことがあるのですが、複雑なモデルでは、ちゃんとCOLLADAを読み解かないと正しく描画できません。
ということで、今回は、ちゃんとモデルの構造を考えて描画してみます。
目標
前回はティーポットの描画を目標にしていましたが、今回は「はちゅね」さんを召喚したいと思います。はちゅねモデルはズザ氏が作成し、rect氏がCOLLADAに変換したものを利用します。以下からダウンロードできます。
このはちゅねモデルですが、ファイルの中身を見てると、ジオメトリが複数があることが分かります。何も考えずにこれらのジオメトリを描画するのは、残念ながらうまくいきません。また、COLLADAには色などの材質(マテリアル)情報も含まれているので、これもちゃんと反映させたい所です。
COLLADAの主要部分
ここでは、COLLADAを描画するのに最低限抑えとけば良さそうな部分だけを取り上げます。以下のリストの階層はCOLLADAのXMLの階層とは関係ありません。
- scene:描画要素を包括する要素です。
- node:モデルの一部分を表す要素です。子ノードをもち、ネストすることもあります。また、モデルの位置情報(モデル変換行列)をもちます。この行列をちゃんと適用しないと正しい位置に描画することが出来ません。
- geometry node:ノードの一種で、恐らく末端のノードです。ジオメトリ情報とマテリアル情報を持ちます*1。
- geometry:頂点属性群やインデックスなどのデータを持つ要素です。
- source:頂点属性の生のデータを持つ要素です。
- triangles:ポリゴンの集合を表す要素で、インデックス情報を持ちます。ここのインデックスを元にsourceのデータを展開できます*2。
- material:マテリアル情報を持ちます。よく分かりませんが、材質に関する具体的な情報は、material要素内で参照されているeffect要素に入ってます。
- effect:シェーディング関係の情報が入ってます。色情報やテクスチャもシェーダーに渡すべき情報なので、ここに含まれるようです。ライティングの手法*3まで指定できるようです。
- asset:モデルの作者やコメントなど、一般的な情報が含まれる要素です。モデルの上方向を表すup_axisや、作者が想定している大きさを表すunitなんかは使えるケースが有るかもしれません。
描画方法
COLLADAの構造を踏まえると、すごく大雑把ですが、以下のような手順で処理を行えば描画できそうです。
1. 描画時に毎回行うのは無駄なので、ジオメトリ情報を前処理する。サクッとVBOを作ってしまえば良いかと思います。
2. 描画の手始めとして、scene要素を見る。sceneが持つノードを次に参照する。
3. ノードを辿る。辿る度に変形情報があればモデル行列に適用していく。pushとpopを忘れずに。
4. ジオメトリノードを見つけたら、geometryとmaterialを特定できるので、materialを適宜シェーダに送り、geometryをglDrawArraysする。
こんな感じで流れは見えるのですが、正直、コードに落としこむのは簡単ではないです。
参考になるかはわかりませんが、ライブラリっぽくしたので、コードをまるっと載せておきます。例のごとく、Python2.7でpycolladaでCOLLADAを読み込んでいます。mgluというのはまだ公開していないオレオレライブラリです。説明はしませんが、意味は感じとれるかと思います。
#!/usr/bin/env python2.7 # -*- coding: utf-8 -*- import numpy as np import collada import mglu class Collada(object): def __init__(self, file): self.mesh = collada.Collada(file) UP_AXIS = collada.asset.UP_AXIS @property def up(self): return self.mesh.assetInfo.upaxis @property def scenes(self): return self.mesh.scenes @property def geometries(self): return self.mesh.geometries @classmethod def extract(cls, vertices, indices): """ :params vertices: 頂点属性の配列 :type vertices: 2次元np.array :params indices: インデックス :type indices: 1次元np.array """ result = [None] * indices.size for i in xrange(indices.size): result[i] = vertices[indices[i]] assert not None in result return np.float32(result) def load_geometry(self, geometry): buffers = [] buf = {} for p in geometry.primitives: if p.normal is None: continue pos = self.extract(p.vertex, p.vertex_index.flatten()) buf["position"] = mglu.VBO(pos) nor = self.extract(p.normal, p.normal_index.flatten()) buf["normal"] = mglu.VBO(nor) buf["texcoord"] = [] for i in range(len(p.texcoordset)): tex = self.extract( p.texcoordset[i], p.texcoord_indexset[i].flatten()) buf["texcoord"].append(mglu.VBO(tex)) buffers.append(buf) return buffers class ColladaRenderer(object): def render_node(self, node): self.node_begin(node.matrix) for child in node.children: # 普通のノードなら辿る if type(child) is collada.scene.Node: for n in node.children: self.render_node(n) break # そうでなければ描画 geometry_node = child for material_node in geometry_node.materials: material = material_node.target effect = material.effect color_attributes = \ ["specular", "ambient", "diffuse", "emission"] for attr in color_attributes: color = effect.__getattribute__(attr) if color is None: break elif type(color) is tuple: renderer.__getattribute__("set_" + attr)(color) else: image = color.sampler.surface.image.pilimage renderer.set_texture(image) if effect.shininess: renderer.set_shininess(effect.shininess) # effect.reflective, effect.reflectivity # effect.transparent, effect.transparency self.render_geometry(geometry_node.geometry) self.node_end() def render_scene(self, scene): """オーバーライド不可 """ for node in scene.nodes: self.render_node(node) #----------------------------- def node_begin(m_matrix): pass def node_end(): pass def set_ambient(self, color): pass def set_specular(self, color): pass def set_diffuse(self, color): pass def set_emission(self, color): pass def set_texture(self, pilimage): pass def set_shininess(self, shininess): pass def render_geometry(self, geometry): pass
テンプレートメソッドパターン風にしました。set_*
でシェーダーに値を渡し、render_geometry
に描画コードを書きます。もっとマテリアルのセット系は増えると思います。また、マテリアル部分はかなり適当なので、間違ってる可能性大です。
ついでなので、殴り書きな使用例も載せちゃいます。mgluに関しては考えるより感じて下さい。色部分はまだバグ持ちなので、render_geometry
内で適当にセットしています。
#!/usr/bin/env python2.7 # -*- coding: utf-8 -*- import sys import numpy as np import pyglet from pyglet.gl import * import mglu import mglu.mesh class Camera(object): def __init__(self, position=[0, 0, 1], target=[0, 0, 0], up=[0, 1, 0], distance=10): self.position = np.float32(position) self.target = np.float32(target) self.up = np.float32(up) self.distance = distance def get_look_at_params(self): v = self.position * self.distance return np.concatenate((v, self.target, self.up)) def get_direction(self): v = self.position - self.target return v / np.linalg.norm(v) class HachuneRenderer(mglu.mesh.ColladaRenderer): def __init__(self): self.collada = mglu.mesh.Collada("mglu/data/negimiku/negimiku.dae") self.geometries = {} for g in self.collada.geometries: self.geometries[g.id] = self.collada.load_geometry(g) def prepare(self, shader, mvp): self.mvp = mvp self.shader = shader def render(self): self.mvp.m_push() self.mvp.m_scale(0.1, 0.1, 0.1) scene = self.collada.scenes[0] self.render_scene(scene) self.mvp.m_pop() def node_begin(self, m_matrix): self.mvp.m_push() self.mvp.m_matrix *= m_matrix def node_end(self): self.mvp.m_pop() def set_ambient(self, color): self.shader.uniform_nfv("u_ambientColor", np.float32(color)) def set_specular(self, color): self.shader.uniform_nfv("u_specularColor", np.float32(color)) def set_diffuse(self, color): self.shader.uniform_nfv("u_diffuseColor", np.float32(color)) def set_emission(self, color): pass def set_texture(self, pilimage): pass def set_shininess(self, shininess): self.shader.uniform_1f("u_shininess", shininess) def render_geometry(self, geometry): # 色 self.shader.uniform_1f("u_alpha", 1.0) self.set_ambient(np.float32([1, 1, 1])) self.set_specular(np.float32([0.5, 0.5, 0.5])) self.set_diffuse(np.float32([0.8, 0.8, 0.8])) self.set_shininess(100) # 変換行列 self.shader.uniform_matrix("u_vMatrix", self.mvp.get_v()) self.shader.uniform_matrix("u_mvMatrix", self.mvp.get_mv()) self.shader.uniform_matrix("u_mvpMatrix", self.mvp.get_mvp()) self.shader.uniform_matrix("u_normalMatrix", np.linalg.inv(self.mvp.get_mv()).T) # テクスチャ #glActiveTexture(GL_TEXTURE0) #self.teapot["texture"].bind() #self.shader.uniform_1i("u_texture", 0) self.shader.uniform_1i("u_useTexture", False) for buf in self.geometries[geometry.id]: self.shader.attribute("a_position", buf["position"]) self.shader.attribute("a_normal", buf["normal"]) self.shader.attribute("a_texcoord", buf["texcoord"][0]) glDrawArrays(GL_TRIANGLES, 0, buf["position"].get_elements_num()) class App(pyglet.window.Window): VERTEX_SHADER = """\ #version 120 attribute vec3 a_position; attribute vec3 a_normal; attribute vec2 a_texcoord; uniform mat4 u_vMatrix; uniform mat4 u_mvMatrix; uniform mat4 u_mvpMatrix; uniform mat4 u_normalMatrix; uniform vec3 u_lightPosition; varying vec3 v_position; varying vec3 v_normal; varying vec3 v_lightPosition; varying vec2 v_texcoord; void main(void) { v_texcoord = a_texcoord; v_lightPosition = (u_vMatrix * vec4(u_lightPosition, 1.0)).xyz; v_position = (u_mvMatrix * vec4(a_position, 1.0)).xyz; v_normal = (u_normalMatrix * vec4(a_normal, 1.0)).xyz; gl_Position = u_mvpMatrix * vec4(a_position, 1.0); } """ FRAGMENT_SHADER = """\ #version 120 // texture uniform bool u_useTexture; uniform sampler2D u_texture; // material uniform float u_alpha; uniform vec3 u_diffuseColor; uniform vec3 u_specularColor; uniform vec3 u_ambientColor; uniform float u_shininess; // light uniform vec3 u_lightAmbientColor; uniform vec3 u_lightDiffuseColor; uniform vec3 u_lightSpecularColor; uniform vec3 u_eyeDirection; // varying varying vec3 v_position; varying vec3 v_normal; varying vec3 v_lightPosition; varying vec2 v_texcoord; void main(void) { vec3 normal = normalize(v_normal); vec3 lightDirection = normalize(v_lightPosition - v_position); float diffuse = clamp(dot(normal, lightDirection), 0.0, 1.0); vec3 halfDirection = normalize(lightDirection + u_eyeDirection); float specular = pow( clamp(dot(normal, halfDirection), 0.0001, 1.0), u_shininess ); vec3 color = vec3(1.0); float alpha = u_alpha; if (u_useTexture) { vec4 texcolor = texture2D(u_texture, v_texcoord); color *= texcolor.rgb; alpha *= texcolor.a; } color *= u_lightAmbientColor * u_ambientColor + u_lightDiffuseColor * u_diffuseColor * diffuse + u_lightSpecularColor * u_specularColor * specular; gl_FragColor = vec4(color, alpha); } """ def __init__(self): super(self.__class__, self).__init__( resizable=True, visible=False) glEnable(GL_DEPTH_TEST) glDepthFunc(GL_LEQUAL) glClearColor(1.0, 1.0, 1.0, 1.0) glClearDepth(1.0) self.shader = mglu.Shader(self.VERTEX_SHADER, self.FRAGMENT_SHADER) self.mvp = mglu.ModelViewProjection() self.camera = Camera(distance=5) self.world = mglu.World() self.use_texture_flag = True self.init_resources() self.FPS = 15 self.frame_count = 0 #pyglet.clock.schedule_interval(self.__update, 1.0 / self.FPS) self.set_visible() def __update(self, dt): self.frame_count += 1 self.update(dt) def on_resize(self, width, height): glViewport(0, 0, width, height) self.mvp.p_perspective(45, float(width)/height, 0.01, 1000) def on_key_press(self, symbol, modifiers): key = pyglet.window.key # 終了 if symbol == key.ESCAPE: pyglet.app.exit() # 視点 factor = 0.5 dx = dy = 0 if symbol == key.UP: dy = 1 if symbol == key.DOWN: dy = -1 if symbol == key.RIGHT: dx = 1 if symbol == key.LEFT: dx = -1 self.camera.target[0] += dx * factor self.camera.target[1] += dy * factor # フラグ if symbol == key.A: self.use_texture_flag = not self.use_texture_flag def on_mouse_press(self, x, y, button, modifiers): if button & pyglet.window.mouse.LEFT: self.world.drag_begin(x, y) def on_mouse_release(self, x, y, button, modifiers): if button & pyglet.window.mouse.LEFT: self.world.drag_end() def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): if buttons & pyglet.window.mouse.LEFT: self.world.drag(x, y) def on_mouse_scroll(self, x, y, scroll_x, scroll_y): self.camera.distance = max(self.camera.distance - scroll_y, 0.1) def init_resources(self): self.hachune_renderer = HachuneRenderer() def on_draw(self): glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) self.mvp.v_identity() self.mvp.v_look_at(*self.camera.get_look_at_params()) self.mvp.m_identity() self.mvp.m_matrix *= self.world.matrix self.shader.use() # ライティング self.shader.uniform_nfv("u_lightPosition", np.float32([10, 10, 10])) self.shader.uniform_nfv("u_lightDiffuseColor", np.float32([1, 1, 1])) self.shader.uniform_nfv("u_lightSpecularColor", np.float32([1, 1, 1])) self.shader.uniform_nfv("u_lightAmbientColor", np.float32([0.2, 0.2, 0.2])) self.shader.uniform_nfv("u_eyeDirection", self.camera.get_direction()) # ティーポットの描画 self.hachune_renderer.prepare(self.shader, self.mvp) self.hachune_renderer.render() self.shader.stop() def update(self, dt): pass if __name__ == "__main__": App() pyglet.app.run() sys.exit(0)
現状、すごくポリゴンなはちゅねが表示できています。
とりあえず今回はここまでとします。次は、色とテクスチャを何とかしたい所です。