Rewrite of exporting system, much more human friendly error messages. Levels now consist of multiple rooms

main
Cameron Murphy Reikes 9 months ago
parent 2fd1c8214f
commit b105160f7e

@ -5,14 +5,14 @@ import shutil
import struct import struct
from mathutils import *; from math import * from mathutils import *; from math import *
from bpy_extras.io_utils import (axis_conversion) from bpy_extras.io_utils import (axis_conversion)
from dataclasses import dataclass
C = bpy.context C = bpy.context
D = bpy.data D = bpy.data
LEVEL_EXPORT_NAME = "level" ROOMS_EXPORT_NAME = "rooms"
EXPORT_DIRECTORY = "../assets/exported_3d" EXPORT_DIRECTORY = "../assets/exported_3d"
print("\n\nLet's get it started")
if os.path.exists(bpy.path.abspath(f"//{EXPORT_DIRECTORY}")): if os.path.exists(bpy.path.abspath(f"//{EXPORT_DIRECTORY}")):
shutil.rmtree(bpy.path.abspath(f"//{EXPORT_DIRECTORY}")) shutil.rmtree(bpy.path.abspath(f"//{EXPORT_DIRECTORY}"))
@ -63,11 +63,6 @@ def normalize_joint_weights(weights):
return result return result
# for the level.bin # for the level.bin
level_object_data = []
collision_cubes = []
placed_entities = []
saved_meshes = set()
mapping = axis_conversion( mapping = axis_conversion(
from_forward = "Y", from_forward = "Y",
from_up = "Z", from_up = "Z",
@ -76,10 +71,6 @@ mapping = axis_conversion(
) )
mapping.resize_4x4() mapping.resize_4x4()
with open(bpy.path.abspath(f"//{EXPORT_DIRECTORY}/shorttest.bin"), "wb") as f:
for i in range(4):
write_u16(f, i)
project_directory = bpy.path.abspath("//") project_directory = bpy.path.abspath("//")
def is_file_in_project(file_path): def is_file_in_project(file_path):
file_name = os.path.basename(file_path) file_name = os.path.basename(file_path)
@ -90,22 +81,24 @@ def is_file_in_project(file_path):
saved_images = set() saved_images = set()
def ensure_tex_saved_and_get_name(o) -> str: def ensure_tex_saved_and_get_name(o) -> str:
"""returns the path to the mesh's texture's png in the exported directory""" """returns the name of the mesh's texture's png in the exported directory, of the current object"""
assert o.type == "MESH", f"Object {o.name} isn't a mesh and is attempting to be saved as such"
mesh_name = o.to_mesh().name mesh_name = o.to_mesh().name
# find the image object in the material of the object
img_obj = None img_obj = None
assert len(o.material_slots) == 1, f"Don't know which material slot to pick from in mesh {mesh_name} object {o.name}" assert len(o.material_slots) == 1, f"Don't know which material slot to pick from in mesh {mesh_name} object {o.name}, there must only be one material slot on every mesh"
mat = o.material_slots[0] mat = o.material_slots[0]
for node in mat.material.node_tree.nodes: for node in mat.material.node_tree.nodes:
if node.type == "TEX_IMAGE": if node.type == "TEX_IMAGE":
assert img_obj == None, f"Material on object {o.name} has more than one image node in its material, so I don't know which image node to use in the game engine. Make sure materials only use one image node"
img_obj = node.image img_obj = node.image
break
assert img_obj, f"Mesh {mesh_name} in its material doesn't have an image object" assert img_obj, f"Mesh {mesh_name} in its material doesn't have an image object"
image_filename = f"{img_obj.name}.png" image_filename = f"{img_obj.name}.png"
if image_filename in saved_images: if image_filename in saved_images:
pass pass
else: else:
save_to = f"//{EXPORT_DIRECTORY}/{image_filename}" save_to = f"//{EXPORT_DIRECTORY}/{image_filename}"
print(f"Saving image {image_filename} to {bpy.path.abspath((save_to))}...")
if img_obj.packed_file: if img_obj.packed_file:
img_obj.save(filepath=bpy.path.abspath(save_to)) img_obj.save(filepath=bpy.path.abspath(save_to))
else: else:
@ -123,272 +116,339 @@ def ensure_tex_saved_and_get_name(o) -> str:
return image_filename return image_filename
def export_armatures():
print("Exporting armatures...")
exported = D.collections.get("Exported")
assert exported != None, f"No exported collection named 'Exported' in scene, very bad! Did everything to get exported get deleted?"
armatures_collection = exported.children.get("Armatures")
assert armatures_collection != None, f"No child named 'Armatures' on the exported collection, this is required"
def object_in_collection(o, collection): A = armatures_collection
"""Probably only works for root level collections""" armatures_to_export = []
return o.users_collection[0].name == collection or (o.users_collection[0] in D.collections[collection].children_recursive) for o in A.objects:
if o.type == "MESH":
# meshes can either be Meshes, or Armatures. Armatures contain all mesh data to draw it, and any anims it has assert o.parent, f"Mesh named {o.name} without parent in armatures collection is invalid, only armatures allowed in there!"
for o in D.objects: assert o.parent.type == "ARMATURE", f"Mesh named {o.name} isn't an armature, and isn't a child of an armature. This isn't allowed."
if o.hide_get(): continue elif o.type == "ARMATURE":
if o.type == "MESH": armatures_to_export.append(o)
if o.parent and o.parent.type == "ARMATURE": else:
mesh_object = o assert False, f"Object named {o.name} is of an unknown type '{o.type}', only objects that are Meshes or Armature are allowed in the armatures collection"
o = o.parent
object_transform_info = (mesh_name, mapping @ o.location, o.rotation_euler, o.scale) for armature_object in armatures_to_export:
if object_in_collection(o, "Level") or object_in_collection(o, "PlacedEntities"): # get the body mesh object
assert False, "Cannot put armatures in the level. The level is for static placed meshes. For dynamic entities, you put them outside of the level collection, their entity kind is encoded, and the game code decides how to draw them" body_object = None
else: for c in armature_object.children:
pass if c.name.startswith("Body"):
#placed_entities.append((mesh_object.name,) + object_transform_info) assert body_object == None, f"On object {armature_object.name}, more than one child has a name that starts with 'Body', specifically '{body_object.name}' and '{c.name}', only one child's name is allowed to start with 'Body' in this object."
armature_name = o.data.name body_object = c
output_filepath = bpy.path.abspath(f"//{EXPORT_DIRECTORY}/{armature_name}.bin")
image_filename = ensure_tex_saved_and_get_name(mesh_object) # initialize important variables
print(f"Exporting armature with image filename {image_filename} to {output_filepath}") mesh_object = body_object
with open(output_filepath, "wb") as f: armature_object = armature_object
write_b8(f, True) output_filepath = bpy.path.abspath(f"//{EXPORT_DIRECTORY}/{armature_object.name}.bin")
write_string(f, image_filename) mesh_image_filename = ensure_tex_saved_and_get_name(mesh_object)
bones_in_armature = [] #print(f"Exporting armature with image filename {mesh_image_filename} to {output_filepath}")
for b in o.data.bones:
bones_in_armature.append(b) with open(output_filepath, "wb") as f:
write_b8(f, True) # first byte is true if it's an armature file
# the inverse model space pos of the bones write_string(f, mesh_image_filename)
write_u64(f, len(bones_in_armature)) bones_in_armature = []
for b in bones_in_armature: for b in armature_object.data.bones:
model_space_pose = mapping @ b.matrix_local bones_in_armature.append(b)
inverse_model_space_pose = (mapping @ b.matrix_local).inverted()
parent_index = -1
if b.parent:
for i in range(len(bones_in_armature)):
if bones_in_armature[i] == b.parent:
parent_index = i
break
if parent_index == -1:
assert False, f"Couldn't find parent of bone {b}"
#print(f"Parent of bone {b.name} is index {parent_index} in list {bones_in_armature}")
write_i32(f, parent_index)
write_4x4matrix(f, model_space_pose)
write_4x4matrix(f, inverse_model_space_pose)
write_f32(f, b.length)
# write the pose information # the inverse model space pos of the bones
write_u64(f, len(bones_in_armature))
for b in bones_in_armature:
model_space_pose = mapping @ b.matrix_local
inverse_model_space_pose = (mapping @ b.matrix_local).inverted()
parent_index = -1
if b.parent:
for i in range(len(bones_in_armature)):
if bones_in_armature[i] == b.parent:
parent_index = i
break
if parent_index == -1:
assert False, f"Couldn't find parent of bone {b}"
#print(f"Parent of bone {b.name} is index {parent_index} in list {bones_in_armature}")
write_i32(f, parent_index)
write_4x4matrix(f, model_space_pose)
write_4x4matrix(f, inverse_model_space_pose)
write_f32(f, b.length)
# write the pose information
# it's very important that the array of pose bones contains the same amount of bones
# as there are in the edit bones. Because the edit bones are exported, etc etc. Cowabunga!
assert(len(armature_object.pose.bones) == len(bones_in_armature))
armature = armature_object
anims = []
assert armature.animation_data, "Armatures are assumed to have an animation right now"
for track in armature.animation_data.nla_tracks:
for strip in track.strips:
anims.append(strip.action)
#print(f"Writing {len(anims)} animations")
write_u64(f, len(anims))
for animation in anims:
write_string(f, animation.name)
# it's very important that the array of pose bones contains the same amount of bones old_action = armature.animation_data.action
# as there are in the edit bones. Because the edit bones are exported, etc etc. Cowabunga! armature.animation_data.action = animation
assert(len(o.pose.bones) == len(bones_in_armature)) startFrame = int(animation.frame_range.x)
endFrame = int(animation.frame_range.y)
armature = o total_frames = (endFrame - startFrame) + 1 # the end frame is inclusive
anims = [] #print(f"Exporting animation {animation.name} with {total_frames} frames")
assert armature.animation_data, "Armatures are assumed to have an animation right now"
for track in armature.animation_data.nla_tracks:
for strip in track.strips:
anims.append(strip.action)
print(f"Writing {len(anims)} animations")
write_u64(f, len(anims))
for animation in anims:
write_string(f, animation.name)
old_action = armature.animation_data.action
armature.animation_data.action = animation
startFrame = int(animation.frame_range.x)
endFrame = int(animation.frame_range.y)
total_frames = (endFrame - startFrame) + 1 # the end frame is inclusive
print(f"Exporting animation {animation.name} with {total_frames} frames")
write_u64(f, total_frames)
time_per_anim_frame = 1.0 / float(bpy.context.scene.render.fps)
for frame in range(startFrame, endFrame+1):
time_through_this_frame_occurs_at = (frame - startFrame) * time_per_anim_frame
bpy.context.scene.frame_set(frame)
write_f32(f, time_through_this_frame_occurs_at)
for pose_bone_i in range(len(o.pose.bones)):
pose_bone = o.pose.bones[pose_bone_i]
# in the engine, it's assumed that the poses are in the same order as the bones
# they're referring to. This checks that that is the case.
assert(pose_bone.bone == bones_in_armature[pose_bone_i])
parent_space_pose = None
if pose_bone.parent:
parent_space_pose = pose_bone.parent.matrix.inverted() @ pose_bone.matrix
else:
parent_space_pose = mapping @ pose_bone.matrix
#parent_space_pose = pose_bone.matrix
#print("parent_space_pose of the bone with no parent:")
#print(parent_space_pose)
#parent_space_pose = mapping @ pose_bone.matrix
translation = parent_space_pose.to_translation()
rotation = parent_space_pose.to_quaternion()
scale = parent_space_pose.to_scale()
write_v3(f, translation)
write_quat(f, rotation)
write_v3(f, scale)
armature.animation_data.action = old_action
# write the mesh data for the armature
bm = bmesh.new()
mesh = mesh_object.to_mesh()
bm.from_mesh(mesh)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.transform(mapping)
bm.to_mesh(mesh)
write_u64(f, total_frames)
vertices = [] time_per_anim_frame = 1.0 / float(bpy.context.scene.render.fps)
armature = o for frame in range(startFrame, endFrame+1):
for polygon in mesh.polygons: time_through_this_frame_occurs_at = (frame - startFrame) * time_per_anim_frame
if len(polygon.loop_indices) == 3: bpy.context.scene.frame_set(frame)
for loopIndex in polygon.loop_indices:
loop = mesh.loops[loopIndex] write_f32(f, time_through_this_frame_occurs_at)
position = mesh.vertices[loop.vertex_index].undeformed_co for pose_bone_i in range(len(armature_object.pose.bones)):
uv = mesh.uv_layers.active.data[loop.index].uv pose_bone = armature_object.pose.bones[pose_bone_i]
normal = loop.normal
# in the engine, it's assumed that the poses are in the same order as the bones
jointIndices = [0,0,0,0] # they're referring to. This checks that that is the case.
jointWeights = [0,0,0,0] assert(pose_bone.bone == bones_in_armature[pose_bone_i])
for jointBindingIndex, group in enumerate(mesh.vertices[loop.vertex_index].groups):
if jointBindingIndex < 4: parent_space_pose = None
groupIndex = group.group
boneName = mesh_object.vertex_groups[groupIndex].name if pose_bone.parent:
jointIndices[jointBindingIndex] = armature.data.bones.find(boneName) parent_space_pose = pose_bone.parent.matrix.inverted() @ pose_bone.matrix
if jointIndices[jointBindingIndex] == -1: else:
# it's fine that this references a real bone, the bone at index 0, parent_space_pose = mapping @ pose_bone.matrix
# because the weight of its influence is 0
jointIndices[jointBindingIndex] = 0 translation = parent_space_pose.to_translation()
jointWeights[jointBindingIndex] = 0.0 rotation = parent_space_pose.to_quaternion()
else: scale = parent_space_pose.to_scale()
jointWeights[jointBindingIndex] = group.weight
write_v3(f, translation)
write_quat(f, rotation)
vertices.append((position, uv, jointIndices, normalize_joint_weights(jointWeights))) write_v3(f, scale)
write_u64(f, len(vertices)) armature.animation_data.action = old_action
vertex_i = 0 # write the mesh data for the armature
for v_and_uv in vertices: bm = bmesh.new()
v, uv, jointIndices, jointWeights = v_and_uv mesh = mesh_object.to_mesh()
write_f32(f, v.x) bm.from_mesh(mesh)
write_f32(f, v.y) bmesh.ops.triangulate(bm, faces=bm.faces)
write_f32(f, v.z) bm.transform(mapping)
write_f32(f, uv.x) bm.to_mesh(mesh)
write_f32(f, uv.y)
for i in range(4):
write_u16(f, jointIndices[i])
for i in range(4):
write_f32(f, jointWeights[i])
vertex_i += 1
print(f"Wrote {len(vertices)} vertices")
else: # if the parent type isn't an armature, i.e just a bog standard mesh
mesh_name = o.to_mesh().name # use this over o.name so instanced objects which refer to the same mesh, both use the same serialized mesh.
object_transform_info = (mesh_name, mapping @ o.location, o.rotation_euler, o.scale)
if object_in_collection(o, "Level") and mesh_name == "CollisionCube": vertices = []
collision_cubes.append((o.location, o.dimensions)) armature = armature_object
else: for polygon in mesh.polygons:
if object_in_collection(o, "Level"): if len(polygon.loop_indices) == 3:
print(f"Object {o.name} has mesh name {o.to_mesh().name}") for loopIndex in polygon.loop_indices:
assert(o.rotation_euler.order == 'XYZ') loop = mesh.loops[loopIndex]
level_object_data.append(object_transform_info) position = mesh.vertices[loop.vertex_index].undeformed_co
elif object_in_collection(o, "PlacedEntities"): uv = mesh.uv_layers.active.data[loop.index].uv
placed_entities.append((o.name,) + object_transform_info) normal = loop.normal
if mesh_name in saved_meshes: jointIndices = [0,0,0,0]
continue jointWeights = [0,0,0,0]
saved_meshes.add(mesh_name) for jointBindingIndex, group in enumerate(mesh.vertices[loop.vertex_index].groups):
print(f"""Mesh name {mesh_name} in level {object_in_collection(o, "Level")} collections {o.users_collection}""") if jointBindingIndex < 4:
image_filename = ensure_tex_saved_and_get_name(o) groupIndex = group.group
boneName = mesh_object.vertex_groups[groupIndex].name
assert(mesh_name != LEVEL_EXPORT_NAME) jointIndices[jointBindingIndex] = armature.data.bones.find(boneName)
output_filepath = bpy.path.abspath(f"//{EXPORT_DIRECTORY}/{mesh_name}.bin") if jointIndices[jointBindingIndex] == -1:
print(f"Exporting mesh to {output_filepath} with image_filename {image_filename}") # it's fine that this references a real bone, the bone at index 0,
with open(output_filepath, "wb") as f: # because the weight of its influence is 0
write_b8(f, False) # if it's an armature or not, first byte of the file jointIndices[jointBindingIndex] = 0
write_string(f, image_filename) # the image filename! jointWeights[jointBindingIndex] = 0.0
else:
bm = bmesh.new() jointWeights[jointBindingIndex] = group.weight
mesh = o.to_mesh()
bm.from_mesh(mesh)
bmesh.ops.triangulate(bm, faces=bm.faces) vertices.append((position, uv, jointIndices, normalize_joint_weights(jointWeights)))
bm.transform(mapping)
bm.to_mesh(mesh) write_u64(f, len(vertices))
vertex_i = 0
for v_and_uv in vertices:
vertices = [] v, uv, jointIndices, jointWeights = v_and_uv
write_f32(f, v.x)
for polygon in mesh.polygons: write_f32(f, v.y)
if len(polygon.loop_indices) == 3: write_f32(f, v.z)
for loopIndex in polygon.loop_indices: write_f32(f, uv.x)
loop = mesh.loops[loopIndex] write_f32(f, uv.y)
position = mesh.vertices[loop.vertex_index].undeformed_co for i in range(4):
uv = mesh.uv_layers.active.data[loop.index].uv write_u16(f, jointIndices[i])
normal = loop.normal for i in range(4):
write_f32(f, jointWeights[i])
vertices.append((position, uv)) vertex_i += 1
#print(f"Wrote {len(vertices)} vertices")
write_u64(f, len(vertices)) print("Success!")
print(f"\n\n{output_filepath} vertices:")
for v_and_uv in vertices: def collect_and_validate_mesh_objects(collection_with_only_mesh_objects):
v, uv = v_and_uv to_return = []
write_f32(f, v.x)
write_f32(f, v.y) for o in collection_with_only_mesh_objects.all_objects:
write_f32(f, v.z) assert o.type == "MESH", f"The collection '{collection_with_only_mesh_objects.name}' is assumed to contain only mesh objects but the object '{o.name}' which is a child of it isn't a mesh object"
write_f32(f, uv.x) to_return.append(o)
write_f32(f, uv.y)
if len(vertices) < 100: return to_return
print(v)
print(f"Wrote {len(vertices)} vertices") def get_startswith(name_of_overarching_thing, iterable, starts_with):
"""
with open(bpy.path.abspath(f"//{EXPORT_DIRECTORY}/{LEVEL_EXPORT_NAME}.bin"), "wb") as f: Gets the thing in iterable that starts with starts with, and makes sure there's only *one* thing that starts with starts with
write_u64(f, len(level_object_data)) name_of_overarching_thing is for the error message, for reporting in what collection things went wrong in.
for o in level_object_data: """
mesh_name, blender_pos, blender_rotation, blender_scale = o to_return = None
print(f"Writing instanced object of mesh {mesh_name}") for thing in iterable:
write_string(f, mesh_name) if thing.name.startswith(starts_with):
write_f32(f, blender_pos.x) assert to_return == None, f"Duplicate thing that starts with '{starts_with}' found in {name_of_overarching_thing} called {thing.name}"
write_f32(f, blender_pos.y) to_return = thing
write_f32(f, blender_pos.z) assert to_return != None, f"Couldn't find thing that starts with '{starts_with}' as a child of '{name_of_overarching_thing}', but one is required"
return to_return
write_f32(f, blender_rotation.x)
write_f32(f, blender_rotation.y) def no_hidden(objects):
write_f32(f, blender_rotation.z) to_return = []
for o in objects:
write_f32(f, blender_scale.x) if not o.hide_get():
write_f32(f, blender_scale.y) to_return.append(o)
write_f32(f, blender_scale.z) return to_return
write_u64(f, len(collision_cubes)) def export_meshes_and_levels():
for c in collision_cubes: print("Exporting meshes and levels")
blender_pos, blender_dims = c exported = D.collections.get("Exported")
write_f32(f, blender_pos.x) assert exported != None, f"No exported collection named 'Exported' in scene, very bad! Did everything to get exported get deleted?"
write_f32(f, -blender_pos.y)
write_f32(f, blender_dims.x) mesh_names_to_export = set()
write_f32(f, blender_dims.y) meshes_to_export = []
assert(blender_dims.x > 0.0)
assert(blender_dims.y > 0.0) # add the collection 'Meshes' objects to mesh_objects_to_export
if True:
write_u64(f, len(placed_entities)) meshes = get_startswith("Exported", exported.children, "Meshes")
for p in placed_entities: to_export = collect_and_validate_mesh_objects(meshes)
# underscore is mesh name, prefer object name for name of npc. More obvious for m in no_hidden(to_export):
object_name, _, blender_pos, blender_rotation, blender_scale = p mesh_names_to_export.add(m.name)
print(f"Writing placed entity '{object_name}'") meshes_to_export.append(m)
write_string(f, object_name)
write_f32(f, blender_pos.x) # export each level: its placed entities, placed meshes, and add to meshes_to_export and mesh_names_to_export. Those must be exported to their correct paths for the rooms to point at valid data.
write_f32(f, blender_pos.y) rooms = exported.children.get("Rooms")
write_f32(f, blender_pos.z) assert rooms != None, f"No child named 'Rooms' on the exported collection, this is required"
with open(bpy.path.abspath(f"//{EXPORT_DIRECTORY}/{ROOMS_EXPORT_NAME}.bin"), "wb") as f:
write_f32(f, blender_rotation.x) write_u64(f, len(rooms.children))
write_f32(f, blender_rotation.y) for room_collection in rooms.children:
write_f32(f, blender_rotation.z) write_string(f, room_collection.name) # the name of the room is the name of the room collection
write_f32(f, blender_scale.x) # placed meshes (exported mesh name (which is the object's name), location, rotation, scale)
write_f32(f, blender_scale.y) placed_meshes = []
write_f32(f, blender_scale.z) if True:
meshes_collection = get_startswith(room_collection.name, room_collection.children, "Meshes")
for m in no_hidden(meshes_collection.objects):
assert m.rotation_euler.order == 'XYZ', f"Invalid rotation euler order for object of name '{m.name}', it's {m.rotation_euler.order} but must be XYZ"
assert m.type == "MESH", f"In meshes collection '{meshes_collection.name}' object {m.name} must be of type 'MESH' but instead is of type {m.type}"
if not m.name in mesh_names_to_export:
mesh_names_to_export.add(m.name)
meshes_to_export.append(m)
placed_meshes.append((m.name, mapping @ m.location, m.rotation_euler, m.scale))
# colliders (location, dimensions)
placed_colliders = []
if True:
colliders_collection = get_startswith(room_collection.name, room_collection.children, "Colliders")
for o in no_hidden(colliders_collection.objects):
assert o.name.startswith("CollisionCube"), f"Object {o.name} doesn't start with 'CollisionCube' and it's in the Colliders group of room {room_collection.name}, colliders must be collision cubes"
placed_colliders.append((o.location, o.dimensions))
# fill out placed_entities with a tuple of (name, location, rotation, scale)
placed_entities = []
if True:
entities_collection = get_startswith(room_collection.name, room_collection.children, "Entities")
for o in no_hidden(entities_collection.objects):
assert o.rotation_euler.order == 'XYZ', f"Invalid rotation euler order for object of name '{o.name}', it's {o.rotation_euler.order} but must be XYZ"
placed_entities.append((o.name, mapping @ o.location, o.rotation_euler, o.scale))
write_u64(f, len(placed_meshes))
for p in placed_meshes:
mesh_name, blender_pos, blender_rotation, blender_scale = p
write_string(f, mesh_name)
write_f32(f, blender_pos.x)
write_f32(f, blender_pos.y)
write_f32(f, blender_pos.z)
write_f32(f, blender_rotation.x)
write_f32(f, blender_rotation.y)
write_f32(f, blender_rotation.z)
write_f32(f, blender_scale.x)
write_f32(f, blender_scale.y)
write_f32(f, blender_scale.z)
write_u64(f, len(placed_colliders))
for c in placed_colliders:
blender_pos, blender_dims = c
write_f32(f, blender_pos.x)
write_f32(f, -blender_pos.y)
write_f32(f, blender_dims.x)
write_f32(f, blender_dims.y)
assert blender_dims.x > 0.0
assert blender_dims.y > 0.0
write_u64(f, len(placed_entities))
for e in placed_entities:
entity_name, blender_pos, blender_rotation, blender_scale = e
write_string(f, entity_name)
write_f32(f, blender_pos.x)
write_f32(f, blender_pos.y)
write_f32(f, blender_pos.z)
write_f32(f, blender_rotation.x)
write_f32(f, blender_rotation.y)
write_f32(f, blender_rotation.z)
write_f32(f, blender_scale.x)
write_f32(f, blender_scale.y)
write_f32(f, blender_scale.z)
# export all the meshes that the rooms file is referring to, and all the meshes that just need to be plain exported
for m in meshes_to_export:
assert o.type == "MESH"
assert o.parent == None, f"Mesh '{m.name}' has a parent, but exporting mesh objects with parents isn't supported"
mesh_name = m.name
image_filename = ensure_tex_saved_and_get_name(m)
output_filepath = bpy.path.abspath(f"//{EXPORT_DIRECTORY}/{mesh_name}.bin")
with open(output_filepath, "wb") as f:
write_b8(f, False) # if it's an armature or not, first byte of the file
write_string(f, image_filename)
bm = bmesh.new()
mesh = m.to_mesh()
assert len(mesh.uv_layers) == 1, f"Mesh object {m.name} has more than 1 uv layer, which isn't allowed"
bm.from_mesh(mesh)
bmesh.ops.triangulate(bm, faces=bm.faces)
bm.transform(mapping)
bm.to_mesh(mesh)
vertices = []
for polygon in mesh.polygons:
if len(polygon.loop_indices) == 3:
for loopIndex in polygon.loop_indices:
loop = mesh.loops[loopIndex]
position = mesh.vertices[loop.vertex_index].undeformed_co
uv = mesh.uv_layers.active.data[loop.index].uv
normal = loop.normal
vertices.append((position, uv))
write_u64(f, len(vertices))
for v_and_uv in vertices:
v, uv = v_and_uv
write_f32(f, v.x)
write_f32(f, v.y)
write_f32(f, v.z)
write_f32(f, uv.x)
write_f32(f, uv.y)
print("Success!")
export_armatures()
export_meshes_and_levels()

Binary file not shown.

225
main.c

@ -1250,14 +1250,37 @@ typedef struct CollisionCylinder
Circle bounds; Circle bounds;
} CollisionCylinder; } CollisionCylinder;
typedef struct typedef struct Room
{ {
Mesh *mesh_list; struct Room *next;
MD_String8 name;
PlacedMesh *placed_mesh_list; PlacedMesh *placed_mesh_list;
CollisionCylinder *collision_list; CollisionCylinder *collision_list;
PlacedEntity *placed_entity_list; PlacedEntity *placed_entity_list;
} Room;
typedef struct
{
Mesh *mesh_list;
Room *room_list;
} ThreeDeeLevel; } ThreeDeeLevel;
Room *get_cur_room(ThreeDeeLevel *level)
{
Room *in_room = 0;
for(Room *cur = level->room_list; cur; cur = cur->next)
{
if(MD_S8Match(cur->name, MD_S8Lit("Forest"), 0))
{
in_room = cur;
break;
}
}
assert(in_room);
return in_room;
}
void ser_BlenderTransform(SerState *ser, BlenderTransform *t) void ser_BlenderTransform(SerState *ser, BlenderTransform *t)
{ {
ser_Vec3(ser, &t->pos); ser_Vec3(ser, &t->pos);
@ -1298,112 +1321,120 @@ ThreeDeeLevel load_level(MD_Arena *arena, MD_String8 binary_file)
}; };
ThreeDeeLevel out = {0}; ThreeDeeLevel out = {0};
// placed meshes MD_u64 num_rooms = 0;
ser_MD_u64(&ser, &num_rooms);
for(MD_u64 i = 0; i < num_rooms; i++)
{ {
MD_u64 num_placed = 0; Room *new_room = MD_PushArray(arena, Room, 1);
ser_MD_u64(&ser, &num_placed); ser_MD_String8(&ser, &new_room->name, arena);
arena->align = 16; // SSE requires quaternions are 16 byte aligned
for(MD_u64 i = 0; i < num_placed; i++)
{
PlacedMesh *new_placed = MD_PushArray(arena, PlacedMesh, 1);
//PlacedMesh *new_placed = calloc(sizeof(PlacedMesh), 1);
ser_MD_String8(&ser, &new_placed->name, arena); // placed meshes
{
MD_u64 num_placed = 0;
ser_MD_u64(&ser, &num_placed);
arena->align = 16; // SSE requires quaternions are 16 byte aligned
for (MD_u64 i = 0; i < num_placed; i++)
{
PlacedMesh *new_placed = MD_PushArray(arena, PlacedMesh, 1);
// PlacedMesh *new_placed = calloc(sizeof(PlacedMesh), 1);
BlenderTransform blender_transform = {0}; ser_MD_String8(&ser, &new_placed->name, arena);
ser_BlenderTransform(&ser, &blender_transform);
new_placed->t = blender_to_game_transform(blender_transform); BlenderTransform blender_transform = {0};
ser_BlenderTransform(&ser, &blender_transform);
MD_StackPush(out.placed_mesh_list, new_placed); new_placed->t = blender_to_game_transform(blender_transform);
//Log("Placed mesh '%.*s' pos %f %f %f rotation %f %f %f %f scale %f %f %f\n", MD_S8VArg(placed_mesh_name), v3varg(new_placed->t.offset), qvarg(new_placed->t.rotation), v3varg(new_placed->t.scale)); MD_StackPush(new_room->placed_mesh_list, new_placed);
// load the mesh if we haven't already // Log("Placed mesh '%.*s' pos %f %f %f rotation %f %f %f %f scale %f %f %f\n", MD_S8VArg(placed_mesh_name), v3varg(new_placed->t.offset), qvarg(new_placed->t.rotation), v3varg(new_placed->t.scale));
bool mesh_found = false; // load the mesh if we haven't already
for(Mesh *cur = out.mesh_list; cur; cur = cur->next) bool mesh_found = false;
{ for (Mesh *cur = out.mesh_list; cur; cur = cur->next)
if(MD_S8Match(cur->name, new_placed->name, 0))
{ {
mesh_found = true; if (MD_S8Match(cur->name, new_placed->name, 0))
new_placed->draw_with = cur; {
assert(cur->name.size > 0); mesh_found = true;
break; new_placed->draw_with = cur;
assert(cur->name.size > 0);
break;
}
} }
}
if(!mesh_found) if (!mesh_found)
{
MD_String8 to_load_filepath = MD_S8Fmt(scratch.arena, "assets/exported_3d/%.*s.bin", MD_S8VArg(new_placed->name));
//Log("Loading mesh '%.*s'...\n", MD_S8VArg(to_load_filepath));
MD_String8 binary_mesh_file = MD_LoadEntireFile(scratch.arena, to_load_filepath);
if(!binary_mesh_file.str)
{
ser.cur_error = (SerError){.failed = true, .why = MD_S8Fmt(ser.error_arena, "Couldn't load file '%.*s'", to_load_filepath)};
}
else
{ {
Mesh *new_mesh = MD_PushArray(arena, Mesh, 1); MD_String8 to_load_filepath = MD_S8Fmt(scratch.arena, "assets/exported_3d/%.*s.bin", MD_S8VArg(new_placed->name));
*new_mesh = load_mesh(arena, binary_mesh_file, new_placed->name); // Log("Loading mesh '%.*s'...\n", MD_S8VArg(to_load_filepath));
MD_StackPush(out.mesh_list, new_mesh); MD_String8 binary_mesh_file = MD_LoadEntireFile(scratch.arena, to_load_filepath);
new_placed->draw_with = new_mesh; if (!binary_mesh_file.str)
{
ser.cur_error = (SerError){.failed = true, .why = MD_S8Fmt(ser.error_arena, "Couldn't load file '%.*s'", to_load_filepath)};
}
else
{
Mesh *new_mesh = MD_PushArray(arena, Mesh, 1);
*new_mesh = load_mesh(arena, binary_mesh_file, new_placed->name);
MD_StackPush(out.mesh_list, new_mesh);
new_placed->draw_with = new_mesh;
}
} }
} }
} }
}
MD_u64 num_collision_cubes; MD_u64 num_collision_cubes;
ser_MD_u64(&ser, &num_collision_cubes); ser_MD_u64(&ser, &num_collision_cubes);
for(MD_u64 i = 0; i < num_collision_cubes; i++) for (MD_u64 i = 0; i < num_collision_cubes; i++)
{
CollisionCylinder *new_cylinder = MD_PushArray(arena, CollisionCylinder, 1);
Vec2 twodee_pos;
Vec2 size;
ser_Vec2(&ser, &twodee_pos);
ser_Vec2(&ser, &size);
new_cylinder->bounds.center = twodee_pos;
new_cylinder->bounds.radius = (size.x + size.y) * 0.5f; // @TODO(Phillip): @Temporary
MD_StackPush(out.collision_list, new_cylinder);
}
// placed entities
{
MD_u64 num_placed = 0;
ser_MD_u64(&ser, &num_placed);
arena->align = 16; // SSE requires quaternions are 16 byte aligned
for(MD_u64 i = 0; i < num_placed; i++)
{ {
PlacedEntity *new_placed = MD_PushArray(arena, PlacedEntity, 1); CollisionCylinder *new_cylinder = MD_PushArray(arena, CollisionCylinder, 1);
MD_String8 placed_entity_name = {0}; Vec2 twodee_pos;
ser_MD_String8(&ser, &placed_entity_name, scratch.arena); Vec2 size;
ser_Vec2(&ser, &twodee_pos);
ser_Vec2(&ser, &size);
new_cylinder->bounds.center = twodee_pos;
new_cylinder->bounds.radius = (size.x + size.y) * 0.5f; // @TODO(Phillip): @Temporary
MD_StackPush(new_room->collision_list, new_cylinder);
}
bool found = false; // placed entities
ARR_ITER_I(CharacterGen, characters, kind) {
MD_u64 num_placed = 0;
ser_MD_u64(&ser, &num_placed);
arena->align = 16; // SSE requires quaternions are 16 byte aligned
for (MD_u64 i = 0; i < num_placed; i++)
{ {
if(MD_S8Match(MD_S8CString(it->enum_name), placed_entity_name, 0)) PlacedEntity *new_placed = MD_PushArray(arena, PlacedEntity, 1);
MD_String8 placed_entity_name = {0};
ser_MD_String8(&ser, &placed_entity_name, scratch.arena);
bool found = false;
ARR_ITER_I(CharacterGen, characters, kind)
{ {
found = true; if (MD_S8Match(MD_S8CString(it->enum_name), placed_entity_name, 0))
new_placed->npc_kind = kind; {
found = true;
new_placed->npc_kind = kind;
}
}
BlenderTransform blender_transform = {0};
ser_BlenderTransform(&ser, &blender_transform);
if (found)
{
new_placed->t = blender_to_game_transform(blender_transform);
MD_StackPush(new_room->placed_entity_list, new_placed);
}
else
{
ser.cur_error = (SerError){.failed = true, .why = MD_S8Fmt(arena, "Couldn't find placed npc kind '%.*s'...\n", MD_S8VArg(placed_entity_name))};
} }
}
BlenderTransform blender_transform = {0};
ser_BlenderTransform(&ser, &blender_transform);
if(found)
{
new_placed->t = blender_to_game_transform(blender_transform);
MD_StackPush(out.placed_entity_list, new_placed);
}
else
{
ser.cur_error = (SerError){.failed = true, .why = MD_S8Fmt(arena, "Couldn't find placed npc kind '%.*s'...\n", MD_S8VArg(placed_entity_name))};
}
//Log("Loaded placed entity '%.*s' at %f %f %f\n", MD_S8VArg(placed_entity_name), v3varg(new_placed->t.offset)); // Log("Loaded placed entity '%.*s' at %f %f %f\n", MD_S8VArg(placed_entity_name), v3varg(new_placed->t.offset));
}
} }
}
MD_StackPush(out.room_list, new_room);
}
assert(!ser.cur_error.failed); assert(!ser.cur_error.failed);
MD_ReleaseScratch(scratch); MD_ReleaseScratch(scratch);
@ -2095,8 +2126,10 @@ void initialize_gamestate_from_threedee_level(GameState *gs, ThreeDeeLevel *leve
{ {
memset(gs, 0, sizeof(GameState)); memset(gs, 0, sizeof(GameState));
Room *in_room = get_cur_room(level);
bool found_player = false; bool found_player = false;
for(PlacedEntity *cur = level->placed_entity_list; cur; cur = cur->next) for(PlacedEntity *cur = in_room->placed_entity_list; cur; cur = cur->next)
{ {
Entity *cur_entity = new_entity(gs); Entity *cur_entity = new_entity(gs);
cur_entity->npc_kind = cur->npc_kind; cur_entity->npc_kind = cur->npc_kind;
@ -3168,7 +3201,6 @@ Armature *armatures[] = {
&angel_armature, &angel_armature,
}; };
Mesh mesh_player = {0};
Mesh mesh_simple_worm = {0}; Mesh mesh_simple_worm = {0};
Mesh mesh_shotgun = {0}; Mesh mesh_shotgun = {0};
@ -3251,17 +3283,14 @@ void init(void)
MD_String8 binary_file; MD_String8 binary_file;
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/level.bin")); binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/rooms.bin"));
level_threedee = load_level(persistent_arena, binary_file); level_threedee = load_level(persistent_arena, binary_file);
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/ExportedWithAnims.bin")); binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/Shotgun.bin"));
mesh_player = load_mesh(persistent_arena, binary_file, MD_S8Lit("ExportedWithAnims.bin")); mesh_shotgun = load_mesh(persistent_arena, binary_file, MD_S8Lit("Shotgun.bin"));
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/ShotgunMesh.bin"));
mesh_shotgun = load_mesh(persistent_arena, binary_file, MD_S8Lit("ShotgunMesh.bin"));
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/ArmatureExportedWithAnims.bin")); binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/NormalGuyArmature.bin"));
player_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("ArmatureExportedWithAnims.bin")); player_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("NormalGuyArmature.bin"));
man_in_black_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Man In Black")); man_in_black_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Man In Black"));
man_in_black_armature.image = image_man_in_black; man_in_black_armature.image = image_man_in_black;
@ -3269,8 +3298,8 @@ void init(void)
angel_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Angel")); angel_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Angel"));
angel_armature.image = image_angel; angel_armature.image = image_angel;
binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/Farmer.bin")); binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/FarmerArmature.bin"));
farmer_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Farmer.bin")); farmer_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("FarmerArmature.bin"));
shifted_farmer_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Farmer.bin")); shifted_farmer_armature = load_armature(persistent_arena, binary_file, MD_S8Lit("Farmer.bin"));
shifted_farmer_armature.image = image_shifted_farmer; shifted_farmer_armature.image = image_shifted_farmer;
@ -4517,7 +4546,7 @@ Vec2 move_and_slide(MoveSlideParams p)
BUFF(CollisionObj, 256) to_check = { 0 }; BUFF(CollisionObj, 256) to_check = { 0 };
// add world boxes // add world boxes
for(CollisionCylinder *cur = level_threedee.collision_list; cur; cur = cur->next) for(CollisionCylinder *cur = get_cur_room(&level_threedee)->collision_list; cur; cur = cur->next)
{ {
BUFF_APPEND(&to_check, ((CollisionObj){cur->bounds, gs.world_entity})); BUFF_APPEND(&to_check, ((CollisionObj){cur->bounds, gs.world_entity}));
} }
@ -5632,7 +5661,7 @@ void frame(void)
// @Place(draw 3d things) // @Place(draw 3d things)
for(PlacedMesh *cur = level_threedee.placed_mesh_list; cur; cur = cur->next) for(PlacedMesh *cur = get_cur_room(&level_threedee)->placed_mesh_list; cur; cur = cur->next)
{ {
float seed = (float)((int64_t)cur % 1024); float seed = (float)((int64_t)cur % 1024);

@ -1,9 +1,9 @@
Urgent: Urgent:
- room system where characters can go to rooms. camera constrained to room bounds, and know which rooms are near them to go to
- REALLY look into characters not being able to shoot eachother - REALLY look into characters not being able to shoot eachother
- Silent threshold for each character from 0 to 1 that controls random chance after every eavesdropped dialog that the character says nothing in response - Silent threshold for each character from 0 to 1 that controls random chance after every eavesdropped dialog that the character says nothing in response
- Tag memories from drama as permanent so they're never forgotten and always seed character behavior - Tag memories from drama as permanent so they're never forgotten and always seed character behavior
- gman character that gives you randomized goals such as (kill so and so) or (befriend blank) or (get john to go to other room), and decides itself with gpt when you're done with the game, when you've 'learned your lesson'. - gman character that gives you randomized goals such as (kill so and so) or (befriend blank) or (get john to go to other room), and decides itself with gpt when you're done with the game, when you've 'learned your lesson'.
- room system where characters can go to rooms. camera constrained to room bounds, and know which rooms are near them to go to
- make them understand they're holding their shotgun more - make them understand they're holding their shotgun more
- Fix everything about the shotgun, put shotgun away - Fix everything about the shotgun, put shotgun away
- can't interact with characters while they're thinking - can't interact with characters while they're thinking

Loading…
Cancel
Save