diff --git a/Chapter02/AnimSpriteComponent.h b/Chapter02/AnimSpriteComponent.h index d5aa387a..e60939dc 100644 --- a/Chapter02/AnimSpriteComponent.h +++ b/Chapter02/AnimSpriteComponent.h @@ -13,7 +13,7 @@ class AnimSpriteComponent : public SpriteComponent { public: AnimSpriteComponent(class Actor* owner, int drawOrder = 100); - // Update animation every frame (overriden from component) + // Update animation every frame (overridden from component) void Update(float deltaTime) override; // Set the textures used for animation void SetAnimTextures(const std::vector& textures); diff --git a/Chapter02/BGSpriteComponent.h b/Chapter02/BGSpriteComponent.h index 0d4c9e50..20df1a23 100644 --- a/Chapter02/BGSpriteComponent.h +++ b/Chapter02/BGSpriteComponent.h @@ -15,7 +15,7 @@ class BGSpriteComponent : public SpriteComponent public: // Set draw order to default to lower (so it's in the background) BGSpriteComponent(class Actor* owner, int drawOrder = 10); - // Update/draw overriden from parent + // Update/draw overridden from parent void Update(float deltaTime) override; void Draw(SDL_Renderer* renderer) override; // Set the textures used for the background diff --git a/Chapter04/Search.cpp b/Chapter04/Search.cpp index 56a83598..ce1e6f41 100644 --- a/Chapter04/Search.cpp +++ b/Chapter04/Search.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include struct GraphNode { diff --git a/Chapter07/SoundEvent.h b/Chapter07/SoundEvent.h index abb88bc9..ae78ac8a 100644 --- a/Chapter07/SoundEvent.h +++ b/Chapter07/SoundEvent.h @@ -16,7 +16,7 @@ class SoundEvent SoundEvent(); // Returns true if associated FMOD event still exists bool IsValid(); - // Restart event from begining + // Restart event from beginning void Restart(); // Stop this event void Stop(bool allowFadeOut = true); diff --git a/Chapter08/InputSystem.cpp b/Chapter08/InputSystem.cpp index da787cf9..87cfdcb1 100644 --- a/Chapter08/InputSystem.cpp +++ b/Chapter08/InputSystem.cpp @@ -43,7 +43,7 @@ ButtonState KeyboardState::GetKeyState(SDL_Scancode keyCode) const bool MouseState::GetButtonValue(int button) const { - return (SDL_BUTTON(button) & mCurrButtons) == 1; + return (SDL_BUTTON(button) & mCurrButtons); } ButtonState MouseState::GetButtonState(int button) const diff --git a/Chapter09/SoundEvent.h b/Chapter09/SoundEvent.h index abb88bc9..ae78ac8a 100644 --- a/Chapter09/SoundEvent.h +++ b/Chapter09/SoundEvent.h @@ -16,7 +16,7 @@ class SoundEvent SoundEvent(); // Returns true if associated FMOD event still exists bool IsValid(); - // Restart event from begining + // Restart event from beginning void Restart(); // Stop this event void Stop(bool allowFadeOut = true); diff --git a/Chapter10/PhysWorld.cpp b/Chapter10/PhysWorld.cpp index 6db12580..5fe45d34 100644 --- a/Chapter10/PhysWorld.cpp +++ b/Chapter10/PhysWorld.cpp @@ -33,6 +33,7 @@ bool PhysWorld::SegmentCast(const LineSegment& l, CollisionInfo& outColl) // Is this closer than previous intersection? if (t < closestT) { + closestT = t; outColl.mPoint = l.PointOnSegment(t); outColl.mNormal = norm; outColl.mBox = box; diff --git a/Chapter10/PhysWorld.h b/Chapter10/PhysWorld.h index db392cb6..62563c29 100644 --- a/Chapter10/PhysWorld.h +++ b/Chapter10/PhysWorld.h @@ -26,7 +26,7 @@ class PhysWorld Vector3 mNormal; // Component collided with class BoxComponent* mBox; - // Owning actor of componnet + // Owning actor of component class Actor* mActor; }; diff --git a/Chapter10/SoundEvent.h b/Chapter10/SoundEvent.h index abb88bc9..ae78ac8a 100644 --- a/Chapter10/SoundEvent.h +++ b/Chapter10/SoundEvent.h @@ -16,7 +16,7 @@ class SoundEvent SoundEvent(); // Returns true if associated FMOD event still exists bool IsValid(); - // Restart event from begining + // Restart event from beginning void Restart(); // Stop this event void Stop(bool allowFadeOut = true); diff --git a/Chapter11/PhysWorld.cpp b/Chapter11/PhysWorld.cpp index 6db12580..5fe45d34 100644 --- a/Chapter11/PhysWorld.cpp +++ b/Chapter11/PhysWorld.cpp @@ -33,6 +33,7 @@ bool PhysWorld::SegmentCast(const LineSegment& l, CollisionInfo& outColl) // Is this closer than previous intersection? if (t < closestT) { + closestT = t; outColl.mPoint = l.PointOnSegment(t); outColl.mNormal = norm; outColl.mBox = box; diff --git a/Chapter11/PhysWorld.h b/Chapter11/PhysWorld.h index db392cb6..62563c29 100644 --- a/Chapter11/PhysWorld.h +++ b/Chapter11/PhysWorld.h @@ -26,7 +26,7 @@ class PhysWorld Vector3 mNormal; // Component collided with class BoxComponent* mBox; - // Owning actor of componnet + // Owning actor of component class Actor* mActor; }; diff --git a/Chapter11/SoundEvent.h b/Chapter11/SoundEvent.h index abb88bc9..ae78ac8a 100644 --- a/Chapter11/SoundEvent.h +++ b/Chapter11/SoundEvent.h @@ -16,7 +16,7 @@ class SoundEvent SoundEvent(); // Returns true if associated FMOD event still exists bool IsValid(); - // Restart event from begining + // Restart event from beginning void Restart(); // Stop this event void Stop(bool allowFadeOut = true); diff --git a/Chapter12/PhysWorld.cpp b/Chapter12/PhysWorld.cpp index 6db12580..5fe45d34 100644 --- a/Chapter12/PhysWorld.cpp +++ b/Chapter12/PhysWorld.cpp @@ -33,6 +33,7 @@ bool PhysWorld::SegmentCast(const LineSegment& l, CollisionInfo& outColl) // Is this closer than previous intersection? if (t < closestT) { + closestT = t; outColl.mPoint = l.PointOnSegment(t); outColl.mNormal = norm; outColl.mBox = box; diff --git a/Chapter12/PhysWorld.h b/Chapter12/PhysWorld.h index db392cb6..62563c29 100644 --- a/Chapter12/PhysWorld.h +++ b/Chapter12/PhysWorld.h @@ -26,7 +26,7 @@ class PhysWorld Vector3 mNormal; // Component collided with class BoxComponent* mBox; - // Owning actor of componnet + // Owning actor of component class Actor* mActor; }; diff --git a/Chapter12/SoundEvent.h b/Chapter12/SoundEvent.h index abb88bc9..ae78ac8a 100644 --- a/Chapter12/SoundEvent.h +++ b/Chapter12/SoundEvent.h @@ -16,7 +16,7 @@ class SoundEvent SoundEvent(); // Returns true if associated FMOD event still exists bool IsValid(); - // Restart event from begining + // Restart event from beginning void Restart(); // Stop this event void Stop(bool allowFadeOut = true); diff --git a/Chapter13/PhysWorld.cpp b/Chapter13/PhysWorld.cpp index 6db12580..5fe45d34 100644 --- a/Chapter13/PhysWorld.cpp +++ b/Chapter13/PhysWorld.cpp @@ -33,6 +33,7 @@ bool PhysWorld::SegmentCast(const LineSegment& l, CollisionInfo& outColl) // Is this closer than previous intersection? if (t < closestT) { + closestT = t; outColl.mPoint = l.PointOnSegment(t); outColl.mNormal = norm; outColl.mBox = box; diff --git a/Chapter13/PhysWorld.h b/Chapter13/PhysWorld.h index db392cb6..62563c29 100644 --- a/Chapter13/PhysWorld.h +++ b/Chapter13/PhysWorld.h @@ -26,7 +26,7 @@ class PhysWorld Vector3 mNormal; // Component collided with class BoxComponent* mBox; - // Owning actor of componnet + // Owning actor of component class Actor* mActor; }; diff --git a/Chapter13/Renderer.cpp b/Chapter13/Renderer.cpp index dd817dc5..3d461b9e 100644 --- a/Chapter13/Renderer.cpp +++ b/Chapter13/Renderer.cpp @@ -444,7 +444,7 @@ void Renderer::DrawFromGBuffer() // Set the point light shader and mesh as active mGPointLightShader->SetActive(); mPointLightMesh->GetVertexArray()->SetActive(); - // Set the view-projeciton matrix + // Set the view-projection matrix mGPointLightShader->SetMatrixUniform("uViewProj", mView * mProjection); // Set the G-buffer textures for sampling diff --git a/Chapter13/SoundEvent.h b/Chapter13/SoundEvent.h index abb88bc9..ae78ac8a 100644 --- a/Chapter13/SoundEvent.h +++ b/Chapter13/SoundEvent.h @@ -16,7 +16,7 @@ class SoundEvent SoundEvent(); // Returns true if associated FMOD event still exists bool IsValid(); - // Restart event from begining + // Restart event from beginning void Restart(); // Stop this event void Stop(bool allowFadeOut = true); diff --git a/Chapter13/Texture.cpp b/Chapter13/Texture.cpp index cedbceed..053a36f5 100644 --- a/Chapter13/Texture.cpp +++ b/Chapter13/Texture.cpp @@ -57,7 +57,7 @@ bool Texture::Load(const std::string& fileName) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - // Enable aniostropic filtering, if supported + // Enable anisotropic filtering, if supported if (GLEW_EXT_texture_filter_anisotropic) { // Get the maximum anisotropy value diff --git a/Chapter14/Actor.h b/Chapter14/Actor.h index 47308f6c..498db989 100644 --- a/Chapter14/Actor.h +++ b/Chapter14/Actor.h @@ -91,7 +91,7 @@ class Actor return t; } - // Search throuch component vector for one of type + // Search through component vector for one of type Component* GetComponentOfType(Component::TypeID type) { Component* comp = nullptr; diff --git a/Chapter14/PhysWorld.cpp b/Chapter14/PhysWorld.cpp index 6db12580..5fe45d34 100644 --- a/Chapter14/PhysWorld.cpp +++ b/Chapter14/PhysWorld.cpp @@ -33,6 +33,7 @@ bool PhysWorld::SegmentCast(const LineSegment& l, CollisionInfo& outColl) // Is this closer than previous intersection? if (t < closestT) { + closestT = t; outColl.mPoint = l.PointOnSegment(t); outColl.mNormal = norm; outColl.mBox = box; diff --git a/Chapter14/PhysWorld.h b/Chapter14/PhysWorld.h index db392cb6..62563c29 100644 --- a/Chapter14/PhysWorld.h +++ b/Chapter14/PhysWorld.h @@ -26,7 +26,7 @@ class PhysWorld Vector3 mNormal; // Component collided with class BoxComponent* mBox; - // Owning actor of componnet + // Owning actor of component class Actor* mActor; }; diff --git a/Chapter14/Renderer.cpp b/Chapter14/Renderer.cpp index dc51f13b..d641c23a 100644 --- a/Chapter14/Renderer.cpp +++ b/Chapter14/Renderer.cpp @@ -439,7 +439,7 @@ void Renderer::DrawFromGBuffer() // Set the point light shader and mesh as active mGPointLightShader->SetActive(); mPointLightMesh->GetVertexArray()->SetActive(); - // Set the view-projeciton matrix + // Set the view-projection matrix mGPointLightShader->SetMatrixUniform("uViewProj", mView * mProjection); // Set the G-buffer textures for sampling diff --git a/Chapter14/SoundEvent.h b/Chapter14/SoundEvent.h index abb88bc9..ae78ac8a 100644 --- a/Chapter14/SoundEvent.h +++ b/Chapter14/SoundEvent.h @@ -16,7 +16,7 @@ class SoundEvent SoundEvent(); // Returns true if associated FMOD event still exists bool IsValid(); - // Restart event from begining + // Restart event from beginning void Restart(); // Stop this event void Stop(bool allowFadeOut = true); diff --git a/Chapter14/Texture.cpp b/Chapter14/Texture.cpp index 18feab2a..1534e53f 100644 --- a/Chapter14/Texture.cpp +++ b/Chapter14/Texture.cpp @@ -58,7 +58,7 @@ bool Texture::Load(const std::string& fileName) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - // Enable aniostropic filtering, if supported + // Enable anisotropic filtering, if supported if (GLEW_EXT_texture_filter_anisotropic) { // Get the maximum anisotropy value diff --git a/Errata.md b/Errata.md index eac69684..3c94c0e0 100644 --- a/Errata.md +++ b/Errata.md @@ -2,12 +2,15 @@ Here are the known errors in the text of the book. If you notice any further errors, please create an issue on this GitHub repository. +* Chapter 2 + - Page 42: The loop over mActors is actually in Game::UnloadData, which is then called from Game::Shutdown + (found by Kevin Runge) * Chapter 3 - Page 67: The return type of Actor::GetForward should be Vector2 (found by Takashi Imagire) - Page 70: When discussing the properties of the dot product, the text incorrectly states that the dot product is associative. It is not associative since the first dot product results in a scalar, and thus it is impossible to perform a second dot product. However, - scalar multiplication is assocaitive over the dot product, since s (a dot b) = (s a) dot b. + scalar multiplication is associative over the dot product, since s (a dot b) = (s a) dot b. (found by Takashi Imagire) * Chapter 4 - Page 101: The last sentence should read "In this case, if you access outMap and the node requested @@ -17,6 +20,12 @@ please create an issue on this GitHub repository. * Chapter 6 - Page 199: The reference to "horizontal" field of view should say "vertical" field of view, since it affects the y-component (found by Takashi Imagire) +* Chapter 7 + - Page 386: The virtual position of sound equation yields a vector rather than a position (The virtual position + of the sound is obtained by adding the camera position to that vector). This equation is required for Ex 7.2 + (found by Joshua Hardman) +* Chapter 10 + - The PhysWorld::SegmentCast function is missing code to update closestT, which is fixed in the source in this repo. * Chapter 13 - Page 396: In the equations for bilinear interpolation, the vFactor component should access the .v - components of P, A, and C, not the .u components (found by Takashi Imagire) \ No newline at end of file + components of P, A, and C, not the .u components (found by Takashi Imagire) diff --git a/Exporter/Blender/gpmesh_export.py b/Exporter/Blender/gpmesh_export.py deleted file mode 100644 index db805265..00000000 --- a/Exporter/Blender/gpmesh_export.py +++ /dev/null @@ -1,122 +0,0 @@ -import bpy - -def write_gpmesh(context, filepath, vertFormat): - # Get current object - obj = bpy.context.scene.objects.active - if obj.type == 'MESH': - mesh = obj.data - f = open(filepath, 'w', encoding='utf-8') - f.write("{\n") - f.write("\t\"version\":1,\n") - f.write("\t\"vertexformat\":\"" + vertFormat + "\",\n") - f.write("\t\"shader\":\"BasicMesh\",\n") - - # For now, only one texture - # figure out the file name... - texname = filepath.split("\\")[-1].split("/")[-1].split(".")[0] - # Make it a png - texname += ".png" - f.write("\t\"textures\":[\n") - f.write("\t\t\"" + texname + "\"\n") - f.write("\t],\n") - - # specular power - f.write("\t\"specularPower\":100,\n") - - # vertices - # We have to create our own storage for because uvs are stored separately - verts = [dict() for x in range(len(mesh.vertices))] - for v in mesh.vertices: - verts[v.index]["pos"] = v.co - verts[v.index]["norm"] = v.normal - - for l in mesh.loops: - verts[l.vertex_index]["uv"] = mesh.uv_layers.active.data[l.index].uv - - f.write("\t\"vertices\":[\n") - first = True - for v in verts: - if first: - f.write("\t\t[") - first = False - else: - f.write(",\n\t\t[") - f.write("%f,%f,%f," % (v["pos"].x, v["pos"].y, v["pos"].z)) - f.write("%f,%f,%f," % (v["norm"].x, v["norm"].y, v["norm"].z)) - f.write("%f,%f" % (v["uv"].x, -1.0 * v["uv"].y)) - f.write("]") - f.write("\n\t],\n") - - # indices - f.write("\t\"indices\":[\n") - first = True - for p in mesh.polygons: - if first: - f.write("\t\t") - first = False - else: - f.write(",\n\t\t") - f.write("[%d,%d,%d]" % (p.vertices[0], p.vertices[1], p.vertices[2])) - f.write("\n\t]\n") - - f.write("}\n") - f.close() - else: - raise ValueError("No mesh selected") - - return {'FINISHED'} - - -# ExportHelper is a helper class, defines filename and -# invoke() function which calls the file selector. -from bpy_extras.io_utils import ExportHelper -from bpy.props import StringProperty, BoolProperty, EnumProperty -from bpy.types import Operator - - -class ExportGPMesh(Operator, ExportHelper): - """Export to Game Programming in C++ mesh format""" - bl_idname = "export_test.gpmesh" # important since its how bpy.ops.import_test.some_data is constructed - bl_label = "Export Mesh" - - # ExportHelper mixin class uses this - filename_ext = ".gpmesh" - - filter_glob = StringProperty( - default="*.gpmesh", - options={'HIDDEN'}, - maxlen=255, # Max internal buffer length, longer would be clamped. - ) - - vertFormat = EnumProperty( - name="Vertex Format", - description="Choose the vertex format", - items=(('PosNormTex', "PosNormTex", "Position, normal, tex coord"), - ('PosTex', "PosTex", "Position, tex coord")), - default='PosNormTex', - ) - - def execute(self, context): - return write_gpmesh(context, self.filepath, self.vertFormat) - - -# Only needed if you want to add into a dynamic menu -def menu_func_export(self, context): - self.layout.operator(ExportGPMesh.bl_idname, text="GPMesh Exporter (.gpmesh)") - - -def register(): - bpy.utils.register_class(ExportGPMesh) - bpy.types.INFO_MT_file_export.append(menu_func_export) - - -def unregister(): - bpy.utils.unregister_class(ExportGPMesh) - bpy.types.INFO_MT_file_export.remove(menu_func_export) - - -if __name__ == "__main__": - register() - - # test call - bpy.ops.export_test.gpmesh('INVOKE_DEFAULT') diff --git a/Exporter/Blender/gpmesh_export_v2.py b/Exporter/Blender/gpmesh_export_v2.py new file mode 100644 index 00000000..5b007e6b --- /dev/null +++ b/Exporter/Blender/gpmesh_export_v2.py @@ -0,0 +1,320 @@ +# Usage: +# +# - You should make a copy of your .blend file eg. 'my-scene-export.blend'! +# +# - The armature's pivot must be the same as the meshe's it is attached to. +# +# - The animation must have at least 2 keyframes and start at position 1. +# +# - UVs are stored per face in Blender. The GPmesh format expects to have _one_ UV per vertex. +# Blender can easily have multiple UV coordinates assigned to the same vertex-index. Thus, +# in order to get UVs exported correctly, vertices must be dublicated. This can be done +# in the following way: +# 1.) Go into edit mode and select all edges. +# 2.) From the menu choose Edge -> Mark Sharp. +# 3.) Switch into object mode and assign the 'Edge Split' modifier. Apply that. Done. +# +# - The engine of the book expects all polygons to be a triangle. So if your mesh has +# n-gons, with n > 3, you have to apply the 'Triangulate' modifier or split your polygons manually. + +bl_info = { + "name": "gpmesh Exporter", + "blender": (3,00,0), + "category": "Export", + "author": "Michael Eggers", + "description": "GPmesh exporter for the book Game Programming in C++" +} + +import bpy +import json + +def generate_gpmesh_json(): + mesh = bpy.context.active_object.data + uv_layer = mesh.uv_layers.active.data + + gpmesh = { + "version": 1, + "vertexformat": "PosNormSkinTex", + "shader": "Skinned", + "textures": [], + "specularPower": 100.0, + "vertices": [], + "indices": [] + } + + for vert in mesh.vertices: + pos = vert.co + normal = vert.normal + gp_vert = [] + gp_vert.extend([pos.y, pos.x, pos.z]) + gp_vert.extend([normal.y, normal.x, normal.z]) + + # get bone indices and their weights that affect this vertex and sort them by weight from high to low + boneToWeightTuples = [] + for group in vert.groups: + u8_weight = int(group.weight * 255) + boneToWeightTuples.append((group.group, u8_weight)) + boneToWeightTuples.sort(key=lambda boneToWeight : boneToWeight[1], reverse=True) + + # Only keep first 4 bones with their weights. As we sorted them by their bone weight (from high to low) + # before, we only keep the once with highest influence. + boneToWeightTuples = boneToWeightTuples[:4] + + # The file format expects always 4 bones. + while len(boneToWeightTuples) < 4: + boneToWeightTuples.append((0, 0)) + + boneIndices = [] + weights = [] + for boneIdx, weight in boneToWeightTuples: + boneIndices.append(boneIdx) + weights.append(weight) + + gp_vert.extend(boneIndices) + gp_vert.extend(weights) + + gpmesh["vertices"].append(gp_vert) + + # UVs are stored separately, because even if multiple vertices share the same pos/normal/.. + # they can have easily have completely differen UVs! + for l in mesh.loops: + uv = uv_layer[l.index].uv + if len(gpmesh["vertices"][l.vertex_index]) <= 14: + gpmesh["vertices"][l.vertex_index].extend([uv.x, -uv.y]) + # print(vert_idx, loop_idx, uv) + + for poly in mesh.polygons: + tri = [] + for loop_index in poly.loop_indices: + vertIndex = mesh.loops[loop_index].vertex_index + tri.append(vertIndex) + + gpmesh["indices"].append(tri) + + textures = [] + materialSlots = bpy.context.active_object.material_slots + for matSlot in materialSlots: + if matSlot.material: + if matSlot.material.node_tree: + for node in matSlot.material.node_tree.nodes: + if node.type == 'TEX_IMAGE': + textures.append("Assets/" + node.image.name) + + gpmesh["textures"] = textures + + return gpmesh + +def find_armature(active_object): + armature = active_object + while armature.parent and armature.type != 'ARMATURE': + armature = armature.parent + + if armature.type == 'ARMATURE': + return armature + return None + +def generate_gpskel_json(): + gpskel = { + "version": 1, + "bonecount": 0, + "bones": [] + } + boneInfos = [] + + armature = find_armature(bpy.context.active_object) + if armature: + armature = armature.data + for i, bone in enumerate(armature.bones): + parentBone = bone.parent + parentIndex = -1 + if parentBone: + parentIndex = armature.bones.find(parentBone.name) + + # local_matrix = (bone.matrix_local if bone.parent is None else bone.parent.matrix_local.inverted() * bone.matrix_local) + local_matrix = bone.matrix_local + if bone.parent: + local_matrix = bone.parent.matrix_local.inverted() @ bone.matrix_local + rot = local_matrix.to_quaternion().inverted() + trans = local_matrix.to_translation() + + boneInfo = { + "name": bone.name, + "index": i, + "parent": parentIndex, + "bindpose": { + "rot": [rot.y, rot.x, rot.z, rot.w], + "trans": [trans.y, trans.x, trans.z] + } + } + boneInfos.append(boneInfo) + + gpskel["bonecount"] = len(armature.bones) + + gpskel["bones"] = boneInfos + + return gpskel + +def local_matrices_for_frame(root, rootMat): + local_matrices = [] + for child in root.children_recursive: + if child.parent.name == root.name: + localTransform = rootMat.inverted() @ child.matrix_basis + local_matrices.append(localTransform) + local_matrices.extend(local_matrices_for_frame(child, localTransform)) + + return local_matrices + +def generate_gpanim_json(action): + gpanim = { + "version": 1, + "sequence": { + "frames": 0, + "length": 1.0, # TODO: Calculate from framerate + "bonecount": 0, + "tracks": [] + } + } + active_object = bpy.context.active_object + # active_object.animation_data.action = action + armature = find_armature(active_object) + armature.animation_data.action = action + frame_start, frame_end = int(action.frame_range.x), int(action.frame_range.y) + gpanim["sequence"]["frames"] = frame_end - 1 # TODO: Hacky (engine expects duplicate of first keyframe at the end but should not count as one) + gpanim["sequence"]["bonecount"] = len(armature.data.bones) + + for i, bones in enumerate(armature.data.bones): + gpanim["sequence"]["tracks"].append({ "bone": i, "transforms": [] }) + + for frame in range(frame_start, frame_end): + bpy.context.scene.frame_set(frame) + + localMat = armature.pose.bones[i].matrix + if armature.pose.bones[i].parent: + localMat = armature.pose.bones[i].parent.matrix.inverted() @ armature.pose.bones[i].matrix + rot = localMat.to_quaternion().inverted() + trans = localMat.to_translation() + gpanim["sequence"]["tracks"][i]["transforms"].append({ "rot": [rot.y, rot.x, rot.z, rot.w], "trans": [trans.y, trans.x, trans.z] }) + + + + # for frame in range(frame_start, frame_end): + # bpy.context.scene.frame_set(frame) + + # rootBone = armature.pose.bones["Root"] + # rootTransform = rootBone.matrix + + # pose = [ rootTransform ] # a pose contains all the bones transforms for this particular frame + + # # now add children to the pose + # pose.extend(local_matrices_for_frame(rootBone, rootBone.matrix)) + + # print(len(pose)) + # print(pose) + + # # add them to the tracks + # for i, localMat in enumerate(pose): + # rot = localMat.to_quaternion() + # trans = localMat.to_translation() + # gpanim["sequence"]["tracks"][i]["transforms"].append({ "rot": [rot.y, rot.x, rot.z, rot.w], "trans": [trans.y, trans.x, trans.z] }) + + + return gpanim + +def write_to_disk(context, filepath, export_gpmesh, export_gpskel, export_gpanim): + print("exporting to gpmesh...") + + if export_gpmesh: + gpmesh = generate_gpmesh_json() + f = open(filepath, 'w', encoding='utf-8') + # f.write("Hello World %s" % use_some_setting) + f.write(json.dumps(gpmesh, sort_keys=False, indent=2)) + f.close() + + if export_gpskel: + gpskel = generate_gpskel_json() + gpskel_filepath = filepath.split(".")[0] + '.gpskel' + f = open(gpskel_filepath, "w", encoding="utf-8") + f.write(json.dumps(gpskel, sort_keys=False, indent=2)) + f.close() + + if export_gpanim: + print("EXPORT GPANIM") + actions = bpy.data.actions + print(actions) + for action in actions: + gpanim = generate_gpanim_json(action) + gpanim_filepath = filepath.split(".")[0] + str(action.name) + '.gpanim' + f = open(gpanim_filepath, "w", encoding="utf-8") + f.write(json.dumps(gpanim, sort_keys=False, indent=2)) + f.close() + + print("Done!") + + return {'FINISHED'} + + +# ExportHelper is a helper class, defines filename and +# invoke() function which calls the file selector. +from bpy_extras.io_utils import ExportHelper +from bpy.props import StringProperty, BoolProperty, EnumProperty +from bpy.types import Operator + + +class ExportGPMESH(Operator, ExportHelper): + """Export mesh in gpmesh format.""" + bl_idname = "export.gpmesh" # important since its how bpy.ops.import_test.some_data is constructed + bl_label = "Export as gpmesh" + + # ExportHelper mixin class uses this + filename_ext = ".gpmesh" + + filter_glob: StringProperty( + default="*.gpmesh", + options={'HIDDEN'}, + maxlen=255, # Max internal buffer length, longer would be clamped. + ) + + # List of operator properties, the attributes will be assigned + # to the class instance from the operator settings before calling. + export_gpmesh: BoolProperty( + name="Export gpmesh", + description="Writes the mesh as .gpmesh to disk", + default=True + ) + + export_gpskel: BoolProperty( + name="Export gpskel", + description="Writes .gpskel file to disk", + default=True + ) + + export_gpanim: BoolProperty( + name="Export gpanim", + description="Writes .gpanim file to disk", + default=True + ) + + def execute(self, context): + return write_to_disk(context, self.filepath, self.export_gpmesh, self.export_gpskel, self.export_gpanim) + + +# Only needed if you want to add into a dynamic menu +def menu_func_export(self, context): + self.layout.operator(ExportGPMESH.bl_idname, text="gpmesh") + +# Register and add to the "file selector" menu (required to use F3 search "Text Export Operator" for quick access) +def register(): + bpy.utils.register_class(ExportGPMESH) + bpy.types.TOPBAR_MT_file_export.append(menu_func_export) + + +def unregister(): + bpy.utils.unregister_class(ExportGPMESH) + bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) + + +if __name__ == "__main__": + register() + + # test call + bpy.ops.export.gpmesh('INVOKE_DEFAULT') diff --git a/Exporter/GPMeshExporter/GPMeshExporter.uplugin b/Exporter/GPMeshExporter/GPMeshExporter.uplugin new file mode 100644 index 00000000..1a56ad6e --- /dev/null +++ b/Exporter/GPMeshExporter/GPMeshExporter.uplugin @@ -0,0 +1,18 @@ +{ + "FileVersion": 3, + "FriendlyName": "Game Programming in C++ Mesh Exporter", + "Version": 1, + "VersionName": "1.0", + "CreatedBy": "", + "CreatedByURL": "", + "Category": "Other", + "Description": "Exports meshes, skeletons, and animations to Game Programming in C++ Formats", + "EnabledByDefault": false, + "Modules": [ + { + "Name": "GPMeshExporter", + "Type": "Developer", + "LoadingPhase": "Default" + } + ] +} \ No newline at end of file diff --git a/Exporter/GPMeshExporter/README.md b/Exporter/GPMeshExporter/README.md new file mode 100644 index 00000000..6cdffd08 --- /dev/null +++ b/Exporter/GPMeshExporter/README.md @@ -0,0 +1,49 @@ +# Game Programming in C++ Unreal Engine 4 Exporter +This plugin allows you to export static meshes, skeletal meshes, skeletons, and animations +from the Unreal Engine 4 format into the Game Programming in C++ format. + +This plugin has been verified to work in Unreal Engine 4.22.3. It likely will work in versions +as early as Unreal Engine 4.19.x, though it has not been tested. + +This code is free to use, but the copyright of the code is maintained by Epic Games, and you +must agree to the Unreal Engine license separately in order to use this plugin. + +# Using the plugin +To use this plugin, follow these steps: +1. Make sure the Unreal 4 project you want to use is setup to work with C++ +2. In the C++ Unreal 4 project directory, create a "Plugins" directory +3. Place the "GPMeshExporter" directory inside "Plugins" +4. Right click on your .uproject and regenerate the project files +5. Now in the editor, you can right click on a static mesh and select + "Asset Actions>Export...". From the file dropdown, select + .gpmesh. Similarly, you can export skeletal meshes to .gpmesh and + animations to .gpanim. + +##A Note on Mesh Orientation +The plug-in does not fix-up or transform the asset in any way. You +should make sure your asset is facing down the X-axis with Z-up +before you export it. + +##A Note on Textures +Right now, the exporter will select the FIRST texture associated with +the "BaseColor" of the FIRST material assigned to the mesh. Multiple +materials/textures will be ignored. + +Also, the exporter assumes that you will follow the convention where the texture +file is in Assets/NameOfTexture. So when it exports, you'll get both +an .gpmesh and a .bmp file in the same directory, place both files in the +Assets directory of the Game Programming in C++ project. + +## A Note on Shaders +By default, static meshes will be exported and refer to the "BasicMesh" +shader. If you want to change this, for now you have to just manually edit +the .gpmesh file to change the shader. Similarly, SkeletalMeshes will +export referring to the "Skinned" shader. + +##A Note on Skeletal Meshes/Skeletons and Animations +Skeletal Meshes will export their textures as static meshes do, and will +additionally export a .gpskel file in the same directory as the skeletal +mesh exports. + +For animations, it is assumed that there is no scale applied -- bones +only export their rotation and translation. diff --git a/Exporter/GPMeshExporter/Resources/Icon128.png b/Exporter/GPMeshExporter/Resources/Icon128.png new file mode 100644 index 00000000..3033ae0e Binary files /dev/null and b/Exporter/GPMeshExporter/Resources/Icon128.png differ diff --git a/Exporter/GPMeshExporter/Source/GPMeshExporter/GPMeshExporter.Build.cs b/Exporter/GPMeshExporter/Source/GPMeshExporter/GPMeshExporter.Build.cs new file mode 100644 index 00000000..c12c5457 --- /dev/null +++ b/Exporter/GPMeshExporter/Source/GPMeshExporter/GPMeshExporter.Build.cs @@ -0,0 +1,40 @@ +// Some copyright should be here... + +using UnrealBuildTool; + +public class GPMeshExporter : ModuleRules +{ + public GPMeshExporter(ReadOnlyTargetRules Target) : base(Target) + { + PrivatePCHHeaderFile = "Private/GPMeshExporterPrivatePCH.h"; + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + "CoreUObject", + "Engine", + "RenderCore", + "UnrealEd", + "ImageWrapper", + // ... add other public dependencies that you statically link with here ... + } + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "Slate", "SlateCore", + // ... add private dependencies that you statically link with here ... + } + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); + } +} diff --git a/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/AnimExporterGP.cpp b/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/AnimExporterGP.cpp new file mode 100644 index 00000000..8eaf1125 --- /dev/null +++ b/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/AnimExporterGP.cpp @@ -0,0 +1,138 @@ +// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. +#include "GPMeshExporterPrivatePCH.h" +#include "AnimExporterGP.h" +#include "Animation/AnimSequence.h" + +UAnimExporterGP::UAnimExporterGP(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SupportedClass = UAnimSequence::StaticClass(); + bText = true; + PreferredFormatIndex = 0; + FormatExtension.Add(TEXT("gpanim")); + FormatDescription.Add(TEXT("GP Animation File")); +} + +bool UAnimExporterGP::ExportText(const FExportObjectInnerContext* Context, UObject* Object, const TCHAR* Type, FOutputDevice& Ar, FFeedbackContext* Warn, uint32 PortFlags /*= 0*/) +{ + UAnimSequence* AnimSeq = CastChecked(Object); + + USkeleton* Skeleton = AnimSeq->GetSkeleton(); + const FReferenceSkeleton& RefSkeleton = Skeleton->GetReferenceSkeleton(); + USkeletalMesh* SkelMesh = Skeleton->GetPreviewMesh(); + if (AnimSeq->SequenceLength == 0.f) + { + // something is wrong + return false; + } + + int32 NumFrames = AnimSeq->GetRawNumberOfFrames(); + const float FrameRate = NumFrames / AnimSeq->SequenceLength; + + // Open another archive + FArchive* File = IFileManager::Get().CreateFileWriter(*UExporter::CurrentFilename); + + // Let's try the header... + File->Logf(TEXT("{")); + File->Logf(TEXT("\t\"version\":1,")); + + File->Logf(TEXT("\t\"sequence\":{")); + File->Logf(TEXT("\t\t\"frames\":%d,"), NumFrames); + File->Logf(TEXT("\t\t\"length\":%f,"), AnimSeq->SequenceLength); + File->Logf(TEXT("\t\t\"bonecount\":%d,"), RefSkeleton.GetNum()); + File->Logf(TEXT("\t\t\"tracks\":[")); + + bool firstOutput = false; + + for (int32 BoneIndex = 0; BoneIndex < RefSkeleton.GetNum(); ++BoneIndex) + { + //int32 BoneTreeIndex = Skeleton->GetSkeletonBoneIndexFromMeshBoneIndex(SkelMesh, BoneIndex); + int32 BoneTrackIndex = Skeleton->GetAnimationTrackIndex(BoneIndex, AnimSeq, true); + + if (BoneTrackIndex == INDEX_NONE && BoneIndex != 0) + { + // If this sequence does not have a track for the current bone, then skip it + continue; + } + + if (firstOutput) + { + File->Logf(TEXT("\t\t\t},")); + } + + firstOutput = true; + + File->Logf(TEXT("\t\t\t{")); + File->Logf(TEXT("\t\t\t\t\"bone\":%d,"), BoneIndex); + File->Logf(TEXT("\t\t\t\t\"transforms\":[")); + float AnimTime = 0.0f; + float AnimEndTime = AnimSeq->SequenceLength; + // Subtracts 1 because NumFrames includes an initial pose for 0.0 second + double TimePerKey = (AnimSeq->SequenceLength / (NumFrames - 1)); + const float AnimTimeIncrement = TimePerKey; + + bool bLastKey = false; + // Step through each frame and add the bone's transformation data + while (!bLastKey) + { + const TArray& BoneTree = Skeleton->GetBoneTree(); + + FTransform BoneAtom; + if (BoneTrackIndex != INDEX_NONE) + { + AnimSeq->GetBoneTransform(BoneAtom, BoneTrackIndex, AnimTime, true); + } + else + { + BoneAtom.SetIdentity(); + } + + bLastKey = AnimTime >= AnimEndTime; + + File->Logf(TEXT("\t\t\t\t\t{")); + + FQuat rot = BoneAtom.GetRotation(); + // For the root bone, we need to fix-up the rotation because Unreal exports + // animations with Y-forward for some reason (maybe because Maya?) + if (BoneIndex == 0) + { + FQuat addRot(FVector(0.0f, 0.0f, 1.0f), -1.57f); + rot = addRot * rot; + } + File->Logf(TEXT("\t\t\t\t\t\t\"rot\":[%f,%f,%f,%f],"), rot.X, rot.Y, rot.Z, rot.W); + FVector trans = BoneAtom.GetTranslation(); + + // Sanjay: If it's skeleton retargeting, change the translation to be from the ref pose skeleton + if (BoneTree[BoneIndex].TranslationRetargetingMode == EBoneTranslationRetargetingMode::Skeleton) + { + const FTransform& BoneTransform = RefSkeleton.GetRefBonePose()[BoneIndex]; + trans = BoneTransform.GetTranslation(); + } + + File->Logf(TEXT("\t\t\t\t\t\t\"trans\":[%f,%f,%f]"), trans.X, trans.Y, trans.Z); + + if (!bLastKey) + { + File->Logf(TEXT("\t\t\t\t\t},")); + } + else + { + File->Logf(TEXT("\t\t\t\t\t}")); + } + + + AnimTime += AnimTimeIncrement; + } + + File->Logf(TEXT("\t\t\t\t]"), BoneIndex); + } + + File->Logf(TEXT("\t\t\t}")); + File->Logf(TEXT("\t\t]")); + File->Logf(TEXT("\t}")); + + File->Logf(TEXT("}")); + delete File; + + return true; +} diff --git a/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/GPMeshExporter.cpp b/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/GPMeshExporter.cpp new file mode 100644 index 00000000..19badd84 --- /dev/null +++ b/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/GPMeshExporter.cpp @@ -0,0 +1,22 @@ +// Some copyright should be here... + +#include "GPMeshExporterPrivatePCH.h" + + + +#define LOCTEXT_NAMESPACE "FGPMeshExporterModule" + +void FGPMeshExporterModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +} + +void FGPMeshExporterModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FGPMeshExporterModule, GPMeshExporter) \ No newline at end of file diff --git a/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/GPMeshExporterPrivatePCH.h b/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/GPMeshExporterPrivatePCH.h new file mode 100644 index 00000000..557f6733 --- /dev/null +++ b/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/GPMeshExporterPrivatePCH.h @@ -0,0 +1,13 @@ +// Some copyright should be here... +#include "CoreUObject.h" +#include "GPMeshExporter.h" +#include "Exporters/Exporter.h" +#include "UnrealEd.h" +#include "StaticMeshResources.h" +#include "Engine/StaticMesh.h" +#include "ImageUtils.h" +#include "EngineModule.h" +#include "RendererInterface.h" + +// You should place include statements to your module's private header files here. You only need to +// add includes for headers that are used in most of your module's source files though. \ No newline at end of file diff --git a/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/GPTextureExporterBMP.cpp b/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/GPTextureExporterBMP.cpp new file mode 100644 index 00000000..2bdaae81 --- /dev/null +++ b/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/GPTextureExporterBMP.cpp @@ -0,0 +1,96 @@ +#include "GPMeshExporterPrivatePCH.h" +#include "GPTextureExporterBMP.h" +#include "Logging/MessageLog.h" +#include "Runtime/ImageWrapper/Public/BmpImageSupport.h" + +/*------------------------------------------------------------------------------ +UTextureExporterBMP implementation. +------------------------------------------------------------------------------*/ +UGPTextureExporterBMP::UGPTextureExporterBMP(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SupportedClass = UTexture2D::StaticClass(); + PreferredFormatIndex = 0; + FormatExtension.Add(TEXT("BMP")); + FormatDescription.Add(TEXT("Windows Bitmap")); + +} + +bool UGPTextureExporterBMP::ExportBinary(UObject* Object, const TCHAR* Type, FArchive& Ar, FFeedbackContext* Warn, int32 FileIndex, uint32 PortFlags) +{ + UTexture2D* Texture = CastChecked(Object); + + if (!Texture->Source.IsValid() || (Texture->Source.GetFormat() != TSF_BGRA8 && Texture->Source.GetFormat() != TSF_RGBA16)) + { + return false; + } + + const bool bIsRGBA16 = Texture->Source.GetFormat() == TSF_RGBA16; + const int32 SourceBytesPerPixel = bIsRGBA16 ? 8 : 4; + +// if (bIsRGBA16) +// { +// FMessageLog ExportWarning("EditorErrors"); +// FFormatNamedArguments Arguments; +// Arguments.Add(TEXT("Name"), FText::FromString(Texture->GetName())); +// ExportWarning.Warning(FText::Format(FText("{Name}: Texture is RGBA16 and cannot be represented at such high bit depth in .bmp. Color will be scaled to RGBA8."), Arguments)); +// ExportWarning.Open(EMessageSeverity::Warning); +// } + + int32 SizeX = Texture->Source.GetSizeX(); + int32 SizeY = Texture->Source.GetSizeY(); + TArray RawData; + Texture->Source.GetMipData(RawData, 0); + + FBitmapFileHeader bmf; + FBitmapInfoHeader bmhdr; + + // File header. + bmf.bfType = 'B' + (256 * (int32)'M'); + bmf.bfReserved1 = 0; + bmf.bfReserved2 = 0; + int32 biSizeImage = SizeX * SizeY * 3; + bmf.bfOffBits = sizeof(FBitmapFileHeader) + sizeof(FBitmapInfoHeader); + bmhdr.biBitCount = 24; + + bmf.bfSize = bmf.bfOffBits + biSizeImage; + Ar << bmf; + + // Info header. + bmhdr.biSize = sizeof(FBitmapInfoHeader); + bmhdr.biWidth = SizeX; + bmhdr.biHeight = SizeY; + bmhdr.biPlanes = 1; + bmhdr.biCompression = BCBI_RGB; + bmhdr.biSizeImage = biSizeImage; + bmhdr.biXPelsPerMeter = 0; + bmhdr.biYPelsPerMeter = 0; + bmhdr.biClrUsed = 0; + bmhdr.biClrImportant = 0; + Ar << bmhdr; + + + // Upside-down scanlines. + for (int32 i = SizeY - 1; i >= 0; i--) + { + uint8* ScreenPtr = &RawData[i*SizeX*SourceBytesPerPixel]; + for (int32 j = SizeX; j>0; j--) + { + if (bIsRGBA16) + { + Ar << ScreenPtr[1]; + Ar << ScreenPtr[3]; + Ar << ScreenPtr[5]; + ScreenPtr += 8; + } + else + { + Ar << ScreenPtr[0]; + Ar << ScreenPtr[1]; + Ar << ScreenPtr[2]; + ScreenPtr += 4; + } + } + } + return true; +} \ No newline at end of file diff --git a/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/SkeletalMeshExporterGP.cpp b/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/SkeletalMeshExporterGP.cpp new file mode 100644 index 00000000..c3dad85a --- /dev/null +++ b/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/SkeletalMeshExporterGP.cpp @@ -0,0 +1,294 @@ +// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. +#include "GPMeshExporterPrivatePCH.h" +#include "SkeletalMeshExporterGP.h" +#include "GPTextureExporterBMP.h" +#include "SkeletalMeshModel.h" + + +USkeletalMeshExporterGP::USkeletalMeshExporterGP(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SupportedClass = USkeletalMesh::StaticClass(); + bText = true; + PreferredFormatIndex = 0; + FormatExtension.Add(TEXT("gpmesh")); + FormatDescription.Add(TEXT("GP Mesh File")); + + BMPExporter = CreateDefaultSubobject(TEXT("BMPExporter")); +} + +bool USkeletalMeshExporterGP::ExportText(const FExportObjectInnerContext* Context, UObject* Object, const TCHAR* Type, FOutputDevice& Ar, FFeedbackContext* Warn, uint32 PortFlags /*= 0*/) +{ + USkeletalMesh* SkeletalMesh = CastChecked(Object); + FString currFileName = UExporter::CurrentFilename; + // Open another archive + FArchive* File = IFileManager::Get().CreateFileWriter(*currFileName); + + // Let's try the header... + File->Logf(TEXT("{")); + File->Logf(TEXT("\t\"version\":1,")); + File->Logf(TEXT("\t\"vertexformat\":\"PosNormSkinTex\",")); + File->Logf(TEXT("\t\"shader\":\"Skinned\",")); + + File->Logf(TEXT("\t\"textures\":[")); + + // See if there are any textures + FString texturePath; + TArray textures; + if (SkeletalMesh->Materials.Num() > 0 && SkeletalMesh->Materials[0].MaterialInterface && + SkeletalMesh->Materials[0].MaterialInterface->GetTexturesInPropertyChain(MP_BaseColor, textures, nullptr, nullptr)) + { + if (textures.Num() > 0) + { + int32 startIdx; + int32 endIdx; + if (currFileName.FindLastChar('/', startIdx) && + currFileName.FindLastChar('.', endIdx)) + { + FString textureName = currFileName.Mid(startIdx + 1, endIdx - startIdx - 1); + texturePath = "Assets/"; + texturePath += textureName; + texturePath += ".bmp"; + + FString outputPath = currFileName.Mid(0, startIdx + 1); + outputPath += textureName; + outputPath += ".bmp"; + + UExporter::ExportToFile(textures[0], BMPExporter, *outputPath, false); + } + } + } + + if (texturePath.IsEmpty()) + { + File->Logf(TEXT("\t\t\"Assets/Default.png\"")); + } + else + { + File->Logf(TEXT("\t\t\"%s\""), *texturePath); + } + + File->Logf(TEXT("\t],")); + + File->Logf(TEXT("\t\"specularPower\":100.0,")); + + const FSkeletalMeshModel* SkelMeshResource = SkeletalMesh->GetImportedModel(); + const FSkeletalMeshLODModel& SourceModel = SkelMeshResource->LODModels[0]; + const int32 VertexCount = SourceModel.GetNumNonClothingVertices(); + TArray Vertices; + GetVertices(SourceModel, Vertices); + //SourceModel.GetNonClothVertices(Vertices); + + // Write the vertices + File->Logf(TEXT("\t\"vertices\":[")); + for (int32 i = 0; i < VertexCount; i++) + { + FSoftSkinVertex& v = Vertices[i]; + FVector Norm(v.TangentZ); + uint8* b = Vertices[i].InfluenceBones; + uint8* w = Vertices[i].InfluenceWeights; + if (i < VertexCount - 1) + { + File->Logf(TEXT("\t\t[%f,%f,%f,%f,%f,%f,%u,%u,%u,%u,%u,%u,%u,%u,%f,%f],"), + v.Position.X, v.Position.Y, v.Position.Z, + Norm.X, Norm.Y, Norm.Z, + b[0], b[1], b[2], b[3], + w[0], w[1], w[2], w[3], + v.UVs[0].X, v.UVs[0].Y); + } + else + { + File->Logf(TEXT("\t\t[%f,%f,%f,%f,%f,%f,%u,%u,%u,%u,%u,%u,%u,%u,%f,%f]"), + v.Position.X, v.Position.Y, v.Position.Z, + Norm.X, Norm.Y, Norm.Z, + b[0], b[1], b[2], b[3], + w[0], w[1], w[2], w[3], + v.UVs[0].X, v.UVs[0].Y); + } + } + + File->Logf(TEXT("\t],")); + + + // Write the indices + File->Logf(TEXT("\t\"indices\":[")); + + uint32 NumIndices = SourceModel.IndexBuffer.Num(); + uint32 CurrIdx = 0; + + check(NumIndices % 3 == 0); + + int32 SectionCount = SourceModel.Sections.Num(); + int32 ClothSectionVertexRemoveOffset = 0; + + for (int32 SectionIndex = 0; SectionIndex < SectionCount; ++SectionIndex) + { + const FSkelMeshSection& Section = SourceModel.Sections[SectionIndex]; + + // Static meshes contain one triangle list per element. + int32 TriangleCount = Section.NumTriangles; + + // Copy over the index buffer into the FBX polygons set. + for (int32 TriangleIndex = 0; TriangleIndex < TriangleCount; ++TriangleIndex) + { + uint32 a = SourceModel.IndexBuffer[Section.BaseIndex + ((TriangleIndex * 3) + 0)] - ClothSectionVertexRemoveOffset; + uint32 b = SourceModel.IndexBuffer[Section.BaseIndex + ((TriangleIndex * 3) + 1)] - ClothSectionVertexRemoveOffset; + uint32 c = SourceModel.IndexBuffer[Section.BaseIndex + ((TriangleIndex * 3) + 2)] - ClothSectionVertexRemoveOffset; + // Is this the last one? + if (SectionIndex == (SectionCount - 1) && TriangleIndex == (TriangleCount - 1)) + { + File->Logf(TEXT("\t\t[%u,%u,%u]"), a, b, c); + } + else + { + File->Logf(TEXT("\t\t[%u,%u,%u],"), a, b, c); + } + } + } + + File->Logf(TEXT("\t]")); + File->Logf(TEXT("}")); + delete File; + + // Now export the skeleton + return ExportSkeleton(SkeletalMesh->RefSkeleton, currFileName); +} + +bool USkeletalMeshExporterGP::ExportSkeleton(const FReferenceSkeleton& RefSkeleton, const FString& currFileName) +{ + if (RefSkeleton.GetNum() == 0) + { + return false; + } + + FString FileName; + int32 startIdx; + int32 endIdx; + if (currFileName.FindLastChar('/', startIdx) && + currFileName.FindLastChar('.', endIdx)) + { + FString skeletonName = currFileName.Mid(startIdx + 1, endIdx - startIdx - 1); + + FileName = currFileName.Mid(0, startIdx + 1); + FileName += skeletonName; + FileName += ".gpskel"; + } + + // Open file for output + FArchive* File = IFileManager::Get().CreateFileWriter(*FileName); + + File->Logf(TEXT("{")); + File->Logf(TEXT("\t\"version\":1,")); + + File->Logf(TEXT("\t\"bonecount\":%d,"), RefSkeleton.GetNum()); + + File->Logf(TEXT("\t\"bones\":[")); + + for (int32 BoneIndex = 0; BoneIndex < RefSkeleton.GetNum(); ++BoneIndex) + { + const FMeshBoneInfo& CurrentBone = RefSkeleton.GetRefBoneInfo()[BoneIndex]; + const FTransform& BoneTransform = RefSkeleton.GetRefBonePose()[BoneIndex]; + File->Logf(TEXT("\t\t{")); + + File->Logf(TEXT("\t\t\t\"name\":\"%s\","), *CurrentBone.ExportName); + + if (BoneIndex != 0) + { + File->Logf(TEXT("\t\t\t\"parent\":%d,"), CurrentBone.ParentIndex); + } + else + { + // Root node + File->Logf(TEXT("\t\t\t\"parent\":-1,")); + } + + File->Logf(TEXT("\t\t\t\"bindpose\":{")); + // I'm assuming we don't need scale, because the FBX export doesn't seem to export scale, either... + FQuat rot = BoneTransform.GetRotation(); + File->Logf(TEXT("\t\t\t\t\"rot\":[%f,%f,%f,%f],"), rot.X, rot.Y, rot.Z, rot.W); + FVector trans = BoneTransform.GetTranslation(); + File->Logf(TEXT("\t\t\t\t\"trans\":[%f,%f,%f]"), trans.X, trans.Y, trans.Z); + File->Logf(TEXT("\t\t\t}")); + + if (BoneIndex == RefSkeleton.GetNum() - 1) + { + File->Logf(TEXT("\t\t}")); + } + else + { + File->Logf(TEXT("\t\t},")); + } + } + + File->Logf(TEXT("\t]")); + + File->Logf(TEXT("}")); + delete File; + + return true; +} + +void USkeletalMeshExporterGP::GetVertices(const class FSkeletalMeshLODModel& Model, TArray& Vertices) const +{ + Vertices.Empty(Model.GetNumNonClothingVertices()); + Vertices.AddUninitialized(Model.GetNumNonClothingVertices()); + + // Initialize the vertex data + // All chunks are combined into one (rigid first, soft next) + FSoftSkinVertex* DestVertex = (FSoftSkinVertex*)Vertices.GetData(); + for (int32 SectionIndex = 0; SectionIndex < Model.Sections.Num(); SectionIndex++) + { + const FSkelMeshSection& Section = Model.Sections[SectionIndex]; + //check(Chunk.NumRigidVertices == Chunk.RigidVertices.Num()); + //check(Chunk.NumSoftVertices == Chunk.SoftVertices.Num()); + // Sanjay:: I guess there aren't rigid vertices anymore? + //for (int32 VertexIndex = 0; VertexIndex < Chunk.RigidVertices.Num(); VertexIndex++) + //{ + // const FRigidSkinVertex& SourceVertex = Chunk.RigidVertices[VertexIndex]; + // DestVertex->Position = SourceVertex.Position; + // DestVertex->TangentX = SourceVertex.TangentX; + // DestVertex->TangentY = SourceVertex.TangentY; + // DestVertex->TangentZ = SourceVertex.TangentZ; + // // store the sign of the determinant in TangentZ.W + // DestVertex->TangentZ.Vector.W = GetBasisDeterminantSignByte(SourceVertex.TangentX, SourceVertex.TangentY, SourceVertex.TangentZ); + + // // copy all texture coordinate sets + // FMemory::Memcpy(DestVertex->UVs, SourceVertex.UVs, sizeof(FVector2D)*MAX_TEXCOORDS); + + // DestVertex->Color = SourceVertex.Color; + // // Sanjay: Changed this to lookup in the bone map for the chunk + // DestVertex->InfluenceBones[0] = Chunk.BoneMap[SourceVertex.Bone]; + // DestVertex->InfluenceWeights[0] = 255; + // for (int32 InfluenceIndex = 1; InfluenceIndex < MAX_TOTAL_INFLUENCES; InfluenceIndex++) + // { + // DestVertex->InfluenceBones[InfluenceIndex] = 0; + // DestVertex->InfluenceWeights[InfluenceIndex] = 0; + // } + // DestVertex++; + //} + //FMemory::Memcpy(DestVertex, Chunk.SoftVertices.GetData(), Chunk.SoftVertices.Num() * sizeof(FSoftSkinVertex)); + //DestVertex += Chunk.SoftVertices.Num(); + + // Sanjay: Manually copy the soft vertex data as well, so we can use the bone map + for (int32 VertexIndex = 0; VertexIndex < Section.SoftVertices.Num(); VertexIndex++) + { + const FSoftSkinVertex& SourceVertex = Section.SoftVertices[VertexIndex]; + DestVertex->Position = SourceVertex.Position; + DestVertex->TangentX = SourceVertex.TangentX; + DestVertex->TangentY = SourceVertex.TangentY; + DestVertex->TangentZ = SourceVertex.TangentZ; + + // copy all texture coordinate sets + FMemory::Memcpy(DestVertex->UVs, SourceVertex.UVs, sizeof(FVector2D)*MAX_TEXCOORDS); + + DestVertex->Color = SourceVertex.Color; + // Sanjay: Changed this to lookup in the bone map for the chunk + for (int32 InfluenceIndex = 0; InfluenceIndex < MAX_TOTAL_INFLUENCES; InfluenceIndex++) + { + DestVertex->InfluenceBones[InfluenceIndex] = Section.BoneMap[SourceVertex.InfluenceBones[InfluenceIndex]]; + DestVertex->InfluenceWeights[InfluenceIndex] = SourceVertex.InfluenceWeights[InfluenceIndex]; + } + DestVertex++; + } + } +} diff --git a/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/StaticMeshExporterGP.cpp b/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/StaticMeshExporterGP.cpp new file mode 100644 index 00000000..d828c3bd --- /dev/null +++ b/Exporter/GPMeshExporter/Source/GPMeshExporter/Private/StaticMeshExporterGP.cpp @@ -0,0 +1,132 @@ +// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. +#include "GPMeshExporterPrivatePCH.h" +#include "StaticMeshExporterGP.h" +#include "GPTextureExporterBMP.h" + + +UStaticMeshExporterGP::UStaticMeshExporterGP(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SupportedClass = UStaticMesh::StaticClass(); + bText = true; + PreferredFormatIndex = 0; + FormatExtension.Add(TEXT("gpmesh")); + FormatDescription.Add(TEXT("GP Mesh File")); + + BMPExporter = CreateDefaultSubobject(TEXT("BMPExporter")); +} + +bool UStaticMeshExporterGP::ExportText(const FExportObjectInnerContext* Context, UObject* Object, const TCHAR* Type, FOutputDevice& Ar, FFeedbackContext* Warn, uint32 PortFlags /*= 0*/) +{ + UStaticMesh* StaticMesh = CastChecked(Object); + + // Open another archive + FArchive* File = IFileManager::Get().CreateFileWriter(*UExporter::CurrentFilename); + + // Let's try the header... + File->Logf(TEXT("{")); + File->Logf(TEXT("\t\"version\":1,")); + File->Logf(TEXT("\t\"vertexformat\":\"PosNormTex\",")); + File->Logf(TEXT("\t\"shader\":\"BasicMesh\",")); + + File->Logf(TEXT("\t\"textures\":[")); + + // See if there are any textures + FString texturePath; + TArray textures; + if (StaticMesh->GetMaterial(0) && + StaticMesh->GetMaterial(0)->GetTexturesInPropertyChain(MP_BaseColor, textures, nullptr, nullptr)) + { + if (textures.Num() > 0) + { + int32 startIdx; + int32 endIdx; + if (UExporter::CurrentFilename.FindLastChar('/', startIdx) && + UExporter::CurrentFilename.FindLastChar('.', endIdx)) + { + FString textureName = UExporter::CurrentFilename.Mid(startIdx + 1, endIdx - startIdx - 1); + texturePath = "Assets/"; + texturePath += textureName; + texturePath += ".bmp"; + + FString outputPath = UExporter::CurrentFilename.Mid(0, startIdx + 1); + outputPath += textureName; + outputPath += ".bmp"; + + UExporter::ExportToFile(textures[0], BMPExporter, *outputPath, false); + } + } + } + + if (texturePath.IsEmpty()) + { + File->Logf(TEXT("\t\t\"Assets/Default.png\"")); + } + else + { + File->Logf(TEXT("\t\t\"%s\""), *texturePath); + } + + File->Logf(TEXT("\t],")); + File->Logf(TEXT("\t\"specularPower\":100.0,")); + + // Currently, we only export LOD 0 of the static mesh. In the future, we could potentially export all available LODs + const FStaticMeshLODResources& RenderData = StaticMesh->GetLODForExport(0); + uint32 VertexCount = RenderData.GetNumVertices(); + + // Write the vertices + File->Logf(TEXT("\t\"vertices\":[")); + for (uint32 i = 0; i < VertexCount; i++) + { + const FVector& Pos = RenderData.VertexBuffers.PositionVertexBuffer.VertexPosition(i); + const FVector& Normal = RenderData.VertexBuffers.StaticMeshVertexBuffer.VertexTangentZ(i); + const FVector2D UV = RenderData.VertexBuffers.StaticMeshVertexBuffer.GetVertexUV(i, 0); + + if (i < VertexCount - 1) + { + File->Logf(TEXT("\t\t[%f,%f,%f,%f,%f,%f,%f,%f],"), + Pos.X, Pos.Y, Pos.Z, + Normal.X, Normal.Y, Normal.Z, + UV.X, UV.Y); + } + else + { + File->Logf(TEXT("\t\t[%f,%f,%f,%f,%f,%f,%f,%f]"), + Pos.X, Pos.Y, Pos.Z, + Normal.X, Normal.Y, Normal.Z, + UV.X, UV.Y); + } + } + + File->Logf(TEXT("\t],")); + + // Write the indices + File->Logf(TEXT("\t\"indices\":[")); + + FIndexArrayView Indices = RenderData.IndexBuffer.GetArrayView(); + uint32 NumIndices = Indices.Num(); + + check(NumIndices % 3 == 0); + for (uint32 i = 0; i < NumIndices / 3; i++) + { + // Wavefront indices are 1 based + uint32 a = Indices[3 * i + 0]; + uint32 b = Indices[3 * i + 1]; + uint32 c = Indices[3 * i + 2]; + + if (i < (NumIndices / 3 - 1)) + { + File->Logf(TEXT("\t\t[%u,%u,%u],"), a, b, c); + } + else + { + File->Logf(TEXT("\t\t[%u,%u,%u]"), a, b, c); + } + } + + File->Logf(TEXT("\t]")); + File->Logf(TEXT("}")); + delete File; + + return true; +} diff --git a/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/AnimExporterGP.h b/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/AnimExporterGP.h new file mode 100644 index 00000000..c21b7499 --- /dev/null +++ b/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/AnimExporterGP.h @@ -0,0 +1,22 @@ +// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. + +//============================================================================= +// StaticMeshExporterOBJ +//============================================================================= + +#pragma once +#include "Exporters/Exporter.h" +#include "AnimExporterGP.generated.h" + +UCLASS() +class UAnimExporterGP : public UExporter +{ + GENERATED_UCLASS_BODY() + + + // Begin UExporter Interface + virtual bool ExportText(const FExportObjectInnerContext* Context, UObject* Object, const TCHAR* Type, FOutputDevice& Ar, FFeedbackContext* Warn, uint32 PortFlags = 0) override; +}; + + + diff --git a/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/GPMeshExporter.h b/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/GPMeshExporter.h new file mode 100644 index 00000000..59419df7 --- /dev/null +++ b/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/GPMeshExporter.h @@ -0,0 +1,16 @@ +// Some copyright should be here... + +#pragma once + +#include "ModuleManager.h" + + + +class FGPMeshExporterModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; \ No newline at end of file diff --git a/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/GPTextureExporterBMP.h b/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/GPTextureExporterBMP.h new file mode 100644 index 00000000..3357268d --- /dev/null +++ b/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/GPTextureExporterBMP.h @@ -0,0 +1,21 @@ +// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. + +//============================================================================= +// TextureExporterBMP +//============================================================================= + +#pragma once +#include "Exporters/Exporter.h" +#include "GPTextureExporterBMP.generated.h" + +UCLASS() +class UGPTextureExporterBMP : public UExporter +{ + GENERATED_UCLASS_BODY() + + + bool ExportBinary(UObject* Object, const TCHAR* Type, FArchive& Ar, FFeedbackContext* Warn, int32 FileIndex = 0, uint32 PortFlags = 0) override; +}; + + + diff --git a/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/SkeletalMeshExporterGP.h b/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/SkeletalMeshExporterGP.h new file mode 100644 index 00000000..81e78f74 --- /dev/null +++ b/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/SkeletalMeshExporterGP.h @@ -0,0 +1,29 @@ +// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. + +//============================================================================= +// StaticMeshExporterOBJ +//============================================================================= + +#pragma once +#include "Exporters/Exporter.h" +#include "Rendering/SkeletalMeshLODModel.h" +#include "SkeletalMeshExporterGP.generated.h" + +UCLASS() +class USkeletalMeshExporterGP : public UExporter +{ + GENERATED_UCLASS_BODY() + + + // Begin UExporter Interface + virtual bool ExportText(const FExportObjectInnerContext* Context, UObject* Object, const TCHAR* Type, FOutputDevice& Ar, FFeedbackContext* Warn, uint32 PortFlags = 0) override; + // End UExporter Interface + virtual bool ExportSkeleton(const struct FReferenceSkeleton& RefSkeleton, const FString& currFileName); + + void GetVertices(const class FSkeletalMeshLODModel& Model, TArray& Vertices) const; + + class UGPTextureExporterBMP* BMPExporter; +}; + + + diff --git a/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/StaticMeshExporterGP.h b/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/StaticMeshExporterGP.h new file mode 100644 index 00000000..6bad6c9b --- /dev/null +++ b/Exporter/GPMeshExporter/Source/GPMeshExporter/Public/StaticMeshExporterGP.h @@ -0,0 +1,25 @@ +// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. + +//============================================================================= +// StaticMeshExporterOBJ +//============================================================================= + +#pragma once +#include "Exporters/Exporter.h" +#include "StaticMeshExporterGP.generated.h" + +UCLASS() +class UStaticMeshExporterGP : public UExporter +{ + GENERATED_UCLASS_BODY() + + + // Begin UExporter Interface + virtual bool ExportText(const FExportObjectInnerContext* Context, UObject* Object, const TCHAR* Type, FOutputDevice& Ar, FFeedbackContext* Warn, uint32 PortFlags = 0) override; + // End UExporter Interface + + class UGPTextureExporterBMP* BMPExporter; +}; + + + diff --git a/README.md b/README.md index 41b4a0db..586604d2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This repository contains the source code for *Game Programming in C++* by Sanjay Madhav. The source code for the chapters is released under the BSD 3-clause -license. See LICENSE for more detail. Note that this license does not apply to +license. See LICENSE for more details. Note that this license does not apply to the code in the External directory. Each External project is licensed separately. # Building the Code