OpenGLでBlenderから書き出したファイルを読み込む

はじめに

この記事はシリーズ記事です。目次はこちら。

この記事ではBlenderからプログラム用のシーンファイルを書き出すアドオンを作成し、そのアドオンで書き出したシーンを読み込むところまで行ってみます。

2020 04 08 20 03 01

Blenderアドオン

まずはシーンファイルを書き出すBlenderアドオンを作成します。

アドオンの作り方は次が参考になります。

アドオンのソースコード

まず最初にアドオン用にディレクトリを切って、__init__.pyを作成します。

<root>/
├── BlenderAddon/
│   └── ScenefileExporter/
│       └── __init__.py
├── OpenGL-PBR-Map/
├── vendors/
└── OpenGL-PBR-Map.sln
__init__.py
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を作成します。

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に対応します。 つまりMeshMaterialを保持し、モデル行列などを保持します。

最初にこの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のプロパティで指定する数値と対応しています。

2020 04 08 12 01 53

次に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ファイルをインストールし有効化します。

2020 04 08 12 31 41

2020 04 08 12 32 05

2020 04 08 12 32 36

アドオンの使い方

まず、Sceneというコレクションをシーンに作成します。 このコレクション内部にあるものが出力されます。

2020 04 08 12 05 04

メッシュとポイントライト、スポットライト、ディレクショナルライトを好きに配置します。 カメラの情報は、プログラムで自由に動かすことを想定してシーンファイルには書き出しません。

2020 04 08 12 07 10

Shift-Dの複製ではなく、Alt-Dのリンク複製を使うことでMeshEntityでMeshを共有することができるため、ファイルサイズを削減できます。

Shift-Dで複製をした場合は次の通り。

2020 04 08 12 05 21

2020 04 08 12 05 30

2020 04 08 12 05 38

CubeとCube.001という違う名前のMeshが割り当てられていることが確認できます。

Alt-Dで複製をした場合は次の通り。

2020 04 08 12 06 09

2020 04 08 12 06 29

2020 04 08 12 06 39

CubeとCube.001のオブジェクトでCubeというメッシュを共有していることがわかります。

メッシュのマテリアルは次のようなノード構成である必要があります。 アドオンではノード構成決め打ちでテクスチャのノードを指定してテクスチャをコピーしているため、ノードの構造が変わったり、テクスチャが欠けたりしてはうまく動きません。

2020 04 08 18 50 12

エクスポートするにはFileメニューからScenefile Exporterを選択をします。

2020 04 08 12 33 55

2020 04 08 12 35 27

出力するディレクトリがすでに存在する場合はエラーになるので、再出力する場合は一旦削除してから出力してください。

試しに次のようなファイルをSceneFile.scenefileという名前で書き出してみると次のようになります。

2020 04 08 12 25 39

2020 04 08 12 25 23

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のマテリアルからコピーされたものです。

2020 04 08 12 37 02

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つですがメッシュの情報は共有しています。 マテリアルの情報は共有していません。

今回のサンプルプログラムで読み込むシーンファイル

次のようなシーンファイルを作成しました。

2020 04 08 19 10 24

2020 04 08 19 10 35

このシーンをSceneFile.scenefileという名前で書き出しておきます。

前回のプログラムからの変更点

書き出したシーンを読み込むプログラムを作成します。

前回のプログラムからの変更点を次に示します。

Sceneクラスの変更

SceneクラスにSceneFileを読み込む機能をもたせます。

まずはヘッダファイルを追加します。

scene.h
#include <glm/glm.hpp>
#include <memory>
#include <unordered_map>#include <vector>

次にシーンを読み込む静的メンバ関数を用意します。

scene.h
  /**
   * @brief 与えられたパスのシーンを読み込む
   * @param path シーンディレクトリのパス
   * @param width 画面の横幅
   * @param height 画面の縦
   * @return ロードされたシーン
   *
   * シーンディレクトリのパスは最後に`/`を含みません。
   */
  static std::unique_ptr<Scene> LoadScene(const std::string path,
                                          const GLuint width,
                                          const GLuint height);

補助用の関数も用意します。

scene.h
 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);

実装は次のとおりです。

scene.cpp
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内のシーンの作成部分を変更します。

application.cpp
    // 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を変更します。

scene_renderer.cpp
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();
}

実行結果

実行結果は次のとおりです。

2020 04 08 20 01 02

ちなみにBlenderの表示は次のとおりです。

2020 04 08 20 01 52

影の有無が違いますね。 次回以降は影の実装を行っていきます。

また、Blenderの方は環境光成分が加わっているようです。 次のようにしてBackgroundを真っ黒にすることでBlenderでの環境光成分をオフにしてOpenGLの描画に合わせられそうです。

2020 04 08 20 02 17

2020 04 08 20 02 30

Blenderの描画と並べてみたところ。

2020 04 08 20 03 01

プログラム全文

プログラム全文はGitHubにアップロードしてあります。

GitHub: MatchaChoco010/OpenGL-PBR-Map at v15