OpenGLでBlenderから書き出したファイルを読み込む
はじめに
この記事はシリーズ記事です。目次はこちら。
この記事ではBlenderからプログラム用のシーンファイルを書き出すアドオンを作成し、そのアドオンで書き出したシーンを読み込むところまで行ってみます。
Blenderアドオン
まずはシーンファイルを書き出すBlenderアドオンを作成します。
アドオンの作り方は次が参考になります。
アドオンのソースコード
まず最初にアドオン用にディレクトリを切って、__init__.py
を作成します。
<root>/
├── BlenderAddon/
│ └── ScenefileExporter/
│ └── __init__.py
├── OpenGL-PBR-Map/
├── vendors/
└── OpenGL-PBR-Map.sln
bl_info = {
"name": "scenefile exporter",
"author": "MatchaChoco010",
"version": (0, 0, 1),
"blender": (2, 82, 0),
"location": "File > Export > Scenefile (.scenefile)",
"description": "export scenefile!",
"warning": "",
"support": "TESTING",
"wiki_url": "",
"tracker_url": "",
"category": "Import-Export"
}
if "bpy" in locals():
import imp
imp.reload(scenefile_exporter)
else:
from . import scenefile_exporter
import bpy
def menu_fn(self, context):
self.layout.separator()
self.layout.operator(scenefile_exporter.ScenefileExporter.bl_idname)
def register():
bpy.utils.register_class(scenefile_exporter.ScenefileExporter)
bpy.types.TOPBAR_MT_file_export.append(menu_fn)
def unregister():
bpy.types.TOPBAR_MT_file_export.remove(menu_fn)
bpy.utils.unregister_class(scenefile_exporter.ScenefileExporter)
if __name__ == "__main__":
register()
次に__init__.py
で呼び出しているscenefile_exporter.py
を作成します。
import os
import shutil
import subprocess
import mathutils
import bmesh
import bpy
from bpy_extras.io_utils import ExportHelper
class ScenefileExporter(bpy.types.Operator, ExportHelper):
# "Scene" と名付けられたCollection内部のオブジェクトを
# scenefileとして書き出す
bl_idname = "export_scene.scenefile_exporter"
bl_label = "Scenefile Exporter"
bl_optioins = {}
filename_ext = ".scenefile"
@classmethod
def poll(cls, context):
return "Scene" in bpy.data.collections
def execute(self, context):
print(self.filepath)
if os.path.exists(self.filepath):
raise Exception("Already exists {0}".format(self.filepath))
os.makedirs(self.filepath)
scene_collection = bpy.data.collections['Scene']
meshes_dict = {}
materials_dict = {}
mesh_entities = []
directional_light = ""
point_lights = []
spot_lights = []
# Meshes
for obj in scene_collection.all_objects:
if not obj.type == "MESH":
continue
me = obj.data
mesh_name = me.name
# Add Mesh
if not mesh_name in meshes_dict:
me_copy = obj.to_mesh()
bm = bmesh.new()
bm.from_mesh(me_copy)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.to_mesh(me_copy)
bm.free()
me_copy.calc_normals_split()
mesh_vertices_text = ""
mesh_normals_text = ""
mesh_uvs_text = ""
for l in me_copy.loops:
co = me_copy.vertices[l.vertex_index].co
n = l.normal
uv = me_copy.uv_layers.active.data[l.index].uv
mesh_vertices_text += "v {0} {1} {2}\n" \
.format(co.x, co.y, co.z)
mesh_normals_text += "vn {0} {1} {2}\n" \
.format(n.x, n.y, n.z)
mesh_uvs_text += "vt {0} {1}\n"\
.format(uv.x, uv.y)
mesh_text = "Mesh: " + mesh_name + "\n" +\
mesh_vertices_text + mesh_normals_text + mesh_uvs_text +\
"MeshEnd\n"
meshes_dict[mesh_name] = mesh_text
del bm, me_copy, mesh_vertices_text, mesh_normals_text, mesh_uvs_text, mesh_text
material = obj.material_slots[0].material
material_name = material.name
# Add Material
if not material_name in materials_dict:
os.makedirs(
os.path.join(self.filepath, "Materials", material_name))
# Material Output-Surface
principledBSDF = material.node_tree.nodes[0].inputs[0].links[0].from_node
# PrincipledBSDF-BaseColor
if len(principledBSDF.inputs[0].links) == 0:
raise Exception(
"{0} - BaseColor Texture Node is missing.".format(material_name))
# PrincipledBSDF-Metallic
elif len(principledBSDF.inputs[4].links) == 0:
raise Exception(
"{0} - Metallic Texture Node is missing".format(material_name))
# PrincipledBSDF-Roughness
elif len(principledBSDF.inputs[7].links) == 0:
raise Exception(
"{0} - Roughness Texture Node is missing".format(material_name))
# PrincipledBSDF-Emission
elif len(principledBSDF.inputs[17].links) == 0:
raise Exception(
"{0} - Emission Node Node is missing".format(material_name))
elif len(principledBSDF.inputs[17].links[0].from_node.inputs[0].links) == 0:
raise Exception(
"{0} - Emission Texture Node is missing".format(material_name))
# PrincipledBSDF-Normal
elif len(principledBSDF.inputs[19].links) == 0:
raise Exception(
"{0} - Normal Node is missing".format(material_name))
elif len(principledBSDF.inputs[19].links[0].from_node.inputs[1].links) == 0:
raise Exception(
"{0} - Normal Texture Node is missing".format(material_name))
baseColorImageNode = principledBSDF.inputs[0].links[0].from_node
metallicImageNode = principledBSDF.inputs[4].links[0].from_node
roughnessImageNode = principledBSDF.inputs[7].links[0].from_node
emissionMultiplyNode = principledBSDF.inputs[17].links[0].from_node
emissionImageNode = emissionMultiplyNode.inputs[0].links[0].from_node
normalMapImageNode = principledBSDF.inputs[19].links[0].\
from_node.inputs[1].links[0].from_node
if baseColorImageNode.image == None:
raise Exception(
"{0} - BaseColor Texture is None".format(material_name))
elif metallicImageNode.image == None:
raise Exception(
"{0} - Metallic Texture is None".format(material_name))
elif roughnessImageNode.image == None:
raise Exception(
"{0} - Roughness Texture is None".format(material_name))
elif emissionImageNode.image == None:
raise Exception(
"{0} - Emission Texture is None".format(material_name))
elif normalMapImageNode.image == None:
raise Exception(
"{0} - NormalMap Texture is None".format(material_name))
baseColor_ext = os.path.splitext(
baseColorImageNode.image.filepath)[1]
shutil.copy2(
bpy.path.abspath(baseColorImageNode.image.filepath),
os.path.join(self.filepath, "Materials", material_name,
"baseColor" + baseColor_ext)
)
metallic_ext = os.path.splitext(
metallicImageNode.image.filepath)[1]
shutil.copy2(
bpy.path.abspath(metallicImageNode.image.filepath),
os.path.join(self.filepath, "Materials", material_name,
"metallic" + metallic_ext)
)
roughness_ext = os.path.splitext(
roughnessImageNode.image.filepath)[1]
shutil.copy2(
bpy.path.abspath(roughnessImageNode.image.filepath),
os.path.join(self.filepath, "Materials", material_name,
"roughness" + roughness_ext)
)
emission_ext = os.path.splitext(
emissionImageNode.image.filepath)[1]
shutil.copy2(
bpy.path.abspath(emissionImageNode.image.filepath),
os.path.join(self.filepath, "Materials", material_name,
"emission" + emission_ext)
)
normalMap_ext = os.path.splitext(
normalMapImageNode.image.filepath)[1]
shutil.copy2(
bpy.path.abspath(normalMapImageNode.image.filepath),
os.path.join(self.filepath, "Materials", material_name,
"normalMap" + normalMap_ext)
)
material_text = "Material: {0}\n".format(material_name)
material_text += "baseColor: Materials/{0}/baseColor{1}\n"\
.format(material_name, baseColor_ext)
material_text += "metallic: Materials/{0}/metallic{1}\n"\
.format(material_name, metallic_ext)
material_text += "roughness: Materials/{0}/roughness{1}\n"\
.format(material_name, roughness_ext)
material_text += "emission: Materials/{0}/emission{1}\n"\
.format(material_name, emission_ext)
material_text += "normalMap: Materials/{0}/normalMap{1}\n"\
.format(material_name, normalMap_ext)
material_text += "emissionIntensity: {0}\n".format(
emissionMultiplyNode.inputs[1].links[0].from_node.outputs[0].default_value * 683.002)
material_text += "MaterialEnd\n"
materials_dict[material_name] = material_text
pos = obj.matrix_world.to_translation()
rot = obj.matrix_world.to_euler('YXZ')
scl = obj.matrix_world.to_scale()
entity_text = "MeshEntity: {0}\n".format(obj.name)
entity_text += "Mesh: {0}\n".format(mesh_name)
entity_text += "Material: {0}\n".format(material_name)
entity_text += "Position: {0} {1} {2}\n".format(
pos.x, pos.y, pos.z)
entity_text += "Rotation: {0} {1} {2} {3}\n".format(
rot.x, rot.y, rot.z, rot.order)
entity_text += "Scale: {0} {1} {2}\n".format(scl.x, scl.y, scl.z)
entity_text += "MeshEntityEnd\n"
mesh_entities.append(entity_text)
# Lights
for obj in scene_collection.all_objects:
if not obj.type == "LIGHT":
continue
light = obj.data
if light.type == "POINT":
pos = obj.matrix_world.to_translation()
# W to lumen at wavelength = 555nm (green)
intensity = light.energy * 683.002
color = light.color
range = light.cutoff_distance
clip_start = light.shadow_buffer_clip_start
shadow_bias = light.shadow_buffer_bias
use_shadow = int(light.use_shadow)
point_light_text = "PointLight: {0}\n".format(obj.name)
point_light_text += "Position: {0} {1} {2}\n".format(
pos.x, pos.y, pos.z)
point_light_text += "Intensity: {0}\n".format(intensity)
point_light_text += "Color: {0} {1} {2}\n".format(
color.r, color.g, color.b)
point_light_text += "Range: {0}\n".format(range)
point_light_text += "ClipStart: {0}\n".format(clip_start)
point_light_text += "ShadowBias: {0}\n".format(shadow_bias)
point_light_text += "UseShadow: {0}\n".format(use_shadow)
point_light_text += "PointLightEnd\n"
point_lights.append(point_light_text)
if light.type == "SUN":
pos = obj.matrix_world.to_translation()
rot = obj.matrix_world.to_euler('YXZ')
# W/m2 to lumen/m2 at wavelength = 555nm (green)
intensity = light.energy * 683.002
color = light.color
dir = mathutils.Vector((0.0, 0.0, -1.0))
dir.rotate(rot)
directional_light += "DirectionalLight: {0}\n".format(obj.name)
directional_light += "Position: {0} {1} {2}\n".format(
pos.x, pos.y, pos.z)
directional_light += "Intensity: {0}\n".format(intensity)
directional_light += "Color: {0} {1} {2}\n".format(
color.r, color.g, color.b)
directional_light += "Direction: {0} {1} {2}\n".format(
dir.x, dir.y, dir.z)
directional_light += "DirectionalLightEnd\n"
if light.type == "SPOT":
pos = obj.matrix_world.to_translation()
rot = obj.matrix_world.to_euler('YXZ')
# W to lumen at wavelength = 555nm (green)
intensity = light.energy * 683.002
color = light.color
range = light.cutoff_distance
clip_start = light.shadow_buffer_clip_start
dir = mathutils.Vector((0.0, 0.0, -1.0))
dir.rotate(rot)
angle = light.spot_size
blend = light.spot_blend
spot_light_text = "SpotLight: {0}\n".format(obj.name)
spot_light_text += "Position: {0} {1} {2}\n".format(
pos.x, pos.y, pos.z)
spot_light_text += "Intensity: {0}\n".format(intensity)
spot_light_text += "Color: {0} {1} {2}\n".format(
color.r, color.g, color.b)
spot_light_text += "Direction: {0} {1} {2}\n".format(
dir.x, dir.y, dir.z)
spot_light_text += "Range: {0}\n".format(range)
spot_light_text += "ClipStart: {0}\n".format(clip_start)
spot_light_text += "Angle: {0}\n".format(angle)
spot_light_text += "Blend: {0}\n".format(blend)
spot_light_text += "SpotLightEnd\n"
spot_lights.append(spot_light_text)
scene_text = "# Scene file\n"
for mesh_text in meshes_dict.values():
scene_text += mesh_text
for material_text in materials_dict.values():
scene_text += material_text
for mesh_entity in mesh_entities:
scene_text += mesh_entity
scene_text += directional_light
for point_light in point_lights:
scene_text += point_light
for spot_light in spot_lights:
scene_text += spot_light
with open(os.path.join(self.filepath, "scenefile.txt"), mode="w") as f:
f.write(scene_text)
return {"FINISHED"}
メインの処理はexecute
メソッドです。
最初に与えられたファイルパスのディレクトリを作成しています。 ディレクトリが既に存在する場合はエラーを吐きます。
def execute(self, context):
print(self.filepath)
if os.path.exists(self.filepath):
raise Exception("Already exists {0}".format(self.filepath))
os.makedirs(self.filepath)
次にScene
という名前のコレクションを取得しています。
このアドオンではシーン全体ではなく、一部を出力するためにScene
というコレクションの内部だけを出力します。
scene_collection = bpy.data.collections['Scene']
次はオブジェクト一時保持用の配列または辞書または文字列を用意しています。
meshes_dict = {}
materials_dict = {}
mesh_entities = []
directional_light = ""
point_lights = []
spot_lights = []
次にScene
内部のオブジェクト全体に対してループを回してMESH
ではないものについてはスキップします。
# Meshes
for obj in scene_collection.all_objects:
if not obj.type == "MESH":
continue
このobj
というのはこれまで作ってきたプログラムのMeshEntity
に対応します。
つまりMesh
とMaterial
を保持し、モデル行列などを保持します。
最初にこのMeshEntityのMeshを保存します。 Meshのデータと名前を取得します。 この名前はBlender側で一意の名前を振ってくれるのでキーとして使えます。
me = obj.data
mesh_name = me.name
このメッシュの名前をキーとして辞書に登録されているかを確認します。 辞書に登録されている場合はMeshEntityが以前のMeshEntityとMeshを共有していることになるので、Mesh自体の読み込みはスキップします。
最初にメッシュをコピーして三角形面化しています。 その後法線を計算し直します。
# Add Mesh
if not mesh_name in meshes_dict:
me_copy = obj.to_mesh()
bm = bmesh.new()
bm.from_mesh(me_copy)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.to_mesh(me_copy)
bm.free()
me_copy.calc_normals_split()
保存用のテキストを用意します。
mesh_vertices_text = ""
mesh_normals_text = ""
mesh_uvs_text = ""
コピーした三角形面化したメッシュのループを回して、頂点位置と法線とUV座標を文字列に追記していきます。
for l in me_copy.loops:
co = me_copy.vertices[l.vertex_index].co
n = l.normal
uv = me_copy.uv_layers.active.data[l.index].uv
mesh_vertices_text += "v {0} {1} {2}\n" \
.format(co.x, co.y, co.z)
mesh_normals_text += "vn {0} {1} {2}\n" \
.format(n.x, n.y, n.z)
mesh_uvs_text += "vt {0} {1}\n"\
.format(uv.x, uv.y)
Mesh保存用の文字列を作成し辞書に登録します。
mesh_text = "Mesh: " + mesh_name + "\n" +\
mesh_vertices_text + mesh_normals_text + mesh_uvs_text +\
"MeshEnd\n"
meshes_dict[mesh_name] = mesh_text
必要なくなった変数を削除します。
del bm, me_copy, mesh_vertices_text, mesh_normals_text, mesh_uvs_text, mesh_text
次にMeshEntityのMaterialを保存します。
objのマテリアルスロットの最初のマテリアルを扱います。 マテリアルが存在しない場合はエラーです。 マテリアルの名前を取得します。 このnameはBlenderが一意の名前を振ってくれるのでキーとして使えます。
material = obj.material_slots[0].material
material_name = material.name
このマテリアルの名前が辞書に登録されているかを確認します。 登録されている場合は以前のMeshEntityとMaterialを共有していることになりますのでスキップします。
# Add Material
if not material_name in materials_dict:
os.makedirs(
os.path.join(self.filepath, "Materials", material_name))
シーンファイルのディレクトリの中にMaterials
というディレクトリを切っています。
このディレクトリの中にテクスチャをコピーしていきます。
マテリアルが後に説明するノードの構造になっていることを仮定してテクスチャパスを特定し、コピーしています。
# Material Output-Surface
principledBSDF = material.node_tree.nodes[0].inputs[0].links[0].from_node
# PrincipledBSDF-BaseColor
if len(principledBSDF.inputs[0].links) == 0:
raise Exception(
"{0} - BaseColor Texture Node is missing.".format(material_name))
# PrincipledBSDF-Metallic
elif len(principledBSDF.inputs[4].links) == 0:
raise Exception(
"{0} - Metallic Texture Node is missing".format(material_name))
# PrincipledBSDF-Roughness
elif len(principledBSDF.inputs[7].links) == 0:
raise Exception(
"{0} - Roughness Texture Node is missing".format(material_name))
# PrincipledBSDF-Emission
elif len(principledBSDF.inputs[17].links) == 0:
raise Exception(
"{0} - Emission Node Node is missing".format(material_name))
elif len(principledBSDF.inputs[17].links[0].from_node.inputs[0].links) == 0:
raise Exception(
"{0} - Emission Texture Node is missing".format(material_name))
# PrincipledBSDF-Normal
elif len(principledBSDF.inputs[19].links) == 0:
raise Exception(
"{0} - Normal Node is missing".format(material_name))
elif len(principledBSDF.inputs[19].links[0].from_node.inputs[1].links) == 0:
raise Exception(
"{0} - Normal Texture Node is missing".format(material_name))
baseColorImageNode = principledBSDF.inputs[0].links[0].from_node
metallicImageNode = principledBSDF.inputs[4].links[0].from_node
roughnessImageNode = principledBSDF.inputs[7].links[0].from_node
emissionMultiplyNode = principledBSDF.inputs[17].links[0].from_node
emissionImageNode = emissionMultiplyNode.inputs[0].links[0].from_node
normalMapImageNode = principledBSDF.inputs[19].links[0].\
from_node.inputs[1].links[0].from_node
if baseColorImageNode.image == None:
raise Exception(
"{0} - BaseColor Texture is None".format(material_name))
elif metallicImageNode.image == None:
raise Exception(
"{0} - Metallic Texture is None".format(material_name))
elif roughnessImageNode.image == None:
raise Exception(
"{0} - Roughness Texture is None".format(material_name))
elif emissionImageNode.image == None:
raise Exception(
"{0} - Emission Texture is None".format(material_name))
elif normalMapImageNode.image == None:
raise Exception(
"{0} - NormalMap Texture is None".format(material_name))
baseColor_ext = os.path.splitext(
baseColorImageNode.image.filepath)[1]
shutil.copy2(
bpy.path.abspath(baseColorImageNode.image.filepath),
os.path.join(self.filepath, "Materials", material_name,
"baseColor" + baseColor_ext)
)
metallic_ext = os.path.splitext(
metallicImageNode.image.filepath)[1]
shutil.copy2(
bpy.path.abspath(metallicImageNode.image.filepath),
os.path.join(self.filepath, "Materials", material_name,
"metallic" + metallic_ext)
)
roughness_ext = os.path.splitext(
roughnessImageNode.image.filepath)[1]
shutil.copy2(
bpy.path.abspath(roughnessImageNode.image.filepath),
os.path.join(self.filepath, "Materials", material_name,
"roughness" + roughness_ext)
)
emission_ext = os.path.splitext(
emissionImageNode.image.filepath)[1]
shutil.copy2(
bpy.path.abspath(emissionImageNode.image.filepath),
os.path.join(self.filepath, "Materials", material_name,
"emission" + emission_ext)
)
normalMap_ext = os.path.splitext(
normalMapImageNode.image.filepath)[1]
shutil.copy2(
bpy.path.abspath(normalMapImageNode.image.filepath),
os.path.join(self.filepath, "Materials", material_name,
"normalMap" + normalMap_ext)
)
マテリアルの情報をテキストに格納し、辞書に登録します。
material_text = "Material: {0}\n".format(material_name)
material_text += "baseColor: Materials/{0}/baseColor{1}\n"\
.format(material_name, baseColor_ext)
material_text += "metallic: Materials/{0}/metallic{1}\n"\
.format(material_name, metallic_ext)
material_text += "roughness: Materials/{0}/roughness{1}\n"\
.format(material_name, roughness_ext)
material_text += "emission: Materials/{0}/emission{1}\n"\
.format(material_name, emission_ext)
material_text += "normalMap: Materials/{0}/normalMap{1}\n"\
.format(material_name, normalMap_ext)
material_text += "emissionIntensity: {0}\n".format(
emissionMultiplyNode.inputs[1].links[0].from_node.outputs[0].default_value * 683.002)
material_text += "MaterialEnd\n"
materials_dict[material_name] = material_text
Blenderでは光の単位に測光量ではなく物理量を使っています。 作成するプログラムでは測光量を使うため683.002をかけて変換しています。 この変換は一番人間が敏感に感じる波長555nmの緑色の光をベースにしています。
次にMeshEntityの情報を格納していきます。
pos = obj.matrix_world.to_translation()
rot = obj.matrix_world.to_euler('YXZ')
scl = obj.matrix_world.to_scale()
entity_text = "MeshEntity: {0}\n".format(obj.name)
entity_text += "Mesh: {0}\n".format(mesh_name)
entity_text += "Material: {0}\n".format(material_name)
entity_text += "Position: {0} {1} {2}\n".format(
pos.x, pos.y, pos.z)
entity_text += "Rotation: {0} {1} {2} {3}\n".format(
rot.x, rot.y, rot.z, rot.order)
entity_text += "Scale: {0} {1} {2}\n".format(scl.x, scl.y, scl.z)
entity_text += "MeshEntityEnd\n"
mesh_entities.append(entity_text)
平行移動成分、回転のオイラー角、スケールを保存し、メッシュとマテリアルの名前を保存しています。
次にライトの情報を保存していきます。
Sceneコレクション内部のLIGHTタイプのオブジェクトについてループを回していきます。
# Lights
for obj in scene_collection.all_objects:
if not obj.type == "LIGHT":
continue
light = obj.data
最初にポイントライトの場合。
if light.type == "POINT":
pos = obj.matrix_world.to_translation()
# W to lumen at wavelength = 555nm (green)
intensity = light.energy * 683.002
color = light.color
range = light.cutoff_distance
clip_start = light.shadow_buffer_clip_start
shadow_bias = light.shadow_buffer_bias
use_shadow = int(light.use_shadow)
point_light_text = "PointLight: {0}\n".format(obj.name)
point_light_text += "Position: {0} {1} {2}\n".format(
pos.x, pos.y, pos.z)
point_light_text += "Intensity: {0}\n".format(intensity)
point_light_text += "Color: {0} {1} {2}\n".format(
color.r, color.g, color.b)
point_light_text += "Range: {0}\n".format(range)
point_light_text += "ClipStart: {0}\n".format(clip_start)
point_light_text += "ShadowBias: {0}\n".format(shadow_bias)
point_light_text += "UseShadow: {0}\n".format(use_shadow)
point_light_text += "PointLightEnd\n"
point_lights.append(point_light_text)
光のエネルギーをW(ワット)からlm(ルーメン)に変換しています。 後に使う影の情報も格納しています。 特にポイントライトはシーン上に大量に配置することになりパフォーマンスが心配なので、必要ない場合には影の計算をオフにできるようにしています。
これらのプロパティの数値はBlenderのプロパティで指定する数値と対応しています。
次にDirectional Lightの場合です。
if light.type == "SUN":
pos = obj.matrix_world.to_translation()
rot = obj.matrix_world.to_euler('YXZ')
# W/m2 to lumen/m2 at wavelength = 555nm (green)
intensity = light.energy * 683.002
color = light.color
dir = mathutils.Vector((0.0, 0.0, -1.0))
dir.rotate(rot)
directional_light += "DirectionalLight: {0}\n".format(obj.name)
directional_light += "Position: {0} {1} {2}\n".format(
pos.x, pos.y, pos.z)
directional_light += "Intensity: {0}\n".format(intensity)
directional_light += "Color: {0} {1} {2}\n".format(
color.r, color.g, color.b)
directional_light += "Direction: {0} {1} {2}\n".format(
dir.x, dir.y, dir.z)
directional_light += "DirectionalLightEnd\n"
ディレクショナルライトはシーンに一つとしているので、配列ではなく文字列に情報をそのまま格納しています。
次にスポットライトの場合です。
if light.type == "SPOT":
pos = obj.matrix_world.to_translation()
rot = obj.matrix_world.to_euler('YXZ')
# W to lumen at wavelength = 555nm (green)
intensity = light.energy * 683.002
color = light.color
range = light.cutoff_distance
clip_start = light.shadow_buffer_clip_start
dir = mathutils.Vector((0.0, 0.0, -1.0))
dir.rotate(rot)
angle = light.spot_size
blend = light.spot_blend
spot_light_text = "SpotLight: {0}\n".format(obj.name)
spot_light_text += "Position: {0} {1} {2}\n".format(
pos.x, pos.y, pos.z)
spot_light_text += "Intensity: {0}\n".format(intensity)
spot_light_text += "Color: {0} {1} {2}\n".format(
color.r, color.g, color.b)
spot_light_text += "Direction: {0} {1} {2}\n".format(
dir.x, dir.y, dir.z)
spot_light_text += "Range: {0}\n".format(range)
spot_light_text += "ClipStart: {0}\n".format(clip_start)
spot_light_text += "Angle: {0}\n".format(angle)
spot_light_text += "Blend: {0}\n".format(blend)
spot_light_text += "SpotLightEnd\n"
spot_lights.append(spot_light_text)
最後に、これらのMeshとMaterialとMeshEntityとLightの情報をscenefile.txtというテキストに書き出します。
scene_text = "# Scene file\n"
for mesh_text in meshes_dict.values():
scene_text += mesh_text
for material_text in materials_dict.values():
scene_text += material_text
for mesh_entity in mesh_entities:
scene_text += mesh_entity
scene_text += directional_light
for point_light in point_lights:
scene_text += point_light
for spot_light in spot_lights:
scene_text += spot_light
with open(os.path.join(self.filepath, "scenefile.txt"), mode="w") as f:
f.write(scene_text)
return {"FINISHED"}
アドオンのインストール
アドオンをzipに固めます。
固めたzipファイルをインストールし有効化します。
アドオンの使い方
まず、Scene
というコレクションをシーンに作成します。
このコレクション内部にあるものが出力されます。
メッシュとポイントライト、スポットライト、ディレクショナルライトを好きに配置します。 カメラの情報は、プログラムで自由に動かすことを想定してシーンファイルには書き出しません。
Shift-Dの複製ではなく、Alt-Dのリンク複製を使うことでMeshEntityでMeshを共有することができるため、ファイルサイズを削減できます。
Shift-Dで複製をした場合は次の通り。
CubeとCube.001という違う名前のMeshが割り当てられていることが確認できます。
Alt-Dで複製をした場合は次の通り。
CubeとCube.001のオブジェクトでCubeというメッシュを共有していることがわかります。
メッシュのマテリアルは次のようなノード構成である必要があります。 アドオンではノード構成決め打ちでテクスチャのノードを指定してテクスチャをコピーしているため、ノードの構造が変わったり、テクスチャが欠けたりしてはうまく動きません。
エクスポートするにはFileメニューからScenefile Exporterを選択をします。
出力するディレクトリがすでに存在する場合はエラーになるので、再出力する場合は一旦削除してから出力してください。
試しに次のようなファイルをSceneFile.scenefile
という名前で書き出してみると次のようになります。
SceneFile.scenefile/
├── Materials/
│ ├── Material/
│ │ ├── baseColor.png
│ │ ├── metallic.png
│ │ ├── roughness.png
│ │ ├── normalMap.png
│ │ └── emission.png
│ └── Material.001/
│ ├── baseColor.png
│ ├── metallic.png
│ ├── roughness.png
│ ├── normalMap.png
│ └── emission.png
└── Sceene.txt
テクスチャは全部Blenderのマテリアルからコピーされたものです。
Sceene.txt
の中身は次のとおりです。
# Scene file
Mesh: Cube
v 1.0 -1.0 -1.0
v -0.9999996423721313 1.0000003576278687 -1.0
v 1.0 0.9999999403953552 -1.0
v -0.9999999403953552 1.0 1.0
v 0.9999993443489075 -1.0000005960464478 1.0
v 1.0000004768371582 0.999999463558197 1.0
v 1.0000004768371582 0.999999463558197 1.0
v 1.0 -1.0 -1.0
v 1.0 0.9999999403953552 -1.0
v 0.9999993443489075 -1.0000005960464478 1.0
v -1.0000001192092896 -0.9999998211860657 -1.0
v 1.0 -1.0 -1.0
v -1.0000001192092896 -0.9999998211860657 -1.0
v -0.9999999403953552 1.0 1.0
v -0.9999996423721313 1.0000003576278687 -1.0
v 1.0 0.9999999403953552 -1.0
v -0.9999999403953552 1.0 1.0
v 1.0000004768371582 0.999999463558197 1.0
v 1.0 -1.0 -1.0
v -1.0000001192092896 -0.9999998211860657 -1.0
v -0.9999996423721313 1.0000003576278687 -1.0
v -0.9999999403953552 1.0 1.0
v -1.0000003576278687 -0.9999996423721313 1.0
v 0.9999993443489075 -1.0000005960464478 1.0
v 1.0000004768371582 0.999999463558197 1.0
v 0.9999993443489075 -1.0000005960464478 1.0
v 1.0 -1.0 -1.0
v 0.9999993443489075 -1.0000005960464478 1.0
v -1.0000003576278687 -0.9999996423721313 1.0
v -1.0000001192092896 -0.9999998211860657 -1.0
v -1.0000001192092896 -0.9999998211860657 -1.0
v -1.0000003576278687 -0.9999996423721313 1.0
v -0.9999999403953552 1.0 1.0
v 1.0 0.9999999403953552 -1.0
v -0.9999996423721313 1.0000003576278687 -1.0
v -0.9999999403953552 1.0 1.0
vn 2.980232949312267e-08 0.0 -1.0
vn 2.980232949312267e-08 0.0 -1.0
vn 2.980232949312267e-08 0.0 -1.0
vn 0.0 0.0 1.0
vn 0.0 0.0 1.0
vn 0.0 0.0 1.0
vn 1.0 0.0 -2.384185791015625e-07
vn 1.0 0.0 -2.384185791015625e-07
vn 1.0 0.0 -2.384185791015625e-07
vn -8.940696716308594e-08 -1.0 -4.76837158203125e-07
vn -8.940696716308594e-08 -1.0 -4.76837158203125e-07
vn -8.940696716308594e-08 -1.0 -4.76837158203125e-07
vn -1.0 2.3841855067985307e-07 -1.4901156930591242e-07
vn -1.0 2.3841855067985307e-07 -1.4901156930591242e-07
vn -1.0 2.3841855067985307e-07 -1.4901156930591242e-07
vn 2.6822084464583895e-07 1.0 2.3841852225814364e-07
vn 2.6822084464583895e-07 1.0 2.3841852225814364e-07
vn 2.6822084464583895e-07 1.0 2.3841852225814364e-07
vn 0.0 0.0 -1.0
vn 0.0 0.0 -1.0
vn 0.0 0.0 -1.0
vn 5.96046660916727e-08 0.0 1.0
vn 5.96046660916727e-08 0.0 1.0
vn 5.96046660916727e-08 0.0 1.0
vn 1.0 -5.960464477539062e-07 3.2782537573439186e-07
vn 1.0 -5.960464477539062e-07 3.2782537573439186e-07
vn 1.0 -5.960464477539062e-07 3.2782537573439186e-07
vn -4.768372150465439e-07 -1.0 1.1920927533992653e-07
vn -4.768372150465439e-07 -1.0 1.1920927533992653e-07
vn -4.768372150465439e-07 -1.0 1.1920927533992653e-07
vn -1.0 2.3841863594498136e-07 -1.1920931797249068e-07
vn -1.0 2.3841863594498136e-07 -1.1920931797249068e-07
vn -1.0 2.3841863594498136e-07 -1.1920931797249068e-07
vn 2.0861631355728605e-07 1.0 8.940701690107744e-08
vn 2.0861631355728605e-07 1.0 8.940701690107744e-08
vn 2.0861631355728605e-07 1.0 8.940701690107744e-08
vt 0.6515151262283325 0.6818181872367859
vt 0.3484848439693451 0.9848484396934509
vt 0.3484848439693451 0.6818182468414307
vt 0.9848484992980957 0.3181818425655365
vt 0.6818182468414307 0.01515158824622631
vt 0.9848484992980957 0.015151516534388065
vt 0.01515160035341978 0.3181818127632141
vt 0.3181818127632141 0.015151533298194408
vt 0.3181819021701813 0.3181817829608917
vt 0.34848493337631226 0.01515163667500019
vt 0.6515152454376221 0.31818175315856934
vt 0.34848496317863464 0.3181819021701813
vt 0.3181818127632141 0.6515151262283325
vt 0.01515151560306549 0.34848493337631226
vt 0.3181818127632141 0.3484848439693451
vt 0.3181817829608917 0.6818181872367859
vt 0.015151542611420155 0.9848484992980957
vt 0.01515151560306549 0.6818181872367859
vt 0.6515151262283325 0.6818181872367859
vt 0.6515151262283325 0.9848484396934509
vt 0.3484848439693451 0.9848484396934509
vt 0.9848484992980957 0.3181818425655365
vt 0.6818182468414307 0.3181817829608917
vt 0.6818182468414307 0.01515158824622631
vt 0.01515160035341978 0.3181818127632141
vt 0.01515151932835579 0.01515151560306549
vt 0.3181818127632141 0.015151533298194408
vt 0.34848493337631226 0.01515163667500019
vt 0.6515151858329773 0.015151518397033215
vt 0.6515152454376221 0.31818175315856934
vt 0.3181818127632141 0.6515151262283325
vt 0.0151515519246459 0.6515151262283325
vt 0.01515151560306549 0.34848493337631226
vt 0.3181817829608917 0.6818181872367859
vt 0.3181818127632141 0.9848483800888062
vt 0.015151542611420155 0.9848484992980957
MeshEnd
Material: Material
baseColor: Materials/Material/baseColor.png
metallic: Materials/Material/metallic.png
roughness: Materials/Material/roughness.png
emission: Materials/Material/emission.png
normalMap: Materials/Material/normalMap.png
emissionIntensity: 6830.0199999999995
MaterialEnd
Material: Material.001
baseColor: Materials/Material.001/baseColor.png
metallic: Materials/Material.001/metallic.png
roughness: Materials/Material.001/roughness.png
emission: Materials/Material.001/emission.png
normalMap: Materials/Material.001/normalMap.png
emissionIntensity: 6830.0199999999995
MaterialEnd
MeshEntity: Cube
Mesh: Cube
Material: Material
Position: 0.0 0.0 0.0
Rotation: 0.0 -0.0 0.0 YXZ
Scale: 1.0 1.0 1.0
MeshEntityEnd
MeshEntity: Cube.001
Mesh: Cube
Material: Material.001
Position: -0.2720106542110443 3.2740299701690674 -0.08419174700975418
Rotation: 0.0 -0.0 0.0 YXZ
Scale: 1.0 1.0 1.0
MeshEntityEnd
PointLight: Lamp
Position: 3.284986734390259 0.3339628577232361 3.482931613922119
Intensity: 409801.19999999995
Color: 1.0 1.0 1.0
Range: 10.0
ClipStart: 0.10000000149011612
ShadowBias: 1.0
UseShadow: 1
PointLightEnd
最初にMeshの頂点属性情報が書き込まれています。 頂点は先頭から3つずつ一つの面です。 正6面体なので三角形面12面分の情報が書き込まれています。
その後、Materialの情報、MeshEntityの情報、ライトの情報と続きます。 MeshEntityは2つですがメッシュの情報は共有しています。 マテリアルの情報は共有していません。
今回のサンプルプログラムで読み込むシーンファイル
次のようなシーンファイルを作成しました。
このシーンをSceneFile.scenefile
という名前で書き出しておきます。
前回のプログラムからの変更点
書き出したシーンを読み込むプログラムを作成します。
前回のプログラムからの変更点を次に示します。
Sceneクラスの変更
SceneクラスにSceneFileを読み込む機能をもたせます。
まずはヘッダファイルを追加します。
#include <glm/glm.hpp>
#include <memory>
#include <unordered_map>#include <vector>
次にシーンを読み込む静的メンバ関数を用意します。
/**
* @brief 与えられたパスのシーンを読み込む
* @param path シーンディレクトリのパス
* @param width 画面の横幅
* @param height 画面の縦
* @return ロードされたシーン
*
* シーンディレクトリのパスは最後に`/`を含みません。
*/
static std::unique_ptr<Scene> LoadScene(const std::string path,
const GLuint width,
const GLuint height);
補助用の関数も用意します。
private:
/**
* @brief 文字列を与えられた文字で分割する
* @param s 分割する文字列
* @param delim 分割に使う文字
* @return 分割された文字列のvector
*/
static std::vector<std::string> SplitString(const std::string& s, char delim);
/**
* @brief Meshをパースする
* @param mesh_texts シーンファイルの1つのMesh部分の行ごとに分割された文字列
* @return パースされたメッシュ
*/
static std::shared_ptr<Mesh> ParseMesh(
const std::vector<std::string>& mesh_texts);
/**
* @brief Materialをパースし読み込む
* @path シーンディレクトリのパス
* @param material_texts
* シーンファイルの一つのMaterialの行ごとに分割された文字列
* @return パースされて読み込まれたMaterial
*/
static std::shared_ptr<Material> ParseMaterial(
const std::string& path, const std::vector<std::string>& material_texts);
実装は次のとおりです。
std::unique_ptr<Scene> Scene::LoadScene(const std::string path,
const GLuint width,
const GLuint height) {
auto scene = std::make_unique<Scene>();
auto scenefile_txt_path = path + "/scenefile.txt";
std::ifstream ifs(scenefile_txt_path);
std::string line;
if (ifs.fail()) {
std::cerr << "Can't open scene file: " << path << std::endl;
return scene;
}
std::unordered_map<std::string, std::shared_ptr<Mesh>> tmp_mesh_map;
std::unordered_map<std::string, std::shared_ptr<Material>> tmp_material_map;
while (std::getline(ifs, line)) {
auto texts = SplitString(line, ' ');
std::cout << "Load : " << line << std::endl;
// コメント
if (texts[0] == "#") continue;
// Mesh
else if (texts[0] == "Mesh:") {
std::string mesh_name = texts[1];
std::vector<std::string> mesh_texts;
mesh_texts.emplace_back(line);
while (std::getline(ifs, line)) {
mesh_texts.emplace_back(line);
if (line == "MeshEnd") break;
}
auto mesh = ParseMesh(mesh_texts);
tmp_mesh_map.insert(std::make_pair(mesh_name, mesh));
}
// Material
else if (texts[0] == "Material:") {
std::string material_name = texts[1];
std::vector<std::string> material_texts;
material_texts.emplace_back(line);
while (std::getline(ifs, line)) {
material_texts.emplace_back(line);
if (line == "MaterialEnd") break;
}
auto material = ParseMaterial(path, material_texts);
tmp_material_map.insert(std::make_pair(material_name, material));
}
// MeshEntity
else if (texts[0] == "MeshEntity:") {
std::shared_ptr<Mesh> mesh;
std::shared_ptr<Material> material;
glm::vec3 position;
glm::vec3 rotation;
glm::vec3 scale;
while (std::getline(ifs, line)) {
auto texts = SplitString(line, ' ');
if (texts[0] == "Mesh:") {
mesh = tmp_mesh_map[texts[1]];
} else if (texts[0] == "Material:") {
material = tmp_material_map[texts[1]];
} else if (texts[0] == "Position:") {
position = glm::vec3(std::stof(texts[1]), std::stof(texts[3]),
-std::stof(texts[2]));
} else if (texts[0] == "Rotation:") {
rotation = glm::vec3(std::stof(texts[1]), std::stof(texts[3]),
-std::stof(texts[2]));
} else if (texts[0] == "Scale:") {
scale = glm::vec3(std::stof(texts[1]), std::stof(texts[3]),
std::stof(texts[2]));
}
if (line == "MeshEntityEnd") break;
}
scene->mesh_entities_.emplace_back(mesh, material, position, rotation,
scale);
}
// Directional Light
else if (texts[0] == "DirectionalLight:") {
GLfloat intensity;
glm::vec3 direction;
glm::vec3 color;
glm::vec3 position;
GLfloat left = -100;
GLfloat right = 100;
GLfloat bottom = -100;
GLfloat top = 100;
GLfloat near = -100;
GLfloat far = 100;
while (std::getline(ifs, line)) {
auto texts = SplitString(line, ' ');
if (texts[0] == "Position:") {
position = glm::vec3(std::stof(texts[1]), std::stof(texts[3]),
-std::stof(texts[2]));
} else if (texts[0] == "Intensity:") {
intensity = std::stof(texts[1]);
} else if (texts[0] == "Color:") {
color = glm::vec3(std::stof(texts[1]), std::stof(texts[2]),
std::stof(texts[3]));
} else if (texts[0] == "Direction:") {
direction = glm::vec3(std::stof(texts[1]), std::stof(texts[3]),
-std::stof(texts[2]));
}
if (line == "DirectionalLightEnd") break;
}
scene->directional_light_ =
std::make_unique<DirectionalLight>(intensity, direction, color);
}
// Point Light
else if (texts[0] == "PointLight:") {
glm::vec3 position;
GLfloat intensity;
glm::vec3 color;
GLfloat near;
GLfloat range;
GLfloat shadow_bias;
bool use_shadow;
while (std::getline(ifs, line)) {
auto texts = SplitString(line, ' ');
if (texts[0] == "Position:") {
position = glm::vec3(std::stof(texts[1]), std::stof(texts[3]),
-std::stof(texts[2]));
} else if (texts[0] == "Intensity:") {
intensity = std::stof(texts[1]);
} else if (texts[0] == "Color:") {
color = glm::vec3(std::stof(texts[1]), std::stof(texts[2]),
std::stof(texts[3]));
} else if (texts[0] == "ClipStart:") {
near = std::stof(texts[1]);
} else if (texts[0] == "Range:") {
range = std::stof(texts[1]);
} else if (texts[0] == "ShadowBias:") {
shadow_bias = std::stof(texts[1]);
} else if (texts[0] == "UseShadow:") {
shadow_bias = std::stoi(texts[1]) == 1;
}
if (line == "PointLightEnd") break;
}
scene->point_lights_.emplace_back(position, intensity, color, range);
}
// Spot Light
else if (texts[0] == "SpotLight:") {
glm::vec3 position;
GLfloat intensity;
glm::vec3 color;
GLfloat near;
GLfloat range;
glm::vec3 direction;
GLfloat angle;
GLfloat blend;
while (std::getline(ifs, line)) {
auto texts = SplitString(line, ' ');
if (texts[0] == "Position:") {
position = glm::vec3(std::stof(texts[1]), std::stof(texts[3]),
-std::stof(texts[2]));
} else if (texts[0] == "Intensity:") {
intensity = std::stof(texts[1]);
} else if (texts[0] == "Color:") {
color = glm::vec3(std::stof(texts[1]), std::stof(texts[2]),
std::stof(texts[3]));
} else if (texts[0] == "ClipStart:") {
near = std::stof(texts[1]);
} else if (texts[0] == "Range:") {
range = std::stof(texts[1]);
} else if (texts[0] == "Direction:") {
direction = glm::vec3(std::stof(texts[1]), std::stof(texts[3]),
-std::stof(texts[2]));
} else if (texts[0] == "Angle:") {
angle = std::stof(texts[1]);
} else if (texts[0] == "Blend:") {
blend = std::stof(texts[1]);
}
if (line == "SpotLightEnd") break;
}
scene->spot_lights_.emplace_back(position, intensity, color, range,
direction, angle, blend);
}
}
return scene;
}
std::vector<std::string> Scene::SplitString(const std::string& s, char delim) {
std::vector<std::string> elems(0);
std::stringstream ss;
ss.str(s);
std::string item;
while (std::getline(ss, item, delim)) {
elems.emplace_back(item);
}
return elems;
}
std::shared_ptr<Mesh> Scene::ParseMesh(
const std::vector<std::string>& mesh_texts) {
std::vector<glm::vec3> vertices;
std::vector<glm::vec3> normals;
std::vector<glm::vec2> uvs;
for (const auto& line : mesh_texts) {
auto texts = SplitString(line, ' ');
if (texts[0] == "v") {
vertices.emplace_back(std::stof(texts[1]), std::stof(texts[3]),
-std::stof(texts[2]));
} else if (texts[0] == "vn") {
normals.emplace_back(std::stof(texts[1]), std::stof(texts[3]),
-std::stof(texts[2]));
} else if (texts[0] == "vt") {
uvs.emplace_back(std::stof(texts[1]), std::stof(texts[2]));
}
}
return std::make_shared<Mesh>(vertices, normals, uvs);
}
std::shared_ptr<Material> Scene::ParseMaterial(
const std::string& path, const std::vector<std::string>& material_texts) {
Texture albedo_texture;
Texture metallic_texture;
Texture roughness_texture;
Texture emissive_texture;
Texture normal_map_texture;
GLfloat emissive_intensity;
for (const auto& line : material_texts) {
auto texts = SplitString(line, ' ');
if (texts[0] == "baseColor:") {
albedo_texture = Texture(path + "/" + texts[1], true);
} else if (texts[0] == "metallic:") {
metallic_texture = Texture(path + "/" + texts[1], false);
} else if (texts[0] == "roughness:") {
roughness_texture = Texture(path + "/" + texts[1], false);
} else if (texts[0] == "emission:") {
emissive_texture = Texture(path + "/" + texts[1], true);
} else if (texts[0] == "normalMap:") {
normal_map_texture = Texture(path + "/" + texts[1], false);
} else if (texts[0] == "emissionIntensity:") {
emissive_intensity = std::stof(texts[1]);
}
}
return std::make_shared<Material>(
std::move(albedo_texture), std::move(metallic_texture),
std::move(roughness_texture), std::move(normal_map_texture),
std::move(emissive_texture), emissive_intensity);
}
基本的に一行ずつ読み出していっているだけです。
Applicationクラスの変更
SceneFile.scenefile
を読み込むように変更します。
Application::Init
内のシーンの作成部分を変更します。
// Sceneの読み込み
scene_ = Scene::LoadScene("SceneFile.scenefile", width, height);
scene_->camera_ = std::make_unique<Camera>(
glm::vec3(-1.37508f, 7.96885f, 21.19848),
glm::vec3(glm::radians(73.2f - 90.0f), glm::radians(-4.61f),
-glm::radians(-0.000004f)),
glm::radians(30.0f),
static_cast<GLfloat>(width) / height, 0.1f, 150.0f);
シーンファイルにはカメラの情報は書き出していませんのでカメラを作成します。 とりあえずBlenderのカメラの位置に合わせて入力します。 BlenderとOpenGLではzとyが-yとzに入れ替わっていることに注意です。
SceneRendererクラスの変更
exposureを変更します。
void SceneRenderer::Render(const Scene& scene, const double delta_time) {
geometry_pass_.Render(scene);
// HDRカラーバッファにDepthとStencilの転写
glBindFramebuffer(GL_READ_FRAMEBUFFER, gbuffer_fbo_);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, hdr_fbo_);
glBlitFramebuffer(0, 0, width_, height_, 0, 0, width_, height_,
GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT, GL_NEAREST);
directional_light_pass_.Render(scene);
point_light_pass_.Render(scene);
spot_light_pass_.Render(scene);
exposure_pass_.SetExposure(0.001f); exposure_pass_.Render();
tonemapping_pass_.Render();
}
実行結果
実行結果は次のとおりです。
ちなみにBlenderの表示は次のとおりです。
影の有無が違いますね。 次回以降は影の実装を行っていきます。
また、Blenderの方は環境光成分が加わっているようです。 次のようにしてBackgroundを真っ黒にすることでBlenderでの環境光成分をオフにしてOpenGLの描画に合わせられそうです。
Blenderの描画と並べてみたところ。
プログラム全文
プログラム全文はGitHubにアップロードしてあります。