diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ede6b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,84 @@ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop + +roms + +Network Trash Folder +Temporary Items +.apdisk + +# Windows +# ========================= + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk +*.db +Debug/ +Release/ +/.vs + +build diff --git a/BoostTestHelpers.cmake b/BoostTestHelpers.cmake new file mode 100644 index 0000000..787fd45 --- /dev/null +++ b/BoostTestHelpers.cmake @@ -0,0 +1,19 @@ +function(add_boost_test SOURCE_FILE_NAME DEPENDENCY_LIB1 DEPENDENCY_LIB2) + get_filename_component(TEST_EXECUTABLE_NAME ${SOURCE_FILE_NAME} NAME_WE) + + add_executable(${TEST_EXECUTABLE_NAME} ${SOURCE_FILE_NAME}) + target_link_libraries(${TEST_EXECUTABLE_NAME} + ${DEPENDENCY_LIB1} ${DEPENDENCY_LIB2} ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY}) + + file(READ "${SOURCE_FILE_NAME}" SOURCE_FILE_CONTENTS) + string(REGEX MATCHALL "BOOST_AUTO_TEST_CASE\\( *([A-Za-z_0-9]+) *\\)" + FOUND_TESTS ${SOURCE_FILE_CONTENTS}) + + foreach(HIT ${FOUND_TESTS}) + string(REGEX REPLACE ".*\\( *([A-Za-z_0-9]+) *\\).*" "\\1" TEST_NAME ${HIT}) + + add_test(NAME "${TEST_EXECUTABLE_NAME}.${TEST_NAME}" + COMMAND ${TEST_EXECUTABLE_NAME} + --run_test=${TEST_NAME} --catch_system_error=yes) + endforeach() +endfunction() diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6d190b3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,85 @@ +# travis has cmake 3.9 +cmake_minimum_required(VERSION 3.9) + +project(FROSTBITE) + +enable_testing() + +set(PROJECT_NAME "Frostbite") +set(PROJECT_Version 0.1) +set(PROJECT_LIBS) + +set(COVERAGE OFF CACHE BOOL "Coverage") + +set(Boost_USE_STATIC_LIBS ON) +find_package(Boost REQUIRED COMPONENTS program_options filesystem) +if(Boost_FOUND) + include_directories(${Boost_INCLUDE_DIRS}) +endif() + +find_package(GLM REQUIRED) +include_directories(${GLM_INCLUDE_DIR}) + +find_package(SDL2 REQUIRED) + +find_package(GLEW REQUIRED) +include_directories(${GLEW_INCLUDE_DIR}) + +# flags +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(WIN32) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc /MP") +endif() +if (COVERAGE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage") +endif() + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release CACHE STRING + "Choose the type of build, options are: None Debug Release" + FORCE) +endif() + +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_DEBUG") +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DNDEBUG") + +if(WIN32) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) + add_definitions(-D_SCL_SECURE_NO_WARNINGS) + add_definitions(-D_UNICODE -DUNICODE) +endif() + +set(SNOWLIB_SRC_FILES + snowlib/AnimationProcessor.cpp + snowlib/AnimData.cpp + snowlib/AnimDecoder.cpp + snowlib/FntDecoder.cpp + snowlib/GameType.h + snowlib/GIFTag.cpp + snowlib/GobFile.cpp + snowlib/Helpers.cpp + snowlib/LmpFile.cpp + snowlib/LmpRepository.cpp + snowlib/Logger.cpp + snowlib/Mesh.cpp + snowlib/TexDecoder.cpp + snowlib/VifDecoder.cpp + snowlib/World.cpp + snowlib/WorldReader.cpp + ) + +include_directories("${FROSTBITE_SOURCE_DIR}/snowlib") + +set(TESTAPP_SRC_FILES + TestApp/Main.cpp + TestApp/ModelWindow.cpp +) + +add_library(snowlib STATIC ${SNOWLIB_SRC_FILES}) + +add_executable(testApp ${TESTAPP_SRC_FILES}) +target_link_libraries(testApp snowlib ${Boost_LIBRARIES} ${GLEW_LIBRARIES} SDL2::SDL2 SDL2::SDL2main opengl32.lib) + +#include(BoostTestHelpers.cmake) + diff --git a/CMakeSettings.json b/CMakeSettings.json new file mode 100644 index 0000000..710bbe3 --- /dev/null +++ b/CMakeSettings.json @@ -0,0 +1,16 @@ +{ + // See https://go.microsoft.com//fwlink//?linkid=834763 for more information about this file. + "configurations": [ + { + "name": "x64-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ "msvc_x64_x64" ], + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "-v", + "ctestCommandArgs": "" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 3ba1217..d6b592f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,22 @@ -# frostbite -experimental implementation of the snowblind engine +Building +======== + +For windows, I use vcpkg to install packages. See https://github.com/Microsoft/vcpkg + +you will want to install the following packages as follows: + +``` +vcpkg install sdl2:x64-windows +vcpkg install glm:x64-windows +vcpkg install glew:x64-windows +vcpkg install boost:x64-windows +``` + +Then you can open the directory in visual studio and it will be recognised as a cmake project. +Whether you use the open folder functionality or call cmake on the command line you will need to +set the toolchain file to the vcpkg one. This lives in the vcpkg directory under `scripts\buildsystems\vcpkg.cmake` + +https://github.com/microsoft/vcpkg/blob/master/docs/examples/installing-and-using-packages.md#cmake-toolchain-file + +note that if you do `vcpkg integrate install` then you don't need to set the toolchain when using visual studio 2019 + diff --git a/TestApp/Main.cpp b/TestApp/Main.cpp new file mode 100644 index 0000000..4063157 --- /dev/null +++ b/TestApp/Main.cpp @@ -0,0 +1,120 @@ +#pragma once +#include "SDL.h" + +#include "ModelWindow.h" +#include "windows.h" +#include "LmpRepository.h" +#include "GobFile.h" +#include "VifDecoder.h" +#include "TexDecoder.h" +#include "AnimDecoder.h" +#include "Model.h" +#include "Texture.h" +#include "AnimData.h" +#include "WorldReader.h" +#include "World.h" + +LmpRepository* lmpRepository; + +TexturedMesh* readTexturedMesh(string dataPath, const char* lmpName, const char* modelName) +{ + LmpFile* lmpFile = lmpRepository->getLmp(lmpName); + LmpEntry* vifEntry = lmpFile->findEntry(modelName, ".vif"); + LmpEntry* texEntry = lmpFile->findEntry(modelName, ".tex"); + TexturedMesh* texturedMesh = new TexturedMesh(); + VifDecoder vifDecoder; + TexDecoder texDecoder; + texturedMesh->meshList = vifDecoder.decode(vifEntry->data, vifEntry->length); + texturedMesh->texture = texDecoder.decode(texEntry->data, 0); + + for (auto& mesh : *texturedMesh->meshList){ + mesh->adjustUVs(texturedMesh->texture->widthPixels, texturedMesh->texture->heightPixels); + } + return texturedMesh; +} + +Model* readModel(string dataPath, const char* lmpName, vector meshNames, const char *anmName) +{ + Model* model = new Model(); + for (string meshName : meshNames){ + TexturedMesh* texturedMesh = readTexturedMesh(dataPath, lmpName, meshName.c_str()); + model->texturedMeshList.push_back(texturedMesh); + } + LmpFile* lmpFile = lmpRepository->getLmp(lmpName); + + LmpEntry* anmEntry = anmName == nullptr ? nullptr : lmpFile->findEntry(anmName, ".anm"); + + if (anmEntry != nullptr){ + AnimDecoder animDecoder; + model->animData = animDecoder.decode(anmEntry->data, anmEntry->length); + } else { + model->animData = nullptr; + } + return model; +} + +int main(int argc, char **argv) { + string dataPath = "D:\\emu\\bgda\\BG\\DATA\\"; + if (argc == 2){ + dataPath = argv[1]; + } + lmpRepository = new LmpRepositoryImpl(dataPath, GameType::DARK_ALLIANCE); + + GobFile cuttownGob = GobFile(dataPath + "CUTTOWN.GOB", GameType::DARK_ALLIANCE); + World* world = WorldReader().readWorld(&cuttownGob, "cuttown"); + + const char* lmpName = "DWARF.LMP"; + vector meshNames; + meshNames.push_back("dwarf"); + meshNames.push_back("hair"); + +// Model* model = readModel(dataPath, "CHEST.LMP", "chest_large", "chest_large_open"); + Model* model = readModel(dataPath, lmpName, meshNames, "l_idlea"); + + + vector animNames = lmpRepository->getLmp(lmpName)->findFilenamesByExtension(".anm"); + + ModelWindow modelWindow; + + modelWindow.init(); + + int maxFrame = model->animData->NumFrames; + + float targetFrameRate = 60.0f / 1000.0f; // frames per millisecond + unsigned int animStartClock = SDL_GetTicks(); + bool done=false; + while (!done){ + unsigned int tick = SDL_GetTicks(); + SDL_Event event; + while (SDL_PollEvent(&event)){ + if (event.type == SDL_QUIT){ + done = true; + } else if (event.type == SDL_KEYDOWN){ + if (event.key.keysym.sym == SDLK_ESCAPE){ + done = true; + } + } + } + unsigned int tock = SDL_GetTicks(); + int animMillis = tock - animStartClock; + int frame = (int)(animMillis * targetFrameRate) % maxFrame; + modelWindow.drawFrame(model, frame); + + unsigned int deltaclock = SDL_GetTicks() - tick; + + if ( deltaclock != 0 ){ + unsigned int currentFPS = 1000 / deltaclock; + char fpsString[64]; + sprintf(fpsString, "FPS: %d\n", currentFPS); + OutputDebugStringA(fpsString); + } + + } + + modelWindow.exit(); + + return 0; +} + + + diff --git a/TestApp/ModelWindow.cpp b/TestApp/ModelWindow.cpp new file mode 100644 index 0000000..b334f58 --- /dev/null +++ b/TestApp/ModelWindow.cpp @@ -0,0 +1,329 @@ +#include "ModelWindow.h" +#include "SDL.h" +#include "Mesh.h" +#include "Model.h" +#include "Texture.h" +#include "Logger.h" +#include "glm/glm.hpp" +#include +#include +#include "AnimationProcessor.h" + +#define check() {\ + GLenum error = glGetError(); \ + if (error != 0){ \ + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Gl Error: %d\n", error); \ + assert(false); \ + } \ +} + +float cubeVertices[] = { + -1.0f, -1.0f, -1.0f, + 1.0f, -1.0f, -1.0f, + 1.0f, 1.0f, -1.0f, + -1.0f, 1.0f, -1.0f, + -1.0f, -1.0f, 1.0f, + 1.0f, -1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + -1.0f, 1.0f, 1.0f + }; + +float cubeColors[] = { + 0.0f, 1.0f, 0.0f, 1.0f, + 0.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 0.5f, 0.0f, 1.0f, + 1.0f, 0.5f, 0.0f, 1.0f, + 1.0f, 0.0f, 0.0f, 1.0f, + 1.0f, 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, 1.0f, + 1.0f, 0.0f, 1.0f, 1.0f + }; + +GLuint cubeIndices[] = { + 0, 4, 5, 0, 5, 1, + 1, 5, 6, 1, 6, 2, + 2, 6, 7, 2, 7, 3, + 3, 7, 4, 3, 4, 0, + 4, 7, 6, 4, 6, 5, + 3, 0, 1, 3, 1, 2 + }; + +float cubeRotation = 0.0f; + +void ModelWindow::drawCube() +{ + // Now render to the main frame buffer + glBindFramebuffer(GL_FRAMEBUFFER,0); + // Clear the background (not really necessary I suppose) + glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); + check(); + glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); + glLoadIdentity(); + glTranslatef(0.0f, 0.0f, -10.0f); + glRotatef(cubeRotation, 1.0f, 1.0f, 1.0f); + check(); + glFrontFace(GL_CW); + check(); + glVertexPointer(3, GL_FLOAT, 0, cubeVertices); + glColorPointer(4, GL_FLOAT, 0, cubeColors); + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_COLOR_ARRAY); + glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, cubeIndices); + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_COLOR_ARRAY); + glLoadIdentity(); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + check(); + glFlush(); + glFinish(); + check(); + + SDL_GL_SwapWindow(window); + + cubeRotation -= 0.15f; +} + +const char* fragmentShaderCode = +"#version 120\n" +"varying vec2 UV;\n" +"uniform sampler2D myTextureSampler;\n" +"void main(){\n" +" gl_FragColor = texture2D( myTextureSampler, UV );\n" +"// gl_FragColor = vec4 (0.0, 1.0, 0.0, 1.0);\n" +"}\n"; + +const char* vertexShaderCode = +"#version 120\n" +"attribute vec3 vertexPosition_modelspace;\n" +"attribute vec2 vertexUV;\n" +"varying vec2 UV;\n" +"uniform mat4 MVP;\n" +"void main(){\n" +" gl_Position = MVP * vec4(vertexPosition_modelspace,1);\n" +" UV = vertexUV;\n" +"}"; + +GLuint LoadShaders() +{ + GLuint VertexShaderID = glCreateShader(GL_VERTEX_SHADER); + GLuint FragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER); + + GLint Result = GL_FALSE; + int InfoLogLength; + + glShaderSource(VertexShaderID, 1, &vertexShaderCode , NULL); + glCompileShader(VertexShaderID); + check(); + + glShaderSource(FragmentShaderID, 1, &fragmentShaderCode , NULL); + glCompileShader(FragmentShaderID); + glGetShaderiv(FragmentShaderID, GL_COMPILE_STATUS, &Result); + glGetShaderiv(FragmentShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength); + if ( InfoLogLength > 0 ){ + std::vector ProgramErrorMessage(InfoLogLength+1); + glGetShaderInfoLog(FragmentShaderID, InfoLogLength, NULL, &ProgramErrorMessage[0]); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s\n", &ProgramErrorMessage[0]); + Logger::getLogger()->log(&ProgramErrorMessage[0]); + } + check(); + GLuint ProgramID = glCreateProgram(); + glAttachShader(ProgramID, VertexShaderID); + glAttachShader(ProgramID, FragmentShaderID); + + + check(); + glLinkProgram(ProgramID); + + // Check the program + glGetProgramiv(ProgramID, GL_LINK_STATUS, &Result); + glGetProgramiv(ProgramID, GL_INFO_LOG_LENGTH, &InfoLogLength); + if ( InfoLogLength > 0 ){ + std::vector ProgramErrorMessage(InfoLogLength+1); + glGetProgramInfoLog(ProgramID, InfoLogLength, NULL, &ProgramErrorMessage[0]); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s\n", &ProgramErrorMessage[0]); + Logger::getLogger()->log(&ProgramErrorMessage[0]); + } + check(); + glDeleteShader(VertexShaderID); + glDeleteShader(FragmentShaderID); + + check(); + return ProgramID; +} + +void ModelWindow::drawMesh(Model* model, int frame) +{ + AnimationProcessor animationProcessor; + + glColor3f(1.0f,1.0f,1.0f); + // Now render to the main frame buffer + glBindFramebuffer(GL_FRAMEBUFFER,0); + // Clear the background (not really necessary I suppose) + glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); + check(); + //glPolygonMode( GL_FRONT_AND_BACK, GL_LINE ); + + check(); + + // Projection matrix : 45° Field of View, 4:3 ratio, display range : 0.1 unit <-> 1000 units + glm::mat4 Projection = glm::perspective(45.0f, (float)screen_width / screen_height, 0.1f, 1000.0f); + // Camera matrix + glm::mat4 View = glm::lookAt( + glm::vec3(0,-100,50), // Camera is at (0,-100,50), in World Space + glm::vec3(0,0,30), // and looks at + glm::vec3(0,0,1) // Head is up + ); + // Model matrix : an identity matrix (model will be at the origin) + glm::mat4 Model = glm::mat4(1.0f); + // Our ModelViewProjection : multiplication of our 3 matrices + glm::mat4 MVP = Projection * View * Model; // Remember, matrix multiplication is the other way around + + + GLuint MatrixID = glGetUniformLocation(shaderProgramID, "MVP"); + check(); + GLuint vertexPosition_modelspaceID = glGetAttribLocation(shaderProgramID, "vertexPosition_modelspace"); + GLuint vertexUVID = glGetAttribLocation(shaderProgramID, "vertexUV"); + GLuint textureSamplerID = glGetUniformLocation(shaderProgramID, "myTextureSampler"); + check(); + + for (TexturedMesh* texturedMesh : model->texturedMeshList){ + + Texture* texture = texturedMesh->texture; + + // Create one OpenGL texture + GLuint textureID; + glGenTextures(1, &textureID); + + glActiveTexture(GL_TEXTURE0); + // "Bind" the newly created texture : all future texture functions will modify this texture + glBindTexture(GL_TEXTURE_2D, textureID); + + // Give the image to OpenGL + glTexImage2D(GL_TEXTURE_2D, 0,GL_RGBA, texture->widthPixels, texture->heightPixels, 0, GL_RGBA, GL_UNSIGNED_BYTE, texture->data); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + check(); + glUseProgram(shaderProgramID); + glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]); + glUniform1i(textureSamplerID, 0); + + for (auto& mesh : *texturedMesh->meshList) + { + FloatVector* animatedPositions = animationProcessor.softwareSkin(mesh, model->animData, frame); + + GLuint vertexbuffer; + glGenBuffers(1, &vertexbuffer); + glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); + glBufferData(GL_ARRAY_BUFFER, mesh->numVertices * 12, animatedPositions != nullptr ? animatedPositions : mesh->positions, GL_STATIC_DRAW); + + GLuint uvbuffer; + glGenBuffers(1, &uvbuffer); + glBindBuffer(GL_ARRAY_BUFFER, uvbuffer); + glBufferData(GL_ARRAY_BUFFER, mesh->numVertices * 8, mesh->uvCoords, GL_STATIC_DRAW); + + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer); + glVertexAttribPointer( + vertexPosition_modelspaceID, // The attribute we want to configure + 3, // size + GL_FLOAT, // type + GL_FALSE, // normalized? + 0, // stride + (void*)0 // array buffer offset + ); + + // 2nd attribute buffer : UVs + glEnableVertexAttribArray(1); + glBindBuffer(GL_ARRAY_BUFFER, uvbuffer); + glVertexAttribPointer( + vertexUVID, // The attribute we want to configure + 2, // size : U+V => 2 + GL_FLOAT, // type + GL_FALSE, // normalized? + 0, // stride + (void*)0 // array buffer offset + ); + glVertexPointer(3, GL_FLOAT, 0, mesh->positions); + + glDrawElements(GL_TRIANGLES, mesh->triangleIndices.size(), GL_UNSIGNED_INT, mesh->triangleIndices.data()); + glDisableVertexAttribArray(0); + glDisableVertexAttribArray(1); + + glDeleteBuffers(1, &vertexbuffer); + glDeleteBuffers(1, &uvbuffer); + + delete[] animatedPositions; + } + glDeleteTextures(1, &textureID); + } + glLoadIdentity(); + + glFlush(); + glFinish(); + check(); + SDL_GL_SwapWindow(window); + + cubeRotation -= 0.015f; +} + +bool ModelWindow::init() +{ + bool success = true; + + /* Enable standard application logging */ + SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO); + + /* Initialize SDL for video output */ + if ( SDL_Init(SDL_INIT_VIDEO) < 0 ) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to initialize SDL: %s\n", SDL_GetError()); + success = false; + } + if (success){ + /* Create a 640x480 OpenGL screen */ + window = SDL_CreateWindow( "Model Viewer", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, SDL_WINDOW_OPENGL ); + if ( !window ) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to create OpenGL window: %s\n", SDL_GetError()); + success = false; + } + } + if (success && !SDL_GL_CreateContext(window)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to create OpenGL context: %s\n", SDL_GetError()); + success = false; + } + if (success){ + GLenum err = glewInit(); + if (GLEW_OK != err){ + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GLEW init failed\n"); + success = false; + } + } + if (success){ + glClearColor(0.15f, 0.25f, 0.35f, 1.0f); + glClear( GL_COLOR_BUFFER_BIT ); + + glMatrixMode(GL_PROJECTION); + shaderProgramID = LoadShaders(); + + glEnable(GL_DEPTH_TEST); + // Accept fragment if it closer to the camera than the former one + glDepthFunc(GL_LESS); + } + return success; +} + +void ModelWindow::exit() +{ + glDeleteProgram(shaderProgramID); + SDL_Quit(); +} + +void ModelWindow::drawFrame(Model* model, int frame) +{ + if (nullptr == model){ + drawCube(); + } else { + drawMesh(model, frame); + } +} diff --git a/TestApp/ModelWindow.h b/TestApp/ModelWindow.h new file mode 100644 index 0000000..04dfa60 --- /dev/null +++ b/TestApp/ModelWindow.h @@ -0,0 +1,48 @@ +#pragma once + +#include "GL/glew.h" + +#include "SDL_opengl.h" +#include "SDL.h" + +class Mesh; +class Model; + +class ModelWindow +{ + uint32_t screen_width; + uint32_t screen_height; + + SDL_Window *window; + + GLuint verbose; + GLuint vshader; + GLuint fshader; + GLuint mshader; + GLuint program; + GLuint program2; + GLuint tex_fb; + GLuint tex; + GLuint buf; +// julia attribs + GLuint unif_color, attr_vertex, unif_scale, unif_offset, unif_tex, unif_centre; +// mandelbrot attribs + GLuint attr_vertex2, unif_scale2, unif_offset2, unif_centre2; + + GLuint shaderProgramID; + +public: + ModelWindow() + { + screen_width=640; screen_height=480; + } + + bool init(); + + void exit(); + + void drawFrame(Model* model, int frame); +private: + void drawCube(); + void drawMesh(Model* model, int frame); +}; diff --git a/snowlib/AnimData.cpp b/snowlib/AnimData.cpp new file mode 100644 index 0000000..7dc0744 --- /dev/null +++ b/snowlib/AnimData.cpp @@ -0,0 +1,69 @@ +#include "AnimData.h" +#include "glm/gtc/matrix_transform.hpp" + +void AnimData::BuildPerFrameFKPoses() +{ + perFrameFKPoses = new AnimPose[NumFrames * NumBones]; + glm::vec3* parentPoints = new glm::vec3[64]; + glm::quat* parentRotations = new glm::quat[64]; + for (int frame = 0; frame < NumFrames; ++frame) { + for (int jointNum = 0; jointNum < NumBones; ++jointNum) { + + int parentIndex = skeletonDef[jointNum]; + const glm::vec3& parentPos = parentPoints[parentIndex]; + const glm::quat& parentRot = parentRotations[parentIndex]; + + // The world position of the child joint is the local position of the child joint rotated by the + // world rotation of the parent and then offset by the world position of the parent. + const AnimPose& pose = perFramePoses[frame * NumBones + jointNum]; + + glm::mat3 m = glm::mat3_cast(parentRot); + glm::vec3 thisPos = m * pose.Position; + thisPos += parentPos; + + // The world rotation of the child joint is the world rotation of the parent rotated by the local rotation of the child. + glm::quat thisRot = glm::normalize(parentRot * glm::normalize(pose.Rotation)); + + AnimPose fkPose; + fkPose.Position = thisPos; + fkPose.Rotation = thisRot; + perFrameFKPoses[frame * NumBones + jointNum] = fkPose; + + parentPoints[parentIndex + 1] = fkPose.Position; + parentRotations[parentIndex + 1] = fkPose.Rotation; + } + } +} + +void AnimData::BuildPerFramePoses() +{ + perFramePoses = new AnimPose[NumFrames * NumBones]; + for (auto& pose : MeshPoses) + { + perFramePoses[pose.FrameNum * NumBones + pose.BoneNum] = pose; + } + for (int bone = 0; bone < NumBones; ++bone) + { + for (int frame = 1; frame < NumFrames; ++frame) + { + const AnimPose& prevPose = perFramePoses[(frame-1) * NumBones + bone]; + if (perFramePoses[frame * NumBones + bone].BoneNum < 0) + { + int frameDiff = frame - prevPose.FrameNum; + float avCoeff = frameDiff / 131072.0f; + glm::quat rotDelta = prevPose.AngularVelocity * avCoeff; + + float velCoeff = frameDiff / 512.0f; + glm::vec3 posDelta = prevPose.Velocity * velCoeff; + + AnimPose& pose = perFramePoses[frame * NumBones + bone]; + pose.BoneNum = bone; + pose.FrameNum = frame; + pose.Position = prevPose.Position + posDelta; + pose.Rotation = glm::normalize(prevPose.Rotation + rotDelta); + pose.AngularVelocity = prevPose.AngularVelocity; + pose.Velocity = prevPose.Velocity; + } + } + } +} \ No newline at end of file diff --git a/snowlib/AnimData.h b/snowlib/AnimData.h new file mode 100644 index 0000000..475fbe8 --- /dev/null +++ b/snowlib/AnimData.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +#include +#include + +struct AnimPose +{ + AnimPose() + { + // Signal uninitialised + BoneNum = -1; + FrameNum = -1; + } + + glm::vec3 Position; + glm::quat Rotation; + + glm::quat AngularVelocity; + glm::vec3 Velocity; + + int BoneNum; + int FrameNum; +}; + + +class AnimData +{ +public: + int NumBones; + int NumFrames; + int Offset4Val; + int Offset14Val; + int Offset18Val; // These are 4 bytes which are all ored together + + int* skeletonDef; + + glm::vec3* bindingPose; + + std::string Other; + + std::vector MeshPoses; + + // With forward kinematics applied, Array numFrames * numBones + AnimPose* perFrameFKPoses; + + AnimPose& getFKPose(int frame, int bone) + { + return perFrameFKPoses[frame * NumBones + bone]; + } + + void BuildPerFrameFKPoses(); + void BuildPerFramePoses(); + +private: + // Array numFrames * numBones + AnimPose* perFramePoses; +}; + diff --git a/snowlib/AnimDecoder.cpp b/snowlib/AnimDecoder.cpp new file mode 100644 index 0000000..9039f8c --- /dev/null +++ b/snowlib/AnimDecoder.cpp @@ -0,0 +1,154 @@ +#include "AnimDecoder.h" +#include "AnimData.h" +#include "DataUtil.h" + +AnimData* AnimDecoder::decode(const unsigned char* data, int length) +{ + // Decode BGDA + int endIndex = length; + AnimData* animData = new AnimData(); + animData->NumBones = DataUtil::getLEInt(data, 0); + animData->Offset4Val = DataUtil::getLEInt(data, 4); + animData->Offset14Val = DataUtil::getLEInt(data, 0x14); + animData->Offset18Val = DataUtil::getLEInt(data, 0x18); + int offset8Val = DataUtil::getLEInt(data, 8); + + int bindingPoseOffset = DataUtil::getLEInt(data, 0x0C); + animData->bindingPose = new glm::vec3[animData->NumBones]; + for (int i = 0; i < animData->NumBones; ++i) + { + animData->bindingPose[i].x = -DataUtil::getLEShort(data, bindingPoseOffset + i * 8 + 0) / 64.0f; + animData->bindingPose[i].y = -DataUtil::getLEShort(data, bindingPoseOffset + i * 8 + 2) / 64.0f; + animData->bindingPose[i].z = -DataUtil::getLEShort(data, bindingPoseOffset + i * 8 + 4) / 64.0f; + } + + // Skeleton structure + int offset10Val = DataUtil::getLEInt(data, 0x10); + animData->skeletonDef = new int[animData->NumBones]; + for (int i = 0; i < animData->NumBones; ++i) + { + animData->skeletonDef[i] = data[offset10Val + i]; + } + + AnimPose* curPose = new AnimPose[animData->NumBones]; + + for (int boneNum = 0; boneNum < animData->NumBones; ++boneNum) + { + AnimPose& pose = curPose[boneNum]; + pose.BoneNum = boneNum; + pose.FrameNum = 0; + int frameOff = offset8Val + boneNum * 0x0e; + + pose.Position.x = DataUtil::getLEShort(data, frameOff) / 64.0f; + pose.Position.y = DataUtil::getLEShort(data, frameOff + 2) / 64.0f; + pose.Position.z = DataUtil::getLEShort(data, frameOff + 4) / 64.0f; + + pose.Rotation.w = DataUtil::getLEShort(data, frameOff + 6) / 4096.0f; + pose.Rotation.x = DataUtil::getLEShort(data, frameOff + 8) / 4096.0f; + pose.Rotation.y = DataUtil::getLEShort(data, frameOff + 0x0A) / 4096.0f; + pose.Rotation.z = DataUtil::getLEShort(data, frameOff + 0x0C) / 4096.0f; + + pose.Velocity.x = pose.Velocity.y = pose.Velocity.z = 0.0f; + pose.AngularVelocity.w = pose.AngularVelocity.x = pose.AngularVelocity.y = pose.AngularVelocity.z = 0.0f; + + // This may give us duplicate frame zero poses, but that's ok. + animData->MeshPoses.push_back(pose); + } + int* curAngVelFrame = (int*)calloc(animData->NumBones, sizeof(int)); + int* curVelFrame = (int*)calloc(animData->NumBones, sizeof(int)); + + animData->NumFrames = 1; + + int totalFrame = 0; + int otherOff = offset8Val + animData->NumBones * 0x0e; + + AnimPose* pose = nullptr; + while (otherOff < endIndex) { + int count = data[otherOff++]; + unsigned char byte2 = data[otherOff++]; + int boneNum = byte2 & 0x3f; + if (boneNum == 0x3f) break; + + totalFrame += count; + + if (pose == nullptr || pose->FrameNum != totalFrame || pose->BoneNum != boneNum) + { + if (pose != nullptr) + { + animData->MeshPoses.push_back(*pose); + } + delete pose; + pose = new AnimPose(); + pose->FrameNum = totalFrame; + pose->BoneNum = boneNum; + pose->Position = curPose[boneNum].Position; + pose->Rotation = curPose[boneNum].Rotation; + pose->AngularVelocity = curPose[boneNum].AngularVelocity; + pose->Velocity = curPose[boneNum].Velocity; + } + + // bit 7 specifies whether to read 4 (set) or 3 elements following + // bit 6 specifies whether they are shorts or bytes (set). + if ((byte2 & 0x80) == 0x80) { + int w, x, y, z; + if ((byte2 & 0x40) == 0x40) { + w = (char)data[otherOff++]; + x = (char)data[otherOff++]; + y = (char)data[otherOff++]; + z = (char)data[otherOff++]; + } else { + w = DataUtil::getLEShort(data, otherOff); + x = DataUtil::getLEShort(data, otherOff+2); + y = DataUtil::getLEShort(data, otherOff+4); + z = DataUtil::getLEShort(data, otherOff+6); + otherOff += 8; + } + glm::quat angVel(w, x, y, z); + + glm::quat prevAngVel = pose->AngularVelocity; + float coeff = (totalFrame - curAngVelFrame[boneNum]) / 131072.0f; + glm::quat angDelta = prevAngVel * coeff; + pose->Rotation = pose->Rotation + angDelta; + + pose->FrameNum = totalFrame; + pose->AngularVelocity = angVel; + + curPose[boneNum].Rotation = pose->Rotation; + curPose[boneNum].AngularVelocity = pose->AngularVelocity; + curAngVelFrame[boneNum] = totalFrame; + } + else + { + int x, y, z; + if ((byte2 & 0x40) == 0x40) { + x = (char)data[otherOff++]; + y = (char)data[otherOff++]; + z = (char)data[otherOff++]; + } else { + x = DataUtil::getLEShort(data, otherOff); + y = DataUtil::getLEShort(data, otherOff + 2); + z = DataUtil::getLEShort(data, otherOff + 4); + otherOff += 6; + } + glm::vec3 vel(x, y, z); + glm::vec3 prevVel = pose->Velocity; + float coeff = (totalFrame - curVelFrame[boneNum]) / 512.0f; + glm::vec3 posDelta = prevVel * coeff; + pose->Position = pose->Position + posDelta; + pose->FrameNum = totalFrame; + pose->Velocity = vel; + + curPose[boneNum].Position = pose->Position; + curPose[boneNum].Velocity = pose->Velocity; + curVelFrame[boneNum] = totalFrame; + } + } + animData->MeshPoses.push_back(*pose); + delete pose; + free(curAngVelFrame); + free(curVelFrame); + animData->NumFrames = totalFrame+1; + animData->BuildPerFramePoses(); + animData->BuildPerFrameFKPoses(); + return animData; +} diff --git a/snowlib/AnimDecoder.h b/snowlib/AnimDecoder.h new file mode 100644 index 0000000..9cc60e1 --- /dev/null +++ b/snowlib/AnimDecoder.h @@ -0,0 +1,9 @@ +#pragma once + +class AnimData; + +class AnimDecoder +{ +public: + AnimData* decode(const unsigned char* data, int length); +}; diff --git a/snowlib/AnimationProcessor.cpp b/snowlib/AnimationProcessor.cpp new file mode 100644 index 0000000..077186e --- /dev/null +++ b/snowlib/AnimationProcessor.cpp @@ -0,0 +1,102 @@ +#include "AnimationProcessor.h" +#include "Mesh.h" +#include "AnimData.h" +#include + +FloatVector* AnimationProcessor::softwareSkin(Mesh* mesh, AnimData* animData, int frame) +{ + FloatVector* animatedPositions; + if (animData == nullptr || frame < 0 || mesh->vertexWeights.empty()){ + animatedPositions = nullptr; + } else { + auto vwIterator = mesh->vertexWeights.begin(); + VertexWeight* vw = &*vwIterator++; + animatedPositions = new FloatVector[mesh->numVertices]; + for (int vnum=0; vnumnumVertices; ++vnum){ + if (vw->endVertex < vnum) + { + vw = &*vwIterator++; + + } + glm::vec4 vpos = glm::vec4(mesh->positions[vnum].x, mesh->positions[vnum].y, mesh->positions[vnum].z, 1.0f); + int bone1No = vw->bone1; + + glm::vec3 bindingPos1 = animData->bindingPose[bone1No]; + const AnimPose& bone1Pose = animData->getFKPose(frame, bone1No); + glm::vec3 joint1Pos = bone1Pose.Position; + + glm::vec3 inverseBindingPos1 = -bindingPos1; + glm::mat4 m1 = glm::translate(glm::mat4(1.0f), inverseBindingPos1); + m1 = glm::mat4_cast(bone1Pose.Rotation) * m1; + glm::vec4 vpos1 = m1 * vpos; + m1 = glm::translate(glm::mat4(1.0f), bone1Pose.Position); + vpos1 = m1 * vpos1; + + if (vw->bone2 == 0xFF) { + vpos = vpos1; + } else { + // multi-bone + int bone2No = vw->bone2; + glm::vec3 bindingPos2 = animData->bindingPose[bone2No]; + const AnimPose& bone2Pose = animData->getFKPose(frame, bone2No); + glm::vec3 joint2Pos = bone2Pose.Position; + + glm::vec3 inverseBindingPos2 = -bindingPos2; + glm::mat4 m2 = glm::translate(glm::mat4(1.0f), inverseBindingPos2); + m2 = glm::mat4_cast(bone2Pose.Rotation) * m2; + glm::vec4 vpos2 = m2 * vpos; + m2 = glm::translate(glm::mat4(1.0f), bone2Pose.Position); + vpos2 = m2 * vpos2; + if (vw->bone3 == 0xFF) { + float boneSum = (float)(vw->boneWeight1 + vw->boneWeight2); + float bone1Coeff = vw->boneWeight1 / boneSum; + float bone2Coeff = vw->boneWeight2 / boneSum; + + vpos = vpos1 * bone1Coeff + vpos2 * bone2Coeff; + } else { + int bone3No = vw->bone3; + glm::vec3 bindingPos3 = animData->bindingPose[bone3No]; + const AnimPose& bone3Pose = animData->getFKPose(frame, bone3No); + glm::vec3 joint3Pos = bone3Pose.Position; + + glm::vec3 inverseBindingPos3 = -bindingPos3; + glm::mat4 m3 = glm::translate(glm::mat4(1.0f), inverseBindingPos3); + m3 = glm::mat4_cast(bone3Pose.Rotation) * m3; + glm::vec4 vpos3 = m3 * vpos; + m3 = glm::translate(glm::mat4(1.0f), bone3Pose.Position); + vpos3 = m3 * vpos3; + if (vw->bone4 == 0xFF) { + float boneSum = (float)(vw->boneWeight1 + vw->boneWeight2 + vw->boneWeight3); + float bone1Coeff = vw->boneWeight1 / boneSum; + float bone2Coeff = vw->boneWeight2 / boneSum; + float bone3Coeff = vw->boneWeight3 / boneSum; + vpos = vpos1 * bone1Coeff + vpos2 * bone2Coeff + vpos3 * bone3Coeff; + } else { + int bone4No = vw->bone4; + glm::vec3 bindingPos4 = animData->bindingPose[bone4No]; + const AnimPose& bone4Pose = animData->getFKPose(frame, bone4No); + glm::vec3 joint4Pos = bone4Pose.Position; + + glm::vec3 inverseBindingPos4 = -bindingPos4; + glm::mat4 m4 = glm::translate(glm::mat4(1.0f), inverseBindingPos4); + m4 = glm::mat4_cast(bone4Pose.Rotation) * m4; + glm::vec4 vpos4 = m4 * vpos; + m4 = glm::translate(glm::mat4(1.0f), bone4Pose.Position); + vpos4 = m4 * vpos4; + + float boneSum = (float)(vw->boneWeight1 + vw->boneWeight2 + vw->boneWeight3 + vw->boneWeight4); + float bone1Coeff = vw->boneWeight1 / boneSum; + float bone2Coeff = vw->boneWeight2 / boneSum; + float bone3Coeff = vw->boneWeight3 / boneSum; + float bone4Coeff = vw->boneWeight4 / boneSum; + vpos = vpos1 * bone1Coeff + vpos2 * bone2Coeff + vpos3 * bone3Coeff + vpos4 * bone4Coeff; + } + } + } + animatedPositions[vnum].x = vpos.x; + animatedPositions[vnum].y = vpos.y; + animatedPositions[vnum].z = vpos.z; + } + } + return animatedPositions; +} diff --git a/snowlib/AnimationProcessor.h b/snowlib/AnimationProcessor.h new file mode 100644 index 0000000..71f18ec --- /dev/null +++ b/snowlib/AnimationProcessor.h @@ -0,0 +1,11 @@ +#pragma once + +struct FloatVector; +class Mesh; +class AnimData; + +class AnimationProcessor +{ +public: + FloatVector* softwareSkin(Mesh* mesh, AnimData* animData, int frame); +}; diff --git a/snowlib/DataAccess.h b/snowlib/DataAccess.h new file mode 100644 index 0000000..dfc198a --- /dev/null +++ b/snowlib/DataAccess.h @@ -0,0 +1,11 @@ +#ifndef DATAACCESS_H_ +#define DATAACCESS_H_ + +// functions for reading data from a buffer. + +int getLEInt32(const unsigned char* data) +{ + return *reinterpret_cast(data); +} + +#endif // DATAACCESS_H_ \ No newline at end of file diff --git a/snowlib/DataUtil.h b/snowlib/DataUtil.h new file mode 100644 index 0000000..aed524a --- /dev/null +++ b/snowlib/DataUtil.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +class DataUtil +{ +public: + static int getLEInt(const unsigned char* data, int offset){ + return *(int32_t *)(data + offset); + } + + static short getLEShort(const unsigned char* data, int offset){ + return *(int16_t *)(data + offset); + } + + static unsigned short getLEUShort(const unsigned char* data, int offset){ + return *(uint16_t *)(data + offset); + } +}; + diff --git a/snowlib/FntDecoder.cpp b/snowlib/FntDecoder.cpp new file mode 100644 index 0000000..912cb74 --- /dev/null +++ b/snowlib/FntDecoder.cpp @@ -0,0 +1,99 @@ + +#include "FntDecoder.h" +#include "DataUtil.h" + +#define GLYPH_MASK 0xf800 + +// Enable debugging in release mode +#pragma optimize( "", off ) + +void FntDecoder::charsToGlyphsInplace(const unsigned char* font, unsigned short* charsOrGlyphs, int len, int ps2FontAddress) +{ + if (len == 0 || charsOrGlyphs[0] == 0 || (charsOrGlyphs[0] & GLYPH_MASK) == GLYPH_MASK) { + // Already glyphs, no conversion necessary + return; + } + for (int i = 0; i < len; ++i) { + int charCode = charsOrGlyphs[i]; + int glyphId = charCodeToGlyphId(font, charCode, ps2FontAddress); + charsOrGlyphs[i] = glyphId; + } +} + +int FntDecoder::charCodeToGlyphId(const unsigned char* font, int charCode, int ps2FontAddress) +{ + int glyphId = 0; + + int numGlyphs = DataUtil::getLEShort(font, 0); + int glyphTableOffset = DataUtil::getLEInt(font, 8) - ps2FontAddress; + + int glyphOffset = glyphTableOffset; + for (int glyphNum = 0; glyphNum < numGlyphs; ++glyphNum) { + int charId = DataUtil::getLEShort(font, glyphOffset); + if (charId == charCode) { + return glyphNum; + } + glyphOffset += 0x10; + } + + return glyphId; +} + +GlyphInfo& FntDecoder::lookupGlyph(const unsigned char* font, int glyphNum, int ps2FontAddress) +{ + int numGlyphs = DataUtil::getLEShort(font, 0); + glyphNum &= ~GLYPH_MASK; + if (glyphNum >= numGlyphs) { + glyphNum = 0; + } + int glyphTableOffset = DataUtil::getLEInt(font, 8) - ps2FontAddress; + return *(GlyphInfo*)(font + glyphTableOffset + 0x10 * glyphNum); +} + +int FntDecoder::getCharAdvance(const unsigned char* font, uint16_t glyphNum, int ps2FontAddress) +{ + int numGlyphs = DataUtil::getLEShort(font, 0); + + int glyphTableOffset = DataUtil::getLEInt(font, 8) - ps2FontAddress; + + glyphNum &= ~GLYPH_MASK; + if (glyphNum >= numGlyphs) { + glyphNum = 0; + } + GlyphInfo* glyph = (GlyphInfo*)(font + glyphTableOffset + 0x10 * glyphNum); + int advance = glyph->width; + + return advance; +} + +int FntDecoder::getKernPairAdjust(const unsigned char* font, uint16_t previousGlyph, uint16_t glyphNum, int ps2FontAddress) +{ + int numGlyphs = DataUtil::getLEShort(font, 0); + + int glyphTableOffset = DataUtil::getLEInt(font, 8) - ps2FontAddress; + int kernTableOffset = DataUtil::getLEInt(font, 0x0C) - ps2FontAddress; + + glyphNum &= ~GLYPH_MASK; + previousGlyph &= ~GLYPH_MASK; + if (glyphNum >= numGlyphs) { + glyphNum = 0; + } + GlyphInfo* glyph = (GlyphInfo*)(font + glyphTableOffset + 0x10 * glyphNum); + int advance = 0; + int kernId = glyph->kernId; + if (previousGlyph > 0) { + while (kernId != 0xFFFF) { + GlyphKernPair* kernPairEntry = (GlyphKernPair*)(font + kernTableOffset + 6 * kernId); + ++kernId; + if (kernPairEntry->glyph1 == 0xFFFF || kernPairEntry->glyph2 != glyphNum) { + kernId = 0xFFFF; + } + else if (kernPairEntry->glyph1 == previousGlyph) { + advance += kernPairEntry->kern; + kernId = 0xFFFF; + } + } + } + + return advance; +} diff --git a/snowlib/FntDecoder.h b/snowlib/FntDecoder.h new file mode 100644 index 0000000..6e6e6f9 --- /dev/null +++ b/snowlib/FntDecoder.h @@ -0,0 +1,35 @@ +#pragma once + +#include "stdint.h" + +class Texture; + +struct GlyphInfo +{ + uint16_t charId; + uint16_t x0; + uint16_t x1; + uint16_t y0; + uint16_t y1; + uint16_t yOffset; + uint16_t width; + uint16_t kernId; +}; + +struct GlyphKernPair +{ + uint16_t glyph1; + uint16_t glyph2; + int16_t kern; +}; + +class FntDecoder +{ +public: + void charsToGlyphsInplace(const unsigned char* font, unsigned short* charsOrGlyphs, int len, int ps2FontAddress); + int charCodeToGlyphId(const unsigned char* font, int charCode, int ps2FontAddress); + GlyphInfo& lookupGlyph(const unsigned char* font, int glyphNum, int ps2FontAddress); + int getCharAdvance(const unsigned char* font, uint16_t glyph, int ps2FontAddress); + int getKernPairAdjust(const unsigned char* font, uint16_t previousGlyph, uint16_t glyph, int ps2FontAddress); +}; + diff --git a/snowlib/GIFTag.cpp b/snowlib/GIFTag.cpp new file mode 100644 index 0000000..025facf --- /dev/null +++ b/snowlib/GIFTag.cpp @@ -0,0 +1,29 @@ +#include "GIFTag.h" +#include "DataUtil.h" + +void GIFTag::parse(const unsigned char* data, int offset) +{ + int low32 = DataUtil::getLEInt(data, offset); + nloop = low32 & 0x7FFF; + eop = (low32 & 0x8000) == 0x8000; + + int next32 = DataUtil::getLEInt(data, offset + 4); + + // bit 32 is bit 0 of next 32 + pre = ((next32 >> (46 - 32)) & 1) == 1; + // prim 11 bits 47 - 57 + prim = ((next32 >> (47 - 32)) & 0x3FF); + flg = ((next32 >> (58 - 32)) & 0x3); + nreg = ((next32 >> (60 - 32)) & 0xf); + + if (0 == nreg){ + nreg = 16; + } + int regs64 = DataUtil::getLEInt(data, offset + 8); + int regs96 = DataUtil::getLEInt(data, offset + 12); + + for (int reg=0; reg < nreg; ++reg){ + int rgs = reg > 7 ? regs96 : regs64; + regs[reg] = (rgs >> ((reg & 7) * 4)) & 0x0f; + } +} diff --git a/snowlib/GIFTag.h b/snowlib/GIFTag.h new file mode 100644 index 0000000..e8fdc77 --- /dev/null +++ b/snowlib/GIFTag.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +class GIFTag +{ +public: + int nloop; + bool eop; + bool pre; + int prim; + int flg; + int nreg; + int regs[16]; + + void parse(const unsigned char* data, int offset); +}; diff --git a/snowlib/GameType.h b/snowlib/GameType.h new file mode 100644 index 0000000..b393eb5 --- /dev/null +++ b/snowlib/GameType.h @@ -0,0 +1,10 @@ +#ifndef GAMETYPE_H_ +#define GAMETYPE_H_ + +enum GameType +{ + DARK_ALLIANCE, + CHAMPIONS_RTA +}; + +#endif // GAMETYPE_H_ diff --git a/snowlib/GobFile.cpp b/snowlib/GobFile.cpp new file mode 100644 index 0000000..b11e3bb --- /dev/null +++ b/snowlib/GobFile.cpp @@ -0,0 +1,49 @@ +#include "GobFile.h" +#include "Logger.h" +#include "DataUtil.h" +#include + +void GobFile::buildLmpFileMap() +{ + lmpFileMap.clear(); + int offset = 0; + while (gobData[offset] != 0){ + std::string lmpName((char*)gobData + offset); + int lmpDataOffset = DataUtil::getLEInt(gobData, offset + 0x20); + int lmpDataLength = DataUtil::getLEInt(gobData, offset + 0x24); + LmpFile* lmpFile = new LmpFile(gameType, gobData, lmpDataOffset, lmpDataLength); + lmpFileMap[lmpName] = lmpFile; + offset += 0x28; + } +} + +void GobFile::readGobFile() +{ + FILE* file = fopen(gobPath.c_str(), "r"); + if (file != nullptr){ + fseek(file, 0, SEEK_END); + long size = ftell(file); + rewind(file); + + gobDataLength = (int)size; + delete[] gobData; + gobData = new unsigned char[gobDataLength]; + + long bytesRead = fread(gobData, 1, size, file); + fclose(file); + } + else { + Logger::getLogger()->logFormat("failed to read file: '%s'\n", gobPath.c_str()); + } +} + + +LmpFile* GobFile::getLmp(string lmpName) +{ + if (gobDataLength == 0){ + readGobFile(); + buildLmpFileMap(); + } + return lmpFileMap[lmpName]; +} + diff --git a/snowlib/GobFile.h b/snowlib/GobFile.h new file mode 100644 index 0000000..068536a --- /dev/null +++ b/snowlib/GobFile.h @@ -0,0 +1,38 @@ +#pragma once + +#include "LmpRepository.h" + +/// A Gob file is a collection of Lmp files. +class GobFile : public LmpRepository +{ + string gobPath; + GameType gameType; + + unsigned char* gobData; + int gobDataLength; + + unordered_map lmpFileMap; + + void readGobFile(); + void buildLmpFileMap(); + +public: + GobFile(string gobPath, GameType gameType) + { + this->gobPath = gobPath; + this->gameType = gameType; + gobData = nullptr; + gobDataLength = 0; + } + + ~GobFile() + { + for (auto& x : lmpFileMap){ + delete x.second; + } + lmpFileMap.clear(); + delete[] gobData; + } + + LmpFile* getLmp(string lmpName); +}; diff --git a/snowlib/Helpers.cpp b/snowlib/Helpers.cpp new file mode 100644 index 0000000..24a7c72 --- /dev/null +++ b/snowlib/Helpers.cpp @@ -0,0 +1,15 @@ +#include "Helpers.h" +#include + +namespace Helpers +{ + const char* strjoin(const char* s1, const char* s2) + { + int len = strlen(s1) + strlen(s2); + char* buf = new char[len+1]; + strcpy(buf, s1); + strcat(buf, s2); + return buf; + } + +} diff --git a/snowlib/Helpers.h b/snowlib/Helpers.h new file mode 100644 index 0000000..51da9d8 --- /dev/null +++ b/snowlib/Helpers.h @@ -0,0 +1,6 @@ +#pragma once + +namespace Helpers +{ + const char* strjoin(const char* s1, const char* s2); +} \ No newline at end of file diff --git a/snowlib/LmpFile.cpp b/snowlib/LmpFile.cpp new file mode 100644 index 0000000..8134d9f --- /dev/null +++ b/snowlib/LmpFile.cpp @@ -0,0 +1,71 @@ +#include "LmpFile.h" +#include "DataAccess.h" +#include "Helpers.h" +#include "Logger.h" +#include "DataUtil.h" + +LmpFile::LmpFile(GameType gameType, const unsigned char* data, int startOffset, int dataLength) +{ + _gameType = gameType; + _data = data; + _startOffset = startOffset; + _dataLength = dataLength; +} + +#define LMP_ENTRY_LEN 64 + +LmpEntry* LmpFile::findEntry(const char* name) +{ + LmpEntry* entry = NULL; + + int numEntries = getLEInt32(_data+_startOffset); + for (int entryNum=0; entryNumlogFormat("Failed to find LMP entry '%s'\n", name); + } + return entry; +} + +LmpEntry* LmpFile::findEntry(const char* name, const char* extension) +{ + return findEntry(Helpers::strjoin(name, extension)); +} + +/* returns 1 iff str ends with suffix */ +int str_ends_with(const char * str, const char * suffix) { + + if( str == nullptr || suffix == nullptr ) + return 0; + + size_t str_len = strlen(str); + size_t suffix_len = strlen(suffix); + + if(suffix_len > str_len) + return 0; + + return 0 == strncmp( str + str_len - suffix_len, suffix, suffix_len ); +} + +std::vector LmpFile::findFilenamesByExtension(const char* extension) +{ + std::vector foundFilenames; + int numEntries = getLEInt32(_data+_startOffset); + int extensionLength = strlen(extension); + for (int entryNum=0; entryNum extensionLength && 0 == strncmp( pName + entryNameLen - extensionLength, extension, extensionLength )){ + foundFilenames.push_back(std::string(pName)); + } + } + return foundFilenames; +} \ No newline at end of file diff --git a/snowlib/LmpFile.h b/snowlib/LmpFile.h new file mode 100644 index 0000000..454cf8a --- /dev/null +++ b/snowlib/LmpFile.h @@ -0,0 +1,47 @@ +#ifndef LMPFILE_H_ +#define LMPFILE_H_ + +#include +#include +#include "GameType.h" + +/// An object that describes an entry in a LmpFile. +class LmpEntry +{ +public: + + LmpEntry(const unsigned char* dataIn, int lenIn) : data(dataIn), length(lenIn){} + + /// Pointer to the start of the file data. + const unsigned char* data; + + /// The length of the file data. + int length; +}; + +class LmpFile +{ +public: + /// Constructor. + LmpFile(GameType gameType, const unsigned char* data, int startOffset, int dataLength); + + /// Find an entry in the Lump. + /// Returns the entry descriptor. Caller is responsible for deleting the returned object. + /// If the file is not found, returns null. + LmpEntry* findEntry(const char* name); + + /// Find an entry in the Lump. + /// Returns the entry descriptor. Caller is responsible for deleting the returned object. + /// If the file is not found, returns null. + LmpEntry* findEntry(const char* name, const char* extension); + + std::vector findFilenamesByExtension(const char* extension); + +private: + GameType _gameType; + const unsigned char* _data; + int _startOffset; + int _dataLength; +}; + +#endif // LMPFILE_H_ diff --git a/snowlib/LmpRepository.cpp b/snowlib/LmpRepository.cpp new file mode 100644 index 0000000..211b214 --- /dev/null +++ b/snowlib/LmpRepository.cpp @@ -0,0 +1,33 @@ +#include "LmpRepository.h" +#include "Logger.h" + +LmpFile* LmpRepositoryImpl::readLmpFile(string lmpName) +{ + LmpFile* lmpFile = nullptr; + string fullpath = dataPath + lmpName; + FILE* file = fopen(fullpath.c_str(), "r"); + if (file != nullptr){ + fseek (file , 0 , SEEK_END); + long size = ftell (file); + rewind (file); + + char* buffer = (char*) malloc (size); + + long bytesRead = fread (buffer,1,size,file); + fclose(file); + + lmpFile = new LmpFile(gameType, (unsigned char *)buffer, 0, size); + } else { + Logger::getLogger()->logFormat("failed to read file: '%s'\n", fullpath.c_str()); + } + return lmpFile; +} + +LmpRepositoryImpl::~LmpRepositoryImpl() +{ + for (auto& x : lmpFileMap){ + delete x.second; + } + lmpFileMap.clear(); +} + diff --git a/snowlib/LmpRepository.h b/snowlib/LmpRepository.h new file mode 100644 index 0000000..bd8ecf5 --- /dev/null +++ b/snowlib/LmpRepository.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include "LmpFile.h" + +using namespace std; + +class LmpRepository +{ +public: + virtual LmpFile* getLmp(string lmpName) = 0; +}; + +/// Stores Lmp Files +class LmpRepositoryImpl : public LmpRepository +{ + unordered_map lmpFileMap; + string dataPath; + GameType gameType; + LmpFile* readLmpFile(string lmpName); + +public: + LmpRepositoryImpl(string dataPath, GameType gameType) + { + this->dataPath = dataPath; + this->gameType = gameType; + } + + ~LmpRepositoryImpl(); + + LmpFile* getLmp(string lmpName) + { + if (lmpFileMap.count(lmpName) == 0){ + lmpFileMap[lmpName] = readLmpFile(lmpName); + } + return lmpFileMap[lmpName]; + } +}; diff --git a/snowlib/Logger.cpp b/snowlib/Logger.cpp new file mode 100644 index 0000000..6fd1851 --- /dev/null +++ b/snowlib/Logger.cpp @@ -0,0 +1,3 @@ +#include "Logger.h" + +Logger* Logger::logger; diff --git a/snowlib/Logger.h b/snowlib/Logger.h new file mode 100644 index 0000000..dde2d58 --- /dev/null +++ b/snowlib/Logger.h @@ -0,0 +1,43 @@ +#pragma once + +#ifdef WIN32 +#include +#endif + +#include +#include + +class Logger +{ +private: + static Logger* logger; + + Logger(){} + +public: + static Logger* getLogger() + { + if (logger == nullptr){ + logger = new Logger(); + } + return logger; + } + + void log(const char* message) + { + std::cerr << message; +#ifdef WIN32 + OutputDebugStringA(message); +#endif + } + + void logFormat(const char* format, ...) + { + char buf[256]; + va_list args; + va_start (args, format); + vsnprintf(buf, 256, format, args); + va_end(args); + log(buf); + } +}; diff --git a/snowlib/Mesh.cpp b/snowlib/Mesh.cpp new file mode 100644 index 0000000..87e72db --- /dev/null +++ b/snowlib/Mesh.cpp @@ -0,0 +1,15 @@ +#include "Mesh.h" +#include "assert.h" + +void Mesh::adjustUVs(int textureWidth, int textureHeight) +{ + for (int i=0; i + +class Mesh +{ +public: + std::vector triangleIndices; + FloatVector* positions; + FloatVector* normals; + FloatPoint* uvCoords; + + int numVertices; + + std::vector vertexWeights; + + void adjustUVs(int textureWidth, int textureHeight); +}; diff --git a/snowlib/Model.h b/snowlib/Model.h new file mode 100644 index 0000000..2312c1e --- /dev/null +++ b/snowlib/Model.h @@ -0,0 +1,19 @@ +#pragma once +#include + +class Texture; +class AnimData; + +class TexturedMesh +{ +public: + std::vector* meshList; + Texture* texture; +}; + +class Model +{ +public: + std::vector texturedMeshList; + AnimData* animData; +}; diff --git a/snowlib/Palette.h b/snowlib/Palette.h new file mode 100644 index 0000000..1373b1d --- /dev/null +++ b/snowlib/Palette.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +class Palette +{ +public: + Palette() + { + rgbaData = nullptr; + numEntries = 0; + } + + ~Palette() + { + delete[] rgbaData; + } + + // in ps2 0x80 is fully transparent and 0 is opaque. + unsigned char* rgbaData; + + int numEntries; + + int32_t getValue(unsigned int idx) + { + return ((int32_t*)rgbaData)[idx]; + } + + void read(const unsigned char* data, int palw, int palh) + { + numEntries = palw * palh; + rgbaData = new unsigned char[numEntries*4]; + memcpy(rgbaData, data, numEntries*4); + } + + void unswizzle() + { + if (numEntries == 256) { + unsigned char* unswizzled = new unsigned char[numEntries*4]; + + int j = 0; + for (int i = 0; i < 256; i += 32, j += 32) { + copy(unswizzled, i, rgbaData, j, 8); + copy(unswizzled, i + 16, rgbaData, j + 8, 8); + copy(unswizzled, i + 8, rgbaData, j + 16, 8); + copy(unswizzled, i + 24, rgbaData, j + 24, 8); + } + delete[] rgbaData; + rgbaData = unswizzled; + } + } + + void copy(unsigned char* unswizzled, int i, unsigned char* swizzled, int j, int num) + { + int32_t* unswizzled32 = (int32_t *)unswizzled; + int32_t* swizzled32 = (int32_t *)swizzled; + for (int x = 0; x < num; ++x) { + unswizzled32[i + x] = swizzled32[j + x]; + } + } +}; diff --git a/snowlib/Scene.h b/snowlib/Scene.h new file mode 100644 index 0000000..8ad97b7 --- /dev/null +++ b/snowlib/Scene.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +class Model; + +class SceneElement +{ +public: + Model* model; + glm::vec3 position; +}; + +class Scene +{ + +}; diff --git a/snowlib/TexDecoder.cpp b/snowlib/TexDecoder.cpp new file mode 100644 index 0000000..49019b1 --- /dev/null +++ b/snowlib/TexDecoder.cpp @@ -0,0 +1,182 @@ +#include "Texture.h" +#include "TexDecoder.h" +#include "DataUtil.h" +#include "GIFTag.h" +#include "Palette.h" + +#pragma optimize( "", off ) + +Texture* TexDecoder::decode(const unsigned char* data, int ps2Addr) +{ + int finalw = DataUtil::getLEShort(data, 0); + int finalh = DataUtil::getLEShort(data, 02); + + int curIdx = DataUtil::getLEInt(data, 0x10); + + return decode(finalw, finalh, data, curIdx - ps2Addr); +} + +Texture* TexDecoder::decode(int finalw, int finalh, const unsigned char* data, int curIdx) +{ + int sourcew = finalw; + int sourceh = finalh; + pixels = nullptr; + pixelsLength = 0; + + GIFTag gifTag; + gifTag.parse(data, curIdx); + + // This is basically heuristics. Writing a full GIF parser is complex and as the texture files are written by a tool, + // we can safely make some assumptions about their structure. + if (gifTag.nloop == 4) { + + int palw = DataUtil::getLEShort(data, curIdx + 0x30); + int palh = DataUtil::getLEShort(data, curIdx + 0x34); + + curIdx += 0x50; + GIFTag gifTag2; + gifTag2.parse(data, curIdx); + + // 8 bit palletised + Palette* palette = new Palette(); + palette->read(data + curIdx + 0x10, palw, palh); + palette->unswizzle(); + + int palLen = palw * palh * 4; + curIdx += (palLen + 0x10); + + GIFTag* gifTag50 = new GIFTag(); + gifTag50->parse(data, curIdx); + curIdx += 0x20; + + int dbw = (sourcew / 2 + 0x07) & ~0x07; + int dbh = (sourceh / 2 + 0x07) & ~0x07; + + + int totalRrw = 0; + bool eop = false; + // Need to find a better way than this. + while (!eop || totalRrw < dbw) { + GIFTag* gifTag3 = new GIFTag(); + gifTag3->parse(data, curIdx); + + int dimOffset = 0x10; + + int thisRrw = DataUtil::getLEShort(data, curIdx + dimOffset); + int thisRrh = DataUtil::getLEShort(data, curIdx + dimOffset + 4); + + int startx = DataUtil::getLEShort(data, curIdx + dimOffset + 20); + int starty = DataUtil::getLEShort(data, curIdx + dimOffset + 22); + + curIdx += gifTag.nloop * 0x10 + 0x10; + readPixels32(data, palette, curIdx, startx, starty, thisRrw, thisRrh, dbw, dbh); + curIdx += thisRrw * thisRrh * 4; + + totalRrw += thisRrw; + eop = gifTag3->eop; + } + if (palLen != 64) { + unswizzle8bpp(dbw * 2, dbh * 2); + sourcew = dbw * 2; + sourceh = dbh * 2; + } else { + sourcew = dbw; + sourceh = dbh; + } + delete palette; + + } else if (gifTag.nloop == 3) { + GIFTag* gifTag2 = new GIFTag(); + gifTag2->parse(data, 0xC0); + + if (gifTag2->flg == 2) { + // image mode + readPixels32(data + 0xD0, finalw, finalh); + } + } + Texture* texture = new Texture(finalw, finalh, sourcew, sourceh, pixels, sourcew * sourceh * 4); + return texture; +} + +void TexDecoder::unswizzle8bpp(int w, int h) +{ + unsigned char* unswizzled = new unsigned char[pixelsLength]; + + int32_t* unswizzled32 = (int32_t*)unswizzled; + int32_t* pixels32 = (int32_t*)pixels; + + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + + int block_location = (y & (~0xf)) * w + (x & (~0xf)) * 2; + int swap_selector = (((y + 2) >> 2) & 0x1) * 4; + int posY = (((y & (~3)) >> 1) + (y & 1)) & 0x7; + int column_location = posY * w * 2 + ((x + swap_selector) & 0x7) * 4; + + int byte_num = ((y >> 1) & 1) + ((x >> 2) & 2); // 0,1,2,3 + + int idx = block_location + column_location + byte_num; + if (idx < pixelsLength) { + unswizzled32[(y * w) + x] = pixels32[idx]; + } + } + } + delete[] pixels; + pixels = unswizzled; +} + +void TexDecoder::readPixels32(const unsigned char* data, Palette* palette, int startOffset, int startx, int starty, int rrw, int rrh, int dbw, int dbh) +{ + if (palette->numEntries == 256) { + int numDestPixels = dbh * dbw * 4; + int widthPixels = dbw * 4; + if (pixels == nullptr) { + pixelsLength = numDestPixels*4; + pixels = new unsigned char[pixelsLength]; + } + int32_t* pixels32 = (int32_t*)pixels; + int idx = startOffset; + for (int y = 0; y < rrh && (y+starty) < dbh; ++y) { + for (int x = 0; x < rrw; ++x) { + int destIdx = (y+starty) * widthPixels + (x + startx) * 4; + + pixels32[destIdx++] = palette->getValue(data[idx++]); + pixels32[destIdx++] = palette->getValue(data[idx++]); + pixels32[destIdx++] = palette->getValue(data[idx++]); + pixels32[destIdx] = palette->getValue(data[idx++]); + } + } + + } else { + int numDestPixels = rrh * dbw; + if (pixels == nullptr) { + pixelsLength = numDestPixels*4; + pixels = new unsigned char[pixelsLength]; + } + int32_t* pixels32 = (int32_t*)pixels; + int idx = startOffset; + bool lowbit = false; + for (int y = 0; y < rrh; ++y) { + for (int x = 0; x < rrw; ++x) { + int destIdx = (y + starty) * dbw + x + startx; + if (!lowbit) { + pixels[destIdx] = palette->getValue(data[idx] >> 4 & 0x0F); + } else { + pixels[destIdx] = palette->getValue(data[idx++] & 0x0F); + } + lowbit = !lowbit; + } + } + } +} + + +void TexDecoder::readPixels32(const unsigned char* data, int w, int h) +{ + int numDestPixels = w * h; + if (pixels == nullptr) { + pixelsLength = numDestPixels*4; + pixels = new unsigned char[pixelsLength]; + } + memcpy(pixels, data, numDestPixels*4); +} \ No newline at end of file diff --git a/snowlib/TexDecoder.h b/snowlib/TexDecoder.h new file mode 100644 index 0000000..8963144 --- /dev/null +++ b/snowlib/TexDecoder.h @@ -0,0 +1,19 @@ +#pragma once + +class Texture; +class Palette; + +class TexDecoder +{ +public: + Texture* decode(const unsigned char* data, int ps2Addr); + Texture* decode(int finalw, int finalh, const unsigned char* data, int curIdx); + +private: + void readPixels32(const unsigned char* data, Palette* palette, int startOffset, int startx, int starty, int rrw, int rrh, int dbw, int dbh); + void readPixels32(const unsigned char* data, int w, int h); + + void unswizzle8bpp(int w, int h); + unsigned char* pixels; + int pixelsLength; +}; diff --git a/snowlib/Texture.h b/snowlib/Texture.h new file mode 100644 index 0000000..8ea7b02 --- /dev/null +++ b/snowlib/Texture.h @@ -0,0 +1,29 @@ +#pragma once + +class Texture +{ +public: + Texture(int logw, int logh, int w, int h, unsigned char* dataIn, int dataLengthIn) + { + logicalWidth = logw; + logicalHeight = logh; + widthPixels = w; + heightPixels = h; + dataLength = dataLengthIn; + data = dataIn; + } + + ~Texture() + { + delete data; + } + + int logicalWidth; + int logicalHeight; + + int widthPixels; + int heightPixels; + + int dataLength; + unsigned char* data; +}; diff --git a/snowlib/VertexDefs.h b/snowlib/VertexDefs.h new file mode 100644 index 0000000..e6a1f3c --- /dev/null +++ b/snowlib/VertexDefs.h @@ -0,0 +1,53 @@ +#pragma once + +struct FloatVector +{ + FloatVector(){} + FloatVector(float xx, float yy, float zz) : x(xx), y(yy), z(zz){} + + float x; + float y; + float z; +}; + +struct ShortVector +{ + ShortVector(short xx, short yy, short zz) : x(xx), y(yy), z(zz){} + + short x; + short y; + short z; +}; + +struct SByteVector +{ + SByteVector(char xx, char yy, char zz) : x(xx), y(yy), z(zz){} + + char x; + char y; + char z; +}; + +struct Point +{ + Point(){} + Point(int xx, int yy) : x(xx), y(yy){} + + int x; + int y; +}; + +struct FloatPoint +{ + FloatPoint() : x(-10000.0), y(-10000.0) {} + FloatPoint(float xx, float yy) : x(xx), y(yy){} + + bool isUninitialised() {return x == -10000.0 && y == -10000.0;} + bool isInitialised() {return !isUninitialised();} + + bool operator!=(FloatPoint& in){return x!=in.x || y!=in.y;} + + float x; + float y; +}; + diff --git a/snowlib/VertexWeight.h b/snowlib/VertexWeight.h new file mode 100644 index 0000000..cd18db3 --- /dev/null +++ b/snowlib/VertexWeight.h @@ -0,0 +1,15 @@ +#pragma once + +struct VertexWeight +{ + int startVertex; + int endVertex; + int bone1; + int bone2; + int bone3; + int bone4; + int boneWeight1; + int boneWeight2; + int boneWeight3; + int boneWeight4; +}; diff --git a/snowlib/VifDecoder.cpp b/snowlib/VifDecoder.cpp new file mode 100644 index 0000000..096c1ef --- /dev/null +++ b/snowlib/VifDecoder.cpp @@ -0,0 +1,476 @@ +#include "VifDecoder.h" +#include "DataUtil.h" +#include "GIFTag.h" +#include "Logger.h" +#include "VertexDefs.h" + +using namespace std; + +struct VLoc +{ + VLoc(int va, int vb, int vc) : v1(va), v2(vb), v3(vc){} + int v1; + int v2; + int v3; +}; + +struct UV +{ + UV(short uu, short vv) : u(uu), v(vv){} + + short u; + short v; +}; + +struct Chunk +{ + int mscalID; + GIFTag* gifTag0; + GIFTag* gifTag1; + vector vertices; + vector normals; + vector vlocs; + vector uvs; + vector vertexWeights; + unsigned short* extraVlocs; + int numExtraVlocs; + vector DIRECTGifTags; +}; + +#define NOP_CMD 0x00 +#define STCYCL_CMD 0x01 +#define ITOP_CMD 0x04 +#define STMOD_CMD 0x05 +#define FLUSH_CMD 0x11 +#define MSCAL_CMD 0x14 +#define STMASK_CMD 0x20 +#define DIRECT_CMD 0x50 + +vector* readVerts(const unsigned char* data, int offset, int endOffset) +{ + auto chunks = new vector(); + Chunk* currentChunk = new Chunk(); + Chunk* previousChunk = nullptr; + while (offset < endOffset) { + int vifCommand = data[offset + 3] & 0x7f; + int numCommand = data[offset + 2]; + int immCommand = DataUtil::getLEShort(data, offset); + switch (vifCommand) { + case NOP_CMD: + case STCYCL_CMD: + case ITOP_CMD: + case STMOD_CMD: + case FLUSH_CMD: + offset += 4; + break; + + case MSCAL_CMD: + if (immCommand != 66 && immCommand != 68 && immCommand != 70) + { + Logger::getLogger()->logFormat("**** Microcode %d not supported", immCommand); + } + currentChunk->mscalID = immCommand; + chunks->push_back(currentChunk); + previousChunk = currentChunk; + currentChunk = new Chunk(); + + offset += 4; + break; + + case STMASK_CMD: + offset += 8; + break; + + case DIRECT_CMD: + + for (int i = 0; i < immCommand; i++) + { + GIFTag gifTag; + gifTag.parse(data, offset + 4 + i*16); + currentChunk->DIRECTGifTags.push_back(gifTag); + } + + offset += 4; + offset += immCommand * 16; + break; + + default: + if ((vifCommand & 0x60) == 0x60) { + // unpack command + bool mask = ((vifCommand & 0x10) == 0x10); + int vn = (vifCommand >> 2) & 3; + int vl = vifCommand & 3; + int addr = immCommand & 0x1ff; + bool flag = (immCommand & 0x8000) == 0x8000; + bool usn = (immCommand & 0x4000) == 0x4000; + + offset += 4; + if (vn == 1 && vl == 1) { + // v2-16 + // The UVs come after the MSCAL instruction. + if (previousChunk != nullptr) { + for (int uvnum = 0; uvnum < numCommand; ++uvnum) { + short u = DataUtil::getLEShort(data, offset); + short v = DataUtil::getLEShort(data, offset + 2); + previousChunk->uvs.push_back(UV(u, v)); + offset += 4; + } + } else { + int numBytes = numCommand * 4; + offset += numBytes; + } + } else if (vn == 2 && vl == 1) { + // v3-16 + // each vertex is 128 bits, so num is the number of vertices + for (int vnum = 0; vnum < numCommand; ++vnum) { + if (!usn) { + short x = DataUtil::getLEShort(data, offset); + short y = DataUtil::getLEShort(data, offset + 2); + short z = DataUtil::getLEShort(data, offset + 4); + offset += 6; + + currentChunk->vertices.push_back(ShortVector(x, y, z)); + } else { + int x = DataUtil::getLEUShort(data, offset); + int y = DataUtil::getLEUShort(data, offset + 2); + int z = DataUtil::getLEUShort(data, offset + 4); + offset += 6; + + currentChunk->vlocs.push_back(VLoc(x,y,z)); + } + } + offset = (offset + 3) & ~3; + } else if (vn == 2 && vl == 2) { + // v3-8 + int idx = offset; + for (int vnum = 0; vnum < numCommand; ++vnum) { + + char x = (char)data[idx++]; + char y = (char)data[idx++]; + char z = (char)data[idx++]; + currentChunk->normals.push_back(SByteVector(x, y, z)); + } + int numBytes = ((numCommand * 3) + 3) & ~3; + offset += numBytes; + } else if (vn == 3 && vl == 0) { + // v4-32 + if (1 == numCommand) { + currentChunk->gifTag0 = new GIFTag(); + currentChunk->gifTag0->parse(data, offset); + } else if (2 == numCommand) { + currentChunk->gifTag0 = new GIFTag(); + currentChunk->gifTag0->parse(data, offset); + currentChunk->gifTag1 = new GIFTag(); + currentChunk->gifTag1->parse(data, offset + 16); + } else { + Logger::getLogger()->log("unknown number of gif commands.\n"); + } + int numBytes = numCommand * 16; + offset += numBytes; + } else if (vn == 3 && vl == 1) { + // v4-16 + int numShorts = numCommand * 4; + if (usn) { + currentChunk->extraVlocs = new unsigned short[numShorts]; + currentChunk->numExtraVlocs = numShorts; + for (int i = 0; i < numCommand; ++i) { + currentChunk->extraVlocs[i*4] = DataUtil::getLEUShort(data, offset + i * 8); + currentChunk->extraVlocs[i * 4 + 1] = DataUtil::getLEUShort(data, offset + i * 8 + 2); + currentChunk->extraVlocs[i * 4 + 2] = DataUtil::getLEUShort(data, offset + i * 8 + 4); + currentChunk->extraVlocs[i * 4 + 3] = DataUtil::getLEUShort(data, offset + i * 8 + 6); + } + } else { + Logger::getLogger()->log("Unsupported tag\n"); + } + offset += numShorts * 2; + } else if (vn == 3 && vl == 2) { + // v4-8 + int numBytes = numCommand * 4; + int curVertex=0; + for (int i = 0; i < numCommand; ++i) { + VertexWeight vw; + vw.startVertex = curVertex; + vw.bone1 = data[offset++] / 4; + vw.boneWeight1 = data[offset++]; + vw.bone2 = data[offset++]; + if (vw.bone2 == 0xFF) { + // Single bone + vw.boneWeight2 = 0; + int count = data[offset++]; + curVertex += count; + } else { + vw.bone2 /= 4; + vw.boneWeight2 = data[offset++]; + ++curVertex; + + if (vw.boneWeight1 + vw.boneWeight2 < 0xFF) + { + ++i; + vw.bone3 = data[offset++] / 4; + vw.boneWeight3 = data[offset++]; + vw.bone4 = data[offset++]; + int bw4 = data[offset++]; + if (vw.bone4 != 0xFF) + { + vw.bone4 /= 4; + vw.boneWeight4 = bw4; + } + } else { + vw.bone3 = 0xFF; + vw.boneWeight3 = 0; + vw.bone4 = 0xFF; + vw.boneWeight4 = 0; + } + + } + vw.endVertex = curVertex - 1; + currentChunk->vertexWeights.push_back(vw); + } + + } else { + offset = endOffset; + } + } else { + Logger::getLogger()->logFormat("Unknown command: %d", vifCommand); + offset = endOffset; + } + break; + } + } + return chunks; +} + +VertexWeight emptyVertexWeight; + +VertexWeight& FindVertexWeight(vector& weights, int vertexNum) +{ + for(auto& weight : weights) { + if (vertexNum >= weight.startVertex && vertexNum <= weight.endVertex) { + return weight; + } + } + if (!weights.empty()) { + Logger::getLogger()->log("Failed to find vertex weight\n"); + } + return emptyVertexWeight; +} + +Mesh* chunksToMesh(vector* chunks) +{ + Mesh* mesh = new Mesh(); + int numVertices = 0; + for (auto& chunk : *chunks) { + numVertices += chunk->gifTag0->nloop; + } + + // Make the assumption that this will be enough to deal with multiple uvs on the same vertex. + mesh->positions = new FloatVector[numVertices*2]; + mesh->normals = new FloatVector[numVertices*2]; + mesh->uvCoords = new FloatPoint[numVertices*2]; + + int vstart = 0; + for (auto& chunk : *chunks) { + if ((chunk->gifTag0->prim & 0x07) != 4) { + Logger::getLogger()->log("Can only deal with tri strips"); + } + int vnum=vstart; + for (auto& vertex : chunk->vertices) { + FloatVector& fv = mesh->positions[vnum++]; + fv.x = vertex.x / 16.0f; + fv.y = vertex.y / 16.0f; + fv.z = vertex.z / 16.0f; + } + vnum=vstart; + for (auto& normal : chunk->normals) { + FloatVector& meshNormal = mesh->normals[vnum++]; + meshNormal.x = normal.x / 127.0f; + meshNormal.y = normal.y / 127.0f; + meshNormal.z = normal.z / 127.0f; + } + for (auto& vw : chunk->vertexWeights) { + VertexWeight vwAdjusted = vw; + vwAdjusted.startVertex += vstart; + const int numChunkVertices = chunk->vertices.size(); + if (vwAdjusted.endVertex >= numChunkVertices) { + vwAdjusted.endVertex = numChunkVertices - 1; + } + vwAdjusted.endVertex += vstart; + if (vw.startVertex <= (numChunkVertices-1)) { + mesh->vertexWeights.push_back(vwAdjusted); + } + } + int vstripLen = chunk->gifTag0->nloop; + int* vstrip = new int[vstripLen]; + int regsPerVertex = chunk->gifTag0->nreg; + int numVlocs = chunk->vlocs.size(); + int numVertsInChunk = chunk->vertices.size(); + for (int vlocIndx = 2; vlocIndx < numVlocs; ++vlocIndx) { + int v = vlocIndx - 2; + int stripIdx2 = (chunk->vlocs[vlocIndx].v2 & 0x1FF) / regsPerVertex; + int stripIdx3 = (chunk->vlocs[vlocIndx].v3 & 0x1FF) / regsPerVertex; + if (stripIdx3 < vstripLen && stripIdx2 < vstripLen) { + vstrip[stripIdx3] = vstrip[stripIdx2] & 0x1FF; + + bool skip2 = (chunk->vlocs[vlocIndx].v3 & 0x8000) == 0x8000; + if (skip2) { + vstrip[stripIdx3] |= 0x8000; + } + } + int stripIdx = (chunk->vlocs[vlocIndx].v1 & 0x1FF) / regsPerVertex; + bool skip = (chunk->vlocs[vlocIndx].v1 & 0x8000) == 0x8000; + + if (v < numVertsInChunk && stripIdx < vstripLen) { + vstrip[stripIdx] = skip ? (v | 0x8000) : v; + } + } + int numExtraVlocs = chunk->extraVlocs[0]; + for (int extraVloc = 0; extraVloc < numExtraVlocs; ++extraVloc) { + int idx = extraVloc * 4 + 4; + int stripIndxSrc = (chunk->extraVlocs[idx] & 0x1FF) / regsPerVertex; + int stripIndxDest = (chunk->extraVlocs[idx + 1] & 0x1FF) / regsPerVertex; ; + vstrip[stripIndxDest] = (chunk->extraVlocs[idx + 1] & 0x8000) | (vstrip[stripIndxSrc] & 0x1FF); + + stripIndxSrc = (chunk->extraVlocs[idx + 2] & 0x1FF) / regsPerVertex; + stripIndxDest = (chunk->extraVlocs[idx + 3] & 0x1FF) / regsPerVertex; ; + vstrip[stripIndxDest] = (chunk->extraVlocs[idx + 3] & 0x8000) | (vstrip[stripIndxSrc] & 0x1FF); + } + int triIdx = 0; + for (int i = 2; i < vstripLen; ++i) { + int vidx1 = vstart + (vstrip[i - 2] & 0xFF); + int vidx2 = vstart + (vstrip[i - 1] & 0xFF); + int vidx3 = vstart + (vstrip[i] & 0xFF); + + int uv1 = i - 2; + int uv2 = i - 1; + int uv3 = i; + + // Flip the faces (indices 1 and 2) to keep the winding rule consistent. + if ((triIdx & 1) == 1) { + int temp = uv1; + uv1 = uv2; + uv2 = temp; + + temp = vidx1; + vidx1 = vidx2; + vidx2 = temp; + } + + if ((vstrip[i] & 0x8000) == 0) { + float udiv = 16.0f; + float vdiv = 16.0f; + + FloatPoint p1(chunk->uvs[uv1].u / udiv, chunk->uvs[uv1].v / vdiv); + FloatPoint p2(chunk->uvs[uv2].u / udiv, chunk->uvs[uv2].v / vdiv); + FloatPoint p3(chunk->uvs[uv3].u / udiv, chunk->uvs[uv3].v / vdiv); + + if (mesh->uvCoords[vidx1].isInitialised() && p1 != mesh->uvCoords[vidx1]) + { + // There is more than 1 uv assigment to this vertex, so we need to duplicate it. + int originalVIdx = vidx1; + vidx1 = vstart + numVertsInChunk; + numVertsInChunk++; + mesh->positions[vidx1] = mesh->positions[originalVIdx]; + mesh->normals[vidx1] = mesh->normals[originalVIdx]; + + VertexWeight& weight = FindVertexWeight(chunk->vertexWeights, originalVIdx - vstart); + if (weight.boneWeight1 > 0) + { + VertexWeight vw = weight; + vw.startVertex = vidx1; + vw.endVertex = vidx1; + mesh->vertexWeights.push_back(vw); + } + } + if (mesh->uvCoords[vidx2].isInitialised() && p2 != mesh->uvCoords[vidx2]) + { + // There is more than 1 uv assigment to this vertex, so we need to duplicate it. + int originalVIdx = vidx2; + vidx2 = vstart + numVertsInChunk; + numVertsInChunk++; + mesh->positions[vidx2] = mesh->positions[originalVIdx]; + mesh->normals[vidx2] = mesh->normals[originalVIdx]; + + VertexWeight& weight = FindVertexWeight(chunk->vertexWeights, originalVIdx - vstart); + if (weight.boneWeight1 > 0) + { + VertexWeight vw = weight; + vw.startVertex = vidx2; + vw.endVertex = vidx2; + mesh->vertexWeights.push_back(vw); + } + } + if (mesh->uvCoords[vidx3].isInitialised() && p3 != mesh->uvCoords[vidx3]) + { + // There is more than 1 uv assigment to this vertex, so we need to duplicate it. + int originalVIdx = vidx3; + vidx3 = vstart + numVertsInChunk; + numVertsInChunk++; + mesh->positions[vidx3] = mesh->positions[originalVIdx]; + mesh->normals[vidx3] = mesh->normals[originalVIdx]; + + VertexWeight& weight = FindVertexWeight(chunk->vertexWeights, originalVIdx - vstart); + if (weight.boneWeight1 > 0) + { + VertexWeight vw = weight; + vw.startVertex = vidx3; + vw.endVertex = vidx3; + mesh->vertexWeights.push_back(vw); + } + } + + mesh->uvCoords[vidx1] = p1; + mesh->uvCoords[vidx2] = p2; + mesh->uvCoords[vidx3] = p3; + + // Double sided hack. Should fix this with normals really + mesh->triangleIndices.push_back(vidx1); + mesh->triangleIndices.push_back(vidx2); + mesh->triangleIndices.push_back(vidx3); + + mesh->triangleIndices.push_back(vidx2); + mesh->triangleIndices.push_back(vidx1); + mesh->triangleIndices.push_back(vidx3); + } + ++triIdx; + } + vstart += numVertsInChunk; + } + FloatVector* positions = new FloatVector[vstart]; + memcpy(positions, mesh->positions, vstart * sizeof(FloatVector)); + delete[] mesh->positions; + mesh->positions = positions; + + FloatVector* normals = new FloatVector[vstart]; + memcpy(normals, mesh->normals, vstart * sizeof(FloatVector)); + delete[] mesh->normals; + mesh->normals = normals; + + FloatPoint* uvCoords = new FloatPoint[vstart]; + memcpy(uvCoords, mesh->uvCoords, vstart * sizeof(FloatPoint)); + delete[] mesh->uvCoords; + mesh->uvCoords = uvCoords; + + mesh->numVertices = vstart; + + return mesh; +} + + +vector* VifDecoder::decode(const unsigned char* data, int base_addr) +{ + vector* meshes = new vector(); + int numMeshes = data[0x12]; + int offset1 = DataUtil::getLEInt(data, 0x24); + for (int meshNum = 0; meshNum < numMeshes; ++meshNum) { + int offsetVerts = DataUtil::getLEInt(data, 0x28 + meshNum * 4) - base_addr; + int offsetEndVerts = DataUtil::getLEInt(data, 0x2C + meshNum * 4)- base_addr; + auto chunks = readVerts(data, offsetVerts, offsetEndVerts); + Mesh* mesh = chunksToMesh(chunks); + meshes->push_back(mesh); + } + return meshes; +} + + + + diff --git a/snowlib/VifDecoder.h b/snowlib/VifDecoder.h new file mode 100644 index 0000000..cca2ddb --- /dev/null +++ b/snowlib/VifDecoder.h @@ -0,0 +1,11 @@ +#pragma once + +#include "Mesh.h" +#include + +class VifDecoder +{ +public: + std::vector* decode(const unsigned char* data, int base_addr); + +}; diff --git a/snowlib/World.cpp b/snowlib/World.cpp new file mode 100644 index 0000000..d72f981 --- /dev/null +++ b/snowlib/World.cpp @@ -0,0 +1,33 @@ +#include "World.h" + +World::World() +{ + topoStartCol = 0; + topoStartRow = 0; + numTopoCols = 0; + numTopoRows = 0; +} + +World::~World() +{ + for (auto& element : topoElements) + { + delete element; + } + for (auto& patch : topoPatches) + { + delete patch; + } +} + +TopoPatch::TopoPatch(int width, int height) +{ + w = width; + h = height; + heights = new int[w * h]; +} + +TopoPatch::~TopoPatch() +{ + delete[] heights; +} diff --git a/snowlib/World.h b/snowlib/World.h new file mode 100644 index 0000000..b32a93c --- /dev/null +++ b/snowlib/World.h @@ -0,0 +1,63 @@ +#pragma once + +#include + +class TopoPatch +{ +public: + TopoPatch(int w, int h); + ~TopoPatch(); + + int x0, y0, w, h; + int minHeight, maxHeight; + int* heights; +}; + +class TopoElement +{ +public: + int llx, lly, urx, ury; + int int8; + TopoPatch* patch; + int flags; + int x0, y0; + int baseHeight; + double cos_alpha, sin_alpha; +}; + +class World +{ +public: + + World(); + ~World(); + + // Topography + // + // The topography is a compressed height map for the world. + // The world is broken down into a coarse grid of cells. + // The topography is specified by a collection of elements. + // A topography element positions and rotates a reusable topography patch. + // A patch is a simple rectangular array of heights which can be used by + // multiple topography elements. + // In order to speed up the processing to derive a height of a given coordinate, + // a list of all the topographical elements that intersect each cell is stored. + + /// The column that the 0th column in topoElementsPerCell represents + int topoStartCol; + /// The row that the 0th row in topoElementsPerCell represents + int topoStartRow; + /// The number of columns in the topoElementsPerCell vector + int numTopoCols; + /// The number of rows in the topoElementsPerCell vector + int numTopoRows; + /// The topo elements that intersect a coarse grid. + std::vector> topoElementsPerCell; + + // All of the topo elements. This collection owns the elements. + std::vector topoElements; + + // All of the topo patches. This collection owns the patches. + std::vector topoPatches; + +}; diff --git a/snowlib/WorldReader.cpp b/snowlib/WorldReader.cpp new file mode 100644 index 0000000..5840dd0 --- /dev/null +++ b/snowlib/WorldReader.cpp @@ -0,0 +1,130 @@ +#include "WorldReader.h" +#include "World.h" +#include "LmpRepository.h" +#include "DataUtil.h" + +World* WorldReader::readWorld(LmpRepository* lmpRepository, const char* name) +{ + World* world = new World(); + std::string worldLmpName = std::string(name) + ".lmp"; + LmpFile* world_lmp = lmpRepository->getLmp(worldLmpName); + LmpEntry* world_entry = world_lmp->findEntry(world_lmp->findFilenamesByExtension("world")[0].c_str()); + decodeWorldFile(world, world_entry->data, world_entry->length); + return world; +} + +void WorldReader::decodeWorldFile(World* world, const unsigned char* data, int dataLength) +{ + int numElements = DataUtil::getLEInt(data, 0); + int offset4 = DataUtil::getLEInt(data, 4); + + decodeTopography(world, data, dataLength); + + int elementBase = DataUtil::getLEInt(data, 0x24); + int i28 = DataUtil::getLEInt(data, 0x28); + int i2C = DataUtil::getLEInt(data, 0x2C); + + int cols_38 = DataUtil::getLEInt(data, 0x30); + int rows_38 = DataUtil::getLEInt(data, 0x34); + int offset_38 = DataUtil::getLEInt(data, 0x38); + + int i3C = DataUtil::getLEInt(data, 0x3C); + int i40 = DataUtil::getLEInt(data, 0x40); + int i44 = DataUtil::getLEInt(data, 0x44); + int i48 = DataUtil::getLEInt(data, 0x48); + + int offset4C = DataUtil::getLEInt(data, 0x4C); + int len50 = DataUtil::getLEInt(data, 0x50); + int offset54 = DataUtil::getLEInt(data, 0x54); + + int texMinxy = DataUtil::getLEInt(data, 0x58); + int texMaxxy = DataUtil::getLEInt(data, 0x5C); + + int texMinx = texMinxy % 100; + int texMiny = texMinxy / 100; + int texMaxx = texMaxxy % 100; + int texMaxy = texMaxxy / 100; + + int offset60 = DataUtil::getLEInt(data, 0x60); + int textureArrayOffset = DataUtil::getLEInt(data, 0x64); + int offset68 = DataUtil::getLEInt(data, 0x68); + int offsetTex6C = DataUtil::getLEInt(data, 0x6C); +} + +void WorldReader::decodeTopography(World* world, const unsigned char* data, int dataLength) +{ + world->topoStartCol = DataUtil::getLEInt(data, 8); + world->topoStartRow = DataUtil::getLEInt(data, 0x0C); + + world->numTopoCols = DataUtil::getLEInt(data, 0x10); + world->numTopoRows = DataUtil::getLEInt(data, 0x14); + int topoElementsPerCellOffset = DataUtil::getLEInt(data, 0x18); + int numTopoElements = DataUtil::getLEInt(data, 0x1C); + int topoArrayOffset = DataUtil::getLEInt(data, 0x20); + + // Allows us to quickly look up patches from the offsets stored in the file. + std::unordered_map patchAddressMap; + + world->topoElements.resize(numTopoElements); + for (int el = 0; el < numTopoElements; ++el){ + int topoElementOffset = topoArrayOffset + el * 0x1C; + TopoElement* element = new TopoElement(); + element->llx = DataUtil::getLEShort(data, topoElementOffset); + element->lly = DataUtil::getLEShort(data, topoElementOffset + 2); + element->urx = DataUtil::getLEShort(data, topoElementOffset + 4); + element->ury = DataUtil::getLEShort(data, topoElementOffset + 6); + element->int8 = DataUtil::getLEInt(data, topoElementOffset + 8); + + int patchOffset = DataUtil::getLEInt(data, topoElementOffset + 0xc); + if (patchAddressMap.find(patchOffset) == patchAddressMap.end()){ + patchAddressMap[patchOffset] = readTopoPatch(data, patchOffset); + } + element->patch = patchAddressMap[patchOffset]; + element->flags = DataUtil::getLEShort(data, topoElementOffset + 0x10); + element->x0 = DataUtil::getLEShort(data, topoElementOffset + 0x12); + element->y0 = DataUtil::getLEShort(data, topoElementOffset + 0x14); + element->baseHeight = DataUtil::getLEShort(data, topoElementOffset + 0x16); + element->cos_alpha = DataUtil::getLEShort(data, topoElementOffset + 0x18) / 32767.0; + element->sin_alpha = DataUtil::getLEShort(data, topoElementOffset + 0x1A) / 32767.0; + world->topoElements.push_back(element); + } + + world->topoElementsPerCell.resize(world->numTopoCols * world->numTopoRows); + + int index = 0; + for (int r = 0; r < world->numTopoRows; ++r){ + for (int c = 0; c < world->numTopoCols; ++c){ + int elementsInCellListOffset = DataUtil::getLEInt(data, topoElementsPerCellOffset + index*4); + + auto& elementList = world->topoElementsPerCell[index]; + + int elementId = DataUtil::getLEShort(data, elementsInCellListOffset); + while (elementId >= 0){ + elementsInCellListOffset += 2; + elementList.push_back(world->topoElements[elementId]); + elementId = DataUtil::getLEShort(data, elementsInCellListOffset); + } + + index++; + } + } +} + +TopoPatch* WorldReader::readTopoPatch(const unsigned char* data, int offset) +{ + int w = DataUtil::getLEInt(data, offset + 8); + int h = DataUtil::getLEInt(data, offset + 0x0c); + TopoPatch* patch = new TopoPatch(w, h); + patch->x0 = DataUtil::getLEInt(data, offset); + patch->y0 = DataUtil::getLEInt(data, offset + 4); + patch->minHeight = DataUtil::getLEShort(data, offset + 0x10); + patch->maxHeight = DataUtil::getLEShort(data, offset + 0x12); + int j = 0; + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + int i = y * w + x; + patch->heights[j++] = DataUtil::getLEShort(data, offset + 0x14 + i * 2); + } + } + return patch; +} \ No newline at end of file diff --git a/snowlib/WorldReader.h b/snowlib/WorldReader.h new file mode 100644 index 0000000..7fdf46e --- /dev/null +++ b/snowlib/WorldReader.h @@ -0,0 +1,16 @@ +#pragma once + +class World; +class LmpRepository; +class TopoPatch; + +class WorldReader +{ +public: + World* readWorld(LmpRepository* lmpRepository, const char* name); + +private: + void decodeWorldFile(World* world, const unsigned char* data, int dataLength); + void decodeTopography(World* world, const unsigned char* data, int dataLength); + TopoPatch* readTopoPatch(const unsigned char* data, int offset); +}; \ No newline at end of file diff --git a/snowlib_test/LmpFileTest.cpp b/snowlib_test/LmpFileTest.cpp new file mode 100644 index 0000000..103626d --- /dev/null +++ b/snowlib_test/LmpFileTest.cpp @@ -0,0 +1,61 @@ +#include "LmpFile.h" +#define BOOST_TEST_MODULE LmpFileTest +#include "boost/test/included/unit_test.hpp" + +const unsigned char lmpData[] = { + // Some initial data that should be skipped, 12 bytes + 0xba, 0xdf, 0x00, 0x0d, + 0xba, 0xdf, 0x00, 0x0d, + 0xba, 0xdf, 0x00, 0x0d, + + 0x02, 0x00, 0x00,0x00, // Number of entries + // Entry 1 + // Name of 56 bytes + 'f', 'i', 'l', 'e', '1', '.', 'a', 'b', + 'c', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + // offset to entry data from start + 64*2+4, 0, 0, 0, + + // length of entry data + 16, 0, 0, 0, + + // Entry 2 + // Name of 56 bytes + 'f', 'i', 'l', 'e', '2', '.', 'a', 'b', + 'd', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + // offset to entry data from start + 64*2+4+16, 0, 0, 0, + + // length of entry data + 16, 0, 0, 0, + + // Entry 1 data, 16 bytes + 1, 2, 3, 4, 5, 6, 7, 8, + 9, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + // Entry 2 data, 16 bytes + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20 +}; + +BOOST_AUTO_TEST_CASE( lmp_test ) +{ + LmpFile* lmpFile = new LmpFile(DARK_ALLIANCE, lmpData, 12, sizeof(lmpData)); + + LmpEntry* entry = lmpFile->findEntry("file2.abd"); + BOOST_CHECK(entry != NULL); + + delete entry; + delete lmpFile; +} \ No newline at end of file