Compare commits

..

No commits in common. "main" and "planet-deep" have entirely different histories.

3241 changed files with 340555 additions and 194662 deletions

7
.gitattributes vendored
View File

@ -1,10 +1,3 @@
*.bmp filter=lfs diff=lfs merge=lfs -text *.bmp filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text
*.anim filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.bin filter=lfs diff=lfs merge=lfs -text
*.dll filter=lfs diff=lfs merge=lfs -text
*.so filter=lfs diff=lfs merge=lfs -text

15
.gitignore vendored
View File

@ -400,17 +400,4 @@ jumpingbird.*
jumpingbird.data jumpingbird.data
build build
build-emcmake build-emcmake
thirdparty thirdparty1
proj-web/build
proj-windows/build
public
web_resources/
pc_resources/
resources_hd/
web_resources_x2/
android_resources/
.artifacts/
*.zip

43
AnimatedModel.h Normal file
View File

@ -0,0 +1,43 @@
#pragma once
#include "Renderer.h"
#include "TextureManager.h"
namespace ZL
{
struct MeshGroup
{
std::vector<std::shared_ptr<Texture>> textures;
std::vector<VertexDataStruct> meshes;
std::vector<VertexRenderStruct> renderMeshes;
};
struct AnimatedModel
{
std::vector<MeshGroup> parts;
void RefreshRenderMeshes()
{
for (int i = 0; i < parts.size(); i++)
{
parts[i].renderMeshes.resize(parts[i].meshes.size());
for (int j = 0; j < parts[i].meshes.size(); j++)
{
parts[i].renderMeshes[j].AssignFrom(parts[i].meshes[j]);
parts[i].renderMeshes[j].RefreshVBO();
}
}
}
};
}

93
AudioPlayerAsync.cpp Normal file
View File

@ -0,0 +1,93 @@
#ifdef AUDIO
#include "AudioPlayerAsync.h"
AudioPlayerAsync::AudioPlayerAsync() : worker(&AudioPlayerAsync::workerThread, this) {}
AudioPlayerAsync::~AudioPlayerAsync() {
{
std::unique_lock<std::mutex> lock(mtx);
stop = true;
cv.notify_all();
}
worker.join();
}
void AudioPlayerAsync::stopAsync() {
std::unique_lock<std::mutex> lock(mtx);
taskQueue.push([this]() {
//audioPlayerMutex.lock();
audioPlayer->stop();
std::this_thread::sleep_for(std::chrono::seconds(1));
//audioPlayerMutex.unlock();
});
cv.notify_one();
}
void AudioPlayerAsync::resetAsync() {
std::unique_lock<std::mutex> lock(mtx);
taskQueue.push([this]() {
//audioPlayerMutex.lock();
audioPlayer.reset();
audioPlayer = std::make_unique<AudioPlayer>();
//audioPlayerMutex.unlock();
});
cv.notify_one();
}
void AudioPlayerAsync::playSoundAsync(std::string soundName) {
soundNameMutex.lock();
latestSoundName = soundName;
soundNameMutex.unlock();
std::unique_lock<std::mutex> lock(mtx);
taskQueue.push([this]() {
//audioPlayerMutex.lock();
if (audioPlayer) {
audioPlayer->playSound(latestSoundName);
}
//audioPlayerMutex.unlock();
});
cv.notify_one();
}
void AudioPlayerAsync::playMusicAsync(std::string musicName) {
musicNameMutex.lock();
latestMusicName = musicName;
musicNameMutex.unlock();
std::unique_lock<std::mutex> lock(mtx);
taskQueue.push([this]() {
//audioPlayerMutex.lock();
if (audioPlayer) {
audioPlayer->playMusic(latestMusicName);
}
//audioPlayerMutex.unlock();
});
cv.notify_one();
}
void AudioPlayerAsync::workerThread() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]() { return !taskQueue.empty() || stop; });
if (stop && taskQueue.empty()) {
break;
}
task = taskQueue.front();
taskQueue.pop();
}
task();
}
}
#endif

53
AudioPlayerAsync.h Normal file
View File

@ -0,0 +1,53 @@
#pragma once
#ifdef AUDIO
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include "cmakeaudioplayer/include/AudioPlayer.hpp"
class AudioPlayerAsync {
public:
AudioPlayerAsync();
~AudioPlayerAsync();
void resetAsync();
void playSoundAsync(std::string soundName);
void playMusicAsync(std::string musicName);
void stopAsync();
void exit()
{
stop = true;
}
std::thread worker;
private:
std::unique_ptr<AudioPlayer> audioPlayer;
//std::mutex audioPlayerMutex;
std::mutex soundNameMutex;
std::mutex musicNameMutex;
std::string latestSoundName;
std::string latestMusicName;
std::mutex mtx;
std::condition_variable cv;
std::queue<std::function<void()>> taskQueue;
bool stop = false;
void workerThread();
};
#endif

719
BoneAnimatedModel.cpp Normal file
View File

@ -0,0 +1,719 @@
#include "BoneAnimatedModel.h"
#include <regex>
#include <string>
#include <fstream>
#include <iostream>
#include <sstream>
namespace ZL
{
int getIndexByValue(const std::string& name, const std::vector<std::string>& words)
{
for (int i = 0; i < words.size(); i++)
{
if (words[i] == name)
{
return i;
}
}
return -1;
}
void BoneSystem::LoadFromFile(const std::string& fileName, const std::string& ZIPFileName)
{
std::ifstream filestream;
std::istringstream zipStream;
if (!ZIPFileName.empty())
{
std::vector<char> fileData = readFileFromZIP(fileName, ZIPFileName);
std::string fileContents(fileData.begin(), fileData.end());
zipStream.str(fileContents);
}
else
{
filestream.open(fileName);
}
std::istream& f = (!ZIPFileName.empty()) ? static_cast<std::istream&>(zipStream) : static_cast<std::istream&>(filestream);
//Skip first 5 lines
std::string tempLine;
for (int i = 0; i < 5; i++)
{
std::getline(f, tempLine);
}
std::getline(f, tempLine);
static const std::regex pattern_count(R"(\d+)");
static const std::regex pattern_float(R"([-]?\d+\.\d+)");
static const std::regex pattern_int(R"([-]?\d+)");
static const std::regex pattern_boneChildren(R"(\'([^\']+)\')");
static const std::regex pattern_bone_weight(R"(\'([^\']+)\'.*?([-]?\d+\.\d+))");
std::smatch match;
int numberBones;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberBones = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
std::vector<Bone> bones;
std::vector<std::string> boneNames;
std::vector<std::string> boneParentNames;
std::unordered_map<std::string, std::vector<std::string>> boneChildren;
bones.resize(numberBones);
boneNames.resize(numberBones);
boneParentNames.resize(numberBones);
for (int i = 0; i < numberBones; i++)
{
std::getline(f, tempLine);
std::string boneName = tempLine.substr(6);
boneNames[i] = boneName;
std::getline(f, tempLine);
std::vector<float> floatValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
bones[i].boneStartWorld = Vector3f{ floatValues[0], floatValues[1], floatValues[2] };
std::getline(f, tempLine); //skip tail
std::getline(f, tempLine); //len
if (std::regex_search(tempLine, match, pattern_float)) {
std::string len_str = match.str();
bones[i].boneLength = std::stof(len_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
//---------- matrix begin
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
bones[i].boneMatrixWorld.m[0] = floatValues[0];
bones[i].boneMatrixWorld.m[0 + 1 * 3] = floatValues[1];
bones[i].boneMatrixWorld.m[0 + 2 * 3] = floatValues[2];
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
bones[i].boneMatrixWorld.m[1] = floatValues[0];
bones[i].boneMatrixWorld.m[1 + 1 * 3] = floatValues[1];
bones[i].boneMatrixWorld.m[1 + 2 * 3] = floatValues[2];
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
bones[i].boneMatrixWorld.m[2] = floatValues[0];
bones[i].boneMatrixWorld.m[2 + 1 * 3] = floatValues[1];
bones[i].boneMatrixWorld.m[2 + 2 * 3] = floatValues[2];
//----------- matrix end
std::getline(f, tempLine); //parent
if (tempLine == " Parent: None")
{
bones[i].parent = -1;
}
else
{
std::string boneParent = tempLine.substr(10);
boneParentNames[i] = boneParent;
}
std::getline(f, tempLine); //children
b = tempLine.cbegin();
e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_boneChildren)) {
boneChildren[boneName].push_back(match.str(1));
b = match.suffix().first;
}
}
//Now process all the bones:
for (int i = 0; i < numberBones; i++)
{
std::string boneName = boneNames[i];
std::string boneParent = boneParentNames[i];
if (boneParent == "")
{
bones[i].parent = -1;
}
else
{
bones[i].parent = getIndexByValue(boneParent, boneNames);
}
for (int j = 0; j < boneChildren[boneName].size(); j++)
{
bones[i].children.push_back(getIndexByValue(boneChildren[boneName][j], boneNames));
}
/*if (boneName == "Bone.020")
{
std::cout << i << std::endl;
}*/
}
startBones = bones;
currentBones = bones;
///std::cout << "Hello!" << std::endl;
std::getline(f, tempLine); //vertice count
int numberVertices;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberVertices = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
std::vector<Vector3f> vertices;
vertices.resize(numberVertices);
for (int i = 0; i < numberVertices; i++)
{
std::getline(f, tempLine);
std::vector<float> floatValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
vertices[i] = Vector3f{floatValues[0], floatValues[1], floatValues[2]};
}
//==== process uv and normals begin
std::cout << "Hello x1" << std::endl;
std::getline(f, tempLine); //===UV Coordinates:
std::getline(f, tempLine); //triangle count
int numberTriangles;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberTriangles = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
// Now process UVs
std::vector<std::array<Vector2f, 3>> uvCoords;
uvCoords.resize(numberTriangles);
for (int i = 0; i < numberTriangles; i++)
{
std::getline(f, tempLine); //Face 0
int uvCount;
std::getline(f, tempLine);
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
uvCount = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
if (uvCount != 3)
{
throw std::runtime_error("more than 3 uvs");
}
std::vector<float> floatValues;
for (int j = 0; j < 3; j++)
{
std::getline(f, tempLine); //UV <Vector (-0.3661, -1.1665)>
auto b = tempLine.cbegin();
auto e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
if (floatValues.size() != 2)
{
throw std::runtime_error("more than 2 uvs---");
}
uvCoords[i][j] = Vector2f{ floatValues[0],floatValues[1] };
}
}
std::cout << "Hello eee" << std::endl;
std::getline(f, tempLine); //===Normals:
std::vector<Vector3f> normals;
normals.resize(numberVertices);
for (int i = 0; i < numberVertices; i++)
{
std::getline(f, tempLine);
std::vector<float> floatValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
normals[i] = Vector3f{ floatValues[0], floatValues[1], floatValues[2] };
}
//==== process uv and normals end
std::getline(f, tempLine); //triangle count.
//numberTriangles; //Need to check if new value is the same as was read before
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberTriangles = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
std::vector<std::array<int, 3>> triangles;
triangles.resize(numberTriangles);
for (int i = 0; i < numberTriangles; i++)
{
std::getline(f, tempLine);
std::vector<int> intValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_int)) {
intValues.push_back(std::stoi(match.str()));
b = match.suffix().first;
}
triangles[i] = { intValues[0], intValues[1], intValues[2] };
}
std::getline(f, tempLine);//=== Vertex Weights ===
std::vector<std::array<BoneWeight, MAX_BONE_COUNT>> localVerticesBoneWeight;
localVerticesBoneWeight.resize(numberVertices);
for (int i = 0; i < numberVertices; i++)
{
std::getline(f, tempLine); //skip Vertex 0:
std::getline(f, tempLine); //vertex group count
int boneCount;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
boneCount = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
if (boneCount > MAX_BONE_COUNT)
{
throw std::runtime_error("more than 5 bones");
}
float sumWeights = 0;
for (int j = 0; j < boneCount; j++)
{
std::getline(f, tempLine); //Group: 'Bone', Weight: 0.9929084181785583
if (std::regex_search(tempLine, match, pattern_bone_weight)) {
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)
std::string word = match.str(1);
double weight = std::stod(match.str(2));
int boneNumber = getIndexByValue(word, boneNames);
localVerticesBoneWeight[i][j].boneIndex = boneNumber;
localVerticesBoneWeight[i][j].weight = weight;
sumWeights += weight;
}
else {
throw std::runtime_error("No match found in the input string.");
}
}
//Normalize weights:
for (int j = 0; j < boneCount; j++)
{
localVerticesBoneWeight[i][j].weight = localVerticesBoneWeight[i][j].weight / sumWeights;
}
}
std::getline(f, tempLine);//=== Animation Keyframes ===
std::getline(f, tempLine);//=== Bone Transforms per Keyframe ===
std::getline(f, tempLine);
int numberKeyFrames;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberKeyFrames = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
animations.resize(1);
animations[0].keyFrames.resize(numberKeyFrames);
for (int i = 0; i < numberKeyFrames; i++)
{
std::getline(f, tempLine);
int numberFrame;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberFrame = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
animations[0].keyFrames[i].frame = numberFrame;
animations[0].keyFrames[i].bones.resize(numberBones);
for (int j = 0; j < numberBones; j++)
{
std::getline(f, tempLine);
std::string boneName = tempLine.substr(8);
int boneNumber = getIndexByValue(boneName, boneNames);
animations[0].keyFrames[i].bones[boneNumber] = startBones[boneNumber];
std::getline(f, tempLine); // Location: <Vector (0.0000, 0.0000, -0.0091)>
std::vector<float> floatValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
animations[0].keyFrames[i].bones[boneNumber].boneStartWorld = Vector3f{ floatValues[0], floatValues[1], floatValues[2] };
std::getline(f, tempLine); // Rotation
std::getline(f, tempLine); // Matrix
//=============== Matrix begin ==================
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[0] = floatValues[0];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[0 + 1 * 4] = floatValues[1];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[0 + 2 * 4] = floatValues[2];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[0 + 3 * 4] = floatValues[3];
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[1] = floatValues[0];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[1 + 1 * 4] = floatValues[1];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[1 + 2 * 4] = floatValues[2];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[1 + 3 * 4] = floatValues[3];
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[2] = floatValues[0];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[2 + 1 * 4] = floatValues[1];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[2 + 2 * 4] = floatValues[2];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[2 + 3 * 4] = floatValues[3];
std::getline(f, tempLine);
b = tempLine.cbegin();
e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[3] = floatValues[0];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[3 + 1 * 4] = floatValues[1];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[3 + 2 * 4] = floatValues[2];
animations[0].keyFrames[i].bones[boneNumber].boneMatrixWorld.m[3 + 3 * 4] = floatValues[3];
//std::getline(f, tempLine);// ignore last matrix line
//=============== Matrix end ==================
}
}
// Now let's process bone weights and vertices
for (int i = 0; i < numberTriangles; i++)
{
mesh.PositionData.push_back(vertices[triangles[i][0]]);
mesh.PositionData.push_back(vertices[triangles[i][1]]);
mesh.PositionData.push_back(vertices[triangles[i][2]]);
verticesBoneWeight.push_back(localVerticesBoneWeight[triangles[i][0]]);
verticesBoneWeight.push_back(localVerticesBoneWeight[triangles[i][1]]);
verticesBoneWeight.push_back(localVerticesBoneWeight[triangles[i][2]]);
mesh.TexCoordData.push_back(uvCoords[i][0]);
mesh.TexCoordData.push_back(uvCoords[i][1]);
mesh.TexCoordData.push_back(uvCoords[i][2]);
}
startMesh = mesh;
}
void BoneSystem::Interpolate(int frame)
{
int startingFrame = -1;
for (int i = 0; i < animations[0].keyFrames.size() - 1; i++)
{
int oldFrame = animations[0].keyFrames[i].frame;
int nextFrame = animations[0].keyFrames[i + 1].frame;
if (frame >= oldFrame && frame < nextFrame)
{
startingFrame = i;
break;
}
}
if (startingFrame == -1)
{
throw std::runtime_error("Exception here");
}
int modifiedFrameNumber = frame - animations[0].keyFrames[startingFrame].frame;
int diffFrames = animations[0].keyFrames[startingFrame + 1].frame - animations[0].keyFrames[startingFrame].frame;
float t = (modifiedFrameNumber + 0.f) / diffFrames;
std::vector<Bone>& oneFrameBones = animations[0].keyFrames[startingFrame].bones;
std::vector<Bone>& nextFrameBones = animations[0].keyFrames[startingFrame+1].bones;
std::vector<Matrix4f> skinningMatrixForEachBone;
//std::vector<Matrix3f> skinningMatrixForEachBone;
skinningMatrixForEachBone.resize(currentBones.size());
for (int i = 0; i < currentBones.size(); i++)
{
currentBones[i].boneStartWorld.v[0] = oneFrameBones[i].boneStartWorld.v[0] + t * (nextFrameBones[i].boneStartWorld.v[0] - oneFrameBones[i].boneStartWorld.v[0]);
currentBones[i].boneStartWorld.v[1] = oneFrameBones[i].boneStartWorld.v[1] + t * (nextFrameBones[i].boneStartWorld.v[1] - oneFrameBones[i].boneStartWorld.v[1]);
currentBones[i].boneStartWorld.v[2] = oneFrameBones[i].boneStartWorld.v[2] + t * (nextFrameBones[i].boneStartWorld.v[2] - oneFrameBones[i].boneStartWorld.v[2]);
Matrix3f oneFrameBonesMatrix;
oneFrameBonesMatrix.m[0] = oneFrameBones[i].boneMatrixWorld.m[0];
oneFrameBonesMatrix.m[1] = oneFrameBones[i].boneMatrixWorld.m[1];
oneFrameBonesMatrix.m[2] = oneFrameBones[i].boneMatrixWorld.m[2];
oneFrameBonesMatrix.m[3] = oneFrameBones[i].boneMatrixWorld.m[0 + 1*4];
oneFrameBonesMatrix.m[4] = oneFrameBones[i].boneMatrixWorld.m[1 + 1*4];
oneFrameBonesMatrix.m[5] = oneFrameBones[i].boneMatrixWorld.m[2 + 1*4];
oneFrameBonesMatrix.m[6] = oneFrameBones[i].boneMatrixWorld.m[0 + 2*4];
oneFrameBonesMatrix.m[7] = oneFrameBones[i].boneMatrixWorld.m[1 + 2*4];
oneFrameBonesMatrix.m[8] = oneFrameBones[i].boneMatrixWorld.m[2 + 2*4];
Matrix3f nextFrameBonesMatrix;
nextFrameBonesMatrix.m[0] = nextFrameBones[i].boneMatrixWorld.m[0];
nextFrameBonesMatrix.m[1] = nextFrameBones[i].boneMatrixWorld.m[1];
nextFrameBonesMatrix.m[2] = nextFrameBones[i].boneMatrixWorld.m[2];
nextFrameBonesMatrix.m[3] = nextFrameBones[i].boneMatrixWorld.m[0 + 1 * 4];
nextFrameBonesMatrix.m[4] = nextFrameBones[i].boneMatrixWorld.m[1 + 1 * 4];
nextFrameBonesMatrix.m[5] = nextFrameBones[i].boneMatrixWorld.m[2 + 1 * 4];
nextFrameBonesMatrix.m[6] = nextFrameBones[i].boneMatrixWorld.m[0 + 2 * 4];
nextFrameBonesMatrix.m[7] = nextFrameBones[i].boneMatrixWorld.m[1 + 2 * 4];
nextFrameBonesMatrix.m[8] = nextFrameBones[i].boneMatrixWorld.m[2 + 2 * 4];
Vector4f q1 = MatrixToQuat(oneFrameBonesMatrix);
Vector4f q2 = MatrixToQuat(nextFrameBonesMatrix);
Vector4f q1_norm = q1.normalized();
Vector4f q2_norm = q2.normalized();
Vector4f result = slerp(q1_norm, q2_norm, t);
Matrix3f boneMatrixWorld3 = QuatToMatrix(result);
currentBones[i].boneMatrixWorld = MakeMatrix4x4(boneMatrixWorld3, currentBones[i].boneStartWorld);
Matrix4f currentBoneMatrixWorld4 = currentBones[i].boneMatrixWorld;
Matrix4f startBoneMatrixWorld4 = animations[0].keyFrames[0].bones[i].boneMatrixWorld;
Matrix4f inverstedStartBoneMatrixWorld4 = InverseMatrix(startBoneMatrixWorld4);
skinningMatrixForEachBone[i] = MultMatrixMatrix(currentBoneMatrixWorld4, inverstedStartBoneMatrixWorld4);
}
/*
for (int i = 0; i < currentBones.size(); i++)
{
currentBones[i].boneStartWorld = oneFrameBones[i].boneStartWorld;
currentBones[i].boneMatrixWorld = oneFrameBones[i].boneMatrixWorld;
//Matrix4f currentBoneMatrixWorld4 = MakeMatrix4x4(currentBones[i].boneMatrixWorld, currentBones[i].boneStartWorld);
//Matrix4f startBoneMatrixWorld4 = MakeMatrix4x4(animations[0].keyFrames[0].bones[i].boneMatrixWorld, animations[0].keyFrames[0].bones[i].boneStartWorld);
Matrix4f currentBoneMatrixWorld4 = currentBones[i].boneMatrixWorld;
Matrix4f startBoneMatrixWorld4 = animations[0].keyFrames[0].bones[i].boneMatrixWorld;
Matrix4f inverstedStartBoneMatrixWorld4 = InverseMatrix(startBoneMatrixWorld4);
skinningMatrixForEachBone[i] = MultMatrixMatrix(currentBoneMatrixWorld4, inverstedStartBoneMatrixWorld4);
if (i == 10)
{
std::cout << i << std::endl;
}
}*/
for (int i = 0; i < mesh.PositionData.size(); i++)
{
Vector4f originalPos = {
startMesh.PositionData[i].v[0],
startMesh.PositionData[i].v[1],
startMesh.PositionData[i].v[2], 1.0};
Vector4f finalPos = Vector4f{0.f, 0.f, 0.f, 0.f};
bool vMoved = false;
//Vector3f finalPos = Vector3f{ 0.f, 0.f, 0.f };
for (int j = 0; j < MAX_BONE_COUNT; j++)
{
if (verticesBoneWeight[i][j].weight != 0)
{
vMoved = true;
//finalPos = finalPos + MultVectorMatrix(originalPos, skinningMatrixForEachBone[verticesBoneWeight[i][j].boneIndex]) * verticesBoneWeight[i][j].weight;
finalPos = finalPos + MultMatrixVector(skinningMatrixForEachBone[verticesBoneWeight[i][j].boneIndex], originalPos) * verticesBoneWeight[i][j].weight;
}
}
if (abs(finalPos.v[0] - originalPos.v[0]) > 1 || abs(finalPos.v[1] - originalPos.v[1]) > 1 || abs(finalPos.v[2] - originalPos.v[2]) > 1)
{
}
if (!vMoved)
{
finalPos = originalPos;
}
mesh.PositionData[i].v[0] = finalPos.v[0];
mesh.PositionData[i].v[1] = finalPos.v[1];
mesh.PositionData[i].v[2] = finalPos.v[2];
}
}
}

60
BoneAnimatedModel.h Normal file
View File

@ -0,0 +1,60 @@
#pragma once
#include "ZLMath.h"
#include "Renderer.h"
#include <unordered_map>
namespace ZL
{
constexpr int MAX_BONE_COUNT = 6;
struct Bone
{
Vector3f boneStartWorld;
float boneLength;
Matrix4f boneMatrixWorld;
// boneVector = boneLength * (0, 1, 0) <20> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// Then multiply by boneMatrixWorld <20> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>
int parent;
std::vector<int> children;
};
struct BoneWeight
{
int boneIndex = -1;
float weight = 0;
};
struct AnimationKeyFrame
{
int frame;
std::vector<Bone> bones;
};
struct Animation
{
std::vector<AnimationKeyFrame> keyFrames;
};
struct BoneSystem
{
VertexDataStruct mesh;
VertexDataStruct startMesh;
std::vector<std::array<BoneWeight, MAX_BONE_COUNT>> verticesBoneWeight;
Matrix4f armatureMatrix;
std::vector<Bone> startBones;
std::vector<Bone> currentBones;
std::vector<Animation> animations;
void LoadFromFile(const std::string& fileName, const std::string& ZIPFileName = "");
void Interpolate(int frame);
};
};

544
CMakeLists.txt Normal file
View File

@ -0,0 +1,544 @@
cmake_minimum_required(VERSION 3.16)
project(space-game001 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(BUILD_CONFIGS Debug Release)
# ==============================
# Папка для всех сторонних либ
# ==============================
set(THIRDPARTY_DIR "${CMAKE_SOURCE_DIR}/thirdparty1")
file(MAKE_DIRECTORY "${THIRDPARTY_DIR}")
macro(log msg)
message(STATUS "${msg}")
endmacro()
# ===========================================
# 1) ZLIB (zlib131.zip zlib-1.3.1) - без изменений
# ===========================================
set(ZLIB_ARCHIVE "${THIRDPARTY_DIR}/zlib131.zip")
set(ZLIB_SRC_DIR "${THIRDPARTY_DIR}/zlib-1.3.1")
set(ZLIB_BUILD_DIR "${ZLIB_SRC_DIR}/build")
set(ZLIB_INSTALL_DIR "${ZLIB_SRC_DIR}/install")
if(NOT EXISTS "${ZLIB_ARCHIVE}")
log("Downloading zlib131.zip ...")
file(DOWNLOAD
"https://www.zlib.net/zlib131.zip"
"${ZLIB_ARCHIVE}"
SHOW_PROGRESS
)
endif()
if(NOT EXISTS "${ZLIB_SRC_DIR}/CMakeLists.txt")
log("Extracting zlib131.zip to zlib-1.3.1 ...")
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xvf "${ZLIB_ARCHIVE}"
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
RESULT_VARIABLE _zlib_extract_res
)
if(NOT _zlib_extract_res EQUAL 0)
message(FATAL_ERROR "Failed to extract zlib archive")
endif()
endif()
file(MAKE_DIRECTORY "${ZLIB_BUILD_DIR}")
# проверяем, собран ли уже zlib
set(_have_zlib FALSE)
foreach(candidate
"${ZLIB_INSTALL_DIR}/lib/zlibstatic.lib"
)
if(EXISTS "${candidate}")
set(_have_zlib TRUE)
break()
endif()
endforeach()
if(NOT _have_zlib)
log("Configuring zlib ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${ZLIB_SRC_DIR}"
-B "${ZLIB_BUILD_DIR}"
-DCMAKE_INSTALL_PREFIX=${ZLIB_INSTALL_DIR}
RESULT_VARIABLE _zlib_cfg_res
)
if(NOT _zlib_cfg_res EQUAL 0)
message(FATAL_ERROR "zlib configure failed")
endif()
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Building ZLIB (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${ZLIB_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _zlib_build_res
)
if(NOT _zlib_build_res EQUAL 0)
message(FATAL_ERROR "ZLIB build failed for configuration ${cfg}")
endif()
log("Installing ZLIB (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${ZLIB_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _zlib_inst_res
)
if(NOT _zlib_inst_res EQUAL 0)
message(FATAL_ERROR "ZLIB install failed for configuration ${cfg}")
endif()
endforeach()
endif()
# ИСПРАВЛЕНИЕ: Используем свойства для конкретных конфигураций
add_library(zlib_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(zlib_external_lib PROPERTIES
# Динамическая линковка (если zlib.lib - это импорт-библиотека для zlibd.dll)
#IMPORTED_LOCATION_DEBUG "${ZLIB_INSTALL_DIR}/lib/zlibd.lib"
#IMPORTED_LOCATION_RELEASE "${ZLIB_INSTALL_DIR}/lib/zlib.lib"
# Можно также указать статические библиотеки, если вы хотите их использовать
IMPORTED_LOCATION_DEBUG "${ZLIB_INSTALL_DIR}/lib/zlibstaticd.lib"
IMPORTED_LOCATION_RELEASE "${ZLIB_INSTALL_DIR}/lib/zlibstatic.lib"
INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INSTALL_DIR}/include"
)
# ===========================================
# 2) SDL2 (release-2.32.10.zip SDL-release-2.32.10) - без изменений
# ===========================================
set(SDL2_ARCHIVE "${THIRDPARTY_DIR}/release-2.32.10.zip")
set(SDL2_SRC_DIR "${THIRDPARTY_DIR}/SDL-release-2.32.10")
set(SDL2_BUILD_DIR "${SDL2_SRC_DIR}/build")
set(SDL2_INSTALL_DIR "${SDL2_SRC_DIR}/install")
if(NOT EXISTS "${SDL2_ARCHIVE}")
log("Downloading SDL2 release-2.32.10.zip ...")
file(DOWNLOAD
"https://github.com/libsdl-org/SDL/archive/refs/tags/release-2.32.10.zip"
"${SDL2_ARCHIVE}"
SHOW_PROGRESS
)
endif()
if(NOT EXISTS "${SDL2_SRC_DIR}/CMakeLists.txt")
log("Extracting SDL2 archive to SDL-release-2.32.10 ...")
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xvf "${SDL2_ARCHIVE}"
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
RESULT_VARIABLE _sdl_extract_res
)
if(NOT _sdl_extract_res EQUAL 0)
message(FATAL_ERROR "Failed to extract SDL2 archive")
endif()
endif()
file(MAKE_DIRECTORY "${SDL2_BUILD_DIR}")
set(_have_sdl2 FALSE)
foreach(candidate
"${SDL2_INSTALL_DIR}/lib/SDL2.lib"
"${SDL2_INSTALL_DIR}/lib/SDL2-static.lib"
"${SDL2_INSTALL_DIR}/lib/SDL2d.lib"
)
if(EXISTS "${candidate}")
set(_have_sdl2 TRUE)
break()
endif()
endforeach()
if(NOT _have_sdl2)
log("Configuring SDL2 ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${SDL2_SRC_DIR}"
-B "${SDL2_BUILD_DIR}"
-DCMAKE_INSTALL_PREFIX=${SDL2_INSTALL_DIR}
-DCMAKE_PREFIX_PATH=${ZLIB_INSTALL_DIR} # путь к zlib для SDL2
RESULT_VARIABLE _sdl_cfg_res
)
if(NOT _sdl_cfg_res EQUAL 0)
message(FATAL_ERROR "SDL2 configure failed")
endif()
# --- ИЗМЕНЕНИЕ: Цикл по конфигурациям Debug и Release ---
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Building SDL2 (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${SDL2_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _sdl_build_res
)
if(NOT _sdl_build_res EQUAL 0)
message(FATAL_ERROR "SDL2 build failed for configuration ${cfg}")
endif()
log("Installing SDL2 (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${SDL2_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _sdl_inst_res
)
if(NOT _sdl_inst_res EQUAL 0)
message(FATAL_ERROR "SDL2 install failed for configuration ${cfg}")
endif()
endforeach()
# ------------------------------------------------------
endif()
# ИСПРАВЛЕНИЕ: SDL2: Используем свойства для конкретных конфигураций
add_library(SDL2_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(SDL2_external_lib PROPERTIES
# Динамическая линковка SDL2
IMPORTED_LOCATION_DEBUG "${SDL2_INSTALL_DIR}/lib/SDL2d.lib"
IMPORTED_LOCATION_RELEASE "${SDL2_INSTALL_DIR}/lib/SDL2.lib"
# Оба include-пути: и include, и include/SDL2
INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INSTALL_DIR}/include;${SDL2_INSTALL_DIR}/include/SDL2"
)
# SDL2main (обычно статическая)
add_library(SDL2main_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(SDL2main_external_lib PROPERTIES
# ИСПРАВЛЕНО: Указываем пути для Debug и Release, используя
# соглашение, что Debug имеет суффикс 'd', а Release нет.
IMPORTED_LOCATION_DEBUG "${SDL2_INSTALL_DIR}/lib/SDL2maind.lib"
IMPORTED_LOCATION_RELEASE "${SDL2_INSTALL_DIR}/lib/SDL2main.lib"
INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INSTALL_DIR}/include"
)
log("-----${SDL2_INSTALL_DIR}/lib/SDL2maind.lib")
# ===========================================
# 3) libpng (v1.6.51.zip libpng-1.6.51) - без изменений
# ===========================================
set(LIBPNG_ARCHIVE "${THIRDPARTY_DIR}/v1.6.51.zip")
set(LIBPNG_SRC_DIR "${THIRDPARTY_DIR}/libpng-1.6.51")
set(LIBPNG_BUILD_DIR "${LIBPNG_SRC_DIR}/build")
set(LIBPNG_INSTALL_DIR "${LIBPNG_SRC_DIR}/install") # на будущее
if(NOT EXISTS "${LIBPNG_ARCHIVE}")
log("Downloading libpng v1.6.51.zip ...")
file(DOWNLOAD
"https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.51.zip"
"${LIBPNG_ARCHIVE}"
SHOW_PROGRESS
)
endif()
if(NOT EXISTS "${LIBPNG_SRC_DIR}/CMakeLists.txt")
log("Extracting libpng v1.6.51.zip to libpng-1.6.51 ...")
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xvf "${LIBPNG_ARCHIVE}"
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
RESULT_VARIABLE _png_extract_res
)
if(NOT _png_extract_res EQUAL 0)
message(FATAL_ERROR "Failed to extract libpng archive")
endif()
endif()
file(MAKE_DIRECTORY "${LIBPNG_BUILD_DIR}")
# Проверяем, есть ли уже .lib (build/Debug или install/lib)
set(_libpng_candidates
"${LIBPNG_BUILD_DIR}/Debug/libpng16_staticd.lib"
"${LIBPNG_BUILD_DIR}/Release/libpng16_static.lib"
)
set(_have_png FALSE)
foreach(candidate IN LISTS _libpng_candidates)
if(EXISTS "${candidate}")
set(_have_png TRUE)
break()
endif()
endforeach()
if(NOT _have_png)
log("Configuring libpng ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${LIBPNG_SRC_DIR}"
-B "${LIBPNG_BUILD_DIR}"
-DCMAKE_INSTALL_PREFIX=${LIBPNG_INSTALL_DIR}
-DCMAKE_PREFIX_PATH=${ZLIB_INSTALL_DIR}
-DZLIB_ROOT=${ZLIB_INSTALL_DIR}
RESULT_VARIABLE _png_cfg_res
)
if(NOT _png_cfg_res EQUAL 0)
message(FATAL_ERROR "libpng configure failed")
endif()
# --- ИЗМЕНЕНИЕ: Цикл по конфигурациям Debug и Release ---
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Building libpng (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${LIBPNG_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _png_build_res
)
if(NOT _png_build_res EQUAL 0)
message(FATAL_ERROR "libpng build failed for configuration ${cfg}")
endif()
# Поскольку вы не используете "cmake --install" для libpng,
# здесь нет необходимости в дополнительном шаге установки.
# Файлы .lib будут сгенерированы в подкаталоге ${LIBPNG_BUILD_DIR}/${cfg} (например, build/Debug или build/Release).
endforeach()
# ------------------------------------------------------
endif()
add_library(libpng_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(libpng_external_lib PROPERTIES
# Предполагая, что libpng использует статический вариант
IMPORTED_LOCATION_DEBUG "${LIBPNG_BUILD_DIR}/Debug/libpng16_staticd.lib"
IMPORTED_LOCATION_RELEASE "${LIBPNG_BUILD_DIR}/Release/libpng16_static.lib"
# png.h, pngconf.h в SRC, pnglibconf.h в BUILD
INTERFACE_INCLUDE_DIRECTORIES "${LIBPNG_SRC_DIR};${LIBPNG_BUILD_DIR}"
)
# ===========================================
# 4) libzip (v1.11.4.zip libzip-1.11.4) - НОВАЯ ЗАВИСИМОСТЬ
# ===========================================
set(LIBZIP_ARCHIVE "${THIRDPARTY_DIR}/v1.11.4.zip")
set(LIBZIP_SRC_DIR "${THIRDPARTY_DIR}/libzip-1.11.4")
set(LIBZIP_BUILD_DIR "${LIBZIP_SRC_DIR}/build")
#set(LIBZIP_INSTALL_DIR "${LIBZIP_SRC_DIR}/install")
set(LIBZIP_BASE_DIR "${LIBZIP_SRC_DIR}/install")
if(NOT EXISTS "${LIBZIP_ARCHIVE}")
log("Downloading libzip v1.11.4.zip ...")
file(DOWNLOAD
"https://github.com/nih-at/libzip/archive/refs/tags/v1.11.4.zip"
"${LIBZIP_ARCHIVE}"
SHOW_PROGRESS
)
endif()
if(NOT EXISTS "${LIBZIP_SRC_DIR}/CMakeLists.txt")
log("Extracting libzip v1.11.4.zip to libzip-1.11.4 ...")
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xvf "${LIBZIP_ARCHIVE}"
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
RESULT_VARIABLE _zip_extract_res
)
if(NOT _zip_extract_res EQUAL 0)
message(FATAL_ERROR "Failed to extract libzip archive")
endif()
endif()
file(MAKE_DIRECTORY "${LIBZIP_BUILD_DIR}")
# Проверяем, собран ли уже libzip
set(_have_zip FALSE)
foreach(candidate
"${LIBZIP_BASE_DIR}-Debug/lib/zip.lib"
"${LIBZIP_BASE_DIR}-Release/lib/zip.lib"
)
if(EXISTS "${candidate}")
set(_have_zip TRUE)
break()
endif()
endforeach()
if(NOT _have_zip)
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Configuring libzip (${cfg})...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${LIBZIP_SRC_DIR}"
-B "${LIBZIP_SRC_DIR}/build-${cfg}"
-DCMAKE_INSTALL_PREFIX=${LIBZIP_BASE_DIR}-${cfg}
-DCMAKE_PREFIX_PATH=${ZLIB_INSTALL_DIR}
-DZLIB_ROOT=${ZLIB_INSTALL_DIR}
-DENABLE_COMMONCRYPTO=OFF
-DENABLE_GNUTLS=OFF
-DENABLE_MBEDTLS=OFF
-DENABLE_OPENSSL=OFF
-DENABLE_WINDOWS_CRYPTO=OFF
-DENABLE_FUZZ=OFF
RESULT_VARIABLE _zip_cfg_res
)
if(NOT _zip_cfg_res EQUAL 0)
message(FATAL_ERROR "libzip configure failed")
endif()
log("Building libzip (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND} --build "${LIBZIP_SRC_DIR}/build-${cfg}" --config ${cfg} -v
RESULT_VARIABLE _zip_build_res
)
if(NOT _zip_build_res EQUAL 0)
message(FATAL_ERROR "libzip build failed for configuration ${cfg}")
endif()
log("Installing libzip (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND} --install "${LIBZIP_SRC_DIR}/build-${cfg}" --config ${cfg} -v
RESULT_VARIABLE _zip_inst_res
)
if(NOT _zip_inst_res EQUAL 0)
message(FATAL_ERROR "libzip install failed for configuration ${cfg}")
endif()
endforeach()
endif()
add_library(libzip_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(libzip_external_lib PROPERTIES
IMPORTED_LOCATION_DEBUG "${LIBZIP_BASE_DIR}-Debug/lib/zip.lib" # ИСПРАВЛЕНО
IMPORTED_LOCATION_RELEASE "${LIBZIP_BASE_DIR}-Release/lib/zip.lib" # ИСПРАВЛЕНО
INTERFACE_INCLUDE_DIRECTORIES "$<IF:$<CONFIG:Debug>,${LIBZIP_BASE_DIR}-Debug/include,${LIBZIP_BASE_DIR}-Release/include>"
# libzip требует zlib для линковки
INTERFACE_LINK_LIBRARIES zlib_external_lib
)
# ===========================================
# Основной проект space-game001
# ===========================================
add_executable(space-game001
main.cpp
Game.cpp
Game.h
Environment.cpp
Environment.h
Renderer.cpp
Renderer.h
ShaderManager.cpp
ShaderManager.h
TextureManager.cpp
TextureManager.h
TextModel.cpp
TextModel.h
AudioPlayerAsync.cpp
AudioPlayerAsync.h
BoneAnimatedModel.cpp
BoneAnimatedModel.h
ZLMath.cpp
ZLMath.h
OpenGlExtensions.cpp
OpenGlExtensions.h
Utils.cpp
Utils.h
SparkEmitter.cpp
SparkEmitter.h
PlanetObject.cpp
PlanetObject.h
PlanetData.cpp
PlanetData.h
Perlin.cpp
Perlin.h
StoneObject.cpp
StoneObject.h
FrameBuffer.cpp
FrameBuffer.h
)
# Установка проекта по умолчанию для Visual Studio
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT space-game001)
# include-пути проекта
target_include_directories(space-game001 PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
#"${CMAKE_CURRENT_SOURCE_DIR}/gl"
#"${CMAKE_CURRENT_SOURCE_DIR}/cmakeaudioplayer/include"
#"${SDL2_INSTALL_DIR}/include"
#"${SDL2_INSTALL_DIR}/include/SDL2"
#"${LIBZIP_INSTALL_DIR}-Release/include" # Добавил include-путь для libzip
)
set_target_properties(space-game001 PROPERTIES
OUTPUT_NAME "space-game001"
)
# Определения препроцессора:
# PNG_ENABLED включает код PNG в TextureManager
# SDL_MAIN_HANDLED отключает переопределение main -> SDL_main
target_compile_definitions(space-game001 PRIVATE
PNG_ENABLED
SDL_MAIN_HANDLED
)
# Линкуем с SDL2main, если он вообще установлен
target_link_libraries(space-game001 PRIVATE SDL2main_external_lib)
# Линкуем сторонние библиотеки
target_link_libraries(space-game001 PRIVATE
SDL2_external_lib
libpng_external_lib
zlib_external_lib
libzip_external_lib
)
# Линкуем OpenGL (Windows)
if(WIN32)
target_link_libraries(space-game001 PRIVATE opengl32)
endif()
# ===========================================
# Копирование SDL2d.dll и zlibd.dll рядом с exe
# ===========================================
if (WIN32)
# SDL2: в Debug - SDL2d.dll, в Release - SDL2.dll
set(SDL2_DLL_SRC "$<IF:$<CONFIG:Debug>,${SDL2_INSTALL_DIR}/bin/SDL2d.dll,${SDL2_INSTALL_DIR}/bin/SDL2.dll>")
set(SDL2_DLL_DST "$<IF:$<CONFIG:Debug>,$<TARGET_FILE_DIR:space-game001>/SDL2d.dll,$<TARGET_FILE_DIR:space-game001>/SDL2.dll>")
set(LIBZIP_DLL_SRC "$<IF:$<CONFIG:Debug>,${LIBZIP_BASE_DIR}-Debug/bin/zip.dll,${LIBZIP_BASE_DIR}-Release/bin/zip.dll>")
add_custom_command(TARGET space-game001 POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Copying DLLs to output folder..."
# Копируем SDL2 (целевое имя всегда SDL2.dll)
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SDL2_DLL_SRC}"
"${SDL2_DLL_DST}"
# Копируем LIBZIP (целевое имя всегда zip.dll)
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${LIBZIP_DLL_SRC}"
"$<TARGET_FILE_DIR:space-game001>/zip.dll"
)
endif()
# ===========================================
# Копирование ресурсов после сборки
# ===========================================
# Какие папки с ресурсами нужно копировать
set(RUNTIME_RESOURCE_DIRS
"resources"
"shaders"
)
# Копируем ресурсы и шейдеры в папку exe и в корень build/
foreach(resdir IN LISTS RUNTIME_RESOURCE_DIRS)
add_custom_command(TARGET space-game001 POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Copying ${resdir} to runtime folders..."
# 1) туда, где лежит exe (build/Debug, build/Release и т.п.)
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/${resdir}"
"$<TARGET_FILE_DIR:space-game001>/${resdir}"
# 2) в корень build, если захочешь запускать из этой папки
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/${resdir}"
"${CMAKE_BINARY_DIR}/${resdir}"
)
endforeach()

View File

@ -1,351 +0,0 @@
# Cutscene System
Cutscenes are defined in JSON and loaded by `CutsceneDatabase`. Each cutscene is a self-contained object with an array of animated image layers and optional subtitle lines.
The file can contain multiple cutscenes:
```json
{
"cutscenes": [
{ "id": "intro", ... },
{ "id": "ending", ... }
]
}
```
Cutscenes and dialogues are loaded from **separate files**:
```cpp
dialogueSystem.loadDatabase("resources/dialogue/uni_interior.json"); // dialogues
dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes.json"); // cutscenes
```
---
## Cutscene object
```json
{
"id": "intro_cutscene",
"skippable": true,
"durationMs": 8000,
"fadeOutMs": 500,
"fadeInMs": 500,
"endFadeOutMs": 500,
"endFadeInMs": 500,
"onFadeInCallback": "",
"imageSegments": [ ... ],
"lines": [ ... ]
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `id` | string | — | Unique identifier used to start the cutscene from C++ or dialogue (**required**) |
| `skippable` | bool | `true` | Whether the player can skip by holding LMB / touch |
| `durationMs` | int | `0` | Minimum content duration in ms. The cutscene will not end before this time even if all subtitle lines have finished. `0` means duration is determined solely by subtitle lines or `imageSegments.endMs` |
| `fadeOutMs` | int | `0` | Duration of the **opening fade** — game world fades to black before the cutscene images appear |
| `fadeInMs` | int | `0` | Duration of the **opening reveal** — cutscene images fade in from black after `fadeOutMs` |
| `endFadeOutMs` | int | `0` | Duration of the **closing fade** — cutscene fades to black at the end of content |
| `endFadeInMs` | int | `0` | Duration of the **closing reveal** — game world fades back in from black |
| `onFadeInCallback` | string | `""` | Lua function name called once the opening fade-in completes (fired after `fadeOutMs + fadeInMs` ms) |
| `imageSegments` | array | `[]` | Image layers with motion — see [Image segments](#image-segments) |
| `lines` | array | `[]` | Subtitle lines shown sequentially — see [Subtitle lines](#subtitle-lines) |
### Timing model
The total cutscene duration is:
```
contentDuration = max(durationMs, max(segment.endMs for all segments))
totalDuration = contentDuration + endFadeOutMs + endFadeInMs
```
The full timeline looks like this:
```
|-- fadeOutMs --|-- fadeInMs --|--- content plays (images + subtitles) ---|-- endFadeOutMs --|-- endFadeInMs --|
world→black black→images images→black black→world
```
---
## Image segments
Each entry in `imageSegments` describes one image layer: when it is visible, how it fades in/out, and how it animates from a start pose to an end pose.
```json
{
"path": "resources/cutscenes/bg_layer.png",
"width": 1280,
"height": 720,
"startMs": 0,
"endMs": 8000,
"fadeInMs": 300,
"fadeOutMs": 300,
"easing": "EaseInOutSine",
"from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 },
"to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 }
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `path` | string | — | Path to the PNG image (**required**) |
| `width` | int | `0` | Logical width used for all UV and aspect-ratio math. `0` uses the actual texture pixel width |
| `height` | int | `0` | Logical height. `0` uses the actual texture pixel height |
| `startMs` | int | `0` | Time (ms from cutscene start) when this layer becomes active |
| `endMs` | int | `0` | Time (ms) when this layer stops being active. Must be > `startMs` |
| `fadeInMs` | int | `0` | Alpha fades from 0 → 1 over this many ms after `startMs`. `0` = instant |
| `fadeOutMs` | int | `0` | Alpha fades from 1 → 0 over this many ms before `endMs`. `0` = instant |
| `easing` | string | `"Linear"` | Easing applied to the pose interpolation — see [Easing types](#easing-types) |
| `from` | pose object | center/1.0 | Pose at `startMs` — see [Image pose](#image-pose) |
| `to` | pose object | same as `from` | Pose at `endMs`. If omitted, the layer stays at `from` the whole time |
Multiple segments can be active at the same time. They are rendered **in declaration order** (first = bottom layer, last = top layer), which enables parallax layering.
---
## Image pose
A pose defines how an image is framed on screen at a given moment.
```json
{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }
```
| Property | Type | Default | Description |
|---|---|---|---|
| `centerX` | float | `0.5` | Normalized X position (0 = left edge of image, 1 = right edge) of the point that is placed at the horizontal center of the screen |
| `centerY` | float | `0.5` | Normalized Y position (0 = top edge, 1 = bottom edge) placed at the screen center |
| `scale` | float | `1.0` | Zoom level. `1.0` = the image fills the screen exactly (aspect-ratio corrected). `2.0` = zoomed in 2×, showing half the image area |
The runtime interpolates all three values independently from `from` to `to` using the chosen easing.
**Coordinate clamping:** `centerX`/`centerY` are automatically clamped so the viewport never shows area outside the image. For a zoomed-in segment (`scale > 1`) you therefore have more freedom to pan; for `scale = 1.0` the center is locked to `0.5/0.5`.
### Pose intuition
| Goal | Config |
|---|---|
| Centered, no zoom | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }` |
| Slightly zoomed in on center | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }` |
| Pan left to right | `from: { "centerX": 0.3, "scale": 1.2 }``to: { "centerX": 0.7, "scale": 1.2 }` |
| Zoom out from close-up | `from: { "scale": 1.8 }``to: { "scale": 1.0 }` |
| Look at top portion | `{ "centerY": 0.2, "scale": 1.3 }` |
---
## Easing types
Controls the interpolation curve applied to pose animation between `from` and `to`.
| Value | Description |
|---|---|
| `"Linear"` | Constant speed (default) |
| `"EaseInSine"` | Slow start, fast end |
| `"EaseOutSine"` | Fast start, slow end |
| `"EaseInOutSine"` | Slow start and end, fast middle |
| `"EaseInQuad"` | Quadratic slow start |
| `"EaseOutQuad"` | Quadratic slow end |
| `"EaseInOutQuad"` | Quadratic slow start and end |
| `"EaseInCubic"` | Cubic slow start |
| `"EaseOutCubic"` | Cubic slow end |
| `"EaseInOutCubic"` | Cubic slow start and end |
For cinematic camera motion `"EaseInOutSine"` or `"EaseInOutCubic"` give the most natural feel.
---
## Subtitle lines
Lines are displayed sequentially on top of the cutscene images. Each line shows until its duration expires (or until the player advances, if `waitForConfirm` is set).
```json
{
"speaker": "Аида Дженибековна",
"text": "Здравствуйте, студенты.",
"durationMs": 3000,
"waitForConfirm": false,
"luaCallback": ""
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `speaker` | string | `""` | Speaker name shown above the subtitle text. Empty = no name bar |
| `text` | string | `""` | Subtitle text. Supports Cyrillic and any codepoint in `resources/symbols.txt` |
| `durationMs` | int | `0` | How long this line is displayed in ms. `0` = auto-computed from text length (~17 chars/sec, minimum 1500 ms) |
| `waitForConfirm` | bool | `false` | When `true`, the line waits for player input (tap/click/Enter) before advancing. No timer runs |
| `luaCallback` | string | `""` | Lua function name called when this line begins. Useful for triggering SFX, spawning effects, etc. |
Subtitle lines run on their own timer that is **independent** of the image segments. The cutscene ends when **both** subtitle lines are exhausted **and** `contentDuration` has elapsed.
---
## C++ API
### Starting a cutscene
```cpp
// Standalone cutscene (not part of a dialogue):
dialogueSystem.startCutscene("intro_cutscene");
// Skip the currently playing cutscene:
dialogueSystem.skipCutscene();
```
### Callbacks
```cpp
// Called when a cutscene begins:
dialogueSystem.setOnCutsceneStarted([]() { /* hide HUD, etc. */ });
// Called when a cutscene ends (receives the cutscene id):
dialogueSystem.setOnCutsceneFinished([](const std::string& id) {
// id == "intro_cutscene"
});
// Called when a subtitle line begins (receives luaCallback value):
dialogueSystem.setOnCutsceneLineStarted([](const std::string& fn) {
scriptEngine.callActivateFunction(fn);
});
// Called when the opening fade-in completes (receives onFadeInCallback value):
dialogueSystem.setOnCutsceneFadeInComplete([](const std::string& fn) {
scriptEngine.callActivateFunction(fn);
});
```
### Triggering from dialogue
A dialogue node of type `CutsceneStart` embeds a cutscene mid-conversation. Dialogue resumes at `next` when the cutscene ends.
```json
{
"id": "node_cutscene",
"type": "CutsceneStart",
"cutsceneId": "intro_cutscene",
"next": "node_after_cutscene"
}
```
---
## Full examples
### Minimal — static image, timed
```json
{
"id": "simple",
"durationMs": 4000,
"fadeOutMs": 300,
"fadeInMs": 300,
"endFadeOutMs": 300,
"endFadeInMs": 300,
"imageSegments": [
{
"path": "resources/cutscenes/city.png",
"width": 1280,
"height": 720,
"startMs": 0,
"endMs": 4000
}
]
}
```
### Two-layer parallax pan
Background moves slowly left-to-right; foreground character moves faster, creating depth.
```json
{
"id": "classroom_intro",
"durationMs": 8000,
"fadeOutMs": 500,
"fadeInMs": 500,
"endFadeOutMs": 500,
"endFadeInMs": 500,
"imageSegments": [
{
"path": "resources/cutscenes/classroom_bg.png",
"width": 1920,
"height": 1080,
"startMs": 0,
"endMs": 8000,
"fadeInMs": 400,
"easing": "EaseInOutSine",
"from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 },
"to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 }
},
{
"path": "resources/cutscenes/classroom_teacher.png",
"width": 1920,
"height": 1080,
"startMs": 0,
"endMs": 8000,
"easing": "EaseInOutSine",
"from": { "centerX": 0.35, "centerY": 0.5, "scale": 1.0 },
"to": { "centerX": 0.65, "centerY": 0.5, "scale": 1.0 }
}
],
"lines": [
{
"speaker": "Аида Дженибековна",
"text": "Здравствуйте, студенты.",
"durationMs": 3000
},
{
"speaker": "Аида Дженибековна",
"text": "Рассаживайтесь.",
"durationMs": 2500
}
]
}
```
### Zoom-in reveal with a second image appearing mid-way
```json
{
"id": "letter_reveal",
"durationMs": 7000,
"fadeOutMs": 400,
"fadeInMs": 600,
"endFadeOutMs": 600,
"endFadeInMs": 400,
"imageSegments": [
{
"path": "resources/cutscenes/desk_bg.png",
"width": 1280,
"height": 720,
"startMs": 0,
"endMs": 7000
},
{
"path": "resources/cutscenes/letter_closeup.png",
"width": 1280,
"height": 720,
"startMs": 2000,
"endMs": 7000,
"fadeInMs": 800,
"easing": "EaseOutCubic",
"from": { "centerX": 0.5, "centerY": 0.5, "scale": 2.5 },
"to": { "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }
}
],
"lines": [
{
"text": "Среди бумаг на столе лежит конверт.",
"durationMs": 2500
},
{
"speaker": "Главный герой",
"text": "«Явитесь в деканат немедленно».",
"durationMs": 3000
}
]
}
```

43
Environment.cpp Normal file
View File

@ -0,0 +1,43 @@
#include "Environment.h"
#include "Utils.h"
#include <GL/gl.h>
namespace ZL {
int Environment::windowHeaderHeight = 0;
int Environment::width = 0;
int Environment::height = 0;
float Environment::zoom = 18.f;
bool Environment::leftPressed = false;
bool Environment::rightPressed = false;
bool Environment::upPressed = false;
bool Environment::downPressed = false;
bool Environment::settings_inverseVertical = false;
SDL_Window* Environment::window = nullptr;
bool Environment::showMouse = false;
bool Environment::exitGameLoop = false;
Matrix3f Environment::shipMatrix = Matrix3f::Identity();
Matrix3f Environment::inverseShipMatrix = Matrix3f::Identity();
bool Environment::tapDownHold = false;
Vector2f Environment::tapDownStartPos = { 0, 0 };
Vector2f Environment::tapDownCurrentPos = { 0, 0 };
Vector3f Environment::shipPosition = {0,0,45000.f};
float Environment::shipVelocity = 0.f;
const float Environment::CONST_Z_NEAR = 5.f;
const float Environment::CONST_Z_FAR = 5000.f;
} // namespace ZL

47
Environment.h Normal file
View File

@ -0,0 +1,47 @@
#pragma once
#include "ZLMath.h"
#ifdef __linux__
#include <SDL2/SDL.h>
#endif
#include "OpenGlExtensions.h"
namespace ZL {
class Environment {
public:
static int windowHeaderHeight;
static int width;
static int height;
static float zoom;
static bool leftPressed;
static bool rightPressed;
static bool upPressed;
static bool downPressed;
static bool settings_inverseVertical;
static Matrix3f shipMatrix;
static Matrix3f inverseShipMatrix;
static SDL_Window* window;
static bool showMouse;
static bool exitGameLoop;
static bool tapDownHold;
static Vector2f tapDownStartPos;
static Vector2f tapDownCurrentPos;
static Vector3f shipPosition;
static float shipVelocity;
static const float CONST_Z_NEAR;
static const float CONST_Z_FAR;
};
} // namespace ZL

49
FrameBuffer.cpp Normal file
View File

@ -0,0 +1,49 @@
#include "FrameBuffer.h"
#include <iostream>
#include "Environment.h"
namespace ZL {
FrameBuffer::FrameBuffer(int w, int h) : width(w), height(h) {
// 1. Ñîçäàåì FBO
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
// 2. Ñîçäàåì òåêñòóðó, êóäà áóäåì "ôîòîãðàôèðîâàòü"
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
// Óñòàíàâëèâàåì ïàðàìåòðû òåêñòóðû
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 3. Ïðèâÿçûâàåì òåêñòóðó ê FBO
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureID, 0);
// Ïðîâåðêà ãîòîâíîñòè
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
std::cerr << "Error: Framebuffer is not complete!" << std::endl;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
FrameBuffer::~FrameBuffer() {
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &textureID);
}
void FrameBuffer::Bind() {
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, width, height); // Âàæíî: óñòàíàâëèâàåì âüþïîðò ïîä ðàçìåð òåêñòóðû
}
void FrameBuffer::Unbind() {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Çäåñü æåëàòåëüíî âîçâðàùàòü âüþïîðò ê ðàçìåðàì ýêðàíà,
// íàïðèìåð, ÷åðåç Environment::width/height
glViewport(0, 0, Environment::width, Environment::height);
}
} // namespace ZL

29
FrameBuffer.h Normal file
View File

@ -0,0 +1,29 @@
#pragma once
#include "OpenGlExtensions.h"
#include <memory>
namespace ZL {
class FrameBuffer {
private:
GLuint fbo = 0;
GLuint textureID = 0;
int width, height;
public:
FrameBuffer(int w, int h);
~FrameBuffer();
// Çàïðåùàåì êîïèðîâàíèå, êàê â VBOHolder
FrameBuffer(const FrameBuffer&) = delete;
FrameBuffer& operator=(const FrameBuffer&) = delete;
void Bind(); // Íà÷àòü ðåíäåð â ýòîò áóôåð
void Unbind(); // Âåðíóòüñÿ ê îáû÷íîìó ðåíäåðó â ýêðàí
GLuint getTextureID() const { return textureID; }
int getWidth() const { return width; }
int getHeight() const { return height; }
};
} // namespace ZL

655
Game.cpp Executable file
View File

@ -0,0 +1,655 @@
#include "Game.h"
#include "AnimatedModel.h"
#include "BoneAnimatedModel.h"
#include "Utils.h"
#include "OpenGlExtensions.h"
#include <iostream>
#include "TextureManager.h"
#include "TextModel.h"
#include "StoneObject.h"
#include <random>
#include <cmath>
namespace ZL
{
#ifdef EMSCRIPTEN
const char* CONST_ZIP_FILE = "space-game001.zip";
#else
const char* CONST_ZIP_FILE = "";
#endif
Vector4f generateRandomQuaternion(std::mt19937& gen)
{
std::normal_distribution<> distrib(0.0, 1.0);
Vector4f randomQuat = {
(float)distrib(gen),
(float)distrib(gen),
(float)distrib(gen),
(float)distrib(gen)
};
return randomQuat.normalized();
}
std::vector<BoxCoords> generateRandomBoxCoords(int N)
{
const float MIN_DISTANCE = 3.0f;
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
const float MIN_COORD = -100.0f;
const float MAX_COORD = 100.0f;
const int MAX_ATTEMPTS = 1000;
std::vector<BoxCoords> boxCoordsArr;
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> distrib(MIN_COORD, MAX_COORD);
int generatedCount = 0;
while (generatedCount < N)
{
bool accepted = false;
int attempts = 0;
while (!accepted && attempts < MAX_ATTEMPTS)
{
Vector3f newPos(
(float)distrib(gen),
(float)distrib(gen),
(float)distrib(gen)
);
accepted = true;
for (const auto& existingBox : boxCoordsArr)
{
Vector3f diff = newPos - existingBox.pos;
float distanceSquared = diff.squaredNorm();
if (distanceSquared < MIN_DISTANCE_SQUARED)
{
accepted = false;
break;
}
}
if (accepted)
{
Vector4f randomQuat = generateRandomQuaternion(gen);
Matrix3f randomMatrix = QuatToMatrix(randomQuat);
boxCoordsArr.emplace_back(BoxCoords{ newPos, randomMatrix });
generatedCount++;
}
attempts++;
}
if (!accepted) {
std::cerr << "Ïðåäóïðåæäåíèå: Íå óäàëîñü ñãåíåðèðîâàòü " << N << " îáúåêòîâ. Ñãåíåðèðîâàíî: " << generatedCount << std::endl;
break;
}
}
return boxCoordsArr;
}
Game::Game()
: window(nullptr)
, glContext(nullptr)
, newTickCount(0)
, lastTickCount(0)
{
std::vector<Vector3f> emissionPoints = {
Vector3f{-2.1f, 0.9f, 5.0f},
Vector3f{2.1f, 0.9f, 5.0f}
};
sparkEmitter = SparkEmitter(emissionPoints, 100.0f);
}
Game::~Game() {
if (glContext) {
SDL_GL_DeleteContext(glContext);
}
if (window) {
SDL_DestroyWindow(window);
}
SDL_Quit();
}
void Game::setup() {
glContext = SDL_GL_CreateContext(ZL::Environment::window);
ZL::BindOpenGlFunctions();
ZL::CheckGlError();
// Initialize renderer
#ifdef EMSCRIPTEN
renderer.shaderManager.AddShaderFromFiles("default", "./shaders/default.vertex", "./shaders/default_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("defaultColor", "./shaders/defaultColor.vertex", "./shaders/defaultColor_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("env", "./shaders/env.vertex", "./shaders/env_web.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "./shaders/defaultAtmosphere.vertex", "./shaders/defaultAtmosphere.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("defaultColorPlanet", "./shaders/defaultColorPlanet.vertex", "./shaders/defaultColorPlanet_web.fragment", CONST_ZIP_FILE);
#else
renderer.shaderManager.AddShaderFromFiles("default", "./shaders/default.vertex", "./shaders/default_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("defaultColor", "./shaders/defaultColor_fog.vertex", "./shaders/defaultColor_fog_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("env", "./shaders/env_sky.vertex", "./shaders/env_sky_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("defaultAtmosphere", "./shaders/defaultAtmosphere.vertex", "./shaders/defaultAtmosphere.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("defaultColor2", "./shaders/defaultColor_fog2.vertex", "./shaders/defaultColor_fog2_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("defaultColorStones", "./shaders/defaultColor_fog_stones.vertex", "./shaders/defaultColor_fog_stones_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("planetBake", "./shaders/planet_bake.vertex", "./shaders/planet_bake_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("planetStone", "./shaders/planet_stone.vertex", "./shaders/planet_stone_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("planetLand", "./shaders/planet_land.vertex", "./shaders/planet_land_desktop.fragment", CONST_ZIP_FILE);
#endif
cubemapTexture = std::make_shared<Texture>(
std::array<TextureDataStruct, 6>{
CreateTextureDataFromBmp24("./resources/sky/space_rt.bmp", CONST_ZIP_FILE),
CreateTextureDataFromBmp24("./resources/sky/space_lf.bmp", CONST_ZIP_FILE),
CreateTextureDataFromBmp24("./resources/sky/space_up.bmp", CONST_ZIP_FILE),
CreateTextureDataFromBmp24("./resources/sky/space_dn.bmp", CONST_ZIP_FILE),
CreateTextureDataFromBmp24("./resources/sky/space_bk.bmp", CONST_ZIP_FILE),
CreateTextureDataFromBmp24("./resources/sky/space_ft.bmp", CONST_ZIP_FILE)
});
cubemap.data = ZL::CreateCubemap(500);
cubemap.RefreshVBO();
//Load texture
spaceshipTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/DefaultMaterial_BaseColor_shine.png", CONST_ZIP_FILE));
spaceshipBase = LoadFromTextFile02("./resources/spaceship006.txt", CONST_ZIP_FILE);
spaceshipBase.RotateByMatrix(QuatToMatrix(QuatFromRotateAroundY(M_PI / 2.0)));
//spaceshipBase.Move(Vector3f{ -0.52998, -13, 0 });
//spaceshipBase.Move(Vector3f{ -0.52998, -10, 10 });
spaceship.AssignFrom(spaceshipBase);
spaceship.RefreshVBO();
//Boxes
boxTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/box/box.png", CONST_ZIP_FILE));
boxBase = LoadFromTextFile02("./resources/box/box.txt", CONST_ZIP_FILE);
boxCoordsArr = generateRandomBoxCoords(50);
boxRenderArr.resize(boxCoordsArr.size());
for (int i = 0; i < boxCoordsArr.size(); i++)
{
boxRenderArr[i].AssignFrom(boxBase);
//boxRenderArr[i].data = CreateBaseConvexPolyhedron(1999);
boxRenderArr[i].RefreshVBO();
}
sparkTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/spark.png", CONST_ZIP_FILE));
sparkEmitter.setTexture(sparkTexture);
buttonTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/button.png", CONST_ZIP_FILE));
button.data.PositionData.push_back({ 100, 100, 0 });
button.data.PositionData.push_back({ 100, 150, 0 });
button.data.PositionData.push_back({ 300, 150, 0 });
button.data.PositionData.push_back({ 100, 100, 0 });
button.data.PositionData.push_back({ 300, 150, 0 });
button.data.PositionData.push_back({ 300, 100, 0 });
button.data.TexCoordData.push_back({ 0,0 });
button.data.TexCoordData.push_back({ 0,1 });
button.data.TexCoordData.push_back({ 1,1 });
button.data.TexCoordData.push_back({ 0,0 });
button.data.TexCoordData.push_back({ 1,1 });
button.data.TexCoordData.push_back({ 1,0 });
button.RefreshVBO();
musicVolumeBarTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/musicVolumeBarTexture.png", CONST_ZIP_FILE));
musicVolumeBar.data.PositionData.push_back({ 1190, 100, 0 });
musicVolumeBar.data.PositionData.push_back({ 1190, 600, 0 });
musicVolumeBar.data.PositionData.push_back({ 1200, 600, 0 });
musicVolumeBar.data.PositionData.push_back({ 1190, 100, 0 });
musicVolumeBar.data.PositionData.push_back({ 1200, 600, 0 });
musicVolumeBar.data.PositionData.push_back({ 1200, 100, 0 });
musicVolumeBar.data.TexCoordData.push_back({ 0,0 });
musicVolumeBar.data.TexCoordData.push_back({ 0,1 });
musicVolumeBar.data.TexCoordData.push_back({ 1,1 });
musicVolumeBar.data.TexCoordData.push_back({ 0,0 });
musicVolumeBar.data.TexCoordData.push_back({ 1,1 });
musicVolumeBar.data.TexCoordData.push_back({ 1,0 });
musicVolumeBar.RefreshVBO();
musicVolumeBarButtonTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/musicVolumeBarButton.png", CONST_ZIP_FILE));
float musicVolumeBarButtonButtonCenterY = 350.0f;
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX - musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY - musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX - musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY + musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX + musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY + musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX - musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY - musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX + musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY + musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX + musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY - musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.TexCoordData.push_back({ 0,0 });
musicVolumeBarButton.data.TexCoordData.push_back({ 0,1 });
musicVolumeBarButton.data.TexCoordData.push_back({ 1,1 });
musicVolumeBarButton.data.TexCoordData.push_back({ 0,0 });
musicVolumeBarButton.data.TexCoordData.push_back({ 1,1 });
musicVolumeBarButton.data.TexCoordData.push_back({ 1,0 });
musicVolumeBarButton.RefreshVBO();
renderer.InitOpenGL();
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
planetObject.init();
rockTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/rock.png", ""));
}
void Game::drawCubemap(float skyPercent)
{
static const std::string defaultShaderName = "default";
static const std::string envShaderName = "env";
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
static const std::string skyPercentUniformName = "skyPercent";
renderer.shaderManager.PushShader(envShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform1f(skyPercentUniformName, skyPercent);
renderer.EnableVertexAttribArray(vPositionName);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.RotateMatrix(Environment::inverseShipMatrix);
CheckGlError();
glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture->getTexID());
renderer.DrawVertexRenderStruct(cubemap);
CheckGlError();
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vPositionName);
renderer.shaderManager.PopShader();
CheckGlError();
}
void Game::drawShip()
{
static const std::string defaultShaderName = "default";
static const std::string envShaderName = "env";
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
renderer.DrawVertexRenderStruct(spaceship);
sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.shaderManager.PopShader();
CheckGlError();
}
void Game::drawBoxes()
{
static const std::string defaultShaderName = "default";
static const std::string envShaderName = "env";
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
for (int i = 0; i < boxCoordsArr.size(); i++)
{
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipPosition);
renderer.TranslateMatrix({ 0.f, 0.f, 45000.f });
renderer.TranslateMatrix(boxCoordsArr[i].pos);
renderer.RotateMatrix(boxCoordsArr[i].m);
glBindTexture(GL_TEXTURE_2D, boxTexture->getTexID());
//glBindTexture(GL_TEXTURE_2D, rockTexture->getTexID());
renderer.DrawVertexRenderStruct(boxRenderArr[i]);
renderer.PopMatrix();
}
renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.shaderManager.PopShader();
CheckGlError();
}
void Game::UpdateVolumeKnob() {
float musicVolumeBarButtonButtonCenterY = volumeBarMinY + musicVolume * (volumeBarMaxY - volumeBarMinY);
auto& pos = musicVolumeBarButton.data.PositionData;
pos[0] = { musicVolumeBarButtonButtonCenterX - musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY - musicVolumeBarButtonButtonRadius, 0 };
pos[1] = { musicVolumeBarButtonButtonCenterX - musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY + musicVolumeBarButtonButtonRadius, 0 };
pos[2] = { musicVolumeBarButtonButtonCenterX + musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY + musicVolumeBarButtonButtonRadius, 0 };
pos[3] = { musicVolumeBarButtonButtonCenterX - musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY - musicVolumeBarButtonButtonRadius, 0 };
pos[4] = { musicVolumeBarButtonButtonCenterX + musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY + musicVolumeBarButtonButtonRadius, 0 };
pos[5] = { musicVolumeBarButtonButtonCenterX + musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY - musicVolumeBarButtonButtonRadius, 0 };
musicVolumeBarButton.RefreshVBO();
}
void Game::UpdateVolumeFromMouse(int mouseX, int mouseY) {
int uiX = mouseX;
int uiY = Environment::height - mouseY;
Environment::shipVelocity = (musicVolume) * (20.0);
if (uiY < volumeBarMinY || uiY > volumeBarMaxY)
{
return;
}
float t = (uiY - volumeBarMinY) / (volumeBarMaxY - volumeBarMinY);
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
musicVolume = t;
UpdateVolumeKnob();
}
void Game::drawUI()
{
static const std::string defaultShaderName = "default";
static const std::string envShaderName = "env";
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
glClear(GL_DEPTH_BUFFER_BIT);
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
renderer.PushProjectionMatrix(Environment::width, Environment::height, -1, 1);
renderer.PushMatrix();
renderer.LoadIdentity();
glBindTexture(GL_TEXTURE_2D, buttonTexture->getTexID());
renderer.DrawVertexRenderStruct(button);
glBindTexture(GL_TEXTURE_2D, musicVolumeBarTexture->getTexID());
renderer.DrawVertexRenderStruct(musicVolumeBar);
glBindTexture(GL_TEXTURE_2D, musicVolumeBarButtonTexture->getTexID());
renderer.DrawVertexRenderStruct(musicVolumeBarButton);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.shaderManager.PopShader();
CheckGlError();
}
void Game::drawScene() {
static const std::string defaultShaderName = "default";
static const std::string envShaderName = "env";
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
glViewport(0, 0, Environment::width, Environment::height);
CheckGlError();
float skyPercent = 0.0;
float distance = planetObject.distanceToPlanetSurface(Environment::shipPosition);
if (distance > 1900.f)
{
skyPercent = 0.0f;
}
else if (distance < 1000.f)
{
skyPercent = 1.0f;
}
else
{
skyPercent = (1900.f - distance) / 900.f;
}
drawCubemap(skyPercent);
planetObject.draw(renderer);
if (planetObject.distanceToPlanetSurface(Environment::shipPosition) > 100.f)
{
glClear(GL_DEPTH_BUFFER_BIT);
}
drawShip();
drawBoxes();
drawUI();
CheckGlError();
}
void Game::processTickCount() {
if (lastTickCount == 0) {
lastTickCount = SDL_GetTicks64();
return;
}
newTickCount = SDL_GetTicks64();
if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) {
size_t delta = (newTickCount - lastTickCount > CONST_MAX_TIME_INTERVAL) ?
CONST_MAX_TIME_INTERVAL : newTickCount - lastTickCount;
//gameObjects.updateScene(delta);
sparkEmitter.update(static_cast<float>(delta));
planetObject.update(static_cast<float>(delta));
if (Environment::tapDownHold) {
float diffx = Environment::tapDownCurrentPos.v[0] - Environment::tapDownStartPos.v[0];
float diffy = Environment::tapDownCurrentPos.v[1] - Environment::tapDownStartPos.v[1];
if (abs(diffy) > 5.0 || abs(diffx) > 5.0) //threshold
{
float rotationPower = sqrtf(diffx * diffx + diffy * diffy);
//std::cout << rotationPower << std::endl;
float deltaAlpha = rotationPower * delta * M_PI / 500000.f;
Vector3f rotationDirection = { diffy, diffx, 0 };
rotationDirection = rotationDirection.normalized();
Vector4f rotateQuat = {
rotationDirection.v[0] * sin(deltaAlpha * 0.5f),
rotationDirection.v[1] * sin(deltaAlpha * 0.5f),
rotationDirection.v[2] * sin(deltaAlpha * 0.5f),
cos(deltaAlpha * 0.5f) };
Matrix3f rotateMat = QuatToMatrix(rotateQuat);
Environment::shipMatrix = MultMatrixMatrix(Environment::shipMatrix, rotateMat);
Environment::inverseShipMatrix = InverseMatrix(Environment::shipMatrix);
}
}
if (fabs(Environment::shipVelocity) > 0.01f)
{
Vector3f velocityDirection = { 0,0, -Environment::shipVelocity * delta / 1000.f };
Vector3f velocityDirectionAdjusted = MultMatrixVector(Environment::shipMatrix, velocityDirection);
Environment::shipPosition = Environment::shipPosition + velocityDirectionAdjusted;
}
lastTickCount = newTickCount;
}
}
void Game::render() {
SDL_GL_MakeCurrent(ZL::Environment::window, glContext);
ZL::CheckGlError();
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawScene();
processTickCount();
SDL_GL_SwapWindow(ZL::Environment::window);
}
void Game::update() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
Environment::exitGameLoop = true;
}
else if (event.type == SDL_MOUSEBUTTONDOWN) {
// 1. Îáðàáîòêà íàæàòèÿ êíîïêè ìûøè
int mx = event.button.x;
int my = event.button.y;
std::cout << mx << " " << my << '\n';
int uiX = mx;
int uiY = Environment::height - my;
if (uiX >= volumeBarMinX - 40 && uiX <= volumeBarMaxX + 40 &&
uiY >= volumeBarMinY - 40 && uiY <= volumeBarMaxY + 40) {
isDraggingVolume = true;
UpdateVolumeFromMouse(mx, my);
}
else {
Environment::tapDownHold = true;
// Êîîðäèíàòû íà÷àëüíîãî íàæàòèÿ
Environment::tapDownStartPos.v[0] = event.button.x;
Environment::tapDownStartPos.v[1] = event.button.y;
// Íà÷àëüíàÿ ïîçèöèÿ òàêæå ñòàíîâèòñÿ òåêóùåé
Environment::tapDownCurrentPos.v[0] = event.button.x;
Environment::tapDownCurrentPos.v[1] = event.button.y;
}
}
else if (event.type == SDL_MOUSEBUTTONUP) {
// 2. Îáðàáîòêà îòïóñêàíèÿ êíîïêè ìûøè
isDraggingVolume = false;
Environment::tapDownHold = false;
}
else if (event.type == SDL_MOUSEMOTION) {
// 3. Îáðàáîòêà ïåðåìåùåíèÿ ìûøè
int mx = event.motion.x;
int my = event.motion.y;
if (isDraggingVolume) {
// Äâèãàåì ìûøü ïî ñëàéäåðó — ìåíÿåì ãðîìêîñòü è ïîçèöèþ êðóæêà
UpdateVolumeFromMouse(mx, my);
}
if (Environment::tapDownHold) {
// Îáíîâëåíèå òåêóùåé ïîçèöèè, åñëè êíîïêà óäåðæèâàåòñÿ
Environment::tapDownCurrentPos.v[0] = event.motion.x;
Environment::tapDownCurrentPos.v[1] = event.motion.y;
}
}
else if (event.type == SDL_MOUSEWHEEL) {
static const float zoomstep = 2.0f;
if (event.wheel.y > 0) {
Environment::zoom -= zoomstep;
}
else if (event.wheel.y < 0) {
Environment::zoom += zoomstep;
}
if (Environment::zoom < zoomstep) {
Environment::zoom = zoomstep;
}
}
else if (event.type == SDL_KEYUP)
{
if (event.key.keysym.sym == SDLK_i)
{
Environment::shipVelocity += 500.f;
}
if (event.key.keysym.sym == SDLK_k)
{
Environment::shipVelocity -= 500.f;
}
if (event.key.keysym.sym == SDLK_o)
{
Environment::shipVelocity += 50.f;
}
if (event.key.keysym.sym == SDLK_l)
{
Environment::shipVelocity -= 50.f;
}
}
}
render();
}
} // namespace ZL

95
Game.h Executable file
View File

@ -0,0 +1,95 @@
#pragma once
#include "OpenGlExtensions.h"
#include "Renderer.h"
#include "Environment.h"
#include "TextureManager.h"
#include "SparkEmitter.h"
#include "PlanetObject.h"
namespace ZL {
struct BoxCoords
{
Vector3f pos;
Matrix3f m;
};
class Game {
public:
Game();
~Game();
void setup();
void update();
void render();
bool shouldExit() const { return Environment::exitGameLoop; }
private:
void processTickCount();
void drawScene();
void drawCubemap(float skyPercent);
void drawShip();
void drawBoxes();
void drawUI();
SDL_Window* window;
SDL_GLContext glContext;
Renderer renderer;
size_t newTickCount;
size_t lastTickCount;
std::shared_ptr<Texture> rockTexture;
std::vector<BoxCoords> boxCoordsArr;
std::vector<VertexRenderStruct> boxRenderArr;
std::shared_ptr<Texture> buttonTexture;
VertexRenderStruct button;
std::shared_ptr<Texture> musicVolumeBarTexture;
VertexRenderStruct musicVolumeBar;
std::shared_ptr<Texture> musicVolumeBarButtonTexture;
VertexRenderStruct musicVolumeBarButton;
bool isDraggingVolume = false;
float musicVolume = 0.0f;
float volumeBarMinX = 1190.0f;
float volumeBarMaxX = 1200.0f;
float volumeBarMinY = 100.0f;
float volumeBarMaxY = 600.0f;
float musicVolumeBarButtonButtonCenterX = 1195.0f;
float musicVolumeBarButtonButtonRadius = 25.0f;
void UpdateVolumeFromMouse(int mouseX, int mouseY);
void UpdateVolumeKnob();
static const size_t CONST_TIMER_INTERVAL = 10;
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
std::shared_ptr<Texture> sparkTexture;
std::shared_ptr<Texture> spaceshipTexture;
std::shared_ptr<Texture> cubemapTexture;
VertexDataStruct spaceshipBase;
VertexRenderStruct spaceship;
VertexRenderStruct cubemap;
std::shared_ptr<Texture> boxTexture;
VertexDataStruct boxBase;
SparkEmitter sparkEmitter;
PlanetObject planetObject;
};
} // namespace ZL

335
OpenGlExtensions.cpp Executable file
View File

@ -0,0 +1,335 @@
#include "OpenGlExtensions.h"
#include "Utils.h"
#include <iostream>
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__)
//====================================================
//===================== GLSL Shaders =================
//====================================================
//Requires GL_VERSION_2_0
PFNGLCREATEPROGRAMPROC glCreateProgram = NULL;
PFNGLDELETEPROGRAMPROC glDeleteProgram = NULL;
PFNGLLINKPROGRAMPROC glLinkProgram = NULL;
PFNGLVALIDATEPROGRAMPROC glValidateProgram = NULL;
PFNGLUSEPROGRAMPROC glUseProgram = NULL;
PFNGLGETPROGRAMIVPROC glGetProgramiv = NULL;
PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog = NULL;
PFNGLCREATESHADERPROC glCreateShader = NULL;
PFNGLDELETESHADERPROC glDeleteShader = NULL;
PFNGLSHADERSOURCEPROC glShaderSource = NULL;
PFNGLCOMPILESHADERPROC glCompileShader = NULL;
PFNGLATTACHSHADERPROC glAttachShader = NULL;
PFNGLDETACHSHADERPROC glDetachShader = NULL;
PFNGLGETSHADERIVPROC glGetShaderiv = NULL;
PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog = NULL;
PFNGLGETATTRIBLOCATIONPROC glGetAttribLocation = NULL;
PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer = NULL;
PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray = NULL;
PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray = NULL;
PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation = NULL;
PFNGLUNIFORMMATRIX3FVPROC glUniformMatrix3fv = NULL;
PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv = NULL;
PFNGLUNIFORM1IPROC glUniform1i = NULL;
PFNGLUNIFORM1FVPROC glUniform1fv = NULL;
PFNGLUNIFORM3FVPROC glUniform2fv = NULL;
PFNGLUNIFORM3FVPROC glUniform3fv = NULL;
PFNGLUNIFORM4FVPROC glUniform4fv = NULL;
PFNGLVERTEXATTRIB1FPROC glVertexAttrib1f = NULL;
PFNGLVERTEXATTRIB2FPROC glVertexAttrib2f = NULL;
PFNGLVERTEXATTRIB3FPROC glVertexAttrib3f = NULL;
PFNGLVERTEXATTRIB4FPROC glVertexAttrib4f = NULL;
PFNGLVERTEXATTRIB2FVPROC glVertexAttrib2fv = NULL;
PFNGLVERTEXATTRIB3FVPROC glVertexAttrib3fv = NULL;
PFNGLVERTEXATTRIB4FVPROC glVertexAttrib4fv = NULL;
PFNGLGETACTIVEATTRIBPROC glGetActiveAttrib = NULL;
PFNGLGETACTIVEUNIFORMPROC glGetActiveUniform = NULL;
//=======================================
//=========== Multitexture ==============
//=======================================
//Requires GL version 1.3
PFNGLACTIVETEXTUREPROC glActiveTexture = NULL;
//=======================================
//========== Vertex buffer ==============
//=======================================
//Requires GL_VERSION_1_5
PFNGLGENBUFFERSPROC glGenBuffers = NULL;
PFNGLDELETEBUFFERSPROC glDeleteBuffers = NULL;
PFNGLBINDBUFFERPROC glBindBuffer = NULL;
PFNGLBUFFERDATAPROC glBufferData = NULL;
PFNGLBUFFERSUBDATAPROC glBufferSubData = NULL;
PFNGLMAPBUFFERPROC glMapBuffer = NULL;
PFNGLUNMAPBUFFERPROC glUnmapBuffer = NULL;
//=========================================
//============ Frame buffer ===============
//=========================================
//Requires GL_ARB_framebuffer_object
PFNGLISRENDERBUFFERPROC glIsRenderbuffer = NULL;
PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer = NULL;
PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers = NULL;
PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers = NULL;
PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage = NULL;
PFNGLGETRENDERBUFFERPARAMETERIVPROC glGetRenderbufferParameteriv = NULL;
PFNGLISFRAMEBUFFERPROC glIsFramebuffer = NULL;
PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer = NULL;
PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers = NULL;
PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers = NULL;
PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus = NULL;
PFNGLFRAMEBUFFERTEXTURE1DPROC glFramebufferTexture1D = NULL;
PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D = NULL;
PFNGLFRAMEBUFFERTEXTURE3DPROC glFramebufferTexture3D = NULL;
PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer = NULL;
PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC glGetFramebufferAttachmentParameteriv = NULL;
PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer = NULL;
PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC glRenderbufferStorageMultisample = NULL;
PFNGLGENERATEMIPMAPPROC glGenerateMipmap = NULL;
PFNGLFRAMEBUFFERTEXTURELAYERPROC glFramebufferTextureLayer = NULL;
//===========================================
//============ Uniform buffer ===============
//===========================================
//Requires GL_ARB_uniform_buffer_object
PFNGLGETUNIFORMINDICESPROC glGetUniformIndices = NULL;
PFNGLGETACTIVEUNIFORMSIVPROC glGetActiveUniformsiv = NULL;
PFNGLGETACTIVEUNIFORMNAMEPROC glGetActiveUniformName = NULL;
PFNGLGETUNIFORMBLOCKINDEXPROC glGetUniformBlockIndex = NULL;
PFNGLGETACTIVEUNIFORMBLOCKIVPROC glGetActiveUniformBlockiv = NULL;
PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC glGetActiveUniformBlockName = NULL;
PFNGLUNIFORMBLOCKBINDINGPROC glUniformBlockBinding = NULL;
PFNGLBINDBUFFERBASEPROC glBindBufferBase = NULL;
PFNGLGENVERTEXARRAYSPROC glGenVertexArrays = NULL;
PFNGLBINDVERTEXARRAYPROC glBindVertexArray = NULL;
PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArray = NULL;
#endif
namespace ZL {
bool BindOpenGlFunctions()
{
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__)
//char* extensionList = (char*)glGetString(GL_EXTENSIONS);
char* glVersion = (char*)glGetString(GL_VERSION);
bool ok = true;
//Requires OpenGL 2.0 or above
if (glVersion[0] >= '2')
{
glActiveTexture = (PFNGLACTIVETEXTUREPROC)wglGetProcAddress("glActiveTexture");
glGenBuffers = (PFNGLGENBUFFERSPROC)wglGetProcAddress("glGenBuffers");
glDeleteBuffers = (PFNGLDELETEBUFFERSPROC)wglGetProcAddress("glDeleteBuffers");
glBindBuffer = (PFNGLBINDBUFFERPROC)wglGetProcAddress("glBindBuffer");
glBufferData = (PFNGLBUFFERDATAPROC)wglGetProcAddress("glBufferData");
glBufferSubData = (PFNGLBUFFERSUBDATAPROC)wglGetProcAddress("glBufferSubData");
glMapBuffer = (PFNGLMAPBUFFERPROC)wglGetProcAddress("glMapBuffer");
glUnmapBuffer = (PFNGLUNMAPBUFFERPROC)wglGetProcAddress("glUnmapBuffer");
glCreateProgram = (PFNGLCREATEPROGRAMPROC)wglGetProcAddress("glCreateProgram");
glDeleteProgram = (PFNGLDELETEPROGRAMPROC)wglGetProcAddress("glDeleteProgram");
glLinkProgram = (PFNGLLINKPROGRAMPROC)wglGetProcAddress("glLinkProgram");
glValidateProgram = (PFNGLVALIDATEPROGRAMPROC)wglGetProcAddress("glValidateProgram");
glUseProgram = (PFNGLUSEPROGRAMPROC)wglGetProcAddress("glUseProgram");
glGetProgramiv = (PFNGLGETPROGRAMIVPROC)wglGetProcAddress("glGetProgramiv");
glGetProgramInfoLog = (PFNGLGETPROGRAMINFOLOGPROC)wglGetProcAddress("glGetProgramInfoLog");
glCreateShader = (PFNGLCREATESHADERPROC)wglGetProcAddress("glCreateShader");
glDeleteShader = (PFNGLDELETESHADERPROC)wglGetProcAddress("glDeleteShader");
glShaderSource = (PFNGLSHADERSOURCEPROC)wglGetProcAddress("glShaderSource");
glCompileShader = (PFNGLCOMPILESHADERPROC)wglGetProcAddress("glCompileShader");
glAttachShader = (PFNGLATTACHSHADERPROC)wglGetProcAddress("glAttachShader");
glDetachShader = (PFNGLDETACHSHADERPROC)wglGetProcAddress("glDetachShader");
glGetShaderiv = (PFNGLGETSHADERIVPROC)wglGetProcAddress("glGetShaderiv");
glGetShaderInfoLog = (PFNGLGETSHADERINFOLOGPROC)wglGetProcAddress("glGetShaderInfoLog");
glGetAttribLocation = (PFNGLGETATTRIBLOCATIONPROC)wglGetProcAddress("glGetAttribLocation");
glVertexAttribPointer = (PFNGLVERTEXATTRIBPOINTERPROC)wglGetProcAddress("glVertexAttribPointer");
glEnableVertexAttribArray = (PFNGLENABLEVERTEXATTRIBARRAYPROC)wglGetProcAddress("glEnableVertexAttribArray");
glDisableVertexAttribArray = (PFNGLDISABLEVERTEXATTRIBARRAYPROC)wglGetProcAddress("glDisableVertexAttribArray");
glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC)wglGetProcAddress("glGetUniformLocation");
glUniformMatrix3fv = (PFNGLUNIFORMMATRIX3FVPROC)wglGetProcAddress("glUniformMatrix3fv");
glUniformMatrix4fv = (PFNGLUNIFORMMATRIX4FVPROC)wglGetProcAddress("glUniformMatrix4fv");
glUniform1i = (PFNGLUNIFORM1IPROC)wglGetProcAddress("glUniform1i");
glUniform1fv = (PFNGLUNIFORM1FVPROC)wglGetProcAddress("glUniform1fv");
glUniform2fv = (PFNGLUNIFORM2FVPROC)wglGetProcAddress("glUniform2fv");
glUniform3fv = (PFNGLUNIFORM3FVPROC)wglGetProcAddress("glUniform3fv");
glUniform4fv = (PFNGLUNIFORM4FVPROC)wglGetProcAddress("glUniform4fv");
glVertexAttrib1f = (PFNGLVERTEXATTRIB1FPROC)wglGetProcAddress("glVertexAttrib1f");
glVertexAttrib2f = (PFNGLVERTEXATTRIB2FPROC)wglGetProcAddress("glVertexAttrib2f");
glVertexAttrib3f = (PFNGLVERTEXATTRIB3FPROC)wglGetProcAddress("glVertexAttrib3f");
glVertexAttrib4f = (PFNGLVERTEXATTRIB4FPROC)wglGetProcAddress("glVertexAttrib4f");
glVertexAttrib2fv = (PFNGLVERTEXATTRIB2FVPROC)wglGetProcAddress("glVertexAttrib2fv");
glVertexAttrib3fv = (PFNGLVERTEXATTRIB3FVPROC)wglGetProcAddress("glVertexAttrib3fv");
glVertexAttrib4fv = (PFNGLVERTEXATTRIB4FVPROC)wglGetProcAddress("glVertexAttrib4fv");
glGetActiveAttrib = (PFNGLGETACTIVEATTRIBPROC)wglGetProcAddress("glGetActiveAttrib");
glGetActiveUniform = (PFNGLGETACTIVEUNIFORMPROC)wglGetProcAddress("glGetActiveUniform");
if (glActiveTexture == NULL ||
glGenBuffers == NULL ||
glDeleteBuffers == NULL ||
glBindBuffer == NULL ||
glBufferData == NULL ||
glBufferSubData == NULL ||
glMapBuffer == NULL ||
glCreateProgram == NULL ||
glDeleteProgram == NULL ||
glLinkProgram == NULL ||
glValidateProgram == NULL ||
glUseProgram == NULL ||
glGetProgramiv == NULL ||
glGetProgramInfoLog == NULL ||
glCreateShader == NULL ||
glDeleteShader == NULL ||
glShaderSource == NULL ||
glCompileShader == NULL ||
glAttachShader == NULL ||
glDetachShader == NULL ||
glGetShaderiv == NULL ||
glGetShaderInfoLog == NULL ||
glGetAttribLocation == NULL ||
glVertexAttribPointer == NULL ||
glEnableVertexAttribArray == NULL ||
glDisableVertexAttribArray == NULL ||
glGetUniformLocation == NULL ||
glUniformMatrix3fv == NULL ||
glUniformMatrix4fv == NULL ||
glUniform1i == NULL ||
glUniform1fv == NULL ||
glUniform2fv == NULL ||
glUniform3fv == NULL ||
glUniform4fv == NULL ||
glEnableVertexAttribArray == NULL ||
glVertexAttrib1f == NULL ||
glVertexAttrib2f == NULL ||
glVertexAttrib3f == NULL ||
glVertexAttrib4f == NULL ||
glVertexAttrib2fv == NULL ||
glVertexAttrib3fv == NULL ||
glVertexAttrib4fv == NULL ||
glGetActiveAttrib == NULL ||
glGetActiveUniform == NULL)
{
ok = false;
}
}
else
{
ok = false;
}
glIsRenderbuffer = (PFNGLISRENDERBUFFERPROC)wglGetProcAddress("glIsRenderbuffer");
glBindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC)wglGetProcAddress("glBindRenderbuffer");
glDeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSPROC)wglGetProcAddress("glDeleteRenderbuffers");
glGenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC)wglGetProcAddress("glGenRenderbuffers");
glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC)wglGetProcAddress("glRenderbufferStorage");
glGetRenderbufferParameteriv = (PFNGLGETRENDERBUFFERPARAMETERIVPROC)wglGetProcAddress("glGetRenderbufferParameteriv");
glIsFramebuffer = (PFNGLISFRAMEBUFFERPROC)wglGetProcAddress("glIsFramebuffer");
glBindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC)wglGetProcAddress("glBindFramebuffer");
glDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC)wglGetProcAddress("glDeleteFramebuffers");
glGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC)wglGetProcAddress("glGenFramebuffers");
glCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC)wglGetProcAddress("glCheckFramebufferStatus");
glFramebufferTexture1D = (PFNGLFRAMEBUFFERTEXTURE1DPROC)wglGetProcAddress("glFramebufferTexture1D");
glFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC)wglGetProcAddress("glFramebufferTexture2D");
glFramebufferTexture3D = (PFNGLFRAMEBUFFERTEXTURE3DPROC)wglGetProcAddress("glFramebufferTexture3D");
glFramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFERPROC)wglGetProcAddress("glFramebufferRenderbuffer");
glGetFramebufferAttachmentParameteriv = (PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC)wglGetProcAddress("glGetFramebufferAttachmentParameteriv");
glBlitFramebuffer = (PFNGLBLITFRAMEBUFFERPROC)wglGetProcAddress("glBlitFramebuffer");
glRenderbufferStorageMultisample = (PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC)wglGetProcAddress("glRenderbufferStorageMultisample");
glGenerateMipmap = (PFNGLGENERATEMIPMAPPROC)wglGetProcAddress("glGenerateMipmap");
glFramebufferTextureLayer = (PFNGLFRAMEBUFFERTEXTURELAYERPROC)wglGetProcAddress("glFramebufferTextureLayer");
if (glIsRenderbuffer == NULL ||
glBindRenderbuffer == NULL ||
glDeleteRenderbuffers == NULL ||
glGenRenderbuffers == NULL ||
glRenderbufferStorage == NULL ||
glGetRenderbufferParameteriv == NULL ||
glIsFramebuffer == NULL ||
glBindFramebuffer == NULL ||
glDeleteFramebuffers == NULL ||
glGenFramebuffers == NULL ||
glCheckFramebufferStatus == NULL ||
glFramebufferTexture1D == NULL ||
glFramebufferTexture2D == NULL ||
glFramebufferTexture3D == NULL ||
glFramebufferRenderbuffer == NULL ||
glGetFramebufferAttachmentParameteriv == NULL ||
glBlitFramebuffer == NULL ||
glRenderbufferStorageMultisample == NULL ||
glGenerateMipmap == NULL ||
glFramebufferTextureLayer == NULL)
{
ok = false;
}
glGetUniformIndices = (PFNGLGETUNIFORMINDICESPROC)wglGetProcAddress("glGetUniformIndices");
glGetActiveUniformsiv = (PFNGLGETACTIVEUNIFORMSIVPROC)wglGetProcAddress("glGetActiveUniformsiv");
glGetActiveUniformName = (PFNGLGETACTIVEUNIFORMNAMEPROC)wglGetProcAddress("glGetActiveUniformName");
glGetUniformBlockIndex = (PFNGLGETUNIFORMBLOCKINDEXPROC)wglGetProcAddress("glGetUniformBlockIndex");
glGetActiveUniformBlockiv = (PFNGLGETACTIVEUNIFORMBLOCKIVPROC)wglGetProcAddress("glGetActiveUniformBlockiv");
glGetActiveUniformBlockName = (PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC)wglGetProcAddress("glGetActiveUniformBlockName");
glUniformBlockBinding = (PFNGLUNIFORMBLOCKBINDINGPROC)wglGetProcAddress("glUniformBlockBinding");
glBindBufferBase = (PFNGLBINDBUFFERBASEPROC)wglGetProcAddress("glBindBufferBase");
if (glGetUniformIndices == NULL ||
glGetActiveUniformsiv == NULL ||
glGetActiveUniformName == NULL ||
glGetUniformBlockIndex == NULL ||
glGetActiveUniformBlockiv == NULL ||
glGetActiveUniformBlockName == NULL ||
glUniformBlockBinding == NULL ||
glBindBufferBase == NULL)
{
ok = false;
}
glGenVertexArrays = (PFNGLGENVERTEXARRAYSPROC)wglGetProcAddress("glGenVertexArrays");
glBindVertexArray = (PFNGLBINDVERTEXARRAYPROC)wglGetProcAddress("glBindVertexArray");
glDeleteVertexArray = (PFNGLDELETEVERTEXARRAYSPROC)wglGetProcAddress("glBindVertexArray");
if (glGenVertexArrays == NULL ||
glBindVertexArray == NULL ||
glDeleteVertexArray == NULL)
{
ok = false;
}
return ok;
#else
return true;
#endif
}
void CheckGlError()
{
size_t error = glGetError();
if (error != GL_NO_ERROR)
{
throw std::runtime_error("Gl error");
}
}
}

160
OpenGlExtensions.h Executable file
View File

@ -0,0 +1,160 @@
#pragma once
#include "SDL.h"
#ifdef EMSCRIPTEN
//#define GL_GLEXT_PROTOTYPES 1
//#define EGL_EGLEXT_PROTOTYPES 1
//#include <SDL2/SDL_opengl.h>
#include <GLES3/gl3.h>
#include "emscripten.h"
#endif
#ifdef __linux__
#include <GL/gl.h>
#include <GL/glu.h>
#include <GLES3/gl3.h>
#endif
#include <exception>
#include <stdexcept>
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__)
#include "windows.h"
#define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp))
#define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp))
//#define GL_GLEXT_PROTOTYPES
#include "gl/gl.h"
#include "gl/glu.h"
#include "gl/glext.h"
#include <memory>
#include <vector>
#include <array>
#include <stack>
#include <unordered_map>
#include <map>
#define _USE_MATH_DEFINES
#include <math.h>
//Requires GL_VERSION_2_0
extern PFNGLCREATEPROGRAMPROC glCreateProgram;
extern PFNGLDELETEPROGRAMPROC glDeleteProgram;
extern PFNGLLINKPROGRAMPROC glLinkProgram;
extern PFNGLVALIDATEPROGRAMPROC glValidateProgram;
extern PFNGLUSEPROGRAMPROC glUseProgram;
extern PFNGLGETPROGRAMIVPROC glGetProgramiv;
extern PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog;
extern PFNGLCREATESHADERPROC glCreateShader;
extern PFNGLDELETESHADERPROC glDeleteShader;
extern PFNGLSHADERSOURCEPROC glShaderSource;
extern PFNGLCOMPILESHADERPROC glCompileShader;
extern PFNGLATTACHSHADERPROC glAttachShader;
extern PFNGLDETACHSHADERPROC glDetachShader;
extern PFNGLGETSHADERIVPROC glGetShaderiv;
extern PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog;
extern PFNGLGETATTRIBLOCATIONPROC glGetAttribLocation;
extern PFNGLVERTEXATTRIBPOINTERPROC glVertexAttribPointer;
extern PFNGLENABLEVERTEXATTRIBARRAYPROC glEnableVertexAttribArray;
extern PFNGLDISABLEVERTEXATTRIBARRAYPROC glDisableVertexAttribArray;
extern PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation;
extern PFNGLUNIFORMMATRIX3FVPROC glUniformMatrix3fv;
extern PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv;
extern PFNGLUNIFORM1IPROC glUniform1i;
extern PFNGLUNIFORM1FVPROC glUniform1fv;
extern PFNGLUNIFORM3FVPROC glUniform2fv;
extern PFNGLUNIFORM3FVPROC glUniform3fv;
extern PFNGLUNIFORM4FVPROC glUniform4fv;
extern PFNGLVERTEXATTRIB1FPROC glVertexAttrib1f;
extern PFNGLVERTEXATTRIB2FPROC glVertexAttrib2f;
extern PFNGLVERTEXATTRIB3FPROC glVertexAttrib3f;
extern PFNGLVERTEXATTRIB4FPROC glVertexAttrib4f;
extern PFNGLVERTEXATTRIB2FVPROC glVertexAttrib2fv;
extern PFNGLVERTEXATTRIB3FVPROC glVertexAttrib3fv;
extern PFNGLVERTEXATTRIB4FVPROC glVertexAttrib4fv;
extern PFNGLGETACTIVEATTRIBPROC glGetActiveAttrib;
extern PFNGLGETACTIVEUNIFORMPROC glGetActiveUniform;
//=======================================
//=========== Multitexture ==============
//=======================================
//Requires GL version 1.3
extern PFNGLACTIVETEXTUREPROC glActiveTexture;
//=======================================
//========== Vertex buffer ==============
//=======================================
//Requires GL_VERSION_1_5
extern PFNGLGENBUFFERSPROC glGenBuffers;
extern PFNGLDELETEBUFFERSPROC glDeleteBuffers;
extern PFNGLBINDBUFFERPROC glBindBuffer;
extern PFNGLBUFFERDATAPROC glBufferData;
extern PFNGLBUFFERSUBDATAPROC glBufferSubData;
extern PFNGLMAPBUFFERPROC glMapBuffer;
extern PFNGLUNMAPBUFFERPROC glUnmapBuffer;
//=========================================
//============ Frame buffer ===============
//=========================================
//Requires GL_ARB_framebuffer_object
extern PFNGLISRENDERBUFFERPROC glIsRenderbuffer;
extern PFNGLBINDRENDERBUFFERPROC glBindRenderbuffer;
extern PFNGLDELETERENDERBUFFERSPROC glDeleteRenderbuffers;
extern PFNGLGENRENDERBUFFERSPROC glGenRenderbuffers;
extern PFNGLRENDERBUFFERSTORAGEPROC glRenderbufferStorage;
extern PFNGLGETRENDERBUFFERPARAMETERIVPROC glGetRenderbufferParameteriv;
extern PFNGLISFRAMEBUFFERPROC glIsFramebuffer;
extern PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer;
extern PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers;
extern PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers;
extern PFNGLCHECKFRAMEBUFFERSTATUSPROC glCheckFramebufferStatus;
extern PFNGLFRAMEBUFFERTEXTURE1DPROC glFramebufferTexture1D;
extern PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D;
extern PFNGLFRAMEBUFFERTEXTURE3DPROC glFramebufferTexture3D;
extern PFNGLFRAMEBUFFERRENDERBUFFERPROC glFramebufferRenderbuffer;
extern PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC glGetFramebufferAttachmentParameteriv;
extern PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer;
extern PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC glRenderbufferStorageMultisample;
extern PFNGLGENERATEMIPMAPPROC glGenerateMipmap;
extern PFNGLFRAMEBUFFERTEXTURELAYERPROC glFramebufferTextureLayer;
//===========================================
//============ Uniform buffer ===============
//===========================================
//Requires GL_ARB_uniform_buffer_object
extern PFNGLGETUNIFORMINDICESPROC glGetUniformIndices;
extern PFNGLGETACTIVEUNIFORMSIVPROC glGetActiveUniformsiv;
extern PFNGLGETACTIVEUNIFORMNAMEPROC glGetActiveUniformName;
extern PFNGLGETUNIFORMBLOCKINDEXPROC glGetUniformBlockIndex;
extern PFNGLGETACTIVEUNIFORMBLOCKIVPROC glGetActiveUniformBlockiv;
extern PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC glGetActiveUniformBlockName;
extern PFNGLUNIFORMBLOCKBINDINGPROC glUniformBlockBinding;
extern PFNGLBINDBUFFERBASEPROC glBindBufferBase;
extern PFNGLGENVERTEXARRAYSPROC glGenVertexArrays;
extern PFNGLBINDVERTEXARRAYPROC glBindVertexArray;
extern PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArray;
#else
#endif
namespace ZL {
bool BindOpenGlFunctions();
void CheckGlError();
}

View File

@ -1,260 +0,0 @@
# Pathfinding System
This document describes the grid-based pathfinding used for the player and all NPCs, including the collision avoidance and movement quality improvements.
---
## Table of Contents
1. [Grid Representation](#1-grid-representation)
2. [Building the Walkable Grid](#2-building-the-walkable-grid)
3. [A\* Path Search](#3-a-path-search)
4. [Path Smoothing](#4-path-smoothing)
5. [Approaching Unreachable Destinations](#5-approaching-unreachable-destinations)
6. [Dynamic Obstacles](#6-dynamic-obstacles)
7. [Path Following](#7-path-following)
8. [Character Collision Resolution](#8-character-collision-resolution)
9. [Dynamic Replanning](#9-dynamic-replanning)
10. [Key Constants Reference](#10-key-constants-reference)
---
## 1. Grid Representation
The world is divided into a uniform 2D grid in the XZ plane (Y is ignored during pathfinding; all characters walk on a flat floor at `floorY`).
Each cell is either **walkable** (`1`) or **blocked** (`0`). The grid is stored as a flat `std::vector<unsigned char>` indexed by `z * gridWidth + x`.
**Parameters** (all configurable in the JSON config file):
| Parameter | Default | Description |
|---|---|---|
| `cellSize` | 0.4 m | Width and depth of one cell |
| `agentRadius` | 0.45 m | Half-width of a character — used to erode free space |
| `objectPadding` | 0.25 m | Extra clearance added around obstacle polygons |
| `boundaryPadding` | 0.0 m | Inward erosion from the edges of navigation areas |
| `floorY` | 0.0 | Y coordinate placed on every path waypoint |
**Grid bounds** are computed from the union of all navigation area polygons plus a padding margin of `cellSize * 2 + agentRadius + objectPadding` on every side.
**Cell coordinate conversion:**
```
cell.x = floor((worldX - minX) / cellSize)
cell.z = floor((worldZ - minZ) / cellSize)
cellCenter.x = minX + (cell.x + 0.5) * cellSize
cellCenter.z = minZ + (cell.z + 0.5) * cellSize
```
---
## 2. Building the Walkable Grid
The grid can be loaded in two ways.
### 2a. Pre-computed grid (`.txt` file)
A plain-text file with a small header followed by rows of `1`/`0` characters:
```
cellSize 0.4
agentRadius 0.45
floorY 0.0
...
minX -5.0
minZ -5.0
gridWidth 50
gridDepth 50
11111111...
10000001...
```
This format is generated by `PathFinder::saveGrid()` after building from polygons and can be loaded much faster than recomputing from geometry.
### 2b. Polygon-based config (`.json` file)
The JSON file lists **navigation areas** (convex or concave walkable regions) and **obstacle polygons** (impassable zones within those regions):
```json
{
"cellSize": 0.4,
"areas": [
{ "name": "main_room", "available": true, "polygon": [[x,z], ...] }
],
"obstacles": [
{ "name": "table", "polygon": [[x,z], ...] }
]
}
```
**Build steps:**
1. **Mark available areas walkable** — every cell whose center lies inside any `available` navigation area polygon gets `walkable = 1`. If `boundaryPadding > 0`, cells too close to the outer edge of the area are left blocked.
2. **Mark obstacle polygons blocked** — cells whose center lies inside an obstacle polygon, or within `agentRadius + objectPadding` of its edges, are set to `0`.
Navigation areas can be toggled at runtime via `PathFinder::setAreaAvailable()`, which rebuilds the entire grid. This is used to open or close doors, gated areas, etc.
---
## 3. A\* Path Search
`PathFinder::findPath(start, end)` runs a standard A\* on the walkable grid.
**Neighbor connectivity:** 8-directional (cardinal + diagonal). Diagonal moves are blocked if either of the two adjacent cardinal cells is unwalkable (no corner-cutting).
**Step costs:** `1.0` for cardinal, `√2 ≈ 1.414` for diagonal.
**Heuristic:** Euclidean distance in cell units to the end cell.
**Start/end snapping:** If the exact cell for `start` or `end` is not walkable, `findNearestWalkableCell` expands a square ring outward (up to radius 8 m) to find the nearest walkable cell. This makes clicking slightly outside the nav mesh still produce a valid path.
**Path reconstruction:** After A\* completes, the cell chain is walked via `cameFrom[]` from `end` back to `start`, reversed, then smoothed (see §4).
**First-waypoint trimming:** If the first waypoint is within `cellSize × 0.75` of `start`, it is dropped (the character is already close enough).
**Last-waypoint precision:** If the requested `end` maps to the same cell as the snapped end cell, the last waypoint is replaced with the exact `end` world position rather than the cell centre.
---
## 4. Path Smoothing
Raw A\* paths follow the grid diagonals and produce staircase-shaped routes. A **string-pulling** (line-of-sight) pass compresses them:
```
anchor = path[0]
result = [anchor]
while anchor is not the last cell:
find the furthest cell 'next' from anchor with unobstructed line of sight
result.append(next)
anchor = next
```
Line-of-sight is checked by stepping along the segment in increments of `cellSize / 2` and verifying that each sampled cell is walkable. The result is a minimal set of waypoints connected by straight, obstacle-free segments.
---
## 5. Approaching Unreachable Destinations
When a player clicks on a point in a disconnected region (e.g., across a thin wall), the original `findPath` returns an empty path and the character does not move. This is surprising — a click on a solid wall sensibly moves the character to the nearest reachable point, but a click into an inaccessible room does nothing.
**`findPathToNearest`** fixes this with a three-step cascade:
1. Try `findPath` with dynamic obstacles (stationary characters are avoided).
2. If empty, retry `findPath` without dynamic obstacles (an NPC blocking a doorway is ignored).
3. If still empty (destination genuinely unreachable), run **nearest-reachable A\***.
**Nearest-reachable A\***, implemented in `findNearestReachableImpl`:
- Runs the identical A\* loop against the static walkable grid.
- While processing cells, tracks `bestIndex` — the already-visited cell with the smallest Euclidean distance (in cell units) to the end cell.
- If A\* exhausts all reachable space without finding `end`, it reconstructs and returns a path to `bestIndex`.
- If `bestIndex` is still the start cell (character is completely isolated), an empty path is returned and the character stays put.
The net effect: clicking anywhere in the world always moves the character as close as possible to the target, matching the behaviour of clicking on a solid wall.
`findPathToNearest` replaces the direct `findPath` call in `Location::setupNavigation`'s path planner lambda, so it applies equally to the player and all NPCs.
---
## 6. Dynamic Obstacles
When a path is planned, other characters can temporarily mark cells as blocked to make the character walk around them rather than through them.
**How it works:**
In `Location::setupNavigation`, every character is given a `PathPlanner` closure. Before calling `findPath`, the closure builds a list of `PathFinder::DynamicObstacle` entries (position + radius) representing nearby characters. `findPath` copies the static walkable grid, stamps zeros in circles around each obstacle, then runs A\* on the modified copy. The static grid is never mutated.
**Which characters become obstacles:**
A character is added as a dynamic obstacle only when **all** of these are true:
- It is not the character currently planning the path (`self`).
- It is alive and enabled.
- **It is not moving** — a moving character is transparent to pathfinding, so it does not block narrow corridors that it is actively passing through.
- Its position lies within `kDynamicObstacleInfluenceDist = 6 m` of the direct line segment from `start` to `end` (distant characters do not affect the search).
**Obstacle radius:** `character.collisionRadius × 0.6`. Using 60 % of the physical collision radius makes path planning less conservative; physical separation at full radius is still enforced by collision resolution (§8).
**Fallback when dynamic obstacles block the only path:**
If step 1 of `findPathToNearest` (with dynamic obstacles) returns empty, step 2 retries without any dynamic obstacles. This handles the common case of an NPC standing in a doorway: the player paths through the NPC's position, and the nudge logic (§8) pushes the NPC aside as the player passes.
---
## 7. Path Following
`Character::setTarget(destination, onArrived)` sets a new walk target. It calls the path planner to generate a waypoint list. The result is stored in `pathWaypoints`; the final destination is also stored in `walkTarget` and `requestedWalkTarget`.
Each frame in `Character::update`:
1. **Active target** — if `pathWaypoints` is non-empty, the character moves toward `pathWaypoints[currentWaypointIndex]`; otherwise it moves toward `walkTarget`.
2. **Movement** — the character advances along the XZ direction at `walkSpeed` m/s and rotates smoothly toward the movement direction at `rotationSpeed` rad/s.
3. **Waypoint advance** — when the character is within `WALK_THRESHOLD = 0.05 m` of the current waypoint, it advances to the next one. When the last waypoint is reached, `pathWaypoints` is cleared and the optional `onArrived` callback is fired.
4. **State machine** — the animation state switches between `STAND` and `WALK` based on whether the character is moving.
`Character::isMoving()` returns `true` if `pathWaypoints` is non-empty or the distance to `walkTarget` exceeds `WALK_THRESHOLD`. This is used by dynamic obstacle filtering and collision nudging.
**Stopping in place:** `Character::stopInPlace()` sets `walkTarget` and `requestedWalkTarget` to the current position and clears `pathWaypoints`. It is called when an external force (collision resolution) displaces a stationary player so that the player does not walk back to their previous target position.
---
## 8. Character Collision Resolution
Pathfinding alone does not prevent two characters from occupying the same space — it only steers paths around stationary characters. Physical separation is handled separately each frame by `Location::resolveCharacterCollisions`.
**Algorithm** (3 iterations per frame):
For every pair `(A, B)` of living, enabled characters:
1. Compute the overlap: `penetration = (collisionRadius_A + collisionRadius_B) - distance(A, B)`.
2. If `penetration > 0`, compute a push direction (A-to-B normal) and a push magnitude of `penetration / 2` per character.
3. Compute candidate new positions `newA` and `newB`.
4. Validate against the navigation grid (`PathFinder::isWalkable`). If a pushed position is unwalkable, only the other character is moved.
5. **Player stays put:** if the player was not moving (`!isMoving()`) before the push, `stopInPlace()` is called after the push so the player does not walk back to the old target.
6. **NPC yielding:** if one character was moving and the other was standing, `nudgeCharacterAside` is called on the standing character.
**`nudgeCharacterAside(standing, awayFrom)`:**
Gives the standing NPC a short walk target so it steps out of the way:
1. Compute the direction from `awayFrom` to the NPC's current position.
2. Try four candidate targets at distance `1.2 m` in directions: straight away, +90°, 90°, 180°.
3. Use the first candidate that is walkable (per `PathFinder::isWalkable`).
4. Call `standing->setTarget(candidate)` — the NPC takes a small step aside, then stands at the new spot.
5. The player is never nudged; combat NPCs can be nudged, but their attack AI immediately overrides the yield target on the next tick.
---
## 9. Dynamic Replanning
When characters move they can displace each other or enter each other's planned paths. `Location::updateDynamicReplans` handles this:
**Every frame:**
1. Measure how much each character moved since the last frame. Characters that moved more than `kMovedEps = 0.05 m` are collected as **movers**.
2. For each mover, find other characters that are currently walking. If the mover's position is within `kReplanTriggerDist = 1.8 m` of the segment `[walker.position → walker.nextWaypoint]`, trigger a replan for the walker via `forceReplan()`.
3. A per-character cooldown of `kReplanCooldownMs = 500 ms` prevents the same character from replanning more often than twice per second.
**`Character::forceReplan()`** re-runs the path planner from the character's current position to its stored `requestedWalkTarget`, updating `pathWaypoints` in place. If the replanned path is empty, the character stops at its current position.
The relatively generous trigger distance (1.8 m vs the old 1.1 m) and cooldown (500 ms vs 300 ms) prevent micro-jitter: small position corrections from collision resolution no longer spam replanning events.
---
## 10. Key Constants Reference
| Constant | Location | Value | Description |
|---|---|---|---|
| `cellSize` | `PathFinder` config | 0.4 m | Grid cell size |
| `agentRadius` | `PathFinder` config | 0.45 m | Character half-width for grid erosion |
| `objectPadding` | `PathFinder` config | 0.25 m | Extra clearance around obstacles |
| `WALK_THRESHOLD` | `Character.h` | 0.05 m | Distance below which a waypoint is considered reached |
| `TARGET_REPLAN_THRESHOLD` | `Character.h` | 0.25 m | Deduplication threshold in `setTarget` |
| `kDynamicObstacleInfluenceDist` | `Location.cpp` | 6.0 m | Max distance from path for a character to become an obstacle |
| `kDynamicObstacleRadiusFraction` | `Location.cpp` | 0.6× | Fraction of collision radius used for dynamic obstacle footprint |
| `kNudgeDist` | `Location.cpp` | 1.2 m | Distance an NPC steps aside when yielding |
| `kReplanTriggerDist` | `Location.cpp` | 1.8 m | Mover must be this close to a walker's path to trigger replan |
| `kReplanCooldownMs` | `Location.cpp` | 500 ms | Minimum interval between replans for any one character |
| `NPC_TALK_DISTANCE` | `Location.cpp` | 1.35 m | Distance at which walking-to-NPC interaction fires |
| `kIterations` (collision) | `Location.cpp` | 3 | Push-apart iterations per frame |

73
Perlin.cpp Normal file
View File

@ -0,0 +1,73 @@
#include "Perlin.h"
#include <cmath>
#include <numeric>
#include <random>
#include <algorithm>
namespace ZL {
PerlinNoise::PerlinNoise() {
p.resize(256);
std::iota(p.begin(), p.end(), 0);
// Ïåðåìåøèâàåì äëÿ ñëó÷àéíîñòè (ìîæíî çàäàòü seed)
std::default_random_engine engine(77777);
std::shuffle(p.begin(), p.end(), engine);
p.insert(p.end(), p.begin(), p.end()); // Äóáëèðóåì äëÿ ïåðåïîëíåíèÿ
}
PerlinNoise::PerlinNoise(uint64_t seed) {
p.resize(256);
std::iota(p.begin(), p.end(), 0);
// Ïåðåìåøèâàåì äëÿ ñëó÷àéíîñòè (èñïîëüçóåì ïåðåäàííûé seed)
std::default_random_engine engine(static_cast<unsigned int>(seed));
std::shuffle(p.begin(), p.end(), engine);
p.insert(p.end(), p.begin(), p.end()); // Äóáëèðóåì äëÿ ïåðåïîëíåíèÿ
}
float PerlinNoise::fade(float t) { return t * t * t * (t * (t * 6 - 15) + 10); }
float PerlinNoise::lerp(float t, float a, float b) { return a + t * (b - a); }
float PerlinNoise::grad(int hash, float x, float y, float z) {
int h = hash & 15;
float u = h < 8 ? x : y;
float v = h < 4 ? y : (h == 12 || h == 14 ? x : z);
return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v);
}
float PerlinNoise::noise(float x, float y, float z) {
int X = (int)floor(x) & 255;
int Y = (int)floor(y) & 255;
int Z = (int)floor(z) & 255;
x -= floor(x);
y -= floor(y);
z -= floor(z);
float u = fade(x);
float v = fade(y);
float w = fade(z);
int A = p[X] + Y, AA = p[A] + Z, AB = p[A + 1] + Z;
int B = p[X + 1] + Y, BA = p[B] + Z, BB = p[B + 1] + Z;
return lerp(w, lerp(v, lerp(u, grad(p[AA], x, y, z), grad(p[BA], x - 1, y, z)),
lerp(u, grad(p[AB], x, y - 1, z), grad(p[BB], x - 1, y - 1, z))),
lerp(v, lerp(u, grad(p[AA + 1], x, y, z - 1), grad(p[BA + 1], x - 1, y, z - 1)),
lerp(u, grad(p[AB + 1], x, y - 1, z - 1), grad(p[BB + 1], x - 1, y - 1, z - 1))));
}
float PerlinNoise::getSurfaceHeight(Vector3f pos, float noiseCoeff) {
// ×àñòîòà øóìà (÷åì áîëüøå, òåì áîëüøå "õîëìîâ")
float frequency = 7.0f;
// Ïîëó÷àåì çíà÷åíèå øóìà (îáû÷íî îò -1 äî 1)
float noiseValue = noise(pos.v[0] * frequency, pos.v[1] * frequency, pos.v[2] * frequency);
// Ìàñøòàáèðóåì: õîòèì îòêëîíåíèå îò 1.0 äî 1.1 (ïðèìåðíî)
float height = 1.0f + (noiseValue * noiseCoeff);
return height;
}
} // namespace ZL

24
Perlin.h Normal file
View File

@ -0,0 +1,24 @@
#pragma once
#include <vector>
#include <cstdint>
#include "ZLMath.h"
namespace ZL {
class PerlinNoise {
std::vector<int> p;
public:
PerlinNoise();
PerlinNoise(uint64_t seed);
float fade(float t);
float lerp(float t, float a, float b);
float grad(int hash, float x, float y, float z);
float noise(float x, float y, float z);
float getSurfaceHeight(Vector3f pos, float noiseCoeff);
};
} // namespace ZL

493
PlanetData.cpp Normal file
View File

@ -0,0 +1,493 @@
#include "PlanetData.h"
#include <iostream>
#include <numeric>
#include <cmath>
#include <algorithm>
namespace ZL {
const float PlanetData::PLANET_RADIUS = 20000.f;
const Vector3f PlanetData::PLANET_CENTER_OFFSET = Vector3f{ 0.f, 0.f, 0.0f };
// --- Êîíñòàíòû äèàïàçîíîâ (ïåðåíåñåíû èç PlanetObject.cpp) ---
VertexID generateEdgeID(const VertexID& id1, const VertexID& id2) {
return id1 < id2 ? id1 + "_" + id2 : id2 + "_" + id1;
}
// Âñïîìîãàòåëüíàÿ ôóíêöèÿ äëÿ ïðîåêöèè (ëîêàëüíàÿ)
static Vector3f projectPointOnPlane(const Vector3f& P, const Vector3f& A, const Vector3f& B, const Vector3f& C) {
Vector3f AB = B + A * (-1.0f);
Vector3f AC = C + A * (-1.0f);
Vector3f N = AB.cross(AC).normalized();
Vector3f AP = P + A * (-1.0f);
float distance = N.dot(AP);
return P + N * (-distance);
}
PlanetData::PlanetData()
: perlin(77777)
, colorPerlin(123123)
, currentLod(0)
{
currentLod = planetMeshLods.size() - 1; // Start with max LOD
initialVertexMap = {
{{ 0.0f, 1.0f, 0.0f}, "A"},
{{ 0.0f, -1.0f, 0.0f}, "B"},
{{ 1.0f, 0.0f, 0.0f}, "C"},
{{-1.0f, 0.0f, 0.0f}, "D"},
{{ 0.0f, 0.0f, 1.0f}, "E"},
{{ 0.0f, 0.0f, -1.0f}, "F"}
};
}
void PlanetData::init() {
for (int i = 0; i < planetMeshLods.size(); i++) {
planetMeshLods[i] = generateSphere(i, 0.025f);
planetMeshLods[i].Scale(PLANET_RADIUS);
planetMeshLods[i].Move(PLANET_CENTER_OFFSET);
}
for (int i = 0; i < planetMeshLodsNoDist.size(); i++) {
planetMeshLodsNoDist[i] = generateSphere(i, 0);
planetMeshLodsNoDist[i].Scale(PLANET_RADIUS);
planetMeshLodsNoDist[i].Move(PLANET_CENTER_OFFSET);
}
planetAtmosphereLod = generateSphere(5, 0);
planetAtmosphereLod.Scale(PLANET_RADIUS * 1.03);
planetAtmosphereLod.Move(PLANET_CENTER_OFFSET);
}
const LodLevel& PlanetData::getLodLevel(int level) const {
return planetMeshLods.at(level);
}
const LodLevel& PlanetData::getLodLevelNoDist(int level) const {
return planetMeshLodsNoDist.at(level);
}
const LodLevel& PlanetData::getAtmosphereLod() const {
return planetAtmosphereLod;
}
int PlanetData::getCurrentLodIndex() const {
return currentLod;
}
int PlanetData::getMaxLodIndex() const {
return static_cast<int>(planetMeshLods.size() - 1);
}
std::pair<float, float> PlanetData::calculateZRange(float dToPlanetSurface) {
float currentZNear;
float currentZFar;
float alpha;
if (dToPlanetSurface > 2000)
{
currentZNear = 1000;
currentZFar = currentZNear * 100;
}
else if (dToPlanetSurface > 1200)
{
currentZNear = 500;
currentZFar = currentZNear * 100;
}
else if (dToPlanetSurface > 650)
{
currentZNear = 250;
currentZFar = currentZNear * 100;
}
else if (dToPlanetSurface > 160)
{
currentZNear = 125;
currentZFar = currentZNear * 150;
}
else if (dToPlanetSurface > 100)
{
currentZNear = 65;
currentZFar = currentZNear * 170;
}
else if (dToPlanetSurface > 40)
{
currentZNear = 32;
currentZFar = 10000.f;
}
else
{
currentZNear = 16;
currentZFar = 5000.f;
}
return { currentZNear, currentZFar };
}
float PlanetData::distanceToPlanetSurface(const Vector3f& viewerPosition) {
Vector3f shipLocalPosition = viewerPosition - PLANET_CENTER_OFFSET;
// Èñïîëüçóåì getTrianglesUnderCamera äëÿ ïîèñêà
// ÂÍÈÌÀÍÈÅ: Çäåñü ðåêóðñèÿ ëîãèêè. Ìåòîä getTrianglesUnderCamera èñïîëüçóåò âíóòðè viewerPosition.
// ×òîáû íå äóáëèðîâàòü êîä, ïåðåïèøåì ëîãèêó ïîèñêà çäåñü èëè áóäåì èñïîëüçîâàòü óæå íàéäåííûé ðàíåå.
// Äëÿ ïðîñòîòû îñòàâèì ëîãèêó êàê â îðèãèíàëå, íî àäàïòèðîâàííóþ.
// ÂÀÆÍÎ: getTrianglesUnderCamera - ýòî "òÿæåëàÿ" ôóíêöèÿ. Â îðèãèíàëå îíà âûçûâàëàñü âíóòðè distanceToPlanetSurface.
// Íî îíà òðåáóåò LOD. Èñïîëüçóåì MAX LOD äëÿ òî÷íîñòè.
// Ðåêóðñèâíûé ïîèñê òðåóãîëüíèêîâ ïîä êàìåðîé.
// Çäåñü íàì íóæíî ðåàëèçîâàòü àíàëîã triangleUnderCamera, íî íå çàâèñÿùèé îò ãëîáàëüíîãî ñîñòîÿíèÿ.
// Â îðèãèíàëå ôóíêöèÿ triangleUnderCamera áûëà ðåêóðñèâíîé.
// Íèæå ÿ ðåàëèçóþ èòåðàòèâíûé èëè ðåêóðñèâíûé ïîäõîä âíóòðè getTrianglesUnderCamera.
// Âðåìåííîå ðåøåíèå: ïîëíàÿ êîïèÿ ëîãèêè ïîèñêà òðåóãîëüíèêà
// Íî íàì íóæåí äîñòóï ê ìåòîäó.
std::vector<int> targetTriangles = getTrianglesUnderCamera(viewerPosition);
if (targetTriangles.empty()) {
return (shipLocalPosition.length() - PLANET_RADIUS);
}
float lowestDistance;
int tri_index = targetTriangles[0];
const auto& posData = planetMeshLods[currentLod].vertexData.PositionData;
size_t data_index = tri_index * 3;
if (data_index + 2 >= posData.size()) {
return (shipLocalPosition.length() - PLANET_RADIUS);
}
const Vector3f& A = posData[data_index];
const Vector3f& B = posData[data_index + 1];
const Vector3f& C = posData[data_index + 2];
Vector3f P_proj = projectPointOnPlane(shipLocalPosition, A, B, C);
Vector3f P_closest = P_proj;
lowestDistance = (shipLocalPosition - P_closest).length();
if (targetTriangles.size() <= 1)
{
return lowestDistance;
}
else
{
for (int i = 0; i < targetTriangles.size(); i++)
{
int tri_index = targetTriangles[i];
const auto& posData = planetMeshLods[currentLod].vertexData.PositionData;
size_t data_index = tri_index * 3;
if (data_index + 2 >= posData.size()) {
return (shipLocalPosition.length() - PLANET_RADIUS);
}
const Vector3f& A = posData[data_index];
const Vector3f& B = posData[data_index + 1];
const Vector3f& C = posData[data_index + 2];
Vector3f P_proj = projectPointOnPlane(shipLocalPosition, A, B, C);
Vector3f P_closest = P_proj;
if (lowestDistance < (shipLocalPosition - P_closest).length())
{
lowestDistance = (shipLocalPosition - P_closest).length();
}
}
return lowestDistance;
}
}
// --- Ðåàëèçàöèÿ getTrianglesUnderCamera (áûâøàÿ triangleUnderCamera) ---
// Âñïîìîãàòåëüíàÿ ðåêóðñèâíàÿ ôóíêöèÿ, ñêðûòàÿ îò ïóáëè÷íîãî API
static std::vector<int> recursiveTriangleSearch(int lod, const Vector3f& pos, const std::array<LodLevel, MAX_LOD_LEVELS>& meshes) {
std::vector<int> r;
// Ëîãèêà óðîâíÿ 0 (áàçîâûé îêòàýäð)
if (lod == 0) {
if (pos.v[1] >= 0) {
if (pos.v[0] >= 0 && pos.v[2] >= 0) r.push_back(0);
if (pos.v[0] >= 0 && pos.v[2] <= 0) r.push_back(1);
if (pos.v[0] <= 0 && pos.v[2] <= 0) r.push_back(2);
if (pos.v[0] <= 0 && pos.v[2] >= 0) r.push_back(3);
}
if (pos.v[1] <= 0) {
if (pos.v[0] >= 0 && pos.v[2] >= 0) r.push_back(4);
if (pos.v[0] <= 0 && pos.v[2] >= 0) r.push_back(5);
if (pos.v[0] <= 0 && pos.v[2] <= 0) r.push_back(6);
if (pos.v[0] >= 0 && pos.v[2] <= 0) r.push_back(7);
}
}
else {
// Ðåêóðñèâíûé øàã
std::vector<int> r0 = recursiveTriangleSearch(lod - 1, pos, meshes);
for (int tri0 : r0) {
Vector3f a = meshes[lod - 1].vertexData.PositionData[tri0 * 3];
Vector3f b = meshes[lod - 1].vertexData.PositionData[tri0 * 3 + 1];
Vector3f c = meshes[lod - 1].vertexData.PositionData[tri0 * 3 + 2];
std::vector<int> result = PlanetData::find_sub_triangle_spherical(a, b, c, pos);
for (int trix : result) {
r.push_back(tri0 * 4 + trix);
}
}
}
return r;
}
std::vector<int> PlanetData::getTrianglesUnderCamera(const Vector3f& viewerPosition) {
// Âûçûâàåì ðåêóðñèþ ñ òåêóùèì LOD
return recursiveTriangleSearch(currentLod, viewerPosition, planetMeshLods);
}
// --- Îñòàëüíûå ìåòîäû (check_spherical_side, generateSphere è ò.ä.) ---
// (Êîïèðóéòå ðåàëèçàöèþ èç ñòàðîãî PlanetObject.cpp,
// çàìåíÿÿ îáðàùåíèÿ ê Environment::shipPosition íà àðãóìåíòû, åñëè íóæíî,
// è èñïîëüçóÿ this->planetMeshLods)
float PlanetData::check_spherical_side(const Vector3f& V1, const Vector3f& V2, const Vector3f& P, const Vector3f& V3, float epsilon) {
Vector3f N_plane = V1.cross(V2);
float sign_P = P.dot(N_plane);
float sign_V3 = V3.dot(N_plane);
return sign_P * sign_V3;
}
bool PlanetData::is_inside_spherical_triangle(const Vector3f& P, const Vector3f& V1, const Vector3f& V2, const Vector3f& V3, float epsilon) {
if (check_spherical_side(V1, V2, P, V3, epsilon) < -epsilon) return false;
if (check_spherical_side(V2, V3, P, V1, epsilon) < -epsilon) return false;
if (check_spherical_side(V3, V1, P, V2, epsilon) < -epsilon) return false;
return true;
}
std::vector<int> PlanetData::find_sub_triangle_spherical(const Vector3f& a_orig, const Vector3f& b_orig, const Vector3f& c_orig, const Vector3f& px_orig) {
const float EPSILON = 1e-6f;
std::vector<int> result;
const Vector3f a = a_orig.normalized();
const Vector3f b = b_orig.normalized();
const Vector3f c = c_orig.normalized();
const Vector3f pxx_normalized = px_orig.normalized();
const Vector3f m_ab = ((a + b) * 0.5f).normalized();
const Vector3f m_bc = ((b + c) * 0.5f).normalized();
const Vector3f m_ac = ((a + c) * 0.5f).normalized();
if (is_inside_spherical_triangle(pxx_normalized, a, m_ab, m_ac, EPSILON)) result.push_back(0);
if (is_inside_spherical_triangle(pxx_normalized, m_ab, b, m_bc, EPSILON)) result.push_back(1);
if (is_inside_spherical_triangle(pxx_normalized, m_ac, m_bc, c, EPSILON)) result.push_back(2);
if (is_inside_spherical_triangle(pxx_normalized, m_ab, m_bc, m_ac, EPSILON)) result.push_back(3);
return result;
}
std::vector<Triangle> PlanetData::subdivideTriangles(const std::vector<Triangle>& input, float noiseCoeff) {
std::vector<Triangle> output;
for (const auto& t : input) {
// Âåðøèíû è èõ ID
const Vector3f& a = t.data[0];
const Vector3f& b = t.data[1];
const Vector3f& c = t.data[2];
const VertexID& id_a = t.ids[0];
const VertexID& id_b = t.ids[1];
const VertexID& id_c = t.ids[2];
// 1. Âû÷èñëÿåì ñåðåäèíû (êîîðäèíàòû)
Vector3f m_ab = ((a + b) * 0.5f).normalized();
Vector3f m_bc = ((b + c) * 0.5f).normalized();
Vector3f m_ac = ((a + c) * 0.5f).normalized();
Vector3f pm_ab = m_ab * perlin.getSurfaceHeight(m_ab, noiseCoeff);
Vector3f pm_bc = m_bc * perlin.getSurfaceHeight(m_bc, noiseCoeff);
Vector3f pm_ac = m_ac * perlin.getSurfaceHeight(m_ac, noiseCoeff);
// 2. Âû÷èñëÿåì ID íîâûõ âåðøèí
VertexID id_mab = generateEdgeID(id_a, id_b);
VertexID id_mbc = generateEdgeID(id_b, id_c);
VertexID id_mac = generateEdgeID(id_a, id_c);
// 3. Ôîðìèðóåì 4 íîâûõ òðåóãîëüíèêà
output.emplace_back(Triangle{ {a, pm_ab, pm_ac}, {id_a, id_mab, id_mac} }); // 0
output.emplace_back(Triangle{ {pm_ab, b, pm_bc}, {id_mab, id_b, id_mbc} }); // 1
output.emplace_back(Triangle{ {pm_ac, pm_bc, c}, {id_mac, id_mbc, id_c} }); // 2
output.emplace_back(Triangle{ {pm_ab, pm_bc, pm_ac}, {id_mab, id_mbc, id_mac} }); // 3
}
return output;
}
LodLevel PlanetData::trianglesToVertices(const std::vector<Triangle>& geometry) {
LodLevel result;
result.triangles = geometry;
size_t vertexCount = geometry.size() * 3;
result.vertexData.PositionData.reserve(vertexCount);
result.vertexData.NormalData.reserve(vertexCount);
result.vertexData.TexCoordData.reserve(vertexCount);
result.vertexData.TangentData.reserve(vertexCount); // Äîáàâëÿåì ðåçåðâ
result.vertexData.BinormalData.reserve(vertexCount);
result.VertexIDs.reserve(vertexCount);
const std::array<Vector2f, 3> triangleUVs = {
Vector2f(0.5f, 1.0f),
Vector2f(0.0f, 0.0f),
Vector2f(1.0f, 0.0f)
};
for (const auto& t : geometry) {
// --- Âû÷èñëÿåì ëîêàëüíûé áàçèñ òðåóãîëüíèêà (êàê â GetRotationForTriangle) ---
Vector3f vA = t.data[0];
Vector3f vB = t.data[1];
Vector3f vC = t.data[2];
Vector3f x_axis = (vC - vB).normalized(); // Íàïðàâëåíèå U
Vector3f edge1 = vB - vA;
Vector3f edge2 = vC - vA;
Vector3f z_axis = edge1.cross(edge2).normalized(); // Íîðìàëü ïëîñêîñòè
// Ïðîâåðêà íàïðàâëåíèÿ íîðìàëè íàðóæó (îò öåíòðà ïëàíåòû)
Vector3f centerToTri = (vA + vB + vC).normalized();
if (z_axis.dot(centerToTri) < 0) {
z_axis = z_axis * -1.0f;
}
Vector3f y_axis = z_axis.cross(x_axis).normalized(); // Íàïðàâëåíèå V
for (int i = 0; i < 3; ++i) {
result.vertexData.PositionData.push_back(t.data[i]);
result.vertexData.NormalData.push_back(z_axis); // Ïëîñêàÿ íîðìàëü ãðàíè äëÿ Parallax
result.vertexData.TexCoordData.push_back(triangleUVs[i]);
// Çàïèñûâàåì âû÷èñëåííûé áàçèñ â êàæäóþ âåðøèíó òðåóãîëüíèêà
result.vertexData.TangentData.push_back(x_axis);
result.vertexData.BinormalData.push_back(y_axis);
result.VertexIDs.push_back(t.ids[i]);
}
}
return result;
}
LodLevel PlanetData::generateSphere(int subdivisions, float noiseCoeff) {
// 1. Èñõîäíûé îêòàýäð è ïðèñâîåíèå ID
std::vector<Triangle> geometry = {
// Âåðõíÿÿ ïîëóñôåðà (Y > 0)
{{ 0.0f, 1.0f, 0.0f}, { 1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f, 1.0f}}, // 0
{{ 0.0f, 1.0f, 0.0f}, { 0.0f, 0.0f, -1.0f}, { 1.0f, 0.0f, 0.0f}}, // 1
{{ 0.0f, 1.0f, 0.0f}, {-1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f, -1.0f}}, // 2
{{ 0.0f, 1.0f, 0.0f}, { 0.0f, 0.0f, 1.0f}, {-1.0f, 0.0f, 0.0f}}, // 3
// Íèæíÿÿ ïîëóñôåðà (Y < 0)
{{ 0.0f, -1.0f, 0.0f}, { 0.0f, 0.0f, 1.0f}, { 1.0f, 0.0f, 0.0f}}, // 4
{{ 0.0f, -1.0f, 0.0f}, { 1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f, -1.0f}}, // 5
{{ 0.0f, -1.0f, 0.0f}, { 0.0f, 0.0f, -1.0f}, {-1.0f, 0.0f, 0.0f}}, // 6
{{ 0.0f, -1.0f, 0.0f}, {-1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f, 1.0f}} // 7
};
// Ïðèñâîåíèå ID èñõîäíûì âåðøèíàì
for (auto& t : geometry) {
for (int i = 0; i < 3; i++) {
// Èñïîëüçóåì map äëÿ ïîëó÷åíèÿ ID ïî ÷èñòûì êîîðäèíàòàì (íîðì. == ÷èñòûå)
t.ids[i] = initialVertexMap[t.data[i].normalized()];
}
}
// 3. Ðàçáèâàåì N ðàç (â subdivideTriangles ãåíåðèðóþòñÿ ID íîâûõ âåðøèí)
for (int i = 0; i < subdivisions; i++) {
geometry = subdivideTriangles(geometry, noiseCoeff);
}
// 4. Ãåíåðèðóåì PositionData, NormalData è VertexIDs
LodLevel lodLevel = trianglesToVertices(geometry);
// 5. Ñîçäàíèå V2T-Map íà îñíîâå VertexIDs (ÒÎÏÎËÎÃÈ×ÅÑÊÈÉ ÊËÞ×)
V2TMap v2tMap;
const auto& finalVertexIDs = lodLevel.VertexIDs;
size_t num_triangles = geometry.size();
for (size_t i = 0; i < num_triangles; ++i) {
for (int j = 0; j < 3; ++j) {
VertexID v_id = finalVertexIDs[i * 3 + j];
// Åñëè êëþ÷ óæå åñòü, ïðîñòî äîáàâëÿåì èíäåêñ
v2tMap[v_id].push_back((int)i);
}
}
lodLevel.v2tMap = v2tMap;
// 6. Ïðèìåíåíèå ôèíàëüíîãî øóìà (åñëè âû õîòåëè, ÷òîáû øóì áûë òîëüêî çäåñü)
// Çäåñü ìû äîëæíû áûëè áû ïðèìåíèòü øóì ê buffer.PositionData,
// íî â âàøåì èñõîäíîì êîäå øóì ïðèìåíÿëñÿ ðàíåå.
// Ïðåäïîëàãàåì, ÷òî øóì áóäåò ïðèìåíåí çäåñü (èíà÷å v2tMap íå áóäåò ñîîòâåòñòâîâàòü).
// ÂÎÑÑÒÀÍÎÂÈÌ ØÀÃ 2, íî äëÿ ôèíàëüíîé ãåîìåòðèè:
for (size_t i = 0; i < lodLevel.vertexData.PositionData.size(); i++) {
Vector3f dir = lodLevel.vertexData.PositionData[i].normalized();
lodLevel.vertexData.PositionData[i] = dir * perlin.getSurfaceHeight(dir, noiseCoeff);
}
// 7. Ãåíåðàöèÿ ColorData
lodLevel.vertexData.ColorData.reserve(geometry.size() * 3);
const Vector3f baseColor = { 0.498f, 0.416f, 0.0f };
const float colorFrequency = 5.0f;
const float colorAmplitude = 0.2f;
const Vector3f offsetR = { 0.1f, 0.2f, 0.3f };
const Vector3f offsetG = { 0.5f, 0.4f, 0.6f };
const Vector3f offsetB = { 0.9f, 0.8f, 0.7f };
for (size_t i = 0; i < geometry.size(); i++) {
for (int j = 0; j < 3; j++) {
// Èñïîëüçóåì íîðìàëèçîâàííûé âåêòîð èç PositionData (êîòîðûé ðàâåí NormalData)
Vector3f dir = lodLevel.vertexData.NormalData[i * 3 + j];
// Âû÷èñëåíèå öâåòîâîãî øóìà
float noiseR = colorPerlin.noise(
(dir.v[0] + offsetR.v[0]) * colorFrequency,
(dir.v[1] + offsetR.v[1]) * colorFrequency,
(dir.v[2] + offsetR.v[2]) * colorFrequency
);
// ... (àíàëîãè÷íî äëÿ noiseG è noiseB)
// Çäåñü ìû èñïîëüçóåì çàãëóøêè, òàê êàê íåò ïîëíîãî îïðåäåëåíèÿ PerlinNoise
float noiseG = 0.0f;
float noiseB = 0.0f;
Vector3f colorOffset = {
noiseR * colorAmplitude,
noiseG * colorAmplitude,
noiseB * colorAmplitude
};
Vector3f finalColor = baseColor + colorOffset;
lodLevel.vertexData.ColorData.push_back(finalColor);
}
}
return lodLevel;
}
// Ïðèìåð findNeighbors:
std::vector<int> PlanetData::findNeighbors(int index, int lod) {
const V2TMap& v2tMap = planetMeshLods[lod].v2tMap;
const auto& vertexIDs = planetMeshLods[lod].VertexIDs;
if ((index * 3 + 2) >= vertexIDs.size()) return {};
std::set<int> neighbors;
for (int i = 0; i < 3; ++i) {
VertexID v_id = vertexIDs[index * 3 + i];
auto it = v2tMap.find(v_id);
if (it != v2tMap.end()) {
for (int tri_index : it->second) {
if (tri_index != index) {
neighbors.insert(tri_index);
}
}
}
}
return std::vector<int>(neighbors.begin(), neighbors.end());
}
}

117
PlanetData.h Normal file
View File

@ -0,0 +1,117 @@
#pragma once
#include "ZLMath.h"
#include "Perlin.h"
#include "Renderer.h" // Äëÿ VertexDataStruct
#include <vector>
#include <array>
#include <string>
#include <map>
#include <set>
namespace ZL {
using VertexID = std::string;
using V2TMap = std::map<VertexID, std::vector<int>>;
VertexID generateEdgeID(const VertexID& id1, const VertexID& id2);
constexpr static int MAX_LOD_LEVELS = 6;
//constexpr static int MAX_LOD_LEVELS = 3;
struct Triangle
{
std::array<Vector3f, 3> data;
std::array<VertexID, 3> ids;
Triangle(Vector3f p1, Vector3f p2, Vector3f p3)
: data{ p1, p2, p3 }
{
}
Triangle(std::array<Vector3f, 3> idata, std::array<VertexID, 3> iids)
: data{ idata }
, ids{ iids }
{
}
};
struct LodLevel
{
std::vector<Triangle> triangles;
VertexDataStruct vertexData;
std::vector<VertexID> VertexIDs;
V2TMap v2tMap;
void Scale(float s)
{
vertexData.Scale(s);
for (auto& t : triangles) {
for (int i = 0; i < 3; i++) {
t.data[i] = t.data[i] * s;
}
}
}
void Move(Vector3f pos)
{
vertexData.Move(pos);
for (auto& t : triangles) {
for (int i = 0; i < 3; i++) {
t.data[i] = t.data[i] + pos;
}
}
}
};
class PlanetData {
public:
static const float PLANET_RADIUS;
static const Vector3f PLANET_CENTER_OFFSET;
private:
PerlinNoise perlin;
PerlinNoise colorPerlin;
std::array<LodLevel, MAX_LOD_LEVELS> planetMeshLods;
std::array<LodLevel, MAX_LOD_LEVELS> planetMeshLodsNoDist;
LodLevel planetAtmosphereLod;
int currentLod; // Ëîãè÷åñêèé òåêóùèé óðîâåíü äåòàëèçàöèè
std::map<Vector3f, VertexID> initialVertexMap;
// Âíóòðåííèå ìåòîäû ãåíåðàöèè
std::vector<Triangle> subdivideTriangles(const std::vector<Triangle>& inputTriangles, float noiseCoeff);
LodLevel trianglesToVertices(const std::vector<Triangle>& triangles);
LodLevel generateSphere(int subdivisions, float noiseCoeff);
// Âñïîìîãàòåëüíûå ìåòîäû ìàòåìàòèêè
static float check_spherical_side(const Vector3f& V1, const Vector3f& V2, const Vector3f& P, const Vector3f& V3, float epsilon = 1e-6f);
static bool is_inside_spherical_triangle(const Vector3f& P, const Vector3f& V1, const Vector3f& V2, const Vector3f& V3, float epsilon);
public:
PlanetData();
void init();
// Ìåòîäû äîñòóïà ê äàííûì (äëÿ ðåíäåðåðà)
const LodLevel& getLodLevel(int level) const;
const LodLevel& getLodLevelNoDist(int level) const;
const LodLevel& getAtmosphereLod() const;
int getCurrentLodIndex() const;
int getMaxLodIndex() const;
// Ëîãèêà
std::pair<float, float> calculateZRange(float distanceToSurface);
float distanceToPlanetSurface(const Vector3f& viewerPosition);
// Âîçâðàùàåò èíäåêñû òðåóãîëüíèêîâ, âèäèìûõ êàìåðîé
std::vector<int> getTrianglesUnderCamera(const Vector3f& viewerPosition);
std::vector<int> findNeighbors(int index, int lod);
static std::vector<int> find_sub_triangle_spherical(const Vector3f& a_orig, const Vector3f& b_orig, const Vector3f& c_orig, const Vector3f& px_orig);
};
} // namespace ZL

570
PlanetObject.cpp Normal file
View File

@ -0,0 +1,570 @@
#include "PlanetObject.h"
#include <random>
#include <cmath>
#include "OpenGlExtensions.h"
#include "Environment.h"
#include "StoneObject.h"
namespace ZL {
Matrix3f GetRotationForTriangle(const Triangle& tri) {
// Для треугольника №0:
// tri.data[0] = {0, 20000, 0} (A)
// tri.data[1] = {0, 0, 20000} (B)
// tri.data[2] = {20000, 0, 0} (C)
Vector3f vA = tri.data[0];
Vector3f vB = tri.data[1];
Vector3f vC = tri.data[2];
// 1. Вычисляем ось X (горизонталь).
// Нам нужна грань BC: от (0, 0, 20000) до (20000, 0, 0)
Vector3f x_axis = (vC - vB).normalized();
// 2. Вычисляем нормаль (ось Z).
// Порядок cross product (AB x AC) определит "лицевую" сторону.
Vector3f edge1 = vB - vA;
Vector3f edge2 = vC - vA;
Vector3f z_axis = edge1.cross(edge2).normalized();
// 3. Вычисляем ось Y (вертикаль).
// В ортонормированном базисе Y всегда перпендикулярна Z и X.
Vector3f y_axis = z_axis.cross(x_axis).normalized();
// 4. Формируем прямую матрицу поворота (Rotation/World Matrix).
Matrix3f rot;
// Столбец 0: Ось X
rot.m[0] = x_axis.v[0];
rot.m[3] = x_axis.v[1];
rot.m[6] = x_axis.v[2];
// Столбец 1: Ось Y
rot.m[1] = y_axis.v[0];
rot.m[4] = y_axis.v[1];
rot.m[7] = y_axis.v[2];
// Столбец 2: Ось Z
rot.m[2] = z_axis.v[0];
rot.m[5] = z_axis.v[1];
rot.m[8] = z_axis.v[2];
return rot;
}
PlanetObject::PlanetObject()
{
}
void PlanetObject::init() {
// 1. Инициализируем данные (генерация мешей)
planetData.init();
// 2. Забираем данные для VBO
// Берем максимальный LOD для начальной отрисовки
int lodIndex = planetData.getMaxLodIndex();
planetRenderStruct.data = planetData.getLodLevel(lodIndex).vertexData;
planetRenderStruct.RefreshVBO();
planetRenderStructCut.data = planetData.getLodLevelNoDist(lodIndex).vertexData;
planetRenderStructCut.data.PositionData.resize(3);
planetRenderStructCut.RefreshVBO();
//sandTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/sand2.png", ""));
sandTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/sand2.png", ""));
stoneTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/rock.png", ""));
// Атмосфера
planetAtmosphereRenderStruct.data = planetData.getAtmosphereLod().vertexData;
planetAtmosphereRenderStruct.RefreshVBO();
planetStones = CreateStoneGroupData(778, planetData.getLodLevel(lodIndex));
planetStones.inflate({ 0/*,1,2,3,4,5,6,7*/ });
planetStonesToBakeRenderStruct.AssignFrom(planetStones.mesh);
planetStonesToBakeRenderStruct.RefreshVBO();
}
void PlanetObject::prepareDrawData() {
if (!drawDataDirty) return;
drawDataDirty = false;
}
void PlanetObject::update(float deltaTimeMs) {
// 1. Получаем базовые треугольники под камерой
auto lr = planetData.getTrianglesUnderCamera(Environment::shipPosition);
int currentLod = planetData.getCurrentLodIndex();
// Временный вектор для сбора новых индексов
std::vector<int> newIndices;
std::set<int> used;
// Рекурсивно (или итеративно, как у тебя) собираем индексы видимых зон
for (int i : lr) {
if (used.insert(i).second) newIndices.push_back(i);
auto neighbors = planetData.findNeighbors(i, currentLod);
for (int n : neighbors) {
if (used.insert(n).second) newIndices.push_back(n);
auto neighbors2 = planetData.findNeighbors(n, currentLod);
for (int n2 : neighbors2) {
if (used.insert(n2).second) newIndices.push_back(n2);
auto neighbors3 = planetData.findNeighbors(n, currentLod);
for (int n3 : neighbors3) {
if (used.insert(n3).second) newIndices.push_back(n3);
}
}
}
}
// 2. Сортируем новый список, чтобы порядок не влиял на сравнение
std::sort(newIndices.begin(), newIndices.end());
// 3. Сравниваем с тем, что было нарисовано в прошлый раз
if (newIndices != triangleIndicesToDraw) {
// Обновляем список индексов (используем move для эффективности)
triangleIndicesToDraw = std::move(newIndices);
// --- ОБНОВЛЯЕМ ЖЕЛТУЮ ЗОНУ (только когда изменился состав треугольников) ---
const auto& fullMesh = planetData.getLodLevel(currentLod).vertexData;
planetRenderYellowStruct.data.PositionData.clear();
planetRenderYellowStruct.data.TexCoordData.clear();
for (int i : triangleIndicesToDraw) {
// Копируем геометрию для подсветки
for (int j = 0; j < 3; ++j) {
planetRenderYellowStruct.data.PositionData.push_back(fullMesh.PositionData[i * 3 + j]);
planetRenderYellowStruct.data.TexCoordData.push_back(fullMesh.TexCoordData[i * 3 + j]);
}
}
if (planetRenderYellowStruct.data.PositionData.size() > 0)
{
planetRenderYellowStruct.RefreshVBO();
}
// --- ОБНОВЛЯЕМ КАМНИ (через новую структуру StoneGroup) ---
if (triangleIndicesToDraw.size() > 0)
{
planetStones.inflate(triangleIndicesToDraw);
// Используем AssignFrom, он внутри сам вызывает RefreshVBO
planetStonesRenderStruct.AssignFrom(planetStones.mesh);
}
}
}
void PlanetObject::bakeStoneTexture(Renderer& renderer) {
glViewport(0, 0, 512, 512);
glClearColor(224 / 255.f, 201 / 255.f, 167 / 255.f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
static const std::string defaultShaderName2 = "planetBake";
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
renderer.shaderManager.PushShader(defaultShaderName2);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
float dist = planetData.distanceToPlanetSurface(Environment::shipPosition);
auto zRange = planetData.calculateZRange(dist);
const float currentZNear = zRange.first;
const float currentZFar = zRange.second;
Triangle tr = planetData.getLodLevelNoDist(planetData.getCurrentLodIndex()).triangles[0];
// 1. Получаем матрицу вращения (оси в столбцах)
Matrix3f mr = GetRotationForTriangle(tr);
// 2. Трансформируем вершины в локальное пространство экрана, чтобы найти габариты
// Используем MultMatrixVector(Matrix, Vector).
// Если ваша функция считает V * M, то передайте Inverse(mr).
Vector3f rA = MultMatrixVector(mr, tr.data[0]);
Vector3f rB = MultMatrixVector(mr, tr.data[1]);
Vector3f rC = MultMatrixVector(mr, tr.data[2]);
// 3. Вычисляем реальные границы треугольника после поворота
float minX = min(rA.v[0], min(rB.v[0], rC.v[0]));
float maxX = max(rA.v[0], max(rB.v[0], rC.v[0]));
float minY = min(rA.v[1], min(rB.v[1], rC.v[1]));
float maxY = max(rA.v[1], max(rB.v[1], rC.v[1]));
// Находим центр и размеры
float width = maxX - minX;
float height = maxY - minY;
float centerX = (minX + maxX) * 0.5f;
float centerY = (minY + maxY) * 0.5f;
width = width * 0.995;
height = height * 0.995;
renderer.PushProjectionMatrix(
centerX - width*0.5, centerX + width * 0.5,
centerY - height * 0.5, centerY + height * 0.5,
150, 200000);
renderer.PushMatrix();
renderer.LoadIdentity();
// Сдвигаем камеру по Z
renderer.TranslateMatrix(Vector3f{ 0, 0, -45000 });
// Применяем вращение
renderer.RotateMatrix(mr);
// Извлекаем нормаль треугольника (это 3-й столбец нашей матрицы вращения)
Vector3f planeNormal = { mr.m[2], mr.m[5], mr.m[8] };
renderer.RenderUniform3fv("uPlanePoint", &tr.data[0].v[0]);
renderer.RenderUniform3fv("uPlaneNormal", &planeNormal.v[0]);
renderer.RenderUniform1f("uMaxHeight", StoneParams::BASE_SCALE * 1.1f); // Соответствует BASE_SCALE + perturbation
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, sandTexture->getTexID());
renderer.PushMatrix();
renderer.DrawVertexRenderStruct(planetRenderStructCut);
renderer.PopMatrix();
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK); // Отсекаем задние грани
if (planetStonesToBakeRenderStruct.data.PositionData.size() > 0)
{
glBindTexture(GL_TEXTURE_2D, stoneTexture->getTexID());
renderer.DrawVertexRenderStruct(planetStonesToBakeRenderStruct);
CheckGlError();
}
glDisable(GL_CULL_FACE); // Не забываем выключить, чтобы не сломать остальной рендер
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.DisableVertexAttribArray(vPositionName);
renderer.shaderManager.PopShader();
CheckGlError();
}
void PlanetObject::draw(Renderer& renderer) {
prepareDrawData();
{
if (stoneMapFB == nullptr)
{
stoneMapFB = std::make_unique<FrameBuffer>(512, 512);
}
stoneMapFB->Bind();
bakeStoneTexture(renderer);
stoneMapFB->Unbind();
}
//bakeStoneTexture(renderer);
glViewport(0, 0, Environment::width, Environment::height);
//--------------------------
drawPlanet(renderer);
//drawYellowZone(renderer);
drawStones(renderer);
drawAtmosphere(renderer);
}
void PlanetObject::drawPlanet(Renderer& renderer)
{
static const std::string defaultShaderName = "planetLand";
static const std::string vPositionName = "vPosition";
static const std::string vColorName = "vColor";
static const std::string vNormalName = "vNormal";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
renderer.shaderManager.PushShader(defaultShaderName);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vColorName);
renderer.EnableVertexAttribArray(vNormalName);
renderer.EnableVertexAttribArray("vTangent");
renderer.EnableVertexAttribArray("vBinormal");
renderer.EnableVertexAttribArray(vTexCoordName);
float dist = planetData.distanceToPlanetSurface(Environment::shipPosition);
auto zRange = planetData.calculateZRange(dist);
const float currentZNear = zRange.first;
const float currentZFar = zRange.second;
// 2. Применяем динамическую матрицу проекции
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
currentZNear, currentZFar);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipPosition);
const Matrix4f viewMatrix = renderer.GetCurrentModelViewMatrix();
renderer.RenderUniform1i("Texture", 0);
renderer.RenderUniform1i("BakedTexture", 1);
Triangle tr = planetData.getLodLevel(planetData.getCurrentLodIndex()).triangles[0]; // Берем базовый треугольник
Matrix3f mr = GetRotationForTriangle(tr); // Та же матрица, что и при запекании
// Позиция камеры (корабля) в мире
renderer.RenderUniform3fv("uViewPos", &Environment::shipPosition.v[0]);
//renderer.RenderUniform1f("uHeightScale", 0.03f);
renderer.RenderUniform1f("uHeightScale", 0.0f);
renderer.RenderUniform1f("uDistanceToPlanetSurface", dist);
renderer.RenderUniform1f("uCurrentZFar", currentZFar);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, stoneMapFB->getTextureID());
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, sandTexture->getTexID());
renderer.DrawVertexRenderStruct(planetRenderStruct);
CheckGlError();
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.DisableVertexAttribArray(vNormalName);
renderer.DisableVertexAttribArray("vTangent");
renderer.DisableVertexAttribArray("vBinormal");
renderer.DisableVertexAttribArray(vColorName);
renderer.DisableVertexAttribArray(vPositionName);
renderer.shaderManager.PopShader();
CheckGlError();
}
void PlanetObject::drawStones(Renderer& renderer)
{
static const std::string defaultShaderName2 = "planetStone";
static const std::string vPositionName = "vPosition";
static const std::string vColorName = "vColor";
static const std::string vNormalName = "vNormal";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
renderer.shaderManager.PushShader(defaultShaderName2);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vColorName);
renderer.EnableVertexAttribArray(vNormalName);
renderer.EnableVertexAttribArray(vTexCoordName);
float dist = planetData.distanceToPlanetSurface(Environment::shipPosition);
auto zRange = planetData.calculateZRange(dist);
const float currentZNear = zRange.first;
const float currentZFar = zRange.second;
// 2. Применяем динамическую матрицу проекции
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
currentZNear, currentZFar);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipPosition);
renderer.RenderUniform1f("uDistanceToPlanetSurface", dist);
renderer.RenderUniform1f("uCurrentZFar", currentZFar);
renderer.RenderUniform3fv("uViewPos", &Environment::shipPosition.v[0]);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
if (planetStonesRenderStruct.data.PositionData.size() > 0)
{
glBindTexture(GL_TEXTURE_2D, stoneTexture->getTexID());
renderer.DrawVertexRenderStruct(planetStonesRenderStruct);
CheckGlError();
}
glDisable(GL_BLEND);
glDisable(GL_CULL_FACE);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.DisableVertexAttribArray(vNormalName);
renderer.DisableVertexAttribArray(vColorName);
renderer.DisableVertexAttribArray(vPositionName);
renderer.shaderManager.PopShader();
CheckGlError();
glClear(GL_DEPTH_BUFFER_BIT);
}
void PlanetObject::drawYellowZone(Renderer& renderer)
{
static const std::string defaultShaderName = "planetLand";
static const std::string vPositionName = "vPosition";
static const std::string vColorName = "vColor";
static const std::string vNormalName = "vNormal";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
float dist = planetData.distanceToPlanetSurface(Environment::shipPosition);
auto zRange = planetData.calculateZRange(dist);
const float currentZNear = zRange.first;
const float currentZFar = zRange.second;
glClear(GL_DEPTH_BUFFER_BIT);
if (planetRenderYellowStruct.data.PositionData.size() > 0)
{
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vColorName);
renderer.EnableVertexAttribArray(vNormalName);
renderer.EnableVertexAttribArray("vTangent");
renderer.EnableVertexAttribArray("vBinormal");
renderer.EnableVertexAttribArray(vTexCoordName);
// 2. Применяем динамическую матрицу проекции
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
currentZNear, currentZFar);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipPosition);
renderer.RenderUniform1f("uDistanceToPlanetSurface", dist);
renderer.RenderUniform1f("uCurrentZFar", currentZFar);
renderer.RenderUniform3fv("uViewPos", &Environment::shipPosition.v[0]);
Vector3f color2 = { 1.0, 1.0, 0.0 };
glBindTexture(GL_TEXTURE_2D, sandTexture->getTexID());
renderer.RenderUniform3fv("uColor", &color2.v[0]);
renderer.DrawVertexRenderStruct(planetRenderYellowStruct);
//glDisable(GL_BLEND);
CheckGlError();
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.DisableVertexAttribArray(vNormalName);
renderer.DisableVertexAttribArray("vTangent");
renderer.DisableVertexAttribArray("vBinormal");
renderer.DisableVertexAttribArray(vColorName);
renderer.DisableVertexAttribArray(vPositionName);
renderer.shaderManager.PopShader();
CheckGlError();
}
}
void PlanetObject::drawAtmosphere(Renderer& renderer) {
static const std::string defaultShaderName = "defaultAtmosphere";
static const std::string vPositionName = "vPosition";
static const std::string vNormalName = "vNormal";
//glClear(GL_DEPTH_BUFFER_BIT);
glDepthMask(GL_FALSE);
renderer.shaderManager.PushShader(defaultShaderName);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vNormalName);
float dist = planetData.distanceToPlanetSurface(Environment::shipPosition);
auto zRange = planetData.calculateZRange(dist);
const float currentZNear = zRange.first;
const float currentZFar = zRange.second;
// 2. Применяем динамическую матрицу проекции
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
currentZNear, currentZFar);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipPosition);
const Matrix4f viewMatrix = renderer.GetCurrentModelViewMatrix();
Vector3f color = { 0.0, 0.5, 1.0 };
renderer.RenderUniform3fv("uColor", &color.v[0]);
renderer.RenderUniformMatrix4fv("ModelViewMatrix", false, &viewMatrix.m[0]);
renderer.RenderUniform1f("uDistanceToPlanetSurface", dist);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);// Аддитивное смешивание для эффекта свечения
renderer.DrawVertexRenderStruct(planetAtmosphereRenderStruct);
glDisable(GL_BLEND);
glDepthMask(GL_TRUE);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vNormalName);
renderer.DisableVertexAttribArray(vPositionName);
renderer.shaderManager.PopShader();
CheckGlError();
}
float PlanetObject::distanceToPlanetSurface(const Vector3f& viewerPosition)
{
return planetData.distanceToPlanetSurface(viewerPosition);
}
} // namespace ZL

63
PlanetObject.h Normal file
View File

@ -0,0 +1,63 @@
#pragma once
#include "ZLMath.h"
#include "Renderer.h"
#include "TextureManager.h"
#include <vector>
#include <chrono>
#include <iostream>
#include <string>
#include <array>
#include <numeric>
#include <random>
#include <algorithm>
#include <map>
#include <set>
#include "Perlin.h"
#include "PlanetData.h"
#include "StoneObject.h"
#include "FrameBuffer.h"
namespace ZL {
class PlanetObject {
public:
// Агрегация: логика и данные теперь здесь
PlanetData planetData;
// Данные только для рендеринга (OpenGL specific)
VertexRenderStruct planetRenderStruct;
VertexRenderStruct planetRenderStructCut;
VertexRenderStruct planetRenderYellowStruct;
VertexRenderStruct planetAtmosphereRenderStruct;
VertexRenderStruct planetStonesRenderStruct;
VertexRenderStruct planetStonesToBakeRenderStruct;
StoneGroup planetStones;
std::vector<int> triangleIndicesToDraw;
std::shared_ptr<Texture> sandTexture;
std::shared_ptr<Texture> stoneTexture;
bool drawDataDirty = true;
void prepareDrawData();
std::unique_ptr<FrameBuffer> stoneMapFB;
public:
PlanetObject();
void init();
void update(float deltaTimeMs);
void bakeStoneTexture(Renderer& renderer);
void draw(Renderer& renderer);
void drawStones(Renderer& renderer);
void drawPlanet(Renderer& renderer);
void drawYellowZone(Renderer& renderer);
void drawAtmosphere(Renderer& renderer);
float distanceToPlanetSurface(const Vector3f& viewerPosition);
};
} // namespace ZL

View File

@ -126,18 +126,6 @@ $(pkg-config --cflags --libs vorbis vorbisfile ogg) \
-lopenal -lopenal
``` ```
Linux new:
sudo apt-get update
sudo apt-get install build-essential cmake pkg-config \
libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev \
libgl1-mesa-dev libpng-dev libz-dev libzip-dev \
libboost-dev libeigen3-dev liblua5.4-dev
sudo apt-get install libglu1-mesa-dev
# Emscripten new # Emscripten new
``` ```
@ -166,12 +154,6 @@ emcc main.cpp Game.cpp Environment.cpp BoneAnimatedModel.cpp ZLMath.cpp Renderer
emrun --no_browser --port 8080 . emrun --no_browser --port 8080 .
``` ```
# Emscripten new
```
emcc src/main.cpp src/Game.cpp src/Environment.cpp src/BoneAnimatedModel.cpp src/TextModel.cpp src/Projectile.cpp src/SparkEmitter.cpp src/UiManager.cpp src/render/Renderer.cpp src/render/ShaderManager.cpp src/render/TextureManager.cpp src/render/FrameBuffer.cpp src/render/OpenGlExtensions.cpp src/utils/Utils.cpp src/utils/TaskManager.cpp src/utils/Perlin.cpp src/planet/PlanetData.cpp src/planet/PlanetObject.cpp src/planet/StoneObject.cpp -O2 -std=c++17 -pthread -sUSE_PTHREADS=1 -sPTHREAD_POOL_SIZE=4 -sTOTAL_MEMORY=4294967296 -sINITIAL_MEMORY=3221225472 -sMAXIMUM_MEMORY=4294967296 -sALLOW_MEMORY_GROWTH=1 -fexceptions -I./thirdparty1/eigen-5.0.0 -I./src -I./thirdparty/libzip-1.11.3/build-emcmake/install/include -IC:/Boost/include/boost-1_84 -L./thirdparty/libzip-1.11.3/build-emcmake/install/lib -lzip -lz -sUSE_SDL_IMAGE=2 -sUSE_SDL=2 -sUSE_LIBPNG=1 --preload-file space-game001.zip -o space-game001.html
```
# License # License
Code: MIT Code: MIT
@ -184,45 +166,4 @@ make -j$(nproc) -C build #Компилируем
Для постройки без звука Для постройки без звука
rm -rf build #Очищаем build папку rm -rf build #Очищаем build папку
cmake -B build -DAUDIO=1 #Пересоздаём конфигурацию CMake cmake -B build -DAUDIO=1 #Пересоздаём конфигурацию CMake
# Cmake Build NSIS and Portable for Windows:
```
cmake --build . --config Release
cpack -C Release
```
Если есть такая ошибка:
CPack Error: Cannot find NSIS compiler makensis: likely it is not installed, or not in your PATH
CPack Error: Could not read NSIS registry value. This is usually caused by NSIS not being installed. Please install NSIS from http://nsis.sourceforge.net
CPack Error: Cannot initialize the generator NSIS
То нужно установить nsis отсюда: https://nsis.sourceforge.io/Download
# Steam windows
```
cmake -DSTEAMSDK=ON ..
cmake --build . --config Release
```
# Steam Linux
```
docker run -it --rm -v "${PWD}:/work2" -w /work2 registry.gitlab.steamos.cloud/steamrt/sniper/sdk:latest bash
apt-get update
apt-get install libboost-dev libeigen3-dev liblua5.4-dev libzip-dev libglu1-mesa-dev
cmake -DSTEAMSDK=ON -DCMAKE_BUILD_TYPE=Release ..
cmake --build . -j 4
```

833
Renderer.cpp Executable file
View File

@ -0,0 +1,833 @@
#include "Renderer.h"
#include <cmath>
namespace ZL {
VBOHolder::VBOHolder()
{
glGenBuffers(1, &Buffer);
}
VBOHolder::~VBOHolder()
{
glDeleteBuffers(1, &Buffer);
}
GLuint VBOHolder::getBuffer()
{
return Buffer;
}
VAOHolder::VAOHolder()
{
#ifndef EMSCRIPTEN
glGenVertexArrays(1, &vao);
#endif
}
VAOHolder::~VAOHolder()
{
#ifndef EMSCRIPTEN
#ifdef __linux__
glDeleteVertexArrays(1, &vao);
#else
//Windows
glDeleteVertexArray(1, &vao);
#endif
#endif
}
GLuint VAOHolder::getBuffer()
{
return vao;
}
VertexDataStruct CreateRect2D(Vector2f center, Vector2f halfWidthHeight, float zLevel)
{
Vector2f posFrom = center - halfWidthHeight;
Vector2f posTo = center + halfWidthHeight;
Vector3f pos1 = { posFrom.v[0], posFrom.v[1], zLevel };
Vector3f pos2 = { posFrom.v[0], posTo.v[1], zLevel };
Vector3f pos3 = { posTo.v[0], posTo.v[1], zLevel };
Vector3f pos4 = { posTo.v[0], posFrom.v[1], zLevel };
Vector2f texCoordPos1 = { 0.0f, 0.0f };
Vector2f texCoordPos2 = { 0.0f, 1.0f };
Vector2f texCoordPos3 = { 1.0f, 1.0f };
Vector2f texCoordPos4 = { 1.0f, 0.0f };
VertexDataStruct result;
result.PositionData.push_back(pos1);
result.PositionData.push_back(pos2);
result.PositionData.push_back(pos3);
result.PositionData.push_back(pos3);
result.PositionData.push_back(pos4);
result.PositionData.push_back(pos1);
result.TexCoordData.push_back(texCoordPos1);
result.TexCoordData.push_back(texCoordPos2);
result.TexCoordData.push_back(texCoordPos3);
result.TexCoordData.push_back(texCoordPos3);
result.TexCoordData.push_back(texCoordPos4);
result.TexCoordData.push_back(texCoordPos1);
return result;
}
VertexDataStruct CreateRectHorizontalSections2D(Vector2f center, Vector2f halfWidthHeight, float zLevel, size_t sectionCount)
{
Vector2f posFrom = center - halfWidthHeight;
Vector2f posTo = center + halfWidthHeight;
float sectionWidth = halfWidthHeight.v[0] * 2.f;
VertexDataStruct result;
for (size_t i = 0; i < sectionCount; i++)
{
Vector3f pos1 = { posFrom.v[0]+sectionWidth*i, posFrom.v[1], zLevel };
Vector3f pos2 = { posFrom.v[0] + sectionWidth * i, posTo.v[1], zLevel };
Vector3f pos3 = { posTo.v[0] + sectionWidth * i, posTo.v[1], zLevel };
Vector3f pos4 = { posTo.v[0] + sectionWidth * i, posFrom.v[1], zLevel };
result.PositionData.push_back(pos1);
result.PositionData.push_back(pos2);
result.PositionData.push_back(pos3);
result.PositionData.push_back(pos3);
result.PositionData.push_back(pos4);
result.PositionData.push_back(pos1);
Vector2f texCoordPos1 = { 0.0f, 0.0f };
Vector2f texCoordPos2 = { 0.0f, 1.0f };
Vector2f texCoordPos3 = { 1.0f, 1.0f };
Vector2f texCoordPos4 = { 1.0f, 0.0f };
result.TexCoordData.push_back(texCoordPos1);
result.TexCoordData.push_back(texCoordPos2);
result.TexCoordData.push_back(texCoordPos3);
result.TexCoordData.push_back(texCoordPos3);
result.TexCoordData.push_back(texCoordPos4);
result.TexCoordData.push_back(texCoordPos1);
}
return result;
}
VertexDataStruct CreateCube3D(float scale)
{
std::array<std::array<Vector3f, 4>, 6> cubeSides;
std::array<Vector3f, 6> cubeColors;
cubeSides[0][0] = { -1, -1, -1 };
cubeSides[0][1] = { -1, 1, -1 };
cubeSides[0][2] = { 1, 1, -1 };
cubeSides[0][3] = { 1, -1, -1 };
cubeSides[1][0] = { -1, -1, 1 };
cubeSides[1][1] = { -1, 1, 1 };
cubeSides[1][2] = { 1, 1, 1 };
cubeSides[1][3] = { 1, -1, 1 };
//------------
cubeSides[2][0] = { -1, -1, -1 };
cubeSides[2][1] = { -1, -1, 1 };
cubeSides[2][2] = { 1, -1, 1 };
cubeSides[2][3] = { 1, -1, -1 };
cubeSides[3][0] = { -1, 1, -1 };
cubeSides[3][1] = { -1, 1, 1 };
cubeSides[3][2] = { 1, 1, 1 };
cubeSides[3][3] = { 1, 1, -1 };
//------------
cubeSides[4][0] = { -1, -1, -1 };
cubeSides[4][1] = { -1, -1, 1 };
cubeSides[4][2] = { -1, 1, 1 };
cubeSides[4][3] = { -1, 1, -1 };
cubeSides[5][0] = { 1, -1, -1 };
cubeSides[5][1] = { 1, -1, 1 };
cubeSides[5][2] = { 1, 1, 1 };
cubeSides[5][3] = { 1, 1, -1 };
//-----------
cubeColors[0] = Vector3f{ 1, 0, 0 };
cubeColors[1] = Vector3f{ 0, 1, 0 };
cubeColors[2] = Vector3f{ 0, 0, 1 };
cubeColors[3] = Vector3f{ 1, 1, 0 };
cubeColors[4] = Vector3f{ 0, 1, 1 };
cubeColors[5] = Vector3f{ 1, 0, 1 };
//-----------
VertexDataStruct result;
for (int i = 0; i < 6; i++)
{
result.PositionData.push_back(cubeSides[i][0] * scale);
result.PositionData.push_back(cubeSides[i][1] * scale);
result.PositionData.push_back(cubeSides[i][2] * scale);
result.PositionData.push_back(cubeSides[i][2] * scale);
result.PositionData.push_back(cubeSides[i][3] * scale);
result.PositionData.push_back(cubeSides[i][0] * scale);
result.ColorData.push_back(cubeColors[i]);
result.ColorData.push_back(cubeColors[i]);
result.ColorData.push_back(cubeColors[i]);
result.ColorData.push_back(cubeColors[i]);
result.ColorData.push_back(cubeColors[i]);
result.ColorData.push_back(cubeColors[i]);
}
return result;
}
VertexDataStruct CreateCubemap(float scale)
{
VertexDataStruct cubemapVertexDataStruct;
// +x
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, scale });
// -x
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, scale });
// +y
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, scale });
// -y
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, scale });
// +z
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, scale });
// -z
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, -scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, -scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ scale, scale, -scale });
cubemapVertexDataStruct.PositionData.push_back({ -scale, scale, -scale });
return cubemapVertexDataStruct;
}
void VertexRenderStruct::RefreshVBO()
{
//Check if main thread, check if data is not empty...
#ifndef EMSCRIPTEN
if (!vao)
{
vao = std::make_shared<VAOHolder>();
}
glBindVertexArray(vao->getBuffer());
#endif
if (!positionVBO)
{
positionVBO = std::make_shared<VBOHolder>();
}
glBindBuffer(GL_ARRAY_BUFFER, positionVBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, data.PositionData.size() * 12, &data.PositionData[0], GL_STATIC_DRAW);
if (data.TexCoordData.size() > 0)
{
if (!texCoordVBO)
{
texCoordVBO = std::make_shared<VBOHolder>();
}
glBindBuffer(GL_ARRAY_BUFFER, texCoordVBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, data.TexCoordData.size() * 8, &data.TexCoordData[0], GL_STATIC_DRAW);
}
if (data.NormalData.size() > 0)
{
if (!normalVBO)
{
normalVBO = std::make_shared<VBOHolder>();
}
glBindBuffer(GL_ARRAY_BUFFER, normalVBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, data.NormalData.size() * 12, &data.NormalData[0], GL_STATIC_DRAW);
}
if (data.TangentData.size() > 0)
{
if (!tangentVBO)
{
tangentVBO = std::make_shared<VBOHolder>();
}
glBindBuffer(GL_ARRAY_BUFFER, tangentVBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, data.TangentData.size() * 12, &data.TangentData[0], GL_STATIC_DRAW);
}
if (data.BinormalData.size() > 0)
{
if (!binormalVBO)
{
binormalVBO = std::make_shared<VBOHolder>();
}
glBindBuffer(GL_ARRAY_BUFFER, binormalVBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, data.BinormalData.size() * 12, &data.BinormalData[0], GL_STATIC_DRAW);
}
if (data.ColorData.size() > 0)
{
if (!colorVBO)
{
colorVBO = std::make_shared<VBOHolder>();
}
glBindBuffer(GL_ARRAY_BUFFER, colorVBO->getBuffer());
glBufferData(GL_ARRAY_BUFFER, data.ColorData.size() * 12, &data.ColorData[0], GL_STATIC_DRAW);
}
}
void VertexDataStruct::Scale(float scale)
{
for (int i = 0; i < PositionData.size(); i++)
{
PositionData[i] = PositionData[i] * scale;
}
}
void VertexDataStruct::Move(Vector3f diff)
{
for (int i = 0; i < PositionData.size(); i++)
{
PositionData[i] = PositionData[i] + diff;
}
}
void VertexDataStruct::SwapZandY()
{
for (int i = 0; i < PositionData.size(); i++)
{
auto value = PositionData[i].v[1];
PositionData[i].v[1] = PositionData[i].v[2];
PositionData[i].v[2] = value;
}
}
void VertexDataStruct::RotateByMatrix(Matrix3f m)
{
for (int i = 0; i < PositionData.size(); i++)
{
PositionData[i] = MultVectorMatrix(PositionData[i], m);
}
for (int i = 0; i < NormalData.size(); i++)
{
NormalData[i] = MultVectorMatrix(NormalData[i], m);
}
for (int i = 0; i < TangentData.size(); i++)
{
TangentData[i] = MultVectorMatrix(TangentData[i], m);
}
for (int i = 0; i < BinormalData.size(); i++)
{
BinormalData[i] = MultVectorMatrix(BinormalData[i], m);
}
}
void VertexRenderStruct::AssignFrom(const VertexDataStruct& v)
{
data = v;
RefreshVBO();
}
void Renderer::InitOpenGL()
{
ModelviewMatrixStack.push(Matrix4f::Identity());
ProjectionMatrixStack.push(Matrix4f::Identity());
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glActiveTexture(GL_TEXTURE0);
#ifndef EMSCRIPTEN
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
#endif
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDepthFunc(GL_LEQUAL);
CheckGlError();
}
void Renderer::PushProjectionMatrix(float width, float height, float zNear, float zFar)
{
Matrix4f m = MakeOrthoMatrix(width, height, zNear, zFar);
ProjectionMatrixStack.push(m);
SetMatrix();
if (ProjectionMatrixStack.size() > CONST_MATRIX_STACK_SIZE)
{
throw std::runtime_error("Projection matrix stack overflow!!!!");
}
}
void Renderer::PushProjectionMatrix(float xmin, float xmax, float ymin, float ymax, float zNear, float zFar)
{
Matrix4f m = MakeOrthoMatrix(xmin, xmax, ymin, ymax, zNear, zFar);
ProjectionMatrixStack.push(m);
SetMatrix();
if (ProjectionMatrixStack.size() > CONST_MATRIX_STACK_SIZE)
{
throw std::runtime_error("Projection matrix stack overflow!!!!");
}
}
void Renderer::PushPerspectiveProjectionMatrix(float fovY, float aspectRatio, float zNear, float zFar)
{
Matrix4f m = MakePerspectiveMatrix(fovY, aspectRatio, zNear, zFar);
ProjectionMatrixStack.push(m);
SetMatrix();
if (ProjectionMatrixStack.size() > CONST_MATRIX_STACK_SIZE)
{
throw std::runtime_error("Projection matrix stack overflow!!!!");
}
}
void Renderer::PopProjectionMatrix()
{
if (ProjectionMatrixStack.size() == 0)
{
throw std::runtime_error("Projection matrix stack underflow!!!!");
}
ProjectionMatrixStack.pop();
SetMatrix();
}
Matrix4f Renderer::GetProjectionModelViewMatrix()
{
return ProjectionModelViewMatrix;
}
Matrix4f Renderer::GetCurrentModelViewMatrix()
{
return ModelviewMatrixStack.top();
}
void Renderer::SetMatrix()
{
if (ProjectionMatrixStack.size() <= 0)
{
throw std::runtime_error("Projection matrix stack out!");
}
if (ModelviewMatrixStack.size() <= 0)
{
throw std::runtime_error("Modelview matrix stack out!");
}
Matrix4f& modelViewMatrix = ModelviewMatrixStack.top();
ProjectionModelViewMatrix = ProjectionMatrixStack.top() * modelViewMatrix;
static const std::string ProjectionModelViewMatrixName = "ProjectionModelViewMatrix";
RenderUniformMatrix4fv(ProjectionModelViewMatrixName, false, &ProjectionModelViewMatrix.m[0]);
static const std::string ModelViewMatrixName = "ModelViewMatrix";
RenderUniformMatrix4fv(ModelViewMatrixName, false, &modelViewMatrix.m[0]);
}
void Renderer::PushMatrix()
{
if (ModelviewMatrixStack.size() == 0)
{
throw std::runtime_error("Modelview matrix stack underflow!!!!");
}
ModelviewMatrixStack.push(ModelviewMatrixStack.top());
if (ModelviewMatrixStack.size() > CONST_MATRIX_STACK_SIZE)
{
throw std::runtime_error("Modelview matrix stack overflow!!!!");
}
}
void Renderer::LoadIdentity()
{
if (ModelviewMatrixStack.size() == 0)
{
throw std::runtime_error("Modelview matrix stack underflow!!!!");
}
ModelviewMatrixStack.pop();
ModelviewMatrixStack.push(Matrix4f::Identity());
SetMatrix();
}
void Renderer::TranslateMatrix(const Vector3f& p)
{
Matrix4f m = Matrix4f::Identity();
m.m[12] = p.v[0];
m.m[13] = p.v[1];
m.m[14] = p.v[2];
m = ModelviewMatrixStack.top() * m;
if (ModelviewMatrixStack.size() == 0)
{
throw std::runtime_error("Modelview matrix stack underflow!!!!");
}
ModelviewMatrixStack.pop();
ModelviewMatrixStack.push(m);
SetMatrix();
}
void Renderer::ScaleMatrix(float scale)
{
Matrix4f m = Matrix4f::Identity();
m.m[0] = scale;
m.m[5] = scale;
m.m[10] = scale;
m = ModelviewMatrixStack.top() * m;
if (ModelviewMatrixStack.size() == 0)
{
throw std::runtime_error("Modelview matrix stack underflow!!!!");
}
ModelviewMatrixStack.pop();
ModelviewMatrixStack.push(m);
SetMatrix();
}
void Renderer::ScaleMatrix(const Vector3f& scale)
{
Matrix4f m = Matrix4f::Identity();
m.m[0] = scale.v[0];
m.m[5] = scale.v[1];
m.m[10] = scale.v[2];
m = ModelviewMatrixStack.top() * m;
if (ModelviewMatrixStack.size() == 0)
{
throw std::runtime_error("Modelview matrix stack underflow!!!!");
}
ModelviewMatrixStack.pop();
ModelviewMatrixStack.push(m);
SetMatrix();
}
void Renderer::RotateMatrix(const Vector4f& q)
{
Matrix3f m3 = QuatToMatrix(q);
Matrix4f m = Matrix4f::Identity();
m.m[0] = m3.m[0];
m.m[1] = m3.m[1];
m.m[2] = m3.m[2];
m.m[4] = m3.m[3];
m.m[5] = m3.m[4];
m.m[6] = m3.m[5];
m.m[8] = m3.m[6];
m.m[9] = m3.m[7];
m.m[10] = m3.m[8];
m = ModelviewMatrixStack.top() * m;
if (ModelviewMatrixStack.size() == 0)
{
throw std::runtime_error("Modelview matrix stack underflow!!!!");
}
ModelviewMatrixStack.pop();
ModelviewMatrixStack.push(m);
SetMatrix();
}
void Renderer::RotateMatrix(const Matrix3f& m3)
{
Matrix4f m = Matrix4f::Identity();
m.m[0] = m3.m[0];
m.m[1] = m3.m[1];
m.m[2] = m3.m[2];
m.m[4] = m3.m[3];
m.m[5] = m3.m[4];
m.m[6] = m3.m[5];
m.m[8] = m3.m[6];
m.m[9] = m3.m[7];
m.m[10] = m3.m[8];
m = ModelviewMatrixStack.top() * m;
if (ModelviewMatrixStack.size() == 0)
{
throw std::runtime_error("Modelview matrix stack underflow!!!!");
}
ModelviewMatrixStack.pop();
ModelviewMatrixStack.push(m);
SetMatrix();
}
void Renderer::PushSpecialMatrix(const Matrix4f& m)
{
if (ModelviewMatrixStack.size() > 64)
{
throw std::runtime_error("Modelview matrix stack overflow!!!!");
}
ModelviewMatrixStack.push(m);
SetMatrix();
}
void Renderer::PopMatrix()
{
if (ModelviewMatrixStack.size() == 0)
{
throw std::runtime_error("Modelview matrix stack underflow!!!!");
}
ModelviewMatrixStack.pop();
SetMatrix();
}
void Renderer::EnableVertexAttribArray(const std::string& attribName)
{
auto shader = shaderManager.GetCurrentShader();
if (shader->attribList.find(attribName) != shader->attribList.end())
glEnableVertexAttribArray(shader->attribList[attribName]);
}
void Renderer::DisableVertexAttribArray(const std::string& attribName)
{
auto shader = shaderManager.GetCurrentShader();
if (shader->attribList.find(attribName) != shader->attribList.end())
glDisableVertexAttribArray(shader->attribList[attribName]);
}
void Renderer::RenderUniformMatrix3fv(const std::string& uniformName, bool transpose, const float* value)
{
auto shader = shaderManager.GetCurrentShader();
auto uniform = shader->uniformList.find(uniformName);
if (uniform != shader->uniformList.end())
{
glUniformMatrix3fv(uniform->second, 1, transpose, value);
}
}
void Renderer::RenderUniformMatrix4fv(const std::string& uniformName, bool transpose, const float* value)
{
auto shader = shaderManager.GetCurrentShader();
auto uniform = shader->uniformList.find(uniformName);
if (uniform != shader->uniformList.end())
{
glUniformMatrix4fv(uniform->second, 1, transpose, value);
}
}
void Renderer::RenderUniform3fv(const std::string& uniformName, const float* value)
{
auto shader = shaderManager.GetCurrentShader();
auto uniform = shader->uniformList.find(uniformName);
if (uniform != shader->uniformList.end())
{
glUniform3fv(uniform->second, 1, value);
}
}
void Renderer::RenderUniform1i(const std::string& uniformName, const int value)
{
auto shader = shaderManager.GetCurrentShader();
auto uniform = shader->uniformList.find(uniformName);
if (uniform != shader->uniformList.end())
{
glUniform1i(uniform->second, value);
}
}
void Renderer::RenderUniform1f(const std::string& uniformName, float value)
{
auto shader = shaderManager.GetCurrentShader();
auto uniform = shader->uniformList.find(uniformName);
if (uniform != shader->uniformList.end())
{
glUniform1fv(uniform->second, 1, &value);
}
}
void Renderer::VertexAttribPointer2fv(const std::string& attribName, int stride, const char* pointer)
{
auto shader = shaderManager.GetCurrentShader();
if (shader->attribList.find(attribName) != shader->attribList.end())
glVertexAttribPointer(shader->attribList[attribName], 2, GL_FLOAT, GL_FALSE, stride, pointer);
}
void Renderer::VertexAttribPointer3fv(const std::string& attribName, int stride, const char* pointer)
{
auto shader = shaderManager.GetCurrentShader();
if (shader->attribList.find(attribName) != shader->attribList.end())
glVertexAttribPointer(shader->attribList[attribName], 3, GL_FLOAT, GL_FALSE, stride, pointer);
}
void Renderer::DrawVertexRenderStruct(const VertexRenderStruct& VertexRenderStruct)
{
static const std::string vNormal("vNormal");
static const std::string vTangent("vTangent");
static const std::string vBinormal("vBinormal");
static const std::string vColor("vColor");
static const std::string vTexCoord("vTexCoord");
static const std::string vPosition("vPosition");
//glBindVertexArray(VertexRenderStruct.vao->getBuffer());
//Check if main thread, check if data is not empty...
if (VertexRenderStruct.data.NormalData.size() > 0)
{
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.normalVBO->getBuffer());
VertexAttribPointer3fv(vNormal, 0, NULL);
}
if (VertexRenderStruct.data.TangentData.size() > 0)
{
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.tangentVBO->getBuffer());
VertexAttribPointer3fv(vTangent, 0, NULL);
}
if (VertexRenderStruct.data.BinormalData.size() > 0)
{
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.binormalVBO->getBuffer());
VertexAttribPointer3fv(vBinormal, 0, NULL);
}
if (VertexRenderStruct.data.ColorData.size() > 0)
{
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.colorVBO->getBuffer());
VertexAttribPointer3fv(vColor, 0, NULL);
}
if (VertexRenderStruct.data.TexCoordData.size() > 0)
{
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.texCoordVBO->getBuffer());
VertexAttribPointer2fv(vTexCoord, 0, NULL);
}
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer());
VertexAttribPointer3fv(vPosition, 0, NULL);
glDrawArrays(GL_TRIANGLES, 0, static_cast<GLsizei>(VertexRenderStruct.data.PositionData.size()));
}
void worldToScreenCoordinates(Vector3f objectPos,
Matrix4f projectionModelView,
int screenWidth, int screenHeight,
int& screenX, int& screenY) {
Vector4f inx = { objectPos.v[0], objectPos.v[1], objectPos.v[2], 1.0f };
Vector4f clipCoords = MultMatrixVector(projectionModelView, inx);
float ndcX = clipCoords.v[0] / clipCoords.v[3];
float ndcY = clipCoords.v[1] / clipCoords.v[3];
screenX = (int)((ndcX + 1.0f) * 0.5f * screenWidth);
screenY = (int)((1.0f + ndcY) * 0.5f * screenHeight);
}
}

144
Renderer.h Executable file
View File

@ -0,0 +1,144 @@
#pragma once
#include "OpenGlExtensions.h"
#include "ZLMath.h"
#include <exception>
#include <stdexcept>
#include "ShaderManager.h"
namespace ZL {
constexpr size_t CONST_MATRIX_STACK_SIZE = 64;
class VBOHolder {
GLuint Buffer;
public:
VBOHolder();
VBOHolder(const VBOHolder& v) = delete;
VBOHolder& operator=(const VBOHolder& v) = delete;
~VBOHolder();
GLuint getBuffer();
};
class VAOHolder {
GLuint vao;
public:
VAOHolder();
VAOHolder(const VAOHolder& v) = delete;
VAOHolder& operator=(const VAOHolder& v) = delete;
~VAOHolder();
GLuint getBuffer();
};
struct VertexDataStruct
{
std::vector<Vector3f> PositionData;
std::vector<Vector2f> TexCoordData;
std::vector<Vector3f> NormalData;
std::vector<Vector3f> TangentData;
std::vector<Vector3f> BinormalData;
std::vector<Vector3f> ColorData;
void RotateByMatrix(Matrix3f m);
void Scale(float scale);
void Move(Vector3f diff);
void SwapZandY();
};
struct VertexRenderStruct
{
VertexDataStruct data;
std::shared_ptr<VAOHolder> vao;
std::shared_ptr<VBOHolder> positionVBO;
std::shared_ptr<VBOHolder> texCoordVBO;
std::shared_ptr<VBOHolder> normalVBO;
std::shared_ptr<VBOHolder> tangentVBO;
std::shared_ptr<VBOHolder> binormalVBO;
std::shared_ptr<VBOHolder> colorVBO;
void RefreshVBO();
void AssignFrom(const VertexDataStruct& v);
};
VertexDataStruct CreateRect2D(Vector2f center, Vector2f halfWidthHeight, float zLevel);
VertexDataStruct CreateRectHorizontalSections2D(Vector2f center, Vector2f halfWidthHeight, float zLevel, size_t sectionCount);
VertexDataStruct CreateCube3D(float scale);
VertexDataStruct CreateCubemap(float scale = 1000.f);
class Renderer
{
protected:
std::stack<Matrix4f> ProjectionMatrixStack;
std::stack<Matrix4f> ModelviewMatrixStack;
Matrix4f ProjectionModelViewMatrix;
public:
ShaderManager shaderManager;
void InitOpenGL();
void PushProjectionMatrix(float width, float height, float zNear = 0.f, float zFar = 1.f);
void PushProjectionMatrix(float xmin, float xmax, float ymin, float ymax, float zNear, float zFar);
void PushPerspectiveProjectionMatrix(float fovY, float aspectRatio, float zNear, float zFar);
void PopProjectionMatrix();
void PushMatrix();
void LoadIdentity();
void TranslateMatrix(const Vector3f& p);
void ScaleMatrix(float scale);
void ScaleMatrix(const Vector3f& scale);
void RotateMatrix(const Vector4f& q);
void RotateMatrix(const Matrix3f& m3);
void PushSpecialMatrix(const Matrix4f& m);
void PopMatrix();
Matrix4f GetProjectionModelViewMatrix();
Matrix4f GetCurrentModelViewMatrix();
void SetMatrix();
void EnableVertexAttribArray(const std::string& attribName);
void DisableVertexAttribArray(const std::string& attribName);
void RenderUniformMatrix3fv(const std::string& uniformName, bool transpose, const float* value);
void RenderUniformMatrix4fv(const std::string& uniformName, bool transpose, const float* value);
void RenderUniform1i(const std::string& uniformName, const int value);
void RenderUniform3fv(const std::string& uniformName, const float* value);
void RenderUniform1f(const std::string& uniformName, float value);
void VertexAttribPointer2fv(const std::string& attribName, int stride, const char* pointer);
void VertexAttribPointer3fv(const std::string& attribName, int stride, const char* pointer);
void DrawVertexRenderStruct(const VertexRenderStruct& VertexRenderStruct);
};
void worldToScreenCoordinates(Vector3f objectPos,
Matrix4f projectionModelView,
int screenWidth, int screenHeight,
int& screenX, int& screenY);
};

215
ShaderManager.cpp Executable file
View File

@ -0,0 +1,215 @@
#include "ShaderManager.h"
#include <iostream>
namespace ZL {
ShaderResource::ShaderResource(const std::string& vertexCode, const std::string& fragmentCode)
{
const int CONST_INFOLOG_LENGTH = 256;
char infoLog[CONST_INFOLOG_LENGTH];
int infoLogLength;
char infoLog2[CONST_INFOLOG_LENGTH];
int infoLogLength2;
int vertexShaderCompiled;
int fragmentShaderCompiled;
int programLinked;
GLuint vertexShader;
GLuint fragmentShader;
int vertexCodeLength = static_cast<int>(strlen(vertexCode.c_str()));
int fragmentCodeLength = static_cast<int>(strlen(fragmentCode.c_str()));
const char* vc = &vertexCode[0];
const char* fc = &fragmentCode[0];
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &(vc), &vertexCodeLength);
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &(fc), &fragmentCodeLength);
glCompileShader(vertexShader);
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexShaderCompiled);
glGetShaderInfoLog(vertexShader, CONST_INFOLOG_LENGTH, &infoLogLength, infoLog);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompiled);
glGetShaderInfoLog(fragmentShader, CONST_INFOLOG_LENGTH, &infoLogLength2, infoLog2);
if (!vertexShaderCompiled)
{
throw std::runtime_error("Failed to compile vertex shader code!");
}
if (!fragmentShaderCompiled)
{
throw std::runtime_error("Failed to compile fragment shader code!");
}
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programLinked);
glGetProgramInfoLog(shaderProgram, CONST_INFOLOG_LENGTH, &infoLogLength, infoLog);
if (!programLinked)
{
shaderProgram = 0;
throw std::runtime_error("Failed to link shader program!");
}
int dummySize; //Dummy
int dummyLen; //Dummy
GLenum dummyEnum;
//================= Parsing all uniforms ================
int activeUniforms;
const int CONST_UNIFORM_NAME_LENGTH = 256;
char uniformName[CONST_UNIFORM_NAME_LENGTH];
glGetProgramiv(shaderProgram, GL_ACTIVE_UNIFORMS, &activeUniforms);
for (int i = 0; i < activeUniforms; i++)
{
glGetActiveUniform(shaderProgram, i, CONST_UNIFORM_NAME_LENGTH, &dummyLen, &dummySize, &dummyEnum, uniformName);
uniformList[uniformName] = glGetUniformLocation(shaderProgram, uniformName);
}
//================= Parsing all attributes ================
int activeAttribs;
const int CONST_ATTRIB_NAME_LENGTH = 256;
char attribName[CONST_ATTRIB_NAME_LENGTH];
glGetProgramiv(shaderProgram, GL_ACTIVE_ATTRIBUTES, &activeAttribs);
for (int i = 0; i < activeAttribs; i++)
{
glGetActiveAttrib(shaderProgram, i, CONST_ATTRIB_NAME_LENGTH, &dummyLen, &dummySize, &dummyEnum, attribName);
attribList[attribName] = glGetAttribLocation(shaderProgram, attribName);
}
}
ShaderResource::~ShaderResource()
{
if (shaderProgram != 0)
{
glDeleteProgram(shaderProgram);
shaderProgram = 0;
}
}
GLuint ShaderResource::getShaderProgram()
{
return shaderProgram;
}
void ShaderManager::AddShaderFromFiles(const std::string& shaderName, const std::string& vertexShaderFileName, const std::string& fragmentShaderFileName, const std::string& ZIPFileName)
{
std::string vertexShader;
std::string fragmentShader;
if (!ZIPFileName.empty()){
std::vector<char> vertexShaderData;
std::vector<char> fragmentShaderData;
vertexShaderData = readFileFromZIP(vertexShaderFileName, ZIPFileName);
fragmentShaderData = readFileFromZIP(fragmentShaderFileName, ZIPFileName);
vertexShader = std::string(vertexShaderData.begin(), vertexShaderData.end());
fragmentShader = std::string(fragmentShaderData.begin(), fragmentShaderData.end());
}else{
vertexShader = readTextFile(vertexShaderFileName);
fragmentShader = readTextFile(fragmentShaderFileName);
}
///std::cout << "Shader: "<< vertexShader << std::endl;
shaderResourceMap[shaderName] = std::make_shared<ShaderResource>(vertexShader, fragmentShader);
}
void ShaderManager::PushShader(const std::string& shaderName)
{
if (shaderStack.size() >= CONST_MAX_SHADER_STACK_SIZE)
{
throw std::runtime_error("Shader stack overflow!");
}
if (shaderResourceMap.find(shaderName) == shaderResourceMap.end())
{
throw std::runtime_error("Shader does not exist!");
}
shaderStack.push(shaderName);
glUseProgram(shaderResourceMap[shaderName]->getShaderProgram());
}
void ShaderManager::PopShader()
{
if (shaderStack.size() == 0)
{
throw std::runtime_error("Shader stack underflow!");
}
shaderStack.pop();
if (shaderStack.size() == 0)
{
glUseProgram(0);
}
else
{
glUseProgram(shaderResourceMap[shaderStack.top()]->getShaderProgram());
}
}
std::shared_ptr<ShaderResource> ShaderManager::GetCurrentShader()
{
if (shaderStack.size() == 0)
{
throw std::runtime_error("Shader stack underflow!");
}
return shaderResourceMap[shaderStack.top()];
}
ShaderSetter::ShaderSetter(ShaderManager& inShaderManager, const std::string& shaderName)
: shaderManager(shaderManager)
{
shaderManager.PushShader(shaderName);
}
ShaderSetter::~ShaderSetter()
{
shaderManager.PopShader();
}
}

64
ShaderManager.h Executable file
View File

@ -0,0 +1,64 @@
#pragma once
#include "OpenGlExtensions.h"
#include "Utils.h"
namespace ZL {
constexpr size_t CONST_MAX_SHADER_STACK_SIZE = 16;
class ShaderResource
{
protected:
GLuint shaderProgram;
std::unordered_map<std::string, GLuint> uniformList;
//std::unordered_map<std::string, std::pair<bool, size_t>> UniformList;
std::map<std::string, GLuint> attribList;
public:
GLuint getShaderProgram();
ShaderResource(const std::string& vertexCode, const std::string& fragmentCode);
~ShaderResource();
public:
friend class ShaderManager;
friend class Renderer;
};
class ShaderManager {
protected:
std::unordered_map<std::string, std::shared_ptr<ShaderResource>> shaderResourceMap;
std::stack<std::string> shaderStack;
public:
void AddShaderFromFiles(const std::string& shaderName, const std::string& vertexShaderFileName, const std::string& fragmentShaderFileName, const std::string& ZIPFileName = "");
void PushShader(const std::string& shaderName);
void PopShader();
std::shared_ptr<ShaderResource> GetCurrentShader();
};
class ShaderSetter
{
protected:
ShaderManager& shaderManager;
public:
ShaderSetter(ShaderManager& inShaderManager, const std::string& shaderName);
~ShaderSetter();
};
}

299
SparkEmitter.cpp Normal file
View File

@ -0,0 +1,299 @@
#include "SparkEmitter.h"
#include <random>
#include <cmath>
#include "OpenGlExtensions.h"
#include "Environment.h"
namespace ZL {
SparkEmitter::SparkEmitter()
: emissionRate(100.0f), isActive(true), drawDataDirty(true), maxParticles(200) {
particles.resize(maxParticles);
drawPositions.reserve(maxParticles * 6);
drawTexCoords.reserve(maxParticles * 6);
lastEmissionTime = std::chrono::steady_clock::now();
sparkQuad.data = VertexDataStruct();
}
SparkEmitter::SparkEmitter(const std::vector<Vector3f>& positions, float rate)
: emissionPoints(positions), emissionRate(rate), isActive(true),
drawDataDirty(true), maxParticles(positions.size() * 100) {
particles.resize(maxParticles);
drawPositions.reserve(maxParticles * 6);
drawTexCoords.reserve(maxParticles * 6);
lastEmissionTime = std::chrono::steady_clock::now();
sparkQuad.data = VertexDataStruct();
}
SparkEmitter::SparkEmitter(const std::vector<Vector3f>& positions,
std::shared_ptr<Texture> tex,
float rate)
: emissionPoints(positions), texture(tex), emissionRate(rate),
isActive(true), drawDataDirty(true), maxParticles(positions.size() * 100) {
particles.resize(maxParticles);
drawPositions.reserve(maxParticles * 6);
drawTexCoords.reserve(maxParticles * 6);
lastEmissionTime = std::chrono::steady_clock::now();
sparkQuad.data = VertexDataStruct();
}
void SparkEmitter::setTexture(std::shared_ptr<Texture> tex) {
texture = tex;
}
void SparkEmitter::prepareDrawData() {
if (!drawDataDirty) return;
drawPositions.clear();
drawTexCoords.clear();
if (getActiveParticleCount() == 0) {
drawDataDirty = false;
return;
}
std::vector<std::pair<const SparkParticle*, float>> sortedParticles;
sortedParticles.reserve(getActiveParticleCount());
for (const auto& particle : particles) {
if (particle.active) {
sortedParticles.push_back({ &particle, particle.position.v[2] });
}
}
std::sort(sortedParticles.begin(), sortedParticles.end(),
[](const auto& a, const auto& b) {
return a.second > b.second;
});
for (const auto& [particlePtr, depth] : sortedParticles) {
const auto& particle = *particlePtr;
Vector3f pos = particle.position;
float size = 0.04f * particle.scale;
drawPositions.push_back({ pos.v[0] - size, pos.v[1] - size, pos.v[2] });
drawTexCoords.push_back({ 0.0f, 0.0f });
drawPositions.push_back({ pos.v[0] - size, pos.v[1] + size, pos.v[2] });
drawTexCoords.push_back({ 0.0f, 1.0f });
drawPositions.push_back({ pos.v[0] + size, pos.v[1] + size, pos.v[2] });
drawTexCoords.push_back({ 1.0f, 1.0f });
drawPositions.push_back({ pos.v[0] - size, pos.v[1] - size, pos.v[2] });
drawTexCoords.push_back({ 0.0f, 0.0f });
drawPositions.push_back({ pos.v[0] + size, pos.v[1] + size, pos.v[2] });
drawTexCoords.push_back({ 1.0f, 1.0f });
drawPositions.push_back({ pos.v[0] + size, pos.v[1] - size, pos.v[2] });
drawTexCoords.push_back({ 1.0f, 0.0f });
}
drawDataDirty = false;
}
void SparkEmitter::draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight) {
if (getActiveParticleCount() == 0) {
return;
}
if (!texture) {
return;
}
prepareDrawData();
if (drawPositions.empty()) {
return;
}
sparkQuad.data.PositionData = drawPositions;
sparkQuad.data.TexCoordData = drawTexCoords;
sparkQuad.RefreshVBO();
static const std::string defaultShaderName = "default";
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
float aspectRatio = static_cast<float>(screenWidth) / static_cast<float>(screenHeight);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, aspectRatio, Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);// Аддитивное смешивание для эффекта свечения
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0, 0, -1.0f * zoom });
renderer.DrawVertexRenderStruct(sparkQuad);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
glDisable(GL_BLEND);
renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.shaderManager.PopShader();
}
void SparkEmitter::update(float deltaTimeMs) {
auto currentTime = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
currentTime - lastEmissionTime).count();
if (isActive && elapsed >= emissionRate) {
emit();
lastEmissionTime = currentTime;
drawDataDirty = true;
}
bool anyChanged = false;
for (auto& particle : particles) {
if (particle.active) {
Vector3f oldPosition = particle.position;
float oldScale = particle.scale;
particle.position.v[0] += particle.velocity.v[0] * deltaTimeMs / 1000.0f;
particle.position.v[1] += particle.velocity.v[1] * deltaTimeMs / 1000.0f;
particle.position.v[2] += particle.velocity.v[2] * deltaTimeMs / 1000.0f;
particle.lifeTime += deltaTimeMs;
if (particle.lifeTime >= particle.maxLifeTime) {
particle.active = false;
anyChanged = true;
}
else {
float lifeRatio = particle.lifeTime / particle.maxLifeTime;
particle.scale = 1.0f - lifeRatio * 0.8f;
if (oldPosition.v[0] != particle.position.v[0] ||
oldPosition.v[1] != particle.position.v[1] ||
oldPosition.v[2] != particle.position.v[2] ||
oldScale != particle.scale) {
anyChanged = true;
}
}
}
}
if (anyChanged) {
drawDataDirty = true;
}
}
void SparkEmitter::emit() {
if (emissionPoints.empty()) return;
bool emitted = false;
for (int i = 0; i < emissionPoints.size(); ++i) {
bool particleFound = false;
for (auto& particle : particles) {
if (!particle.active) {
initParticle(particle, i);
particle.active = true;
particle.lifeTime = 0;
particle.position = emissionPoints[i];
particle.emitterIndex = i;
particleFound = true;
emitted = true;
break;
}
}
if (!particleFound && !particles.empty()) {
size_t oldestIndex = 0;
float maxLifeTime = 0;
for (size_t j = 0; j < particles.size(); ++j) {
if (particles[j].lifeTime > maxLifeTime) {
maxLifeTime = particles[j].lifeTime;
oldestIndex = j;
}
}
initParticle(particles[oldestIndex], i);
particles[oldestIndex].active = true;
particles[oldestIndex].lifeTime = 0;
particles[oldestIndex].position = emissionPoints[i];
particles[oldestIndex].emitterIndex = i;
emitted = true;
}
}
if (emitted) {
drawDataDirty = true;
}
}
void SparkEmitter::setEmissionPoints(const std::vector<Vector3f>& positions) {
emissionPoints = positions;
maxParticles = positions.size() * 100;
particles.resize(maxParticles);
drawDataDirty = true;
}
void SparkEmitter::initParticle(SparkParticle& particle, int emitterIndex) {
particle.velocity = getRandomVelocity(emitterIndex);
particle.scale = 1.0f;
particle.maxLifeTime = 800.0f + (rand() % 400);
particle.emitterIndex = emitterIndex;
}
Vector3f SparkEmitter::getRandomVelocity(int emitterIndex) {
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_real_distribution<> angleDist(0, 2 * M_PI);
static std::uniform_real_distribution<> speedDist(0.5f, 2.0f);
static std::uniform_real_distribution<> zSpeedDist(1.0f, 3.0f);
float angle = angleDist(gen);
float speed = speedDist(gen);
float zSpeed = zSpeedDist(gen);
if (emitterIndex == 0) {
return Vector3f{
cosf(angle) * speed - 0.3f,
sinf(angle) * speed,
zSpeed
};
}
else {
return Vector3f{
cosf(angle) * speed + 0.3f,
sinf(angle) * speed,
zSpeed
};
}
}
const std::vector<SparkParticle>& SparkEmitter::getParticles() const {
return particles;
}
size_t SparkEmitter::getActiveParticleCount() const {
size_t count = 0;
for (const auto& particle : particles) {
if (particle.active) {
count++;
}
}
return count;
}
} // namespace ZL

66
SparkEmitter.h Normal file
View File

@ -0,0 +1,66 @@
#pragma once
#include "ZLMath.h"
#include "Renderer.h"
#include "TextureManager.h"
#include <vector>
#include <chrono>
namespace ZL {
struct SparkParticle {
Vector3f position;
Vector3f velocity;
float scale;
float lifeTime;
float maxLifeTime;
bool active;
int emitterIndex;
SparkParticle() : position({ 0,0,0 }), velocity({ 0,0,0 }), scale(1.0f),
lifeTime(0), maxLifeTime(1000.0f), active(false), emitterIndex(0) {
}
};
class SparkEmitter {
private:
std::vector<SparkParticle> particles;
std::vector<Vector3f> emissionPoints;
std::chrono::steady_clock::time_point lastEmissionTime;
float emissionRate;
bool isActive;
std::vector<Vector3f> drawPositions;
std::vector<Vector2f> drawTexCoords;
bool drawDataDirty;
VertexRenderStruct sparkQuad;
std::shared_ptr<Texture> texture;
int maxParticles;
void prepareDrawData();
public:
SparkEmitter();
SparkEmitter(const std::vector<Vector3f>& positions, float rate = 100.0f);
SparkEmitter(const std::vector<Vector3f>& positions,
std::shared_ptr<Texture> tex,
float rate = 100.0f);
void setEmissionPoints(const std::vector<Vector3f>& positions);
void setTexture(std::shared_ptr<Texture> tex);
void update(float deltaTimeMs);
void emit();
void draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight);
const std::vector<SparkParticle>& getParticles() const;
size_t getActiveParticleCount() const;
private:
void initParticle(SparkParticle& particle, int emitterIndex);
Vector3f getRandomVelocity(int emitterIndex);
};
} // namespace ZL

276
StoneObject.cpp Normal file
View File

@ -0,0 +1,276 @@
#include "StoneObject.h"
#include "Utils.h"
#include <GL/gl.h>
#include <random>
#include <cmath>
#include "Renderer.h"
#include "PlanetData.h"
namespace ZL {
// --- ÊÎÍÑÒÀÍÒÛ ÏÀÐÀÌÅÒÐÎÂ (êàê âû ïðîñèëè) ---
const float StoneParams::BASE_SCALE = 17.0f; // Îáùèé ðàçìåð êàìíÿ
//const float StoneParams::BASE_SCALE = 5000.0f; // Îáùèé ðàçìåð êàìíÿ
//const float StoneParams::MIN_AXIS_SCALE = 1.0f; // Ìèíèìàëüíîå ðàñòÿæåíèå/ñæàòèå ïî îñè
//const float StoneParams::MAX_AXIS_SCALE = 1.0f; // Ìàêñèìàëüíîå ðàñòÿæåíèå/ñæàòèå ïî îñè
//const float StoneParams::MIN_PERTURBATION = 0.0f; // Ìèíèìàëüíîå ðàäèàëüíîå âîçìóùåíèå âåðøèíû
//const float StoneParams::MAX_PERTURBATION = 0.0f; // Ìàêñèìàëüíîå ðàäèàëüíîå âîçìóùåíèå âåðøèíû
const float StoneParams::MIN_AXIS_SCALE = 0.5f; // Ìèíèìàëüíîå ðàñòÿæåíèå/ñæàòèå ïî îñè
const float StoneParams::MAX_AXIS_SCALE = 1.5f; // Ìàêñèìàëüíîå ðàñòÿæåíèå/ñæàòèå ïî îñè
const float StoneParams::MIN_PERTURBATION = 0.05f; // Ìèíèìàëüíîå ðàäèàëüíîå âîçìóùåíèå âåðøèíû
const float StoneParams::MAX_PERTURBATION = 0.25f; // Ìàêñèìàëüíîå ðàäèàëüíîå âîçìóùåíèå âåðøèíû
const int StoneParams::STONES_PER_TRIANGLE = 100;
// Âñïîìîãàòåëüíàÿ ôóíêöèÿ äëÿ ïîëó÷åíèÿ ñëó÷àéíîãî ÷èñëà â äèàïàçîíå [min, max]
float getRandomFloat(std::mt19937& gen, float min, float max) {
std::uniform_real_distribution<> distrib(min, max);
return static_cast<float>(distrib(gen));
}
// Âñïîìîãàòåëüíàÿ ôóíêöèÿ äëÿ ãåíåðàöèè ñëó÷àéíîé òî÷êè íà òðåóãîëüíèêå
// Èñïîëüçóåò áàðèöåíòðè÷åñêèå êîîðäèíàòû
Vector3f GetRandomPointOnTriangle(const Triangle& t, std::mt19937& engine) {
std::uniform_real_distribution<> distrib(0.0f, 1.0f);
float r1 = getRandomFloat(engine, 0.0f, 1.0f);
float r2 = getRandomFloat(engine, 0.0f, 1.0f);
// Ïðåîáðàçîâàíèå r1, r2 äëÿ ïîëó÷åíèÿ ðàâíîìåðíîãî ðàñïðåäåëåíèÿ
float a = 1.0f - std::sqrt(r1);
float b = std::sqrt(r1) * r2;
float c = 1.0f - a - b; // c = sqrt(r1) * (1 - r2)
// Áàðèöåíòðè÷åñêèå êîîðäèíàòû
// P = a*p1 + b*p2 + c*p3
Vector3f p1_term = t.data[0] * a;
Vector3f p2_term = t.data[1] * b;
Vector3f p3_term = t.data[2] * c;
return p1_term + p2_term + p3_term;
}
// Èêîñàýäð (íà îñíîâå çîëîòîãî ñå÷åíèÿ phi)
// Êîîðäèíàòû ìîãóò áûòü âû÷èñëåíû çàðàíåå äëÿ êîíñòàíòíîãî èêîñàýäðà.
// Çäåñü òîëüêî îáúÿâëåíèå, ÷òîáû ïîêàçàòü èäåþ.
VertexDataStruct CreateBaseConvexPolyhedron(uint64_t seed) {
// const size_t SUBDIVISION_LEVEL = 1; // Óðîâåíü ïîäðàçäåëåíèÿ (äëÿ áîëåå êðóãëîãî êàìíÿ, ïîêà îïóñòèì)
std::mt19937 engine(static_cast<unsigned int>(seed));
// Çîëîòîå ñå÷åíèå
const float t = (1.0f + std::sqrt(5.0f)) / 2.0f;
// 12 âåðøèí èêîñàýäðà
std::vector<Vector3f> initialVertices = {
{ -1, t, 0 }, { 1, t, 0 }, { -1, -t, 0 }, { 1, -t, 0 },
{ 0, -1, t }, { 0, 1, t }, { 0, -1, -t }, { 0, 1, -t },
{ t, 0, -1 }, { t, 0, 1 }, { -t, 0, -1 }, { -t, 0, 1 }
};
// 20 òðåóãîëüíûõ ãðàíåé (èíäåêñû âåðøèí)
std::vector<std::array<int, 3>> faces = {
// 5 òðåóãîëüíèêîâ âîêðóã âåðøèíû 0
{0, 11, 5}, {0, 5, 1}, {0, 1, 7}, {0, 7, 10}, {0, 10, 11},
// 5 ñìåæíûõ ïîëîñ
{1, 5, 9}, {5, 11, 4}, {11, 10, 2}, {10, 7, 6}, {7, 1, 8},
// 5 òðåóãîëüíèêîâ âîêðóã âåðøèíû 3
{3, 9, 4}, {3, 4, 2}, {3, 2, 6}, {3, 6, 8}, {3, 8, 9},
// 5 ñìåæíûõ ïîëîñ
{4, 9, 5}, {2, 4, 11}, {6, 2, 10}, {8, 6, 7}, {9, 8, 1}
};
// 1. Íîðìàëèçàöèÿ è Âîçìóùåíèå (Perturbation)
for (Vector3f& v : initialVertices) {
v = v.normalized() * StoneParams::BASE_SCALE; // Íîðìàëèçàöèÿ ê ñôåðå ðàäèóñà BASE_SCALE
// Ðàäèàëüíîå âîçìóùåíèå:
float perturbation = getRandomFloat(engine, StoneParams::MIN_PERTURBATION, StoneParams::MAX_PERTURBATION);
v = v * (1.0f + perturbation);
}
// 2. Òðàíñôîðìàöèÿ (Ìàñøòàáèðîâàíèå è Ïîâîðîò)
// Ñëó÷àéíûå ìàñøòàáû ïî îñÿì
Vector3f scaleFactors = {
getRandomFloat(engine, StoneParams::MIN_AXIS_SCALE, StoneParams::MAX_AXIS_SCALE),
getRandomFloat(engine, StoneParams::MIN_AXIS_SCALE, StoneParams::MAX_AXIS_SCALE),
getRandomFloat(engine, StoneParams::MIN_AXIS_SCALE, StoneParams::MAX_AXIS_SCALE)
};
// Ïðèìåíÿåì ìàñøòàáèðîâàíèå
for (Vector3f& v : initialVertices) {
v.v[0] *= scaleFactors.v[0];
v.v[1] *= scaleFactors.v[1];
v.v[2] *= scaleFactors.v[2];
}
// Ñëó÷àéíûé ïîâîðîò (íàïðèìåð, âîêðóã òðåõ îñåé)
Vector4f qx = QuatFromRotateAroundX(getRandomFloat(engine, 0.0f, 360.0f));
Vector4f qy = QuatFromRotateAroundY(getRandomFloat(engine, 0.0f, 360.0f));
Vector4f qz = QuatFromRotateAroundZ(getRandomFloat(engine, 0.0f, 360.0f));
Vector4f qFinal = slerp(qx, qy, 0.5f); // Ïðîñòîé ïðèìåð êîìáèíèðîâàíèÿ
qFinal = slerp(qFinal, qz, 0.5f).normalized();
Matrix3f rotationMatrix = QuatToMatrix(qFinal);
for (Vector3f& v : initialVertices) {
v = MultMatrixVector(rotationMatrix, v);
}
// 3. Ñãëàæåííûå Íîðìàëè è Ôîðìèðîâàíèå Mesh
VertexDataStruct result;
// Êàðòà äëÿ íàêîïëåíèÿ íîðìàëåé ïî óíèêàëüíûì ïîçèöèÿì âåðøèí
// (Òðåáóåò îïðåäåëåííîãî îïåðàòîðà < äëÿ Vector3f â ZLMath.h, êîòîðûé ó âàñ åñòü)
std::map<Vector3f, Vector3f> smoothNormalsMap;
// Ïðåäâàðèòåëüíîå çàïîëíåíèå êàðòû íîðìàëÿìè
for (const auto& face : faces) {
Vector3f p1 = initialVertices[face[0]];
Vector3f p2 = initialVertices[face[1]];
Vector3f p3 = initialVertices[face[2]];
// Íîðìàëü ãðàíè: (p2 - p1) x (p3 - p1)
Vector3f faceNormal = (p2 - p1).cross(p3 - p1).normalized();
smoothNormalsMap[p1] = smoothNormalsMap[p1] + faceNormal;
smoothNormalsMap[p2] = smoothNormalsMap[p2] + faceNormal;
smoothNormalsMap[p3] = smoothNormalsMap[p3] + faceNormal;
}
// Íîðìàëèçàöèÿ íàêîïëåííûõ íîðìàëåé
for (auto& pair : smoothNormalsMap) {
pair.second = pair.second.normalized();
}
// 4. Ôèíàëüíîå çàïîëíåíèå VertexDataStruct è Òåêñòóðíûå Êîîðäèíàòû
for (const auto& face : faces) {
Vector3f p1 = initialVertices[face[0]];
Vector3f p2 = initialVertices[face[1]];
Vector3f p3 = initialVertices[face[2]];
// Ïîçèöèè
result.PositionData.push_back(p1);
result.PositionData.push_back(p2);
result.PositionData.push_back(p3);
// Ñãëàæåííûå Íîðìàëè (èç êàðòû)
result.NormalData.push_back(smoothNormalsMap[p1]);
result.NormalData.push_back(smoothNormalsMap[p2]);
result.NormalData.push_back(smoothNormalsMap[p3]);
// Òåêñòóðíûå Êîîðäèíàòû (Ïëàíàðíàÿ ïðîåêöèÿ íà ïëîñêîñòü ãðàíè)
// p1 -> (0, 0), p2 -> (L_12, 0), p3 -> (L_13 * cos(angle), L_13 * sin(angle))
// Ãäå L_xy - äëèíà âåêòîðà, angle - óãîë ìåæäó p2-p1 è p3-p1
Vector3f uAxis = (p2 - p1).normalized();
Vector3f vRaw = p3 - p1;
// Ïðîåêöèÿ vRaw íà uAxis
float uProjLen = vRaw.dot(uAxis);
// Âåêòîð V ïåðïåíäèêóëÿðíûé U: vRaw - uProj
Vector3f vAxisRaw = vRaw - (uAxis * uProjLen);
// Äëèíà îñè V
float vLen = vAxisRaw.length();
// Íîðìàëèçîâàííàÿ îñü V
Vector3f vAxis = vAxisRaw.normalized();
// Êîîðäèíàòû (u, v) äëÿ p1, p2, p3 îòíîñèòåëüíî p1
Vector2f uv1 = { 0.0f, 0.0f };
Vector2f uv2 = { (p2 - p1).length(), 0.0f }; // p2-p1 âäîëü îñè U
Vector2f uv3 = { uProjLen, vLen }; // p3-p1: u-êîìïîíåíòà = uProjLen, v-êîìïîíåíòà = vLen
// Íàõîäèì ìàêñèìàëüíûé ðàçìåð, ÷òîáû ìàñøòàáèðîâàòü â [0, 1]
float maxUV = max(uv2.v[0], max(uv3.v[0], uv3.v[1]));
if (maxUV > 0.000001f) {
// Ìàñøòàáèðóåì:
result.TexCoordData.push_back(uv1);
result.TexCoordData.push_back(uv2 * (1.f / maxUV));
result.TexCoordData.push_back(uv3 * (1.f / maxUV));
}
else {
// Ïðåäîòâðàùåíèå äåëåíèÿ íà íîëü äëÿ âûðîæäåííûõ ãðàíåé
result.TexCoordData.push_back({ 0.0f, 0.0f });
result.TexCoordData.push_back({ 0.0f, 0.0f });
result.TexCoordData.push_back({ 0.0f, 0.0f });
}
}
return result;
}
StoneGroup CreateStoneGroupData(uint64_t globalSeed, const LodLevel& planetLodLevel) {
StoneGroup group;
// Ðåçåðâèðóåì ìåñòî ïîä âñå òðåóãîëüíèêè òåêóùåãî LOD
group.allInstances.resize(planetLodLevel.triangles.size());
for (size_t tIdx = 0; tIdx < planetLodLevel.triangles.size(); ++tIdx) {
const Triangle& tri = planetLodLevel.triangles[tIdx];
std::mt19937 engine(static_cast<unsigned int>(globalSeed));
for (int i = 0; i < StoneParams::STONES_PER_TRIANGLE; ++i) {
StoneInstance instance;
instance.seed = globalSeed;// + tIdx * 1000 + i; // Óíèêàëüíûé ñèä äëÿ êàæäîãî êàìíÿ
instance.position = GetRandomPointOnTriangle(tri, engine);
//instance.position = Vector3f(5000.0f, 5000.0f, 5000.0f);
// Ãåíåðèðóåì ñëó÷àéíûå ïàðàìåòðû îäèí ðàç
instance.scale = {
getRandomFloat(engine, 0.5f, 1.5f),
getRandomFloat(engine, 0.5f, 1.5f),
getRandomFloat(engine, 0.5f, 1.5f)
};
Vector4f qx = QuatFromRotateAroundX(getRandomFloat(engine, 0.0f, 360.0f));
Vector4f qy = QuatFromRotateAroundY(getRandomFloat(engine, 0.0f, 360.0f));
Vector4f qz = QuatFromRotateAroundZ(getRandomFloat(engine, 0.0f, 360.0f));
instance.rotation = slerp(slerp(qx, qy, 0.5f), qz, 0.5f).normalized();
group.allInstances[tIdx].push_back(instance);
}
}
return group;
}
void StoneGroup::inflate(const std::vector<int>& triangleIndices) {
// 1. Î÷èùàåì òåêóùèé ìåø ïåðåä çàïîëíåíèåì
mesh.PositionData.clear();
mesh.NormalData.clear();
mesh.TexCoordData.clear();
static VertexDataStruct baseStone = CreateBaseConvexPolyhedron(1337);
// 2. Íàïîëíÿåì ìåø òîëüêî äëÿ âèäèìûõ òðåóãîëüíèêîâ
for (int tIdx : triangleIndices) {
if (tIdx >= allInstances.size()) continue;
for (const auto& inst : allInstances[tIdx]) {
Matrix3f rotMat = QuatToMatrix(inst.rotation);
for (size_t j = 0; j < baseStone.PositionData.size(); ++j) {
Vector3f p = baseStone.PositionData[j];
Vector3f n = baseStone.NormalData[j];
// Ìàñøòàá -> Ïîâîðîò -> Ñìåùåíèå
p.v[0] *= inst.scale.v[0];
p.v[1] *= inst.scale.v[1];
p.v[2] *= inst.scale.v[2];
mesh.PositionData.push_back(MultMatrixVector(rotMat, p) + inst.position);
mesh.NormalData.push_back(MultMatrixVector(rotMat, n));
mesh.TexCoordData.push_back(baseStone.TexCoordData[j]);
}
}
}
}
} // namespace ZL

41
StoneObject.h Normal file
View File

@ -0,0 +1,41 @@
#pragma once
#include "ZLMath.h"
#include "Renderer.h"
#include "PlanetData.h"
namespace ZL {
struct StoneParams
{
static const float BASE_SCALE; // Îáùèé ðàçìåð êàìíÿ
static const float MIN_AXIS_SCALE; // Ìèíèìàëüíîå ðàñòÿæåíèå/ñæàòèå ïî îñè
static const float MAX_AXIS_SCALE; // Ìàêñèìàëüíîå ðàñòÿæåíèå/ñæàòèå ïî îñè
static const float MIN_PERTURBATION; // Ìèíèìàëüíîå ðàäèàëüíîå âîçìóùåíèå âåðøèíû
static const float MAX_PERTURBATION; // Ìàêñèìàëüíîå ðàäèàëüíîå âîçìóùåíèå âåðøèíû
static const int STONES_PER_TRIANGLE;
};
struct StoneInstance {
uint64_t seed;
Vector3f position;
Vector3f scale;
Vector4f rotation;
};
struct StoneGroup {
// mesh.PositionData è ïðî÷èå áóäóò çàïîëíÿòüñÿ â inflate()
VertexDataStruct mesh;
// Âíåøíèé âåêòîð — èíäåêñ òðåóãîëüíèêà ïëàíåòû,
// âíóòðåííèé — ñïèñîê êàìíåé íà ýòîì òðåóãîëüíèêå
std::vector<std::vector<StoneInstance>> allInstances;
// Î÷èùàåò ñòàðóþ ãåîìåòðèþ è ãåíåðèðóåò íîâóþ äëÿ óêàçàííûõ èíäåêñîâ
void inflate(const std::vector<int>& triangleIndices);
};
// Òåïåðü âîçâðàùàåò çàãîòîâêó ñî âñåìè ïàðàìåòðàìè, íî áåç òÿæåëîãî ìåøà
StoneGroup CreateStoneGroupData(uint64_t globalSeed, const LodLevel& planetLodLevel);
} // namespace ZL

390
TextModel.cpp Normal file
View File

@ -0,0 +1,390 @@
#include "TextModel.h"
#include <regex>
#include <string>
#include <fstream>
#include <iostream>
#include <sstream>
namespace ZL
{
VertexDataStruct LoadFromTextFile(const std::string& fileName, const std::string& ZIPFileName)
{
VertexDataStruct result;
std::ifstream filestream;
std::istringstream zipStream;
if (!ZIPFileName.empty())
{
std::vector<char> fileData = readFileFromZIP(fileName, ZIPFileName);
std::string fileContents(fileData.begin(), fileData.end());
zipStream.str(fileContents);
}
else
{
filestream.open(fileName);
}
// Создаем ссылку f на нужный поток после этого код ниже остается без изменений
std::istream& f = (!ZIPFileName.empty()) ? static_cast<std::istream&>(zipStream) : static_cast<std::istream&>(filestream);
//Skip first 5 lines
std::string tempLine;
std::getline(f, tempLine);
static const std::regex pattern_count(R"(\d+)");
static const std::regex pattern_float(R"([-]?\d+\.\d+)");
static const std::regex pattern_int(R"([-]?\d+)");
std::smatch match;
int numberVertices;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberVertices = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
std::vector<Vector3f> vertices;
vertices.resize(numberVertices);
for (int i = 0; i < numberVertices; i++)
{
std::getline(f, tempLine);
std::vector<float> floatValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
vertices[i] = Vector3f{ floatValues[0], floatValues[1], floatValues[2] };
}
std::cout << "UV Coordinates" << std::endl;
std::getline(f, tempLine); //===UV Coordinates:
std::getline(f, tempLine); //triangle count
int numberTriangles;
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
numberTriangles = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
// Now process UVs
std::vector<std::array<Vector2f, 3>> uvCoords;
uvCoords.resize(numberTriangles);
for (int i = 0; i < numberTriangles; i++)
{
std::getline(f, tempLine); //Face 0
int uvCount;
std::getline(f, tempLine);
if (std::regex_search(tempLine, match, pattern_count)) {
std::string number_str = match.str();
uvCount = std::stoi(number_str);
}
else {
throw std::runtime_error("No number found in the input string.");
}
if (uvCount != 3)
{
throw std::runtime_error("more than 3 uvs");
}
std::vector<float> floatValues;
for (int j = 0; j < 3; j++)
{
std::getline(f, tempLine); //UV <Vector (-0.3661, -1.1665)>
auto b = tempLine.cbegin();
auto e = tempLine.cend();
floatValues.clear();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
if (floatValues.size() != 2)
{
throw std::runtime_error("more than 2 uvs---");
}
uvCoords[i][j] = Vector2f{ floatValues[0],floatValues[1] };
}
}
std::cout << "Normals go" << std::endl;
std::getline(f, tempLine); //===Normals:
std::vector<Vector3f> normals;
normals.resize(numberVertices);
for (int i = 0; i < numberVertices; i++)
{
std::getline(f, tempLine);
std::vector<float> floatValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
normals[i] = Vector3f{ floatValues[0], floatValues[1], floatValues[2] };
}
std::cout << "Triangles go:" << std::endl;
std::getline(f, tempLine); //===Triangles: 3974
std::vector<std::array<int, 3>> triangles;
triangles.resize(numberTriangles);
for (int i = 0; i < numberTriangles; i++)
{
std::getline(f, tempLine);
std::vector<int> intValues;
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_int)) {
intValues.push_back(std::stoi(match.str()));
b = match.suffix().first;
}
triangles[i] = { intValues[0], intValues[1], intValues[2] };
}
std::cout << "Process vertices" << std::endl;
// Now let's process vertices
for (int i = 0; i < numberTriangles; i++)
{
result.PositionData.push_back(vertices[triangles[i][0]]);
result.PositionData.push_back(vertices[triangles[i][1]]);
result.PositionData.push_back(vertices[triangles[i][2]]);
/*
result.NormalData.push_back(normals[triangles[i][0]]);
result.NormalData.push_back(normals[triangles[i][1]]);
result.NormalData.push_back(normals[triangles[i][2]]);
*/
result.TexCoordData.push_back(uvCoords[i][0]);
result.TexCoordData.push_back(uvCoords[i][1]);
result.TexCoordData.push_back(uvCoords[i][2]);
}
//Swap from Blender format to OpenGL format
for (int i = 0; i < result.PositionData.size(); i++)
{
Vector3f tempVec = result.PositionData[i];
result.PositionData[i].v[0] = tempVec.v[1];
result.PositionData[i].v[1] = tempVec.v[2];
result.PositionData[i].v[2] = tempVec.v[0];
/*
tempVec = result.NormalData[i];
result.NormalData[i].v[0] = tempVec.v[1];
result.NormalData[i].v[1] = tempVec.v[2];
result.NormalData[i].v[2] = tempVec.v[0];*/
}
return result;
}
VertexDataStruct LoadFromTextFile02(const std::string& fileName, const std::string& ZIPFileName)
{
VertexDataStruct result;
std::ifstream filestream;
std::istringstream zipStream;
// --- 1. Открытие потока (без изменений) ---
if (!ZIPFileName.empty())
{
std::vector<char> fileData = readFileFromZIP(fileName, ZIPFileName);
std::string fileContents(fileData.begin(), fileData.end());
zipStream.str(fileContents);
}
else
{
filestream.open(fileName);
if (!filestream.is_open()) {
throw std::runtime_error("Failed to open file: " + fileName);
}
}
std::istream& f = (!ZIPFileName.empty()) ? static_cast<std::istream&>(zipStream) : static_cast<std::istream&>(filestream);
std::string tempLine;
std::smatch match;
// Обновленные регулярки
// pattern_float стал чуть надежнее для чисел вида "0" или "-1" без точки, если вдруг Python округлит до int
static const std::regex pattern_count(R"(\d+)");
static const std::regex pattern_float(R"([-]?\d+(\.\d+)?)");
static const std::regex pattern_int(R"([-]?\d+)");
// --- 2. Парсинг Вершин (Pos + Norm + UV) ---
// Ищем заголовок ===Vertices
while (std::getline(f, tempLine)) {
if (tempLine.find("===Vertices") != std::string::npos) break;
}
int numberVertices = 0;
if (std::regex_search(tempLine, match, pattern_count)) {
numberVertices = std::stoi(match.str());
}
else {
throw std::runtime_error("Vertices header not found or invalid.");
}
// Временные буферы для хранения "уникальных" вершин перед разверткой по индексам
std::vector<Vector3f> tempPositions(numberVertices);
std::vector<Vector3f> tempNormals(numberVertices);
std::vector<Vector2f> tempUVs(numberVertices);
for (int i = 0; i < numberVertices; i++)
{
std::getline(f, tempLine);
// Строка вида: V 0: Pos(x, y, z) Norm(x, y, z) UV(u, v)
std::vector<float> floatValues;
floatValues.reserve(8); // Ожидаем ровно 8 чисел (3 pos + 3 norm + 2 uv)
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_float)) {
floatValues.push_back(std::stof(match.str()));
b = match.suffix().first;
}
// Проверка целостности строки (ID вершины regex может поймать первым, но нас интересуют данные)
// Обычно ID идет первым (0), потом 3+3+2 float. Итого 9 чисел, если считать ID.
// Ваш Python пишет "V 0:", regex поймает 0. Потом 8 флоатов.
// Если regex ловит ID вершины как float (что вероятно), нам нужно смещение.
// ID - floatValues[0]
// Pos - [1], [2], [3]
// Norm - [4], [5], [6]
// UV - [7], [8]
if (floatValues.size() < 9) {
throw std::runtime_error("Malformed vertex line at index " + std::to_string(i));
}
tempPositions[i] = Vector3f{ floatValues[1], floatValues[2], floatValues[3] };
tempNormals[i] = Vector3f{ floatValues[4], floatValues[5], floatValues[6] };
tempUVs[i] = Vector2f{ floatValues[7], floatValues[8] };
}
// --- 3. Парсинг Треугольников (Индексов) ---
// Пропускаем пустые строки до заголовка треугольников
while (std::getline(f, tempLine)) {
if (tempLine.find("===Triangles") != std::string::npos) break;
}
int numberTriangles = 0;
if (std::regex_search(tempLine, match, pattern_count)) {
numberTriangles = std::stoi(match.str());
}
else {
throw std::runtime_error("Triangles header not found.");
}
// Резервируем память в result, чтобы избежать лишних аллокаций
result.PositionData.reserve(numberTriangles * 3);
result.NormalData.reserve(numberTriangles * 3);
result.TexCoordData.reserve(numberTriangles * 3);
for (int i = 0; i < numberTriangles; i++)
{
std::getline(f, tempLine);
// Строка вида: Tri: 0 1 2
std::vector<int> indices;
indices.reserve(3);
auto b = tempLine.cbegin();
auto e = tempLine.cend();
while (std::regex_search(b, e, match, pattern_int)) {
indices.push_back(std::stoi(match.str()));
b = match.suffix().first;
}
if (indices.size() != 3) {
throw std::runtime_error("Malformed triangle line at index " + std::to_string(i));
}
// --- 4. Заполнение VertexDataStruct (Flattening) ---
// Берем данные из временных буферов по индексам и кладем в итоговый массив
for (int k = 0; k < 3; k++) {
int idx = indices[k];
result.PositionData.push_back(tempPositions[idx]);
result.NormalData.push_back(tempNormals[idx]);
result.TexCoordData.push_back(tempUVs[idx]);
}
}
// --- 5. Конвертация координат (Blender -> OpenGL/Engine) ---
// Сохраняем вашу логику смены осей: X->Z, Y->X, Z->Y
for (size_t i = 0; i < result.PositionData.size(); i++)
{
Vector3f originalPos = result.PositionData[i];
result.PositionData[i].v[0] = originalPos.v[1]; // New X = Old Y
result.PositionData[i].v[1] = originalPos.v[2]; // New Y = Old Z
result.PositionData[i].v[2] = originalPos.v[0]; // New Z = Old X
Vector3f originalNorm = result.NormalData[i];
result.NormalData[i].v[0] = originalNorm.v[1];
result.NormalData[i].v[1] = originalNorm.v[2];
result.NormalData[i].v[2] = originalNorm.v[0];
}
std::cout << "Model loaded: " << numberVertices << " verts, " << numberTriangles << " tris." << std::endl;
return result;
}
}

12
TextModel.h Normal file
View File

@ -0,0 +1,12 @@
#pragma once
#include "ZLMath.h"
#include "Renderer.h"
#include <unordered_map>
namespace ZL
{
VertexDataStruct LoadFromTextFile(const std::string& fileName, const std::string& ZIPFileName = "");
VertexDataStruct LoadFromTextFile02(const std::string& fileName, const std::string& ZIPFileName = "");
}

430
TextureManager.cpp Executable file
View File

@ -0,0 +1,430 @@
#include "TextureManager.h"
#ifdef PNG_ENABLED
#include "png.h"
#endif
#include <iostream>
namespace ZL
{
Texture::Texture(const TextureDataStruct& texData)
{
width = texData.width;
height = texData.height;
glGenTextures(1, &texID);
if (texID == 0)
{
throw std::runtime_error("glGenTextures did not work");
}
glBindTexture(GL_TEXTURE_2D, texID);
CheckGlError();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
CheckGlError();
//This should be only for Windows
//glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
CheckGlError();
if (texData.bitSize == TextureDataStruct::BS_24BIT)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, static_cast<GLsizei>(texData.width), static_cast<GLsizei>(texData.height), 0, GL_RGB, GL_UNSIGNED_BYTE, &texData.data[0]);
}
else
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<GLsizei>(texData.width), static_cast<GLsizei>(texData.height), 0, GL_RGBA, GL_UNSIGNED_BYTE, &texData.data[0]);
}
CheckGlError();
}
Texture::Texture(const std::array<TextureDataStruct, 6>& texDataArray)
{
// Ïðîâåðêà, ÷òî âñå ãðàíè èìåþò îäèíàêîâûå ðàçìåðû
width = texDataArray[0].width;
height = texDataArray[0].height;
for (size_t i = 1; i < 6; ++i) {
if (texDataArray[i].width != width || texDataArray[i].height != height) {
throw std::runtime_error("Cubemap faces must have the same dimensions");
}
}
glGenTextures(1, &texID);
if (texID == 0)
{
throw std::runtime_error("glGenTextures did not work for cubemap");
}
glBindTexture(GL_TEXTURE_CUBE_MAP, texID);
CheckGlError();
// Íàñòðîéêà ïàðàìåòðîâ äëÿ Cubemap
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Èñïîëüçóåì GL_LINEAR äëÿ MIN_FILTER, òàê êàê ìèïìàïû çäåñü íå ãåíåðèðóþòñÿ
// Åñëè áû èñïîëüçîâàëèñü ìèïìàïû (e.g., GL_LINEAR_MIPMAP_LINEAR), íóæíî áûëî áû âûçâàòü glGenerateMipmap.
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
// Îáÿçàòåëüíûå ïàðàìåòðû îáåðòêè äëÿ Cubemap
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// GL_TEXTURE_WRAP_R íå ïîääåðæèâàåòñÿ â WebGL 1.0/OpenGL ES 2.0 è âûçûâàåò îøèáêó.
// Îãðàíè÷èâàåì åãî âûçîâ òîëüêî äëÿ íàñòîëüíûõ ïëàòôîðì.
#ifndef EMSCRIPTEN
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
#endif
CheckGlError(); // Ïðîâåðêà ïîñëå óñòàíîâêè ïàðàìåòðîâ
// Çàãðóçêà äàííûõ äëÿ êàæäîé èç 6 ãðàíåé
// GL_TEXTURE_CUBE_MAP_POSITIVE_X + i äàåò ãðàíè: +X (0), -X (1), +Y (2), -Y (3), +Z (4), -Z (5)
for (int i = 0; i < 6; ++i)
{
GLint internalFormat;
GLenum format;
// Â WebGL 1.0/OpenGL ES 2.0 âíóòðåííèé ôîðìàò (internalFormat)
// äîëæåí ñòðîãî ñîîòâåòñòâîâàòü ôîðìàòó äàííûõ (format).
if (texDataArray[i].bitSize == TextureDataStruct::BS_24BIT)
{
internalFormat = GL_RGB; // internalFormat
format = GL_RGB; // format
}
else // BS_32BIT
{
internalFormat = GL_RGBA; // internalFormat
format = GL_RGBA; // format
}
glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, // Öåëåâàÿ ãðàíü
0, // Óðîâåíü MIP-òåêñòóðû
internalFormat, // Âíóòðåííèé ôîðìàò (äîëæåí ñîâïàäàòü ñ ôîðìàòîì)
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
0, // Ãðàíèöà (âñåãäà 0)
format, // Ôîðìàò èñõîäíûõ äàííûõ
GL_UNSIGNED_BYTE, // Òèï äàííûõ
texDataArray[i].data.data() // Óêàçàòåëü íà äàííûå
);
CheckGlError();
}
// Ñíèìàåì ïðèâÿçêó äëÿ ÷èñòîòû
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
}
Texture::~Texture()
{
glDeleteTextures(1, &texID);
texID = 0;
}
GLuint Texture::getTexID()
{
return texID;
}
size_t Texture::getWidth()
{
return width;
}
size_t Texture::getHeight()
{
return height;
}
TextureDataStruct CreateTextureDataFromBmp24(const std::string& fullFileName, const std::string& ZIPFileName)
{
TextureDataStruct texData;
std::vector<char> fileArr;
fileArr = !ZIPFileName.empty() ? readFileFromZIP(fullFileName, ZIPFileName) : readFile(fullFileName);
size_t fileSize = fileArr.size();
if (fileSize < 22)
{
throw std::runtime_error("File is too short or not correct!");
}
//This refers to BITMAPV5HEADER
texData.width = *reinterpret_cast<uint32_t*>(&fileArr[18]);
texData.height = *reinterpret_cast<uint32_t*>(&fileArr[22]);
texData.bitSize = TextureDataStruct::BS_24BIT;
size_t dataSize = texData.width * texData.height * 3;
texData.data.resize(dataSize);
size_t pos = *reinterpret_cast<uint32_t*>(&fileArr[10]);
size_t x = 0;
for (size_t i = 0; i < texData.width; i++)
for (size_t j = 0; j < texData.height; j++)
{
if (pos + 3 > fileSize)
{
throw std::runtime_error("File is too short!");
}
x = (i * texData.height + j) + (i * texData.height + j) + (i * texData.height + j);
texData.data[x + 2] = fileArr[pos++];
texData.data[x + 1] = fileArr[pos++];
texData.data[x + 0] = fileArr[pos++];
}
return texData;
}
TextureDataStruct CreateTextureDataFromBmp32(const std::string& fullFileName, const std::string& ZIPFileName)
{
TextureDataStruct texData;
std::vector<char> fileArr;
fileArr = !ZIPFileName.empty() ? readFileFromZIP(fullFileName, ZIPFileName) : readFile(fullFileName);
size_t fileSize = fileArr.size();
if (fileSize < 22)
{
throw std::runtime_error("File is too short or not correct!");
}
//This refers to BITMAPV5HEADER
texData.width = *reinterpret_cast<uint32_t*>(&fileArr[18]);
texData.height = *reinterpret_cast<uint32_t*>(&fileArr[22]);
texData.bitSize = TextureDataStruct::BS_32BIT;
size_t dataSize = texData.width * texData.height * 4;
texData.data.resize(dataSize);
size_t pos = *reinterpret_cast<uint32_t*>(&fileArr[10]);
size_t x = 0;
for (size_t i = 0; i < texData.width; i++)
for (size_t j = 0; j < texData.height; j++)
{
if (pos + 4 > fileSize)
{
throw std::runtime_error("File is too short!");
}
x = (i * texData.height + j) + (i * texData.height + j) + (i * texData.height + j) + (i * texData.height + j);
texData.data[x + 2] = fileArr[pos++];
texData.data[x + 1] = fileArr[pos++];
texData.data[x + 0] = fileArr[pos++];
texData.data[x + 3] = fileArr[pos++];
}
return texData;
}
#ifdef PNG_ENABLED
// Ñòðóêòóðà äëÿ õðàíåíèÿ äàííûõ î ôàéëå/ìàññèâå è òåêóùåé ïîçèöèè ÷òåíèÿ
struct png_data_t {
const char* data;
size_t size;
size_t offset;
};
// Ïîëüçîâàòåëüñêàÿ ôóíêöèÿ ÷òåíèÿ äëÿ libpng
// 'png_ptr' - óêàçàòåëü íà ñòðóêòóðó png
// 'out_ptr' - êóäà çàïèñûâàòü ïðî÷èòàííûå äàííûå
// 'bytes_to_read' - ñêîëüêî áàéò íóæíî ïðî÷èòàòü
void user_read_data(png_structp png_ptr, png_bytep out_ptr, png_size_t bytes_to_read) {
// Ïîëó÷àåì óêàçàòåëü íà íàøó ñòðóêòóðó png_data_t, êîòîðóþ ìû óñòàíîâèëè ñ ïîìîùüþ png_set_read_fn
png_data_t* data = (png_data_t*)png_get_io_ptr(png_ptr);
if (data->offset + bytes_to_read > data->size) {
// Ïîïûòêà ïðî÷èòàòü áîëüøå, ÷åì åñòü â ìàññèâå.
// Âìåñòî âûçîâà ñòàíäàðòíîé îøèáêè, ìû ìîæåì ïðîñòî ïðî÷èòàòü îñòàòîê èëè âûçâàòü îøèáêó.
//  ýòîì ñëó÷àå ìû âûçîâåì îøèáêó libpng.
png_error(png_ptr, "PNG Read Error: Attempted to read past end of data buffer.");
bytes_to_read = data->size - data->offset; // Óñòàíàâëèâàåì, ÷òîáû ïðî÷èòàòü îñòàâøååñÿ
}
// Êîïèðóåì äàííûå èç íàøåãî ìàññèâà â áóôåð libpng
std::memcpy(out_ptr, data->data + data->offset, bytes_to_read);
// Îáíîâëÿåì ñìåùåíèå
data->offset += bytes_to_read;
}
// Ïîëüçîâàòåëüñêàÿ ôóíêöèÿ ïðåäóïðåæäåíèé (ïî æåëàíèþ, ìîæíî èñïîëüçîâàòü nullptr)
void user_warning_fn(png_structp png_ptr, png_const_charp warning_msg) {
// Çäåñü ìîæíî ðåàëèçîâàòü ëîãèðîâàíèå ïðåäóïðåæäåíèé
//throw std::runtime_error();
std::cout << "PNG Warning: " << warning_msg << std::endl;
}
// Ïîëüçîâàòåëüñêàÿ ôóíêöèÿ îøèáîê (îáÿçàòåëüíà äëÿ setjmp)
void user_error_fn(png_structp png_ptr, png_const_charp error_msg) {
// Çäåñü ìîæíî ðåàëèçîâàòü ëîãèðîâàíèå îøèáîê
std::cout << "PNG Error: " << error_msg << std::endl;
// Îáÿçàòåëüíî âûçûâàåì longjmp äëÿ âûõîäà èç ïðîöåññà ÷òåíèÿ/çàïèñè PNG
longjmp(png_jmpbuf(png_ptr), 1);
}
TextureDataStruct CreateTextureDataFromPng(const std::vector<char>& fileArr)
{
TextureDataStruct texData;
// Ñòðóêòóðà äëÿ óïðàâëåíèÿ ÷òåíèåì èç ìàññèâà
png_data_t png_data = { fileArr.data(), fileArr.size(), 0 };
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png) {
throw std::runtime_error("Could not create PNG read structure");
}
png_infop info = png_create_info_struct(png);
if (!info) {
png_destroy_read_struct(&png, nullptr, nullptr);
throw std::runtime_error("Could not create PNG info structure");
}
// === Óñòàíîâêà ïîëüçîâàòåëüñêèõ ôóíêöèé ÷òåíèÿ è îáðàáîòêè îøèáîê ===
// 1. Óñòàíîâêà îáðàáîò÷èêà îøèáîê è longjmp
if (setjmp(png_jmpbuf(png))) {
png_destroy_read_struct(&png, &info, nullptr);
throw std::runtime_error("Error during PNG read (longjmp was executed)");
}
// 2. Óñòàíîâêà ïîëüçîâàòåëüñêèõ ôóíêöèé äëÿ îáðàáîòêè îøèáîê è ïðåäóïðåæäåíèé
// Âìåñòî nullptr â error_ptr è warning_ptr ìîæíî ïåðåäàòü óêàçàòåëü íà ñâîþ ñòðóêòóðó äàííûõ, åñëè íåîáõîäèìî
png_set_error_fn(png, nullptr, user_error_fn, user_warning_fn);
// 3. Óñòàíîâêà ïîëüçîâàòåëüñêîé ôóíêöèè ÷òåíèÿ è ïåðåäà÷à åé íàøåé ñòðóêòóðû png_data
png_set_read_fn(png, &png_data, user_read_data);
// ===================================================================
png_read_info(png, info);
texData.width = png_get_image_width(png, info);
texData.height = png_get_image_height(png, info);
png_byte color_type = png_get_color_type(png, info);
png_byte bit_depth = png_get_bit_depth(png, info);
// === Áëîê ïðåîáðàçîâàíèé (îñòàâëåí áåç èçìåíåíèé) ===
if (bit_depth == 16)
png_set_strip_16(png);
if (color_type == PNG_COLOR_TYPE_PALETTE)
png_set_palette_to_rgb(png);
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
png_set_expand_gray_1_2_4_to_8(png);
if (png_get_valid(png, info, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(png);
if (color_type == PNG_COLOR_TYPE_RGB ||
color_type == PNG_COLOR_TYPE_GRAY ||
color_type == PNG_COLOR_TYPE_PALETTE)
png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
if (color_type == PNG_COLOR_TYPE_GRAY ||
color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
png_set_gray_to_rgb(png);
png_read_update_info(png, info);
// ====================================================
// === ×òåíèå ïèêñåëåé (îñòàâëåí áåç èçìåíåíèé) ===
png_bytep* row_pointers = (png_bytep*)malloc(sizeof(png_bytep) * texData.height);
for (int y = 0; y < texData.height; y++) {
row_pointers[y] = (png_byte*)malloc(png_get_rowbytes(png, info));
}
png_read_image(png, row_pointers);
bool has_alpha = (color_type & PNG_COLOR_MASK_ALPHA) || (png_get_valid(png, info, PNG_INFO_tRNS));
size_t dataSize;
if (has_alpha)
{
texData.bitSize = TextureDataStruct::BS_32BIT;
}
else
{
texData.bitSize = TextureDataStruct::BS_24BIT;
}
int channels = has_alpha ? 4 : 3;
dataSize = texData.width * texData.height * channels;
texData.data.resize(dataSize);
for (int y = texData.height - 1; y >= 0; y--) {
png_bytep row = row_pointers[texData.height - 1 - y];
for (int x = 0; x < texData.width; x++) {
png_bytep px = &(row[x * 4]);
texData.data[(y * texData.width + x) * channels + 0] = px[0]; // R
texData.data[(y * texData.width + x) * channels + 1] = px[1]; // G
texData.data[(y * texData.width + x) * channels + 2] = px[2]; // B
if (has_alpha) {
texData.data[(y * texData.width + x) * channels + 3] = px[3]; // A
}
}
free(row_pointers[texData.height - 1 - y]);
}
free(row_pointers);
// ==================================================
png_destroy_read_struct(&png, &info, nullptr);
return texData;
}
TextureDataStruct CreateTextureDataFromPng(const std::string& fullFileName, const std::string& ZIPFileName)
{
std::vector<char> fileArr;
fileArr = !ZIPFileName.empty() ? readFileFromZIP(fullFileName, ZIPFileName) : readFile(fullFileName);
if (fileArr.empty()) {
throw std::runtime_error("Could not read file data into memory");
}
// Âûçûâàåì íîâóþ ôóíêöèþ, êîòîðàÿ ðàáîòàåò ñ ìàññèâîì áàéò
return CreateTextureDataFromPng(fileArr);
}
#endif
}

56
TextureManager.h Executable file
View File

@ -0,0 +1,56 @@
#pragma once
#include "OpenGlExtensions.h"
#include "Utils.h"
#ifdef EMSCRIPTEN
#define PNG_ENABLED
#endif
namespace ZL
{
struct TextureDataStruct
{
size_t width;
size_t height;
std::vector<char> data;
enum BitSize {
BS_24BIT,
BS_32BIT
};
BitSize bitSize;
};
class Texture
{
size_t width = 0;
size_t height = 0;
GLuint texID = 0;
public:
Texture(const TextureDataStruct& texData);
//Cubemap texture:
Texture(const std::array<TextureDataStruct, 6>& texDataArray);
~Texture();
GLuint getTexID();
size_t getWidth();
size_t getHeight();
};
TextureDataStruct CreateTextureDataFromBmp24(const std::string& fullFileName, const std::string& ZIPFileName="");
TextureDataStruct CreateTextureDataFromBmp32(const std::string& fullFileName, const std::string& ZIPFileName="");
#ifdef PNG_ENABLED
TextureDataStruct CreateTextureDataFromPng(const std::string& fullFileName, const std::string& ZIPFileName = "");
#endif
}

527
UI.md
View File

@ -1,527 +0,0 @@
# UI System
UI layouts are defined in JSON files and loaded at runtime by `UiManager`. Each file has a single `"root"` node that is the top-level container.
The coordinate system has the origin at the **bottom-left** of the screen. Y increases upward.
The virtual canvas size is defined by `Environment::projectionWidth` × `Environment::projectionHeight`.
```json
{
"root": { ... }
}
```
---
## Common Node Properties
These properties are available on every node type.
| Property | Type | Default | Description |
|---|---|---|---|
| `name` | string | `""` | Unique name used to find the node from C++ code |
| `x` | float | `0` | Horizontal offset from the parent's origin (or gravity-adjusted position) |
| `y` | float | `0` | Vertical offset |
| `width` | float \| `"match_parent"` | `0` | Width in virtual pixels. `"match_parent"` fills the parent |
| `height` | float \| `"match_parent"` | `0` | Height in virtual pixels |
| `horizontal_gravity` | `"left"` \| `"center"` \| `"right"` | `"left"` | Positions the node horizontally inside a **FrameLayout** parent |
| `vertical_gravity` | `"bottom"` \| `"center"` \| `"top"` | `"bottom"` | Positions the node vertically inside a **FrameLayout** parent |
| `visible` | bool | `true` | Whether the node (and all its children) are rendered and interactive. Can be toggled at runtime via `setNodeVisible` |
---
## Containers
### FrameLayout
Children are positioned using absolute `x`/`y` offsets and/or `horizontal_gravity` / `vertical_gravity`.
```json
{
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [ ... ]
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `children` | array | `[]` | Child nodes |
---
### LinearLayout
Children are stacked automatically in a row or column. Gravity and align properties control the layout of the block and its children.
```json
{
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 10,
"width": 400,
"height": 600,
"children": [ ... ]
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `orientation` | `"vertical"` \| `"horizontal"` | `"vertical"` | Direction children are stacked |
| `spacing` | float | `0` | Gap in pixels between consecutive children |
| `vertical_align` | `"top"` \| `"center"` \| `"bottom"` | `"top"` | **Vertical** alignment of the child block inside this layout. For vertical orientation, controls how the whole stack is aligned; for horizontal orientation, controls each child's cross-axis alignment |
| `horizontal_align` | `"left"` \| `"center"` \| `"right"` | `"left"` | **Horizontal** alignment of the child block. For horizontal orientation, controls how the whole row is aligned; for vertical orientation, controls each child's cross-axis alignment |
| `children` | array | `[]` | Child nodes, laid out in order |
---
## Widgets
### Button
An image-only clickable button. Swaps textures on hover/press.
```json
{
"type": "Button",
"name": "closeButton",
"width": 90,
"height": 90,
"x": 580,
"y": 240,
"horizontal_gravity": "center",
"vertical_gravity": "center",
"textures": {
"normal": "resources/w/ui/img/Close001_State=Default.png",
"hover": "resources/w/ui/img/Close001_State=Selected.png",
"pressed": "resources/w/ui/img/Close001_State=Tap.png",
"disabled": "resources/w/ui/img/Close001_State=Disabled.png"
},
"border": 4,
"clickZoneWidth": 80,
"clickZoneHeight": 80
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `textures.normal` | string | — | Texture path shown in the default state (**required**) |
| `textures.hover` | string | — | Texture path shown when the mouse hovers |
| `textures.pressed` | string | — | Texture path shown while pressed |
| `textures.disabled` | string | — | Texture path shown when the button is disabled |
| `border` | float | `0` | Inset (pixels) applied to the hit-test zone on all sides |
| `clickZoneWidth` | float | `0` | Explicit hit-test width; `0` uses the widget width |
| `clickZoneHeight` | float | `0` | Explicit hit-test height; `0` uses the widget height |
**C++ callbacks:**
```cpp
uiManager.setButtonCallback("closeButton", [](const std::string&) { /* click */ });
uiManager.setButtonPressCallback("closeButton", [](const std::string&) { /* press */ });
```
---
### TextButton
A button that renders a text label on top of an optional background texture.
```json
{
"type": "TextButton",
"name": "item1name",
"width": 270,
"height": 60,
"text": "Main Quest",
"fontSize": 32,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": false,
"topAligned": false,
"textPaddingX": 12,
"textPaddingY": -8,
"wrap": true,
"color": [1.0, 1.0, 1.0, 1.0],
"textures": {
"normal": "resources/w/red.png",
"hover": "resources/w/red.png",
"pressed": "resources/w/red.png"
},
"border": 0,
"clickZoneWidth": 0,
"clickZoneHeight": 0
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `text` | string | `""` | Label text. Supports Cyrillic and any codepoint in `resources/symbols.txt` |
| `fontSize` | int | `32` | Font size in pixels |
| `fontPath` | string | `"resources/fonts/DroidSans.ttf"` | Path to the TTF font file |
| `textCentered` | bool | `true` | Horizontally centers the text within the widget. When `false`, text starts at `textPaddingX` from the left edge |
| `topAligned` | bool | `false` | When `true`, the first text line is placed near the top of the widget; when `false`, the text is vertically centered |
| `textPaddingX` | float | `12` | Left padding when `textCentered` is `false`; also used to compute the wrapping width |
| `textPaddingY` | float | `0` | Vertical offset applied to the text baseline |
| `wrap` | bool | `false` | Wraps text that exceeds `width - textPaddingX * 2` pixels |
| `color` | [R, G, B, A] | `[1,1,1,1]` | Text color, each channel 0..1 |
| `textures.*` | string | — | Background textures (all optional — button can be text-only) |
| `border` | float | `0` | Hit-test inset |
| `clickZoneWidth` / `clickZoneHeight` | float | `0` | Explicit hit-test size; `0` uses the widget size |
**C++ callbacks:**
```cpp
uiManager.setTextButtonCallback("item1name", [](const std::string&) { /* click */ });
uiManager.setTextButtonPressCallback("item1name", [](const std::string&) { /* press */ });
// Programmatic updates
uiManager.setTextButtonText("item1name", "New Quest Name");
uiManager.setTextButtonColor("item1name", {1.f, 0.f, 0.f, 1.f});
```
---
### TextView
A non-interactive text display widget.
```json
{
"type": "TextView",
"name": "quest_description",
"x": 170,
"y": 390,
"width": 1000,
"height": 300,
"text": "Long description here.",
"fontSize": 32,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": false,
"topAligned": true,
"wrap": true,
"paddingX": 0,
"paddingY": 4,
"maxLines": 10,
"color": [1.0, 1.0, 0.0, 1.0]
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `text` | string | `""` | Display text. Supports Cyrillic and any codepoint in `resources/symbols.txt` |
| `fontSize` | int | `32` | Font size in pixels |
| `fontPath` | string | `"resources/fonts/DroidSans.ttf"` | Path to the TTF font file |
| `textCentered` | bool | `true` | Horizontally centers the text when `true`; left-aligns from `paddingX` when `false` |
| `topAligned` | bool | `false` | When `true`, the first line is placed near the top edge; when `false`, text is vertically centered |
| `wrap` | bool | `false` | Wraps text at `width - paddingX * 2` pixels |
| `paddingX` | float | `0` | Left/right padding used for alignment and wrap width |
| `paddingY` | float | `0` | Vertical inset applied when `topAligned` is `true` |
| `maxLines` | int | `0` | Maximum number of lines to display; `0` means unlimited. Truncated text gets `...` |
| `color` | [R, G, B, A] | `[1,1,1,1]` | Text color |
> **Legacy note:** If none of `wrap`, `topAligned`, `paddingX`, `paddingY`, or `maxLines` are set, the text is drawn centered on `(x + width/2, y + height/2)` for backward compatibility.
**C++ updates:**
```cpp
uiManager.setText("quest_description", "New text here.");
uiManager.setTextColor("quest_description", {1.f, 1.f, 0.f, 1.f});
```
---
### TextField
An interactive single-line text input field. Receives keyboard input when focused.
```json
{
"type": "TextField",
"name": "playerName",
"x": 100,
"y": 300,
"width": 400,
"height": 50,
"placeholder": "Enter name...",
"fontSize": 28,
"fontPath": "resources/fonts/DroidSans.ttf",
"maxLength": 64,
"color": [1.0, 1.0, 1.0, 1.0],
"placeholderColor": [0.5, 0.5, 0.5, 1.0],
"backgroundColor": [0.2, 0.2, 0.2, 1.0],
"borderColor": [0.5, 0.5, 0.5, 1.0]
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `placeholder` | string | `""` | Text shown when the field is empty |
| `fontSize` | int | `32` | Font size in pixels |
| `fontPath` | string | `"resources/fonts/DroidSans.ttf"` | Path to the TTF font file |
| `maxLength` | int | `256` | Maximum number of characters |
| `color` | [R, G, B, A] | `[1,1,1,1]` | Input text color |
| `placeholderColor` | [R, G, B, A] | `[0.5,0.5,0.5,1]` | Placeholder text color |
| `backgroundColor` | [R, G, B, A] | `[0.2,0.2,0.2,1]` | Field background color |
| `borderColor` | [R, G, B, A] | `[0.5,0.5,0.5,1]` | Border color |
**C++ callbacks and queries:**
```cpp
uiManager.setTextFieldCallback("playerName", [](const std::string& name, const std::string& value) {
// called on every keystroke
});
std::string current = uiManager.getTextFieldValue("playerName");
```
---
### Slider
A draggable slider that returns a normalized value in the range `[0, 1]`.
```json
{
"type": "Slider",
"name": "volumeSlider",
"x": 100,
"y": 200,
"width": 40,
"height": 300,
"orientation": "vertical",
"value": 0.75,
"textures": {
"track": "resources/w/ui/img/slider_track.png",
"knob": "resources/w/ui/img/slider_knob.png"
}
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `textures.track` | string | — | Texture for the slider track |
| `textures.knob` | string | — | Texture for the draggable knob |
| `orientation` | `"vertical"` \| `"horizontal"` | `"vertical"` | Drag direction |
| `value` | float | `0` | Initial normalized value `[0, 1]` |
**C++ callbacks:**
```cpp
uiManager.setSliderCallback("volumeSlider", [](const std::string& name, float value) {
// value is 0..1
});
uiManager.setSliderValue("volumeSlider", 0.5f);
```
---
### StaticImage
A non-interactive image. Supports optional fade-in and pulse-scale animations.
```json
{
"type": "StaticImage",
"name": "background",
"width": 1266,
"height": 585,
"horizontal_gravity": "center",
"vertical_gravity": "center",
"texture": "resources/w/ui/img/journal/QuestJournal003.png",
"fadeIn": {
"durationMs": 600
},
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
}
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `texture` | string | — | Path to the PNG texture |
| `fadeIn.durationMs` | float | — | If present, the image fades in over this many milliseconds each time the UI is shown |
| `pulse.minScale` | float | `0.9` | Minimum scale during the pulse cycle |
| `pulse.maxScale` | float | `1.1` | Maximum scale during the pulse cycle |
| `pulse.periodMs` | float | `1000` | Duration of one full pulse cycle in milliseconds |
**C++ pop-in animation** (scales the node from 0 → 1, ease-out quad):
```cpp
uiManager.startPopIn("background", 300.0f); // duration in milliseconds
```
Typically called immediately after making a node visible. The node is automatically removed from the animation list when the scale reaches 1.
---
## Touch / Click Priority
All `Button` and `TextButton` nodes in a layout are collected into a single ordered list during `collectButtonsAndSliders` (depth-first traversal of the node tree, which matches JSON declaration order). When a touch or mouse-down event arrives, the list is scanned **in reverse** — later-declared nodes are checked first — and the **first hit wins**. At most one element (button or textButton, regardless of type) fires per touch.
**Practical rule:** place background "catch-all" elements (e.g. a full-screen transparent exit button) **early** in the JSON, and foreground interactive elements **later**. The later-declared element will always win when they overlap.
```json
{
"type": "FrameLayout",
"children": [
{
"type": "Button",
"name": "phoneExitButton", // declared first → lowest priority
"width": "match_parent",
"height": "match_parent",
"textures": { "normal": "resources/transparent.png", ... }
},
{
"type": "TextButton",
"name": "chat2button", // declared later → wins over phoneExitButton
...
}
]
}
```
This behaviour is consistent across `Button` and `TextButton` — there is no inherent type priority, only declaration order matters.
---
## Animations
Animations can be defined on **Button** and **TextButton** nodes and started from C++ code. Each animation is a named sequence of steps.
```json
{
"type": "Button",
"name": "myButton",
"width": 100,
"height": 100,
"textures": { "normal": "resources/w/btn.png" },
"animations": {
"bounce": {
"repeat": false,
"steps": [
{ "type": "move", "to": [0, 20], "duration": 0.15, "easing": "easeout" },
{ "type": "move", "to": [0, 0], "duration": 0.15, "easing": "easein" },
{ "type": "wait", "duration": 0.1 }
]
},
"pulse": {
"repeat": true,
"steps": [
{ "type": "scale", "to": [1.1, 1.1], "duration": 0.4, "easing": "easeout" },
{ "type": "scale", "to": [1.0, 1.0], "duration": 0.4, "easing": "easein" }
]
}
}
}
```
### Animation sequence properties
| Property | Type | Default | Description |
|---|---|---|---|
| `repeat` | bool | `false` | Whether the sequence loops after the last step |
| `steps` | array | — | Ordered list of animation steps |
### Step properties
| Property | Type | Description |
|---|---|---|
| `type` | `"move"` \| `"scale"` \| `"wait"` | Step kind |
| `to` | [x, y] | Target offset (`move`) or scale factors (`scale`) |
| `duration` | float (seconds) | Duration of the step. `0` applies the target instantly |
| `easing` | `"linear"` \| `"easein"` \| `"easeout"` | Interpolation curve (default `"linear"`) |
**C++ control:**
```cpp
uiManager.startAnimationOnNode("myButton", "bounce");
uiManager.stopAnimationOnNode("myButton", "bounce");
uiManager.setAnimationCallback("myButton", "bounce", []() {
// called when the non-repeating sequence finishes
});
```
---
## C++ API Quick Reference
### Loading and navigation
```cpp
uiManager.loadFromFile("resources/w/ui/screen.json", renderer);
uiManager.pushMenuFromFile("resources/w/ui/popup.json", renderer); // push on stack
uiManager.popMenu(); // restore previous UI
uiManager.clearMenuStack();
```
### Finding nodes
```cpp
auto node = uiManager.findNode("myNode");
auto btn = uiManager.findButton("myButton");
auto tbtn = uiManager.findTextButton("item1name");
auto tv = uiManager.findTextView("quest_description");
auto img = uiManager.findStaticImage("background");
auto slider = uiManager.findSlider("volumeSlider");
auto tf = uiManager.findTextField("playerName");
```
### Visibility
```cpp
uiManager.setNodeVisible("hint5", false);
bool visible = uiManager.getNodeVisible("hint5");
```
### Pop-in animation
Scales a node from 0 to 1 using an ease-out curve. Useful for chat bubble reveals and similar "appear" effects.
```cpp
uiManager.startPopIn("messageBubble", 300.0f); // node name, duration ms
```
Set the node's `scaleX`/`scaleY` to `0` and call `setNodeVisible` before calling `startPopIn` to avoid a one-frame flash at full size.
### Dynamic node repositioning
`node->localY` (and `localX`) can be modified directly on a node pointer, then a layout recalculation applied:
```cpp
auto node = uiManager.findNode("messageBubble");
node->localY = 350.0f; // new bottom-Y (for vertical_gravity: bottom nodes)
uiManager.updateAllLayouts(); // recomputes screenRect and rebuilds meshes
```
This is how the phone chat manager repositions bubbles as new messages arrive.
### Per-frame update
```cpp
uiManager.update(deltaMs); // advance animations and fade-ins
uiManager.draw(renderer); // render everything
```
---
## Dialogue → UI integration (phone chat bubbles)
Dialogue nodes in JSON can carry a `"bubbleSlot"` field naming a `StaticImage` UI node. When the dialogue runtime presents that line, it fires the `onBubbleSlotReady` callback with the slot name, which the game uses to reveal the corresponding bubble image.
```json
{
"id": "line_1",
"type": "Line",
"speaker": "Айпери",
"text": "...",
"next": "line_2",
"bubbleSlot": "message01in"
}
```
Lines without `"bubbleSlot"` (or with an empty value) do not trigger any UI change — useful for internal monologue lines that have no corresponding chat image.
**C++ wiring:**
```cpp
dialogueSystem.setOnBubbleSlotReady([](const std::string& slotName) {
// slotName == "message01in" etc.
menuManager.revealPhoneChatBubble(slotName);
});
```

105
Utils.cpp Executable file
View File

@ -0,0 +1,105 @@
#include "Utils.h"
#include <cstring>
#include <iterator>
#include <vector>
#include <iostream>
#include <algorithm>
#include <fstream>
#ifdef EMSCRIPTEN
#include <zip.h>
#endif
namespace ZL
{
std::string readTextFile(const std::string& filename)
{
std::ifstream f(filename);
std::string str((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
return str;
}
std::vector<char> readFile(const std::string& filename)
{
std::ifstream file(filename, std::ios::binary);
file.unsetf(std::ios::skipws);
std::streampos fileSize;
file.seekg(0, std::ios::end);
fileSize = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<char> vec;
vec.reserve(fileSize);
vec.insert(vec.begin(),
std::istream_iterator<char>(file),
std::istream_iterator<char>());
return vec;
}
std::vector<char> readFileFromZIP(const std::string& filename, const std::string& zipfilename) {
#ifdef EMSCRIPTEN
const std::string zipPath = zipfilename;
int zipErr;
zip_t* archive = zip_open(zipPath.c_str(), ZIP_RDONLY, &zipErr);
if (!archive) {
throw std::runtime_error("Ошибка открытия ZIP: " + zipPath);
}
std::string cleanFilename = filename;
if (cleanFilename.rfind("./", 0) == 0) {
cleanFilename = cleanFilename.substr(2);
}
std::cout << "Ищем в ZIP: " << cleanFilename << std::endl;
zip_file_t* zipFile = zip_fopen(archive, cleanFilename.c_str(), 0);
if (!zipFile) {
zip_close(archive);
throw std::runtime_error("Файл не найден в ZIP: " + cleanFilename);
}
zip_stat_t fileStat;
if (zip_stat(archive, cleanFilename.c_str(), 0, &fileStat) != 0) {
zip_fclose(zipFile);
zip_close(archive);
throw std::runtime_error("Ошибка чтения ZIP-статистики.");
}
std::vector<char> fileData;
fileData.resize(fileStat.size);
zip_fread(zipFile, fileData.data(), fileData.size());
zip_fclose(zipFile);
zip_close(archive);
return fileData;
#else
return {};
#endif
}
bool findString(const char* in, char* list)
{
size_t thisLength = strlen(in);
while (*list != 0)
{
size_t length = strcspn(list, " ");
if (thisLength == length)
if (!strncmp(in, list, length))
return true;
list += length;
list += 1;
}
return false;
}
};

20
Utils.h Executable file
View File

@ -0,0 +1,20 @@
#pragma once
#include <string>
#include <vector>
#include <exception>
#include <map>
#include <stack>
#include <memory>
#include <unordered_map>
namespace ZL
{
std::string readTextFile(const std::string& filename);
std::vector<char> readFile(const std::string& filename);
std::vector<char> readFileFromZIP(const std::string& filename, const std::string& zipfilename);
bool findString(const char* in, char* list);
}

799
ZLMath.cpp Normal file
View File

@ -0,0 +1,799 @@
#include "ZLMath.h"
#include <exception>
#include <cmath>
namespace ZL {
Vector2f operator+(const Vector2f& x, const Vector2f& y)
{
Vector2f result;
result.v[0] = x.v[0] + y.v[0];
result.v[1] = x.v[1] + y.v[1];
return result;
}
Vector2f operator-(const Vector2f& x, const Vector2f& y)
{
Vector2f result;
result.v[0] = x.v[0] - y.v[0];
result.v[1] = x.v[1] - y.v[1];
return result;
}
Vector3f operator+(const Vector3f& x, const Vector3f& y)
{
Vector3f result;
result.v[0] = x.v[0] + y.v[0];
result.v[1] = x.v[1] + y.v[1];
result.v[2] = x.v[2] + y.v[2];
return result;
}
Vector3f operator-(const Vector3f& x, const Vector3f& y)
{
Vector3f result;
result.v[0] = x.v[0] - y.v[0];
result.v[1] = x.v[1] - y.v[1];
result.v[2] = x.v[2] - y.v[2];
return result;
}
Vector3f operator-(const Vector3f& x)
{
Vector3f result;
result.v[0] = -x.v[0];
result.v[1] = -x.v[1];
result.v[2] = -x.v[2];
return result;
}
Vector4f operator+(const Vector4f& x, const Vector4f& y)
{
Vector4f result;
result.v[0] = x.v[0] + y.v[0];
result.v[1] = x.v[1] + y.v[1];
result.v[2] = x.v[2] + y.v[2];
result.v[3] = x.v[3] + y.v[3];
return result;
}
Vector4f operator-(const Vector4f& x, const Vector4f& y)
{
Vector4f result;
result.v[0] = x.v[0] - y.v[0];
result.v[1] = x.v[1] - y.v[1];
result.v[2] = x.v[2] - y.v[2];
result.v[3] = x.v[3] - y.v[3];
return result;
}
Matrix3f Matrix3f::Identity()
{
Matrix3f r;
r.m[0] = 1.f;
r.m[1] = 0.f;
r.m[2] = 0.f;
r.m[3] = 0.f;
r.m[4] = 1.f;
r.m[5] = 0.f;
r.m[6] = 0.f;
r.m[7] = 0.f;
r.m[8] = 1.f;
return r;
}
Matrix4f Matrix4f::Identity()
{
Matrix4f r;
r.m[0] = 1.f;
r.m[1] = 0.f;
r.m[2] = 0.f;
r.m[3] = 0.f;
r.m[4] = 0.f;
r.m[5] = 1.f;
r.m[6] = 0.f;
r.m[7] = 0.f;
r.m[8] = 0.f;
r.m[9] = 0.f;
r.m[10] = 1.f;
r.m[11] = 0.f;
r.m[12] = 0.f;
r.m[13] = 0.f;
r.m[14] = 0.f;
r.m[15] = 1.f;
return r;
}
Matrix4f operator*(const Matrix4f& m1, const Matrix4f& m2)
{
Matrix4f r;
r.m[0] = m1.m[0] * m2.m[0] + m1.m[4] * m2.m[1] + m1.m[8] * m2.m[2] + m1.m[12] * m2.m[3];
r.m[1] = m1.m[1] * m2.m[0] + m1.m[5] * m2.m[1] + m1.m[9] * m2.m[2] + m1.m[13] * m2.m[3];
r.m[2] = m1.m[2] * m2.m[0] + m1.m[6] * m2.m[1] + m1.m[10] * m2.m[2] + m1.m[14] * m2.m[3];
r.m[3] = m1.m[3] * m2.m[0] + m1.m[7] * m2.m[1] + m1.m[11] * m2.m[2] + m1.m[15] * m2.m[3];
r.m[4] = m1.m[0] * m2.m[4] + m1.m[4] * m2.m[5] + m1.m[8] * m2.m[6] + m1.m[12] * m2.m[7];
r.m[5] = m1.m[1] * m2.m[4] + m1.m[5] * m2.m[5] + m1.m[9] * m2.m[6] + m1.m[13] * m2.m[7];
r.m[6] = m1.m[2] * m2.m[4] + m1.m[6] * m2.m[5] + m1.m[10] * m2.m[6] + m1.m[14] * m2.m[7];
r.m[7] = m1.m[3] * m2.m[4] + m1.m[7] * m2.m[5] + m1.m[11] * m2.m[6] + m1.m[15] * m2.m[7];
r.m[8] = m1.m[0] * m2.m[8] + m1.m[4] * m2.m[9] + m1.m[8] * m2.m[10] + m1.m[12] * m2.m[11];
r.m[9] = m1.m[1] * m2.m[8] + m1.m[5] * m2.m[9] + m1.m[9] * m2.m[10] + m1.m[13] * m2.m[11];
r.m[10] = m1.m[2] * m2.m[8] + m1.m[6] * m2.m[9] + m1.m[10] * m2.m[10] + m1.m[14] * m2.m[11];
r.m[11] = m1.m[3] * m2.m[8] + m1.m[7] * m2.m[9] + m1.m[11] * m2.m[10] + m1.m[15] * m2.m[11];
r.m[12] = m1.m[0] * m2.m[12] + m1.m[4] * m2.m[13] + m1.m[8] * m2.m[14] + m1.m[12] * m2.m[15];
r.m[13] = m1.m[1] * m2.m[12] + m1.m[5] * m2.m[13] + m1.m[9] * m2.m[14] + m1.m[13] * m2.m[15];
r.m[14] = m1.m[2] * m2.m[12] + m1.m[6] * m2.m[13] + m1.m[10] * m2.m[14] + m1.m[14] * m2.m[15];
r.m[15] = m1.m[3] * m2.m[12] + m1.m[7] * m2.m[13] + m1.m[11] * m2.m[14] + m1.m[15] * m2.m[15];
return r;
}
Matrix4f MakeOrthoMatrix(float width, float height, float zNear, float zFar)
{
float depthRange = zFar - zNear;
if (depthRange <= 0)
{
throw std::runtime_error("zFar must be greater than zNear");
}
Matrix4f r;
r.m[0] = 2.f / width;
r.m[1] = 0;
r.m[2] = 0;
r.m[3] = 0;
r.m[4] = 0;
r.m[5] = 2.f / height;
r.m[6] = 0;
r.m[7] = 0;
r.m[8] = 0;
r.m[9] = 0;
r.m[10] = -1 / depthRange;
r.m[11] = 0;
r.m[12] = -1;
r.m[13] = -1;
r.m[14] = zNear / depthRange;
r.m[15] = 1;
return r;
}
Matrix4f MakeOrthoMatrix(float xmin, float xmax, float ymin, float ymax, float zNear, float zFar)
{
float width = xmax - xmin;
float height = ymax - ymin;
float depthRange = zFar - zNear;
if (width <= 0 || height <= 0 || depthRange <= 0)
{
throw std::runtime_error("Invalid dimensions for orthogonal matrix");
}
Matrix4f r;
// Масштабирование
r.m[0] = 2.f / width;
r.m[5] = 2.f / height;
r.m[10] = -1.f / depthRange;
// Обнуление неиспользуемых компонентов
r.m[1] = r.m[2] = r.m[3] = 0;
r.m[4] = r.m[6] = r.m[7] = 0;
r.m[8] = r.m[9] = r.m[11] = 0;
// Трансляция (смещение)
r.m[12] = -(xmax + xmin) / width;
r.m[13] = -(ymax + ymin) / height;
r.m[14] = zNear / depthRange;
r.m[15] = 1.f;
return r;
}
Matrix4f MakePerspectiveMatrix(float fovY, float aspectRatio, float zNear, float zFar)
{
float tanHalfFovy = tan(fovY / 2.f);
Matrix4f r;
if (zNear >= zFar || aspectRatio == 0)
{
throw std::runtime_error("Invalid perspective parameters");
}
r.m[0] = 1.f / (aspectRatio * tanHalfFovy);
r.m[1] = 0;
r.m[2] = 0;
r.m[3] = 0;
r.m[4] = 0;
r.m[5] = 1.f / (tanHalfFovy);
r.m[6] = 0;
r.m[7] = 0;
r.m[8] = 0;
r.m[9] = 0;
r.m[10] = -(zFar + zNear) / (zFar - zNear);
r.m[11] = -1;
r.m[12] = 0;
r.m[13] = 0;
r.m[14] = -(2.f * zFar * zNear) / (zFar - zNear);
r.m[15] = 0;
return r;
}
Matrix3f QuatToMatrix(const Vector4f& q)
{
Matrix3f m;
float wx, wy, wz, xx, yy, yz, xy, xz, zz, s, x2, y2, z2;
s = 2.0f / (q.v[0] * q.v[0] + q.v[1] * q.v[1] + q.v[2] * q.v[2] + q.v[3] * q.v[3]);
x2 = q.v[0] * s;
y2 = q.v[1] * s;
z2 = q.v[2] * s;
wx = q.v[3] * x2; wy = q.v[3] * y2; wz = q.v[3] * z2;
xx = q.v[0] * x2; xy = q.v[1] * x2; xz = q.v[2] * x2;
yy = q.v[1] * y2; yz = q.v[2] * y2;
zz = q.v[2] * z2;
m.m[0] = 1.0f - (yy + zz);
m.m[1] = xy + wz;
m.m[2] = xz - wy;
m.m[3] = xy - wz;
m.m[4] = 1.0f - (xx + zz);
m.m[5] = yz + wx;
m.m[6] = xz + wy;
m.m[7] = yz - wx;
m.m[8] = 1.0f - (xx + yy);
return m;
}
Vector4f MatrixToQuat(const Matrix3f& m)
{
Vector4f r;
float trace = m.m[0] + m.m[4] + m.m[8];
if (trace > 0)
{
float s = 0.5f / sqrtf(trace + 1.0f);
r.v[3] = 0.25f / s;
r.v[0] = (m.m[5] - m.m[7]) * s;
r.v[1] = (m.m[6] - m.m[2]) * s;
r.v[2] = (m.m[1] - m.m[3]) * s;
}
else if (m.m[0] > m.m[4] && m.m[0] > m.m[8])
{
float s = 2.0f * sqrtf(1.0f + m.m[0] - m.m[4] - m.m[8]);
r.v[3] = (m.m[5] - m.m[7]) / s;
r.v[0] = 0.25f * s;
r.v[1] = (m.m[1] + m.m[3]) / s;
r.v[2] = (m.m[6] + m.m[2]) / s;
}
else if (m.m[4] > m.m[8])
{
float s = 2.0f * sqrtf(1.0f + m.m[4] - m.m[0] - m.m[8]);
r.v[3] = (m.m[6] - m.m[2]) / s;
r.v[0] = (m.m[1] + m.m[3]) / s;
r.v[1] = 0.25f * s;
r.v[2] = (m.m[5] + m.m[7]) / s;
}
else
{
float s = 2.0f * sqrtf(1.0f + m.m[8] - m.m[0] - m.m[4]);
r.v[3] = (m.m[1] - m.m[3]) / s;
r.v[0] = (m.m[6] + m.m[2]) / s;
r.v[1] = (m.m[5] + m.m[7]) / s;
r.v[2] = 0.25f * s;
}
return r.normalized();
}
Vector4f QuatFromRotateAroundX(float angle)
{
Vector4f result;
result.v[0] = sinf(angle * 0.5f);
result.v[1] = 0.f;
result.v[2] = 0.f;
result.v[3] = cosf(angle * 0.5f);
return result;
}
Vector4f QuatFromRotateAroundY(float angle)
{
Vector4f result;
result.v[0] = 0.f;
result.v[1] = sinf(angle * 0.5f);
result.v[2] = 0.f;
result.v[3] = cosf(angle * 0.5f);
return result;
}
Vector4f QuatFromRotateAroundZ(float angle)
{
Vector4f result;
result.v[0] = 0.f;
result.v[1] = 0.f;
result.v[2] = sinf(angle * 0.5f);
result.v[3] = cosf(angle * 0.5f);
return result;
}
Matrix3f TransposeMatrix(const Matrix3f& m)
{
Matrix3f r;
r.m[0] = m.m[0];
r.m[1] = m.m[3];
r.m[2] = m.m[6];
r.m[3] = m.m[1];
r.m[4] = m.m[4];
r.m[5] = m.m[7];
r.m[6] = m.m[2];
r.m[7] = m.m[5];
r.m[8] = m.m[8];
return r;
}
Matrix3f InverseMatrix(const Matrix3f& m)
{
float d;
Matrix3f r;
d = m.m[0] * (m.m[4] * m.m[8] - m.m[5] * m.m[7]);
d -= m.m[1] * (m.m[3] * m.m[8] - m.m[6] * m.m[5]);
d += m.m[2] * (m.m[3] * m.m[7] - m.m[6] * m.m[4]);
if (fabs(d) < 0.01f)
{
throw std::runtime_error("Error: matrix cannot be inversed!");
}
else
{
r.m[0] = (m.m[4] * m.m[8] - m.m[5] * m.m[7]) / d;
r.m[1] = -(m.m[1] * m.m[8] - m.m[2] * m.m[7]) / d;
r.m[2] = (m.m[1] * m.m[5] - m.m[2] * m.m[4]) / d;
r.m[3] = -(m.m[3] * m.m[8] - m.m[5] * m.m[6]) / d;
r.m[4] = (m.m[0] * m.m[8] - m.m[2] * m.m[6]) / d;
r.m[5] = -(m.m[0] * m.m[5] - m.m[2] * m.m[3]) / d;
r.m[6] = (m.m[3] * m.m[7] - m.m[6] * m.m[4]) / d;
r.m[7] = -(m.m[0] * m.m[7] - m.m[6] * m.m[1]) / d;
r.m[8] = (m.m[0] * m.m[4] - m.m[1] * m.m[3]) / d;
};
return r;
}
Matrix4f InverseMatrix(const Matrix4f& mat)
{
Matrix4f inv;
float det;
inv.m[0] = mat.m[5] * mat.m[10] * mat.m[15] -
mat.m[5] * mat.m[11] * mat.m[14] -
mat.m[9] * mat.m[6] * mat.m[15] +
mat.m[9] * mat.m[7] * mat.m[14] +
mat.m[13] * mat.m[6] * mat.m[11] -
mat.m[13] * mat.m[7] * mat.m[10];
inv.m[4] = -mat.m[4] * mat.m[10] * mat.m[15] +
mat.m[4] * mat.m[11] * mat.m[14] +
mat.m[8] * mat.m[6] * mat.m[15] -
mat.m[8] * mat.m[7] * mat.m[14] -
mat.m[12] * mat.m[6] * mat.m[11] +
mat.m[12] * mat.m[7] * mat.m[10];
inv.m[8] = mat.m[4] * mat.m[9] * mat.m[15] -
mat.m[4] * mat.m[11] * mat.m[13] -
mat.m[8] * mat.m[5] * mat.m[15] +
mat.m[8] * mat.m[7] * mat.m[13] +
mat.m[12] * mat.m[5] * mat.m[11] -
mat.m[12] * mat.m[7] * mat.m[9];
inv.m[12] = -mat.m[4] * mat.m[9] * mat.m[14] +
mat.m[4] * mat.m[10] * mat.m[13] +
mat.m[8] * mat.m[5] * mat.m[14] -
mat.m[8] * mat.m[6] * mat.m[13] -
mat.m[12] * mat.m[5] * mat.m[10] +
mat.m[12] * mat.m[6] * mat.m[9];
inv.m[1] = -mat.m[1] * mat.m[10] * mat.m[15] +
mat.m[1] * mat.m[11] * mat.m[14] +
mat.m[9] * mat.m[2] * mat.m[15] -
mat.m[9] * mat.m[3] * mat.m[14] -
mat.m[13] * mat.m[2] * mat.m[11] +
mat.m[13] * mat.m[3] * mat.m[10];
inv.m[5] = mat.m[0] * mat.m[10] * mat.m[15] -
mat.m[0] * mat.m[11] * mat.m[14] -
mat.m[8] * mat.m[2] * mat.m[15] +
mat.m[8] * mat.m[3] * mat.m[14] +
mat.m[12] * mat.m[2] * mat.m[11] -
mat.m[12] * mat.m[3] * mat.m[10];
inv.m[9] = -mat.m[0] * mat.m[9] * mat.m[15] +
mat.m[0] * mat.m[11] * mat.m[13] +
mat.m[8] * mat.m[1] * mat.m[15] -
mat.m[8] * mat.m[3] * mat.m[13] -
mat.m[12] * mat.m[1] * mat.m[11] +
mat.m[12] * mat.m[3] * mat.m[9];
inv.m[13] = mat.m[0] * mat.m[9] * mat.m[14] -
mat.m[0] * mat.m[10] * mat.m[13] -
mat.m[8] * mat.m[1] * mat.m[14] +
mat.m[8] * mat.m[2] * mat.m[13] +
mat.m[12] * mat.m[1] * mat.m[10] -
mat.m[12] * mat.m[2] * mat.m[9];
inv.m[2] = mat.m[1] * mat.m[6] * mat.m[15] -
mat.m[1] * mat.m[7] * mat.m[14] -
mat.m[5] * mat.m[2] * mat.m[15] +
mat.m[5] * mat.m[3] * mat.m[14] +
mat.m[13] * mat.m[2] * mat.m[7] -
mat.m[13] * mat.m[3] * mat.m[6];
inv.m[6] = -mat.m[0] * mat.m[6] * mat.m[15] +
mat.m[0] * mat.m[7] * mat.m[14] +
mat.m[4] * mat.m[2] * mat.m[15] -
mat.m[4] * mat.m[3] * mat.m[14] -
mat.m[12] * mat.m[2] * mat.m[7] +
mat.m[12] * mat.m[3] * mat.m[6];
inv.m[10] = mat.m[0] * mat.m[5] * mat.m[15] -
mat.m[0] * mat.m[7] * mat.m[13] -
mat.m[4] * mat.m[1] * mat.m[15] +
mat.m[4] * mat.m[3] * mat.m[13] +
mat.m[12] * mat.m[1] * mat.m[7] -
mat.m[12] * mat.m[3] * mat.m[5];
inv.m[14] = -mat.m[0] * mat.m[5] * mat.m[14] +
mat.m[0] * mat.m[6] * mat.m[13] +
mat.m[4] * mat.m[1] * mat.m[14] -
mat.m[4] * mat.m[2] * mat.m[13] -
mat.m[12] * mat.m[1] * mat.m[6] +
mat.m[12] * mat.m[2] * mat.m[5];
inv.m[3] = -mat.m[1] * mat.m[6] * mat.m[11] +
mat.m[1] * mat.m[7] * mat.m[10] +
mat.m[5] * mat.m[2] * mat.m[11] -
mat.m[5] * mat.m[3] * mat.m[10] -
mat.m[9] * mat.m[2] * mat.m[7] +
mat.m[9] * mat.m[3] * mat.m[6];
inv.m[7] = mat.m[0] * mat.m[6] * mat.m[11] -
mat.m[0] * mat.m[7] * mat.m[10] -
mat.m[4] * mat.m[2] * mat.m[11] +
mat.m[4] * mat.m[3] * mat.m[10] +
mat.m[8] * mat.m[2] * mat.m[7] -
mat.m[8] * mat.m[3] * mat.m[6];
inv.m[11] = -mat.m[0] * mat.m[5] * mat.m[11] +
mat.m[0] * mat.m[7] * mat.m[9] +
mat.m[4] * mat.m[1] * mat.m[11] -
mat.m[4] * mat.m[3] * mat.m[9] -
mat.m[8] * mat.m[1] * mat.m[7] +
mat.m[8] * mat.m[3] * mat.m[5];
inv.m[15] = mat.m[0] * mat.m[5] * mat.m[10] -
mat.m[0] * mat.m[6] * mat.m[9] -
mat.m[4] * mat.m[1] * mat.m[10] +
mat.m[4] * mat.m[2] * mat.m[9] +
mat.m[8] * mat.m[1] * mat.m[6] -
mat.m[8] * mat.m[2] * mat.m[5];
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
det = mat.m[0] * inv.m[0] + mat.m[1] * inv.m[4] + mat.m[2] * inv.m[8] + mat.m[3] * inv.m[12];
if (std::fabs(det) < 0.01f)
{
throw std::runtime_error("Error: matrix cannot be inversed!");
}
// <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
det = 1.0f / det;
for (int i = 0; i < 16; i++)
{
inv.m[i] *= det;
}
return inv;
}
Matrix3f CreateZRotationMatrix(float angle)
{
Matrix3f result = Matrix3f::Identity();
result.m[0] = cosf(angle);
result.m[1] = -sinf(angle);
result.m[3] = sinf(angle);
result.m[4] = cosf(angle);
return result;
}
Matrix4f MultMatrixMatrix(const Matrix4f& m1, const Matrix4f& m2)
{
Matrix4f rx;
rx.m[0] = m1.m[0] * m2.m[0] + m1.m[4] * m2.m[1] + m1.m[8] * m2.m[2] + m1.m[12] * m2.m[3];
rx.m[1] = m1.m[1] * m2.m[0] + m1.m[5] * m2.m[1] + m1.m[9] * m2.m[2] + m1.m[13] * m2.m[3];
rx.m[2] = m1.m[2] * m2.m[0] + m1.m[6] * m2.m[1] + m1.m[10] * m2.m[2] + m1.m[14] * m2.m[3];
rx.m[3] = m1.m[3] * m2.m[0] + m1.m[7] * m2.m[1] + m1.m[11] * m2.m[2] + m1.m[15] * m2.m[3];
rx.m[4] = m1.m[0] * m2.m[4] + m1.m[4] * m2.m[5] + m1.m[8] * m2.m[6] + m1.m[12] * m2.m[7];
rx.m[5] = m1.m[1] * m2.m[4] + m1.m[5] * m2.m[5] + m1.m[9] * m2.m[6] + m1.m[13] * m2.m[7];
rx.m[6] = m1.m[2] * m2.m[4] + m1.m[6] * m2.m[5] + m1.m[10] * m2.m[6] + m1.m[14] * m2.m[7];
rx.m[7] = m1.m[3] * m2.m[4] + m1.m[7] * m2.m[5] + m1.m[11] * m2.m[6] + m1.m[15] * m2.m[7];
rx.m[8] = m1.m[0] * m2.m[8] + m1.m[4] * m2.m[9] + m1.m[8] * m2.m[10] + m1.m[12] * m2.m[11];
rx.m[9] = m1.m[1] * m2.m[8] + m1.m[5] * m2.m[9] + m1.m[9] * m2.m[10] + m1.m[13] * m2.m[11];
rx.m[10] = m1.m[2] * m2.m[8] + m1.m[6] * m2.m[9] + m1.m[10] * m2.m[10] + m1.m[14] * m2.m[11];
rx.m[11] = m1.m[3] * m2.m[8] + m1.m[7] * m2.m[9] + m1.m[11] * m2.m[10] + m1.m[15] * m2.m[11];
rx.m[12] = m1.m[0] * m2.m[12] + m1.m[4] * m2.m[13] + m1.m[8] * m2.m[14] + m1.m[12] * m2.m[15];
rx.m[13] = m1.m[1] * m2.m[12] + m1.m[5] * m2.m[13] + m1.m[9] * m2.m[14] + m1.m[13] * m2.m[15];
rx.m[14] = m1.m[2] * m2.m[12] + m1.m[6] * m2.m[13] + m1.m[10] * m2.m[14] + m1.m[14] * m2.m[15];
rx.m[15] = m1.m[3] * m2.m[12] + m1.m[7] * m2.m[13] + m1.m[11] * m2.m[14] + m1.m[15] * m2.m[15];
return rx;
}
Matrix3f MultMatrixMatrix(const Matrix3f& m1, const Matrix3f& m2)
{
Matrix3f r;
r.m[0] = m1.m[0] * m2.m[0] + m1.m[3] * m2.m[1] + m1.m[6] * m2.m[2];
r.m[1] = m1.m[1] * m2.m[0] + m1.m[4] * m2.m[1] + m1.m[7] * m2.m[2];
r.m[2] = m1.m[2] * m2.m[0] + m1.m[5] * m2.m[1] + m1.m[8] * m2.m[2];
r.m[3] = m1.m[0] * m2.m[3] + m1.m[3] * m2.m[4] + m1.m[6] * m2.m[5];
r.m[4] = m1.m[1] * m2.m[3] + m1.m[4] * m2.m[4] + m1.m[7] * m2.m[5];
r.m[5] = m1.m[2] * m2.m[3] + m1.m[5] * m2.m[4] + m1.m[8] * m2.m[5];
r.m[6] = m1.m[0] * m2.m[6] + m1.m[3] * m2.m[7] + m1.m[6] * m2.m[8] ;
r.m[7] = m1.m[1] * m2.m[6] + m1.m[4] * m2.m[7] + m1.m[7] * m2.m[8];
r.m[8] = m1.m[2] * m2.m[6] + m1.m[5] * m2.m[7] + m1.m[8] * m2.m[8];
return r;
}
Matrix3f MakeTranslationMatrix(const Vector3f& p)
{
Matrix3f r = Matrix3f::Identity();
r.m[12] = p.v[0];
r.m[13] = p.v[1];
r.m[14] = p.v[2];
return r;
}
Matrix3f MakeScaleMatrix(float scale)
{
Matrix3f r = Matrix3f::Identity();
r.m[0] = scale;
r.m[5] = scale;
r.m[10] = scale;
return r;
}
Matrix3f MakeRotationMatrix(const Vector3f& p)
{
Matrix3f r = Matrix3f::Identity();
r.m[12] = p.v[0];
r.m[13] = p.v[1];
r.m[14] = p.v[2];
return r;
}
Vector2f operator*(Vector2f v, float scale)
{
Vector2f r = v;
r.v[0] = v.v[0] * scale;
r.v[1] = v.v[1] * scale;
return r;
}
Vector3f operator*(Vector3f v, float scale)
{
Vector3f r = v;
r.v[0] = v.v[0] * scale;
r.v[1] = v.v[1] * scale;
r.v[2] = v.v[2] * scale;
return r;
}
Vector4f operator*(Vector4f v, float scale)
{
Vector4f r = v;
r.v[0] = v.v[0] * scale;
r.v[1] = v.v[1] * scale;
r.v[2] = v.v[2] * scale;
r.v[3] = v.v[3] * scale;
return r;
}
Vector3f MultVectorMatrix(Vector3f v, Matrix3f mt)
{
Vector3f r;
r.v[0] = v.v[0] * mt.m[0] + v.v[1] * mt.m[1] + v.v[2] * mt.m[2];
r.v[1] = v.v[0] * mt.m[3] + v.v[1] * mt.m[4] + v.v[2] * mt.m[5];
r.v[2] = v.v[0] * mt.m[6] + v.v[1] * mt.m[7] + v.v[2] * mt.m[8];
return r;
}
Vector4f MultVectorMatrix(Vector4f v, Matrix4f mt)
{
Vector4f r;
r.v[0] = v.v[0] * mt.m[0] + v.v[1] * mt.m[1] + v.v[2] * mt.m[2] + v.v[3] * mt.m[3];
r.v[1] = v.v[0] * mt.m[4] + v.v[1] * mt.m[5] + v.v[2] * mt.m[6] + v.v[3] * mt.m[7];
r.v[2] = v.v[0] * mt.m[8] + v.v[1] * mt.m[9] + v.v[2] * mt.m[10] + v.v[3] * mt.m[11];
r.v[3] = v.v[0] * mt.m[12] + v.v[1] * mt.m[13] + v.v[2] * mt.m[14] + v.v[3] * mt.m[15];
return r;
}
Vector4f MultMatrixVector(Matrix4f mt, Vector4f v)
{
Vector4f r;
r.v[0] = v.v[0] * mt.m[0] + v.v[1] * mt.m[4] + v.v[2] * mt.m[8] + v.v[3] * mt.m[12];
r.v[1] = v.v[0] * mt.m[1] + v.v[1] * mt.m[5] + v.v[2] * mt.m[9] + v.v[3] * mt.m[13];
r.v[2] = v.v[0] * mt.m[2] + v.v[1] * mt.m[6] + v.v[2] * mt.m[10] + v.v[3] * mt.m[14];
r.v[3] = v.v[0] * mt.m[3] + v.v[1] * mt.m[7] + v.v[2] * mt.m[11] + v.v[3] * mt.m[15];
return r;
}
Vector3f MultMatrixVector(Matrix3f mt, Vector3f v)
{
Vector3f r;
r.v[0] = v.v[0] * mt.m[0] + v.v[1] * mt.m[3] + v.v[2] * mt.m[6];
r.v[1] = v.v[0] * mt.m[1] + v.v[1] * mt.m[4] + v.v[2] * mt.m[7];
r.v[2] = v.v[0] * mt.m[2] + v.v[1] * mt.m[5] + v.v[2] * mt.m[8];
return r;
}
Vector4f slerp(const Vector4f& q1, const Vector4f& q2, float t)
{
const float epsilon = 1e-6f;
// Нормализация входных кватернионов
Vector4f q1_norm = q1.normalized();
Vector4f q2_norm = q2.normalized();
float cosTheta = q1_norm.dot(q2_norm);
// Если q1 и q2 близки к противоположным направлениям, корректируем q2
Vector4f q2_adjusted = q2_norm;
if (cosTheta < 0.0f) {
q2_adjusted.v[0] = -q2_adjusted.v[0];
q2_adjusted.v[1] = -q2_adjusted.v[1];
q2_adjusted.v[2] = -q2_adjusted.v[2];
q2_adjusted.v[3] = -q2_adjusted.v[3];
cosTheta = -cosTheta;
}
// Если кватернионы близки, используем линейную интерполяцию
if (cosTheta > 1.0f - epsilon) {
Vector4f result;
result.v[0] = q1_norm.v[0] + t * (q2_adjusted.v[0] - q1_norm.v[0]);
result.v[1] = q1_norm.v[1] + t * (q2_adjusted.v[1] - q1_norm.v[1]);
result.v[2] = q1_norm.v[2] + t * (q2_adjusted.v[2] - q1_norm.v[2]);
result.v[3] = q1_norm.v[3] + t * (q2_adjusted.v[3] - q1_norm.v[3]);
return result.normalized();
}
// Иначе используем сферическую интерполяцию
float theta = std::acos(cosTheta);
float sinTheta = std::sin(theta);
float coeff1 = std::sin((1.0f - t) * theta) / sinTheta;
float coeff2 = std::sin(t * theta) / sinTheta;
Vector4f result;
result.v[0] = coeff1 * q1_norm.v[0] + coeff2 * q2_adjusted.v[0];
result.v[1] = coeff1 * q1_norm.v[1] + coeff2 * q2_adjusted.v[1];
result.v[2] = coeff1 * q1_norm.v[2] + coeff2 * q2_adjusted.v[2];
result.v[3] = coeff1 * q1_norm.v[3] + coeff2 * q2_adjusted.v[3];
return result.normalized();
}
Matrix4f MakeMatrix4x4(const Matrix3f& m, const Vector3f pos)
{
Matrix4f r;
r.m[0] = m.m[0];
r.m[1] = m.m[1];
r.m[2] = m.m[2];
r.m[3] = 0;
r.m[4] = m.m[3];
r.m[5] = m.m[4];
r.m[6] = m.m[5];
r.m[7] = 0;
r.m[8] = m.m[6];
r.m[9] = m.m[7];
r.m[10] = m.m[8];
r.m[11] = 0;
r.m[12] = pos.v[0];
r.m[13] = pos.v[1];
r.m[14] = pos.v[2];
r.m[15] = 1.0;
return r;
}
};

179
ZLMath.h Executable file
View File

@ -0,0 +1,179 @@
#pragma once
#include <array>
#include <exception>
#include <stdexcept>
#include <cmath>
namespace ZL {
struct Vector4f
{
std::array<float, 4> v = { 0.f, 0.f, 0.f, 0.f };
Vector4f normalized() const {
double norm = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2] + v[3] * v[3]);
if (norm <= 0.000001f) {
return { 0,0, 0, 0};
}
Vector4f r;
r.v[0] = v[0] / norm;
r.v[1] = v[1] / norm;
r.v[2] = v[2] / norm;
r.v[3] = v[3] / norm;
return r;
}
double dot(const Vector4f& other) const {
return v[0] * other.v[0] + v[1] * other.v[1] + v[2] * other.v[2] + v[3] * other.v[3];
}
};
struct Vector3f
{
std::array<float, 3> v = { 0.f, 0.f, 0.f };
Vector3f()
{
}
Vector3f(float x, float y, float z)
: v{x,y,z}
{
}
Vector3f normalized() const {
double norm = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
if (norm <= 0.000001f) {
return { 0,0,0 };
}
Vector3f r;
r.v[0] = v[0] / norm;
r.v[1] = v[1] / norm;
r.v[2] = v[2] / norm;
return r;
}
float squaredNorm() const {
return v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
}
float length() const
{
return sqrt(squaredNorm());
}
float dot(const Vector3f& other) const {
return v[0] * other.v[0] + v[1] * other.v[1] + v[2] * other.v[2];
}
Vector3f cross(const Vector3f& other) const {
return Vector3f(
v[1] * other.v[2] - v[2] * other.v[1],
v[2] * other.v[0] - v[0] * other.v[2],
v[0] * other.v[1] - v[1] * other.v[0]
);
}
bool operator<(const Vector3f& other) const {
if (v[0] != other.v[0]) return v[0] < other.v[0];
if (v[1] != other.v[1]) return v[1] < other.v[1];
return v[2] < other.v[2];
}
};
struct Vector2f
{
std::array<float, 2> v = {0.f, 0.f};
Vector2f()
{
}
Vector2f(float x, float y)
: v{ x,y }
{
}
};
Vector2f operator+(const Vector2f& x, const Vector2f& y);
Vector2f operator-(const Vector2f& x, const Vector2f& y);
Vector3f operator+(const Vector3f& x, const Vector3f& y);
Vector3f operator-(const Vector3f& x, const Vector3f& y);
Vector4f operator+(const Vector4f& x, const Vector4f& y);
Vector4f operator-(const Vector4f& x, const Vector4f& y);
Vector3f operator-(const Vector3f& x);
struct Matrix3f
{
std::array<float, 9> m = { 0.f, 0.f, 0.f,
0.f, 0.f, 0.f,
0.f, 0.f, 0.f, };
static Matrix3f Identity();
};
struct Matrix4f
{
std::array<float, 16> m = { 0.f, 0.f, 0.f, 0.f,
0.f, 0.f, 0.f, 0.f,
0.f, 0.f, 0.f, 0.f,
0.f, 0.f, 0.f, 0.f };
static Matrix4f Identity();
float& operator()(int row, int col) {
//return m[row * 4 + col]; //OpenGL specific
return m[col * 4 + row];
}
const float& operator()(int row, int col) const {
//return m[row * 4 + col];
return m[col * 4 + row];
}
};
Matrix4f operator*(const Matrix4f& m1, const Matrix4f& m2);
Matrix4f MakeOrthoMatrix(float width, float height, float zNear, float zFar);
Matrix4f MakeOrthoMatrix(float xmin, float xmax, float ymin, float ymax, float zNear, float zFar);
Matrix4f MakePerspectiveMatrix(float fovY, float aspectRatio, float zNear, float zFar);
Matrix3f QuatToMatrix(const Vector4f& q);
Vector4f MatrixToQuat(const Matrix3f& m);
Vector4f QuatFromRotateAroundX(float angle);
Vector4f QuatFromRotateAroundY(float angle);
Vector4f QuatFromRotateAroundZ(float angle);
Vector2f operator*(Vector2f v, float scale);
Vector3f operator*(Vector3f v, float scale);
Vector4f operator*(Vector4f v, float scale);
Vector3f MultVectorMatrix(Vector3f v, Matrix3f mt);
Vector4f MultVectorMatrix(Vector4f v, Matrix4f mt);
Vector4f MultMatrixVector(Matrix4f mt, Vector4f v);
Vector3f MultMatrixVector(Matrix3f mt, Vector3f v);
Vector4f slerp(const Vector4f& q1, const Vector4f& q2, float t);
Matrix3f InverseMatrix(const Matrix3f& m);
Matrix4f InverseMatrix(const Matrix4f& m);
Matrix3f MultMatrixMatrix(const Matrix3f& m1, const Matrix3f& m2);
Matrix4f MultMatrixMatrix(const Matrix4f& m1, const Matrix4f& m2);
Matrix4f MakeMatrix4x4(const Matrix3f& m, const Vector3f pos);
};

View File

@ -1,65 +0,0 @@
import bpy
def append_layered_action_5_0(source_obj_name, target_obj_name):
source_obj = bpy.data.objects.get(source_obj_name)
target_obj = bpy.data.objects.get(target_obj_name)
if not (source_obj and target_obj):
print("Ошибка: Объекты не найдены")
return
src_action = source_obj.animation_data.action
tgt_action = target_obj.animation_data.action
# 1. Получаем слои (обычно первый)
src_layer = src_action.layers[0]
tgt_layer = tgt_action.layers[0]
# 2. Получаем стрипы
src_strip = src_layer.strips[0]
tgt_strip = tgt_layer.strips[0]
# Смещение (опираемся на конец диапазона целевого экшена)
offset = tgt_action.frame_range[1]
# 3. Итерируемся по channelbags в исходном стрипе
for src_bag in src_strip.channelbags:
# Ищем или создаем соответствующий bag в целевом стрипе
# Обычно они сопоставляются по имени или типу (например, 'Keyframe Channel Bag')
# В простейшем случае берем первый или сопоставляем по индексу
dst_bag = None
if len(tgt_strip.channelbags) > 0:
# Пытаемся найти по названию (если оно есть) или берем тот же индекс
dst_bag = tgt_strip.channelbags[0]
if not dst_bag:
# Если в целевом стрипе нет сумок, это странно, но можно создать
# (Метод создания может зависеть от конкретного подтипа стрипа в 5.0)
continue
#print(f"Обработка channelbag: {src_bag.name}, кривых: {len(src_bag.fcurves)}")
# 4. Итерируемся по fcurves внутри сумки
for src_fcurve in src_bag.fcurves:
dst_fcurve = dst_bag.fcurves.find(src_fcurve.data_path, index=src_fcurve.array_index)
if not dst_fcurve:
dst_fcurve = dst_bag.fcurves.new(data_path=src_fcurve.data_path, index=src_fcurve.array_index)
# 5. Копируем ключи с офсетом
for keyframe in src_fcurve.keyframe_points:
new_frame = keyframe.co[0] + offset
new_value = keyframe.co[1]
new_kp = dst_fcurve.keyframe_points.insert(new_frame, new_value, options={'FAST'})
new_kp.interpolation = keyframe.interpolation
# Обновляем интерполяцию
for fc in dst_bag.fcurves:
fc.update()
print(f"Анимация успешно дозаписана. Новый конец: {tgt_action.frame_range[1]}")
append_layered_action_5_0('Armature.001', 'Armature')

View File

@ -1,192 +0,0 @@
import bpy
import bmesh
# Имена mesh и арматуры
mesh_name = "arm"
armature_name = "Reference"
# Находим объект mesh по имени
mesh_obj = bpy.data.objects.get(mesh_name)
# Находим объект арматуры по имени
armature_obj = bpy.data.objects.get(armature_name)
# Устанавливаем текущий кадр на 0
bpy.context.scene.frame_set(0)
# Принудительно обновляем сцену, чтобы применить анимацию
bpy.context.view_layer.update()
# Открываем файл для записи
with open("C:\\Work\\Projects\\witcher001\\resources\\w\\zombie002.txt", "w") as file:
# Обработка арматуры и анимации
if armature_obj and armature_obj.type == 'ARMATURE':
file.write("=== Armature Matrix ===\n")
for row in armature_obj.matrix_world:
file.write(f"{row}\n")
file.write(f"=== Armature Bones: {len(armature_obj.data.bones)}\n")
for bone in armature_obj.data.bones:
# Записываем имя кости, длину и связи
file.write(f"Bone: {bone.name}\n")
file.write(f" HEAD_LOCAL: {bone.head_local}\n")
file.write(f" TAIL_LOCAL: {bone.tail_local}\n")
file.write(f" Length: {(bone.tail_local - bone.head_local).length}\n")
for row in bone.matrix:
file.write(f" {row}\n")
file.write(f" Parent: {bone.parent.name if bone.parent else 'None'}\n")
file.write(f" Children: {[child.name for child in bone.children]}\n")
# Обработка mesh
if mesh_obj and mesh_obj.type == 'MESH':
# Создаем копию mesh, чтобы не изменять оригинал
mesh_copy = mesh_obj.copy()
mesh_copy.data = mesh_obj.data.copy()
bpy.context.collection.objects.link(mesh_copy)
# Убедимся, что объект активен
bpy.context.view_layer.objects.active = mesh_copy
mesh_copy.select_set(True)
# Применяем модификатор Armature (если он есть)
for modifier in mesh_copy.modifiers:
if modifier.type == 'ARMATURE':
# Включаем модификатор, если он отключен
if not modifier.show_viewport:
modifier.show_viewport = True
if not modifier.show_render:
modifier.show_render = True
# Проверяем, что модификатор связан с арматурой
if modifier.object is None:
print(f"Модификатор Armature на объекте {mesh_copy.name} не связан с арматурой. Пропускаем.")
continue
# Временно применяем модификатор, чтобы получить правильные координаты вершин
try:
bpy.ops.object.modifier_apply(modifier=modifier.name)
except RuntimeError as e:
print(f"Ошибка при применении модификатора Armature: {e}")
continue
# Переходим в режим редактирования
bpy.ops.object.mode_set(mode='EDIT')
# Получаем BMesh представление mesh
bm = bmesh.from_edit_mesh(mesh_copy.data)
# Записываем список вершин
file.write(f"===Vertices: {len(bm.verts)}\n")
for vertex in bm.verts:
file.write(f"Vertex {vertex.index}: {vertex.co}\n")
# Убедимся, что у меша есть UV слой
uv_layer = bm.loops.layers.uv.active
if not uv_layer:
file.write("UV слой не найден.\n")
if uv_layer:
file.write(f"===UV Coordinates:\n")
file.write(f"Face count: {len(bm.faces)}\n")
for face in bm.faces:
file.write(f"Face {face.index}\n")
file.write(f"UV Count: {len(face.loops)}\n")
for loop in face.loops:
uv_coords = loop[uv_layer].uv
file.write(f" UV {uv_coords}\n")
# Записываем нормали
file.write(f"===Normals:\n")
for vertex in bm.verts:
file.write(f"Vertex {vertex.index}: Normal {vertex.normal}\n")
# Записываем треугольники (индексы вершин)
file.write(f"===Triangles: {len(bm.faces)}\n")
for face in bm.faces:
if len(face.verts) == 3: # Проверяем, что это треугольник
verts_indices = [vert.index for vert in face.verts]
file.write(f"Triangle: {verts_indices}\n")
# Возвращаемся в объектный режим
bpy.ops.object.mode_set(mode='OBJECT')
# Записываем веса вершин
file.write("=== Vertex Weights ===\n")
for vertex in mesh_copy.data.vertices:
file.write(f"Vertex {vertex.index}:\n")
file.write(f"Vertex groups: {len(vertex.groups)}\n")
for group in vertex.groups:
group_name = mesh_copy.vertex_groups[group.group].name
file.write(f" Group: '{group_name}', Weight: {group.weight}\n")
# Удаляем временную копию mesh
bpy.data.objects.remove(mesh_copy)
else:
file.write(f"Объект с именем '{mesh_name}' не найден или не является mesh.\n")
# Обработка арматуры и анимации
if armature_obj and armature_obj.type == 'ARMATURE':
# Получаем все ключевые кадры для арматуры
file.write("=== Animation Keyframes ===\n")
if armature_obj.animation_data and armature_obj.animation_data.action:
action = armature_obj.animation_data.action
# Собираем все уникальные ключевые кадры
keyframes = set()
# Логика для Blender 5.0 (Strip-based / ChannelBag structure)
if hasattr(action, "layers"):
for layer in action.layers:
if hasattr(layer, "strips"):
for strip in layer.strips:
# Проверяем наличие channelbags (согласно вашему dir(strip))
if hasattr(strip, "channelbags"):
for bag in strip.channelbags:
for fcurve in bag.fcurves:
for keyframe in fcurve.keyframe_points:
keyframes.add(int(keyframe.co[0]))
# На случай, если в этой версии используется единственное число
elif hasattr(strip, "channelbag") and strip.channelbag:
for fcurve in strip.channelbag.fcurves:
for keyframe in fcurve.keyframe_points:
keyframes.add(int(keyframe.co[0]))
# Фоллбек для Legacy экшенов
if not keyframes and hasattr(action, "fcurves"):
for fcurve in action.fcurves:
for keyframe in fcurve.keyframe_points:
keyframes.add(int(keyframe.co[0]))
keyframes = sorted(keyframes)
# Сортируем ключевые кадры
keyframes = sorted(keyframes)
# Сохраняем координаты и матрицы поворота для каждой кости на каждом ключевом кадре
file.write("=== Bone Transforms per Keyframe ===\n")
file.write(f"Keyframes: {len(keyframes)}\n")
for frame in keyframes:
# Устанавливаем текущий кадр
bpy.context.scene.frame_set(frame)
bpy.context.view_layer.update() # Обновляем сцену
file.write(f"Frame: {frame}\n")
for bone in armature_obj.pose.bones:
# Получаем координаты и матрицу поворота кости в мировом пространстве
matrix = bone.matrix
location = matrix.translation
rotation = matrix.to_euler()
# Записываем данные
file.write(f" Bone: {bone.name}\n")
file.write(f" Location: {location}\n")
file.write(f" Rotation: {rotation}\n")
file.write(f" Matrix:\n")
for row in matrix:
file.write(f" {row}\n")
else:
file.write(f"Объект с именем '{armature_name}' не найден или не является арматурой.\n")
print("Данные сохранены в файл 'mesh_armature_and_animation_data.txt'")

View File

@ -1,228 +0,0 @@
import bpy
import bmesh
#!
# Имена mesh и арматуры
mesh_name = "Joined"
armature_name = "Armature"
# Находим объект mesh по имени
mesh_obj = bpy.data.objects.get(mesh_name)
# Находим объект арматуры по имени
armature_obj = bpy.data.objects.get(armature_name)
# Устанавливаем текущий кадр на 0
bpy.context.scene.frame_set(0)
# Принудительно обновляем сцену, чтобы применить анимацию
bpy.context.view_layer.update()
# Открываем файл для записи
with open("C:\\Work\\Media\\witcher\\2026-04-13\\output\\gg_stand_idle001.txt", "w") as file:
# Обработка арматуры и анимации
if armature_obj and armature_obj.type == 'ARMATURE':
file.write("=== Armature Matrix ===\n")
for row in armature_obj.matrix_world:
file.write(f"{row}\n")
file.write(f"=== Armature Bones: {len(armature_obj.data.bones)}\n")
for bone in armature_obj.data.bones:
# Записываем имя кости, длину и связи
file.write(f"Bone: {bone.name}\n")
file.write(f" HEAD_LOCAL: {bone.head_local}\n")
file.write(f" TAIL_LOCAL: {bone.tail_local}\n")
file.write(f" Length: {(bone.tail_local - bone.head_local).length}\n")
for row in bone.matrix:
file.write(f" {row}\n")
file.write(f" Parent: {bone.parent.name if bone.parent else 'None'}\n")
file.write(f" Children: {[child.name for child in bone.children]}\n")
# Обработка mesh
if mesh_obj and mesh_obj.type == 'MESH':
# Создаем копию mesh, чтобы не изменять оригинал
mesh_copy = mesh_obj.copy()
mesh_copy.data = mesh_obj.data.copy()
bpy.context.collection.objects.link(mesh_copy)
# Убедимся, что объект активен
bpy.context.view_layer.objects.active = mesh_copy
mesh_copy.select_set(True)
# Применяем модификатор Armature (если он есть)
for modifier in mesh_copy.modifiers:
if modifier.type == 'ARMATURE':
# Включаем модификатор, если он отключен
if not modifier.show_viewport:
modifier.show_viewport = True
if not modifier.show_render:
modifier.show_render = True
# Проверяем, что модификатор связан с арматурой
if modifier.object is None:
print(f"Модификатор Armature на объекте {mesh_copy.name} не связан с арматурой. Пропускаем.")
continue
# Временно применяем модификатор, чтобы получить правильные координаты вершин
try:
bpy.ops.object.modifier_apply(modifier=modifier.name)
except RuntimeError as e:
print(f"Ошибка при применении модификатора Armature: {e}")
continue
# Переходим в режим редактирования
bpy.ops.object.mode_set(mode='EDIT')
# Получаем BMesh представление mesh
bm = bmesh.from_edit_mesh(mesh_copy.data)
# Записываем список вершин
file.write(f"===Vertices: {len(bm.verts)}\n")
for vertex in bm.verts:
file.write(f"Vertex {vertex.index}: {vertex.co}\n")
# Убедимся, что у меша есть UV слой
uv_layer = bm.loops.layers.uv.active
if not uv_layer:
file.write("UV слой не найден.\n")
if uv_layer:
file.write(f"===UV Coordinates:\n")
file.write(f"Face count: {len(bm.faces)}\n")
for face in bm.faces:
file.write(f"Face {face.index}\n")
file.write(f"UV Count: {len(face.loops)}\n")
for loop in face.loops:
uv_coords = loop[uv_layer].uv
file.write(f" UV {uv_coords}\n")
# Записываем нормали
file.write(f"===Normals:\n")
for vertex in bm.verts:
file.write(f"Vertex {vertex.index}: Normal {vertex.normal}\n")
# Записываем треугольники (индексы вершин)
file.write(f"===Triangles: {len(bm.faces)}\n")
for face in bm.faces:
if len(face.verts) == 3: # Проверяем, что это треугольник
verts_indices = [vert.index for vert in face.verts]
file.write(f"Triangle: {verts_indices}\n")
# Возвращаемся в объектный режим
bpy.ops.object.mode_set(mode='OBJECT')
# Записываем веса вершин
file.write("=== Vertex Weights (Max 5 bones per vertex) ===\n")
MAX_BONES = 5
for vertex in mesh_copy.data.vertices:
# Извлекаем все группы и веса для текущей вершины
all_weights = []
for group_element in vertex.groups:
all_weights.append({
'index': group_element.group,
'weight': group_element.weight
})
# Если костей больше лимита, фильтруем и перераспределяем
if len(all_weights) > MAX_BONES:
# Сортируем по весу (от большего к меньшему)
all_weights.sort(key=lambda x: x['weight'], reverse=True)
# Берем только топ-5
kept_weights = all_weights[:MAX_BONES]
# Считаем сумму весов оставшихся костей для нормализации
total_weight = sum(gw['weight'] for gw in kept_weights)
if total_weight > 0:
for gw in kept_weights:
gw['weight'] /= total_weight
else:
# На случай, если у всех веса были по 0.0 (редкий баг меша)
kept_weights[0]['weight'] = 1.0
final_weights = kept_weights
else:
final_weights = all_weights
file.write(f"Vertex {vertex.index}:\n")
file.write(f"Vertex groups: {len(final_weights)}\n")
for gw in final_weights:
group_name = mesh_copy.vertex_groups[gw['index']].name
file.write(f" Group: '{group_name}', Weight: {gw['weight']:.6f}\n")
# Удаляем временную копию mesh
bpy.data.objects.remove(mesh_copy)
else:
file.write(f"Объект с именем '{mesh_name}' не найден или не является mesh.\n")
# Обработка арматуры и анимации
if armature_obj and armature_obj.type == 'ARMATURE':
# Получаем все ключевые кадры для арматуры
file.write("=== Animation Keyframes ===\n")
if armature_obj.animation_data and armature_obj.animation_data.action:
action = armature_obj.animation_data.action
# Собираем все уникальные ключевые кадры
keyframes = set()
# Логика для Blender 5.0 (Strip-based / ChannelBag structure)
if hasattr(action, "layers"):
for layer in action.layers:
if hasattr(layer, "strips"):
for strip in layer.strips:
# Проверяем наличие channelbags (согласно вашему dir(strip))
if hasattr(strip, "channelbags"):
for bag in strip.channelbags:
for fcurve in bag.fcurves:
for keyframe in fcurve.keyframe_points:
keyframes.add(int(keyframe.co[0]))
# На случай, если в этой версии используется единственное число
elif hasattr(strip, "channelbag") and strip.channelbag:
for fcurve in strip.channelbag.fcurves:
for keyframe in fcurve.keyframe_points:
keyframes.add(int(keyframe.co[0]))
# Фоллбек для Legacy экшенов
if not keyframes and hasattr(action, "fcurves"):
for fcurve in action.fcurves:
for keyframe in fcurve.keyframe_points:
keyframes.add(int(keyframe.co[0]))
keyframes = sorted(keyframes)
# Сортируем ключевые кадры
keyframes = sorted(keyframes)
# Сохраняем координаты и матрицы поворота для каждой кости на каждом ключевом кадре
file.write("=== Bone Transforms per Keyframe ===\n")
file.write(f"Keyframes: {len(keyframes)}\n")
for frame in keyframes:
# Устанавливаем текущий кадр
bpy.context.scene.frame_set(frame)
bpy.context.view_layer.update() # Обновляем сцену
file.write(f"Frame: {frame}\n")
for bone in armature_obj.pose.bones:
# Получаем координаты и матрицу поворота кости в мировом пространстве
matrix = bone.matrix
location = matrix.translation
rotation = matrix.to_euler()
# Записываем данные
file.write(f" Bone: {bone.name}\n")
file.write(f" Location: {location}\n")
file.write(f" Rotation: {rotation}\n")
file.write(f" Matrix:\n")
for row in matrix:
file.write(f" {row}\n")
else:
file.write(f"Объект с именем '{armature_name}' не найден или не является арматурой.\n")
print("Данные сохранены в файл 'mesh_armature_and_animation_data.txt'")

View File

@ -1,111 +0,0 @@
import bpy
import bmesh
import mathutils
import random
import math
class SolidTreeGenerator:
def __init__(self, levels=5, length=3.0, radius=0.3):
self.levels = levels
self.base_length = length
self.base_radius = radius
# Хранилище для данных: (start_pos, end_pos, radius_start, radius_end)
self.branches_data = []
def calculate_tree(self, start_pos, direction, length, radius, level):
if level <= 0 or length < 0.1:
return
end_pos = start_pos + direction * length
# Сохраняем данные сегмента
self.branches_data.append({
'start': start_pos.copy(),
'end': end_pos.copy(),
'r_start': radius,
'r_end': radius * 0.7
})
# 1. Основной ствол (продолжение)
trunk_dir = (direction + self.get_random_vector(0.1)).normalized()
self.calculate_tree(end_pos, trunk_dir, length * 0.8, radius * 0.7, level - 1)
# 2. Боковые ветки (ветвление)
if level > 1:
num_sides = random.randint(2, 3) # Минимум 2 ветки для видимости
for _ in range(num_sides):
# Создаем вектор, сильно отклоненный от ствола (30-60 градусов)
axis = self.get_random_vector(1.0).normalized()
angle = math.radians(random.uniform(30, 60))
side_dir = direction.copy()
side_dir.rotate(mathutils.Quaternion(axis, angle))
# Боковые ветки короче
self.calculate_tree(end_pos, side_dir, length * 0.6, radius * 0.5, level - 1)
def get_random_vector(self, intensity):
return mathutils.Vector((
random.uniform(-intensity, intensity),
random.uniform(-intensity, intensity),
random.uniform(-intensity, intensity)
))
def build_mesh(self):
mesh = bpy.data.meshes.new("TreeMesh")
obj = bpy.data.objects.new("Tree", mesh)
bpy.context.collection.objects.link(obj)
bm = bmesh.new()
skin_layer = bm.verts.layers.skin.verify()
# Словарь для предотвращения дублирования вершин в одной точке
# Ключ - кортеж координат, Значение - объект вершины BMesh
vert_map = {}
for b in self.branches_data:
# Превращаем координаты в кортежи для словаря
s_key = tuple(round(v, 4) for v in b['start'])
e_key = tuple(round(v, 4) for v in b['end'])
# Получаем или создаем начальную вершину
if s_key not in vert_map:
v_start = bm.verts.new(b['start'])
v_start[skin_layer].radius = (b['r_start'], b['r_start'])
vert_map[s_key] = v_start
else:
v_start = vert_map[s_key]
# Получаем или создаем конечную вершину
if e_key not in vert_map:
v_end = bm.verts.new(b['end'])
v_end[skin_layer].radius = (b['r_end'], b['r_end'])
vert_map[e_key] = v_end
else:
v_end = vert_map[e_key]
# Создаем ребро, если его еще нет
if not bm.edges.get((v_start, v_end)):
bm.edges.new((v_start, v_end))
# Находим корень (самую нижнюю точку) и помечаем его
root_v = min(bm.verts, key=lambda v: v.co.z)
root_v[skin_layer].use_root = True
bm.to_mesh(mesh)
bm.free()
# Модификаторы
obj.modifiers.new(name="Skin", type='SKIN')
sub = obj.modifiers.new(name="Subdiv", type='SUBSURF')
sub.levels = 1 # Для начала 1, чтобы не тормозило
# Очистка сцены
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Запуск
generator = SolidTreeGenerator(levels=5, length=3.0, radius=0.4)
generator.calculate_tree(mathutils.Vector((0,0,0)), mathutils.Vector((0,0,1)), 3.0, 0.4, 5)
generator.build_mesh()

File diff suppressed because it is too large Load Diff

View File

@ -1,58 +0,0 @@
# cmake/FetchDependencies.cmake
set(THIRDPARTY_DIR "${CMAKE_CURRENT_LIST_DIR}/../thirdparty")
if(NOT EXISTS "${THIRDPARTY_DIR}")
file(MAKE_DIRECTORY "${THIRDPARTY_DIR}")
endif()
macro(check_and_download URL ARCHIVE_NAME EXTRACTED_DIR_NAME CHECK_FILE)
set(ARCHIVE_PATH "${THIRDPARTY_DIR}/${ARCHIVE_NAME}")
set(SRC_PATH "${THIRDPARTY_DIR}/${EXTRACTED_DIR_NAME}")
if(NOT EXISTS "${ARCHIVE_PATH}")
message(STATUS "Downloading ${ARCHIVE_NAME}...")
file(DOWNLOAD "${URL}" "${ARCHIVE_PATH}" SHOW_PROGRESS)
endif()
if(NOT EXISTS "${SRC_PATH}/${CHECK_FILE}")
message(STATUS "Extracting ${ARCHIVE_NAME}...")
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xvf "${ARCHIVE_PATH}"
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
)
endif()
endmacro()
# 1) ZLIB (Нужна только для инклудов, если не используете emscripten порты)
check_and_download("https://www.zlib.net/zlib132.zip" "zlib132.zip" "zlib-1.3.2" "CMakeLists.txt")
# 2) SDL2
check_and_download("https://github.com/libsdl-org/SDL/archive/refs/tags/release-2.32.10.zip" "release-2.32.10.zip" "SDL-release-2.32.10" "CMakeLists.txt")
# 3) LibPNG
check_and_download("https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.51.zip" "v1.6.51.zip" "libpng-1.6.51" "CMakeLists.txt")
# 4) LibZip
check_and_download("https://github.com/nih-at/libzip/archive/refs/tags/v1.11.4.zip" "v1.11.4.zip" "libzip-1.11.4" "CMakeLists.txt")
# 5) Eigen
check_and_download("https://gitlab.com/libeigen/eigen/-/archive/5.0.0/eigen-5.0.0.zip" "eigen-5.0.0.zip" "eigen-5.0.0" "signature_of_eigen3_matrix_library")
# 6) Boost
check_and_download("https://archives.boost.io/release/1.90.0/source/boost_1_90_0.zip" "boost_1_90_0.zip" "boost_1_90_0" "boost")
# 7) FreeType
check_and_download("https://download.savannah.gnu.org/releases/freetype/freetype-2.14.1.tar.gz" "freetype-2.14.1.tar.gz" "freetype-2.14.1" "CMakeLists.txt")
# 8) SDL_ttf
check_and_download("https://github.com/libsdl-org/SDL_ttf/archive/refs/tags/release-2.24.0.zip" "release-2.24.0.zip" "SDL_ttf-release-2.24.0" "CMakeLists.txt")
# 9) Lua
check_and_download("https://github.com/lua/lua/archive/refs/tags/v5.4.8.zip" "lua-v5.4.8.zip" "lua-5.4.8" "lapi.c")
# 10) sol2 (header-only C++ bindings for Lua)
check_and_download("https://github.com/ThePhD/sol2/archive/refs/tags/v3.3.0.zip" "sol2-v3.3.0.zip" "sol2-3.3.0" "include/sol/sol.hpp")
# 11) SDL2_mixer
check_and_download("https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-2.8.0.zip" "SDL_mixer-release-2.8.0.zip" "SDL_mixer-release-2.8.0" "CMakeLists.txt")

View File

@ -1,28 +0,0 @@
# cmake/FetchDependenciesLinux.cmake
set(THIRDPARTY_DIR "${CMAKE_CURRENT_LIST_DIR}/../thirdparty")
if(NOT EXISTS "${THIRDPARTY_DIR}")
file(MAKE_DIRECTORY "${THIRDPARTY_DIR}")
endif()
macro(check_and_download URL ARCHIVE_NAME EXTRACTED_DIR_NAME CHECK_FILE)
set(ARCHIVE_PATH "${THIRDPARTY_DIR}/${ARCHIVE_NAME}")
set(SRC_PATH "${THIRDPARTY_DIR}/${EXTRACTED_DIR_NAME}")
if(NOT EXISTS "${ARCHIVE_PATH}")
message(STATUS "Downloading ${ARCHIVE_NAME}...")
file(DOWNLOAD "${URL}" "${ARCHIVE_PATH}" SHOW_PROGRESS)
endif()
if(NOT EXISTS "${SRC_PATH}/${CHECK_FILE}")
message(STATUS "Extracting ${ARCHIVE_NAME}...")
execute_process(
COMMAND ${CMAKE_COMMAND} -E tar xvf "${ARCHIVE_PATH}"
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
)
endif()
endmacro()
# 1) sol2 (header-only C++ bindings for Lua)
check_and_download("https://github.com/ThePhD/sol2/archive/refs/tags/v3.3.0.zip" "sol2-v3.3.0.zip" "sol2-3.3.0" "include/sol/sol.hpp")

View File

@ -1,728 +0,0 @@
# cmake/ThirdParty.cmake
include("${CMAKE_CURRENT_LIST_DIR}/FetchDependencies.cmake")
macro(log msg)
message(STATUS "[ThirdParty] ${msg}")
endmacro()
set(BUILD_CONFIGS Debug Release)
# Map MinSizeRel and RelWithDebInfo to Release libs for all imported targets.
# Without this CMake warns that IMPORTED_LOCATION is missing for those configs.
set(CMAKE_MAP_IMPORTED_CONFIG_MINSIZEREL Release)
set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release)
# ===========================================
# 1) ZLIB (zlib131.zip zlib-1.3.2) - без изменений
# ===========================================
set(ZLIB_SRC_DIR "${THIRDPARTY_DIR}/zlib-1.3.2")
set(ZLIB_BUILD_DIR "${ZLIB_SRC_DIR}/build")
set(ZLIB_INSTALL_DIR "${ZLIB_SRC_DIR}/install")
file(MAKE_DIRECTORY "${ZLIB_BUILD_DIR}")
# проверяем, собран ли уже zlib
set(_have_zlib FALSE)
foreach(candidate
"${ZLIB_INSTALL_DIR}/lib/zlibstatic.lib"
)
if(EXISTS "${candidate}")
set(_have_zlib TRUE)
break()
endif()
endforeach()
if(NOT _have_zlib)
log("Configuring zlib ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${ZLIB_SRC_DIR}"
-B "${ZLIB_BUILD_DIR}"
-DCMAKE_INSTALL_PREFIX=${ZLIB_INSTALL_DIR}
RESULT_VARIABLE _zlib_cfg_res
)
if(NOT _zlib_cfg_res EQUAL 0)
message(FATAL_ERROR "zlib configure failed")
endif()
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Building ZLIB (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${ZLIB_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _zlib_build_res
)
if(NOT _zlib_build_res EQUAL 0)
message(FATAL_ERROR "ZLIB build failed for configuration ${cfg}")
endif()
log("Installing ZLIB (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${ZLIB_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _zlib_inst_res
)
if(NOT _zlib_inst_res EQUAL 0)
message(FATAL_ERROR "ZLIB install failed for configuration ${cfg}")
endif()
endforeach()
endif()
# ИСПРАВЛЕНИЕ: Используем свойства для конкретных конфигураций
add_library(zlib_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(zlib_external_lib PROPERTIES
# Динамическая линковка (если zlib.lib - это импорт-библиотека для zlibd.dll)
#IMPORTED_LOCATION_DEBUG "${ZLIB_INSTALL_DIR}/lib/zlibd.lib"
#IMPORTED_LOCATION_RELEASE "${ZLIB_INSTALL_DIR}/lib/zlib.lib"
# Можно также указать статические библиотеки, если вы хотите их использовать
IMPORTED_LOCATION_DEBUG "${ZLIB_INSTALL_DIR}/lib/zsd.lib"
IMPORTED_LOCATION_RELEASE "${ZLIB_INSTALL_DIR}/lib/zs.lib"
INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INSTALL_DIR}/include"
)
# ===========================================
# 2) SDL2 (release-2.32.10.zip SDL-release-2.32.10) - без изменений
# ===========================================
set(SDL2_SRC_DIR "${THIRDPARTY_DIR}/SDL-release-2.32.10")
set(SDL2_BUILD_DIR "${SDL2_SRC_DIR}/build")
set(SDL2_INSTALL_DIR "${SDL2_SRC_DIR}/install")
file(MAKE_DIRECTORY "${SDL2_BUILD_DIR}")
set(_have_sdl2 FALSE)
foreach(candidate
"${SDL2_INSTALL_DIR}/lib/SDL2.lib"
"${SDL2_INSTALL_DIR}/lib/SDL2-static.lib"
"${SDL2_INSTALL_DIR}/lib/SDL2d.lib"
)
if(EXISTS "${candidate}")
set(_have_sdl2 TRUE)
break()
endif()
endforeach()
if(NOT _have_sdl2)
log("Configuring SDL2 ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${SDL2_SRC_DIR}"
-B "${SDL2_BUILD_DIR}"
-DCMAKE_INSTALL_PREFIX=${SDL2_INSTALL_DIR}
-DCMAKE_PREFIX_PATH=${ZLIB_INSTALL_DIR} # путь к zlib для SDL2
RESULT_VARIABLE _sdl_cfg_res
)
if(NOT _sdl_cfg_res EQUAL 0)
message(FATAL_ERROR "SDL2 configure failed")
endif()
# --- ИЗМЕНЕНИЕ: Цикл по конфигурациям Debug и Release ---
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Building SDL2 (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${SDL2_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _sdl_build_res
)
if(NOT _sdl_build_res EQUAL 0)
message(FATAL_ERROR "SDL2 build failed for configuration ${cfg}")
endif()
log("Installing SDL2 (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${SDL2_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _sdl_inst_res
)
if(NOT _sdl_inst_res EQUAL 0)
message(FATAL_ERROR "SDL2 install failed for configuration ${cfg}")
endif()
endforeach()
# ------------------------------------------------------
endif()
# ИСПРАВЛЕНИЕ: SDL2: Используем свойства для конкретных конфигураций
add_library(SDL2_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(SDL2_external_lib PROPERTIES
# Динамическая линковка SDL2
IMPORTED_LOCATION_DEBUG "${SDL2_INSTALL_DIR}/lib/SDL2d.lib"
IMPORTED_LOCATION_RELEASE "${SDL2_INSTALL_DIR}/lib/SDL2.lib"
# Оба include-пути: и include, и include/SDL2
INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INSTALL_DIR}/include;${SDL2_INSTALL_DIR}/include/SDL2"
)
# SDL2main (обычно статическая)
add_library(SDL2main_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(SDL2main_external_lib PROPERTIES
# ИСПРАВЛЕНО: Указываем пути для Debug и Release, используя
# соглашение, что Debug имеет суффикс 'd', а Release нет.
IMPORTED_LOCATION_DEBUG "${SDL2_INSTALL_DIR}/lib/SDL2maind.lib"
IMPORTED_LOCATION_RELEASE "${SDL2_INSTALL_DIR}/lib/SDL2main.lib"
INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INSTALL_DIR}/include"
)
log("-----${SDL2_INSTALL_DIR}/lib/SDL2maind.lib")
# ===========================================
# 3) libpng (v1.6.51.zip libpng-1.6.51) - без изменений
# ===========================================
set(LIBPNG_SRC_DIR "${THIRDPARTY_DIR}/libpng-1.6.51")
set(LIBPNG_BUILD_DIR "${LIBPNG_SRC_DIR}/build")
set(LIBPNG_INSTALL_DIR "${LIBPNG_SRC_DIR}/install") # на будущее
file(MAKE_DIRECTORY "${LIBPNG_BUILD_DIR}")
# Проверяем, есть ли уже .lib (build/Debug или install/lib)
set(_libpng_candidates
"${LIBPNG_BUILD_DIR}/Debug/libpng16_staticd.lib"
"${LIBPNG_BUILD_DIR}/Release/libpng16_static.lib"
)
set(_have_png FALSE)
foreach(candidate IN LISTS _libpng_candidates)
if(EXISTS "${candidate}")
set(_have_png TRUE)
break()
endif()
endforeach()
if(NOT _have_png)
log("Configuring libpng ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${LIBPNG_SRC_DIR}"
-B "${LIBPNG_BUILD_DIR}"
-DCMAKE_INSTALL_PREFIX=${LIBPNG_INSTALL_DIR}
-DCMAKE_PREFIX_PATH=${ZLIB_INSTALL_DIR}
-DZLIB_ROOT=${ZLIB_INSTALL_DIR}
RESULT_VARIABLE _png_cfg_res
)
if(NOT _png_cfg_res EQUAL 0)
message(FATAL_ERROR "libpng configure failed")
endif()
# --- ИЗМЕНЕНИЕ: Цикл по конфигурациям Debug и Release ---
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Building libpng (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${LIBPNG_BUILD_DIR}" --config ${cfg}
RESULT_VARIABLE _png_build_res
)
if(NOT _png_build_res EQUAL 0)
message(FATAL_ERROR "libpng build failed for configuration ${cfg}")
endif()
# Поскольку вы не используете "cmake --install" для libpng,
# здесь нет необходимости в дополнительном шаге установки.
# Файлы .lib будут сгенерированы в подкаталоге ${LIBPNG_BUILD_DIR}/${cfg} (например, build/Debug или build/Release).
endforeach()
# ------------------------------------------------------
endif()
add_library(libpng_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(libpng_external_lib PROPERTIES
# Предполагая, что libpng использует статический вариант
IMPORTED_LOCATION_DEBUG "${LIBPNG_BUILD_DIR}/Debug/libpng16_staticd.lib"
IMPORTED_LOCATION_RELEASE "${LIBPNG_BUILD_DIR}/Release/libpng16_static.lib"
# png.h, pngconf.h в SRC, pnglibconf.h в BUILD
INTERFACE_INCLUDE_DIRECTORIES "${LIBPNG_SRC_DIR};${LIBPNG_BUILD_DIR}"
)
# ===========================================
# 4) libzip (v1.11.4.zip libzip-1.11.4) - НОВАЯ ЗАВИСИМОСТЬ
# ===========================================
set(LIBZIP_SRC_DIR "${THIRDPARTY_DIR}/libzip-1.11.4")
set(LIBZIP_BUILD_DIR "${LIBZIP_SRC_DIR}/build")
#set(LIBZIP_INSTALL_DIR "${LIBZIP_SRC_DIR}/install")
set(LIBZIP_BASE_DIR "${LIBZIP_SRC_DIR}/install")
file(MAKE_DIRECTORY "${LIBZIP_BUILD_DIR}")
# Проверяем, собран ли уже libzip
set(_have_zip FALSE)
foreach(candidate
"${LIBZIP_BASE_DIR}-Debug/lib/zip.lib"
"${LIBZIP_BASE_DIR}-Release/lib/zip.lib"
)
if(EXISTS "${candidate}")
set(_have_zip TRUE)
break()
endif()
endforeach()
if(NOT _have_zip)
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Configuring libzip (${cfg})...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${LIBZIP_SRC_DIR}"
-B "${LIBZIP_SRC_DIR}/build-${cfg}"
-DCMAKE_INSTALL_PREFIX=${LIBZIP_BASE_DIR}-${cfg}
-DCMAKE_PREFIX_PATH=${ZLIB_INSTALL_DIR}
-DZLIB_ROOT=${ZLIB_INSTALL_DIR}
-DENABLE_COMMONCRYPTO=OFF
-DENABLE_GNUTLS=OFF
-DENABLE_MBEDTLS=OFF
-DENABLE_OPENSSL=OFF
-DENABLE_WINDOWS_CRYPTO=OFF
-DENABLE_FUZZ=OFF
RESULT_VARIABLE _zip_cfg_res
)
if(NOT _zip_cfg_res EQUAL 0)
message(FATAL_ERROR "libzip configure failed")
endif()
log("Building libzip (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND} --build "${LIBZIP_SRC_DIR}/build-${cfg}" --config ${cfg} -v
RESULT_VARIABLE _zip_build_res
)
if(NOT _zip_build_res EQUAL 0)
message(FATAL_ERROR "libzip build failed for configuration ${cfg}")
endif()
log("Installing libzip (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND} --install "${LIBZIP_SRC_DIR}/build-${cfg}" --config ${cfg} -v
RESULT_VARIABLE _zip_inst_res
)
if(NOT _zip_inst_res EQUAL 0)
message(FATAL_ERROR "libzip install failed for configuration ${cfg}")
endif()
endforeach()
endif()
add_library(libzip_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(libzip_external_lib PROPERTIES
IMPORTED_LOCATION_DEBUG "${LIBZIP_BASE_DIR}-Debug/lib/zip.lib" # ИСПРАВЛЕНО
IMPORTED_LOCATION_RELEASE "${LIBZIP_BASE_DIR}-Release/lib/zip.lib" # ИСПРАВЛЕНО
INTERFACE_INCLUDE_DIRECTORIES "$<IF:$<CONFIG:Debug>,${LIBZIP_BASE_DIR}-Debug/include,${LIBZIP_BASE_DIR}-Release/include>"
# libzip требует zlib для линковки
INTERFACE_LINK_LIBRARIES zlib_external_lib
)
# ===========================================
# 5) FreeType (2.14.1) - dependency for SDL_ttf
# ===========================================
set(FREETYPE_SRC_DIR "${THIRDPARTY_DIR}/freetype-2.14.1")
set(FREETYPE_BASE_DIR "${FREETYPE_SRC_DIR}/install")
set(_have_freetype TRUE)
foreach(cfg IN LISTS BUILD_CONFIGS)
if(NOT EXISTS "${FREETYPE_BASE_DIR}-${cfg}/lib/freetype.lib" AND
NOT EXISTS "${FREETYPE_BASE_DIR}-${cfg}/lib/freetyped.lib")
set(_have_freetype FALSE)
endif()
endforeach()
if(NOT _have_freetype)
foreach(cfg IN LISTS BUILD_CONFIGS)
log("Configuring FreeType (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${FREETYPE_SRC_DIR}"
-B "${FREETYPE_SRC_DIR}/build-${cfg}"
-DCMAKE_INSTALL_PREFIX=${FREETYPE_BASE_DIR}-${cfg}
-DCMAKE_PREFIX_PATH="${ZLIB_INSTALL_DIR};${LIBPNG_INSTALL_DIR}"
-DCMAKE_DISABLE_FIND_PACKAGE_HarfBuzz=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_BZip2=TRUE
-DFT_DISABLE_BROTLI=ON
-DBUILD_SHARED_LIBS=OFF
RESULT_VARIABLE _ft_cfg_res
)
if(NOT _ft_cfg_res EQUAL 0)
message(FATAL_ERROR "FreeType configure failed for ${cfg}")
endif()
log("Building FreeType (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${FREETYPE_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _ft_build_res
)
if(NOT _ft_build_res EQUAL 0)
message(FATAL_ERROR "FreeType build failed for ${cfg}")
endif()
log("Installing FreeType (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${FREETYPE_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _ft_inst_res
)
if(NOT _ft_inst_res EQUAL 0)
message(FATAL_ERROR "FreeType install failed for ${cfg}")
endif()
endforeach()
endif()
set(_ft_debug_lib "")
foreach(cand
"${FREETYPE_BASE_DIR}-Debug/lib/freetyped.lib"
"${FREETYPE_BASE_DIR}-Debug/lib/freetype.lib"
)
if(EXISTS "${cand}")
set(_ft_debug_lib "${cand}")
break()
endif()
endforeach()
set(_ft_release_lib "")
foreach(cand
"${FREETYPE_BASE_DIR}-Release/lib/freetype.lib"
)
if(EXISTS "${cand}")
set(_ft_release_lib "${cand}")
break()
endif()
endforeach()
if(_ft_debug_lib STREQUAL "" OR _ft_release_lib STREQUAL "")
message(FATAL_ERROR "FreeType libs not found in ${FREETYPE_BASE_DIR}-Debug/Release")
endif()
add_library(freetype_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(freetype_external_lib PROPERTIES
IMPORTED_LOCATION_DEBUG "${_ft_debug_lib}"
IMPORTED_LOCATION_RELEASE "${_ft_release_lib}"
)
target_include_directories(freetype_external_lib INTERFACE
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include/freetype2,${FREETYPE_BASE_DIR}-Release/include/freetype2>"
"$<IF:$<CONFIG:Debug>,${FREETYPE_BASE_DIR}-Debug/include,${FREETYPE_BASE_DIR}-Release/include>"
)
# ===========================================
# 6) SDL_ttf (2.24.0)
# ===========================================
set(SDL2TTF_SRC_DIR "${THIRDPARTY_DIR}/SDL_ttf-release-2.24.0")
set(SDL2TTF_BASE_DIR "${SDL2TTF_SRC_DIR}/install")
set(_have_sdl2ttf TRUE)
foreach(cfg IN LISTS BUILD_CONFIGS)
if(NOT EXISTS "${SDL2TTF_BASE_DIR}-${cfg}/lib/SDL2_ttf.lib" AND
NOT EXISTS "${SDL2TTF_BASE_DIR}-${cfg}/lib/SDL2_ttfd.lib")
set(_have_sdl2ttf FALSE)
endif()
endforeach()
if(NOT _have_sdl2ttf)
foreach(cfg IN LISTS BUILD_CONFIGS)
if(cfg STREQUAL "Debug")
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2d.lib")
else()
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2.lib")
endif()
set(_FT_PREFIX "${FREETYPE_BASE_DIR}-${cfg}")
set(_FT_LIB "")
foreach(cand
"${_FT_PREFIX}/lib/freetyped.lib"
"${_FT_PREFIX}/lib/freetype.lib"
)
if(EXISTS "${cand}")
set(_FT_LIB "${cand}")
break()
endif()
endforeach()
if(_FT_LIB STREQUAL "")
message(FATAL_ERROR "FreeType library not found for ${cfg}")
endif()
log("Configuring SDL_ttf (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${SDL2TTF_SRC_DIR}"
-B "${SDL2TTF_SRC_DIR}/build-${cfg}"
-DCMAKE_INSTALL_PREFIX=${SDL2TTF_BASE_DIR}-${cfg}
-DCMAKE_PREFIX_PATH=${_FT_PREFIX};${SDL2_INSTALL_DIR}
-DSDL2_LIBRARY=${_SDL2_LIB}
-DSDL2_INCLUDE_DIR=${SDL2_INSTALL_DIR}/include/SDL2
-DFREETYPE_LIBRARY=${_FT_LIB}
-DFREETYPE_INCLUDE_DIR=${_FT_PREFIX}/include/freetype2
-DFREETYPE_INCLUDE_DIRS=${_FT_PREFIX}/include/freetype2
-DSDL2TTF_VENDORED=OFF
-DSDL2TTF_SAMPLES=OFF
RESULT_VARIABLE _ttf_cfg_res
)
if(NOT _ttf_cfg_res EQUAL 0)
message(FATAL_ERROR "SDL_ttf configure failed for ${cfg}")
endif()
log("Building SDL_ttf (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${SDL2TTF_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _ttf_build_res
)
if(NOT _ttf_build_res EQUAL 0)
message(FATAL_ERROR "SDL_ttf build failed for ${cfg}")
endif()
log("Installing SDL_ttf (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${SDL2TTF_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _ttf_inst_res
)
if(NOT _ttf_inst_res EQUAL 0)
message(FATAL_ERROR "SDL_ttf install failed for ${cfg}")
endif()
endforeach()
endif()
set(_ttf_debug_lib "")
foreach(cand
"${SDL2TTF_BASE_DIR}-Debug/lib/SDL2_ttfd.lib"
"${SDL2TTF_BASE_DIR}-Debug/lib/SDL2_ttf.lib"
)
if(EXISTS "${cand}")
set(_ttf_debug_lib "${cand}")
break()
endif()
endforeach()
set(_ttf_release_lib "")
foreach(cand
"${SDL2TTF_BASE_DIR}-Release/lib/SDL2_ttf.lib"
)
if(EXISTS "${cand}")
set(_ttf_release_lib "${cand}")
break()
endif()
endforeach()
if(_ttf_debug_lib STREQUAL "" OR _ttf_release_lib STREQUAL "")
message(FATAL_ERROR "SDL_ttf libs not found in install-Debug / install-Release")
endif()
add_library(SDL2_ttf_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(SDL2_ttf_external_lib PROPERTIES
IMPORTED_LOCATION_DEBUG "${_ttf_debug_lib}"
IMPORTED_LOCATION_RELEASE "${_ttf_release_lib}"
INTERFACE_INCLUDE_DIRECTORIES
"$<IF:$<CONFIG:Debug>,${SDL2TTF_BASE_DIR}-Debug/include,${SDL2TTF_BASE_DIR}-Release/include>;$<IF:$<CONFIG:Debug>,${SDL2TTF_BASE_DIR}-Debug/include/SDL2,${SDL2TTF_BASE_DIR}-Release/include/SDL2>"
INTERFACE_LINK_LIBRARIES
"SDL2_external_lib;freetype_external_lib"
)
# ===========================================
# 7) Eigen (5.0.0.zip eigen-5.0.0) - HEADER-ONLY
# ===========================================
set(EIGEN_SRC_DIR "${THIRDPARTY_DIR}/eigen-5.0.0")
if(NOT TARGET eigen_external_lib)
add_library(eigen_external_lib INTERFACE)
target_include_directories(eigen_external_lib INTERFACE "${EIGEN_SRC_DIR}")
endif()
# ===========================================
# 8) Boost (1.90.0) - HEADER-ONLY
# ===========================================
set(BOOST_VERSION "1.90.0")
set(BOOST_ARCHIVE_NAME "boost_1_90_0.zip")
set(BOOST_ARCHIVE "${THIRDPARTY_DIR}/${BOOST_ARCHIVE_NAME}")
# Внутри архива папка называется boost_1_90_0
set(BOOST_SRC_DIR "${THIRDPARTY_DIR}/boost_1_90_0")
if(NOT TARGET boost_external_lib)
add_library(boost_external_lib INTERFACE)
# Boost заголовки находятся непосредственно в корне распакованной папки
target_include_directories(boost_external_lib INTERFACE "${BOOST_SRC_DIR}")
endif()
# ===========================================
# 9) Lua (5.5.0) - embedded scripting language
# ===========================================
set(LUA_SRC_DIR "${THIRDPARTY_DIR}/lua-5.4.8")
if(NOT TARGET lua_static)
file(GLOB LUA_SOURCES "${LUA_SRC_DIR}/*.c")
# Exclude the standalone interpreter, compiler, and unity-build wrapper.
# onelua.c #includes all other .c files compiling it alongside them
# causes every symbol to be defined twice.
list(REMOVE_ITEM LUA_SOURCES
"${LUA_SRC_DIR}/lua.c"
"${LUA_SRC_DIR}/luac.c"
"${LUA_SRC_DIR}/onelua.c"
)
add_library(lua_static STATIC ${LUA_SOURCES})
target_include_directories(lua_static PUBLIC "${LUA_SRC_DIR}")
target_compile_definitions(lua_static PRIVATE _CRT_SECURE_NO_WARNINGS)
endif()
# ===========================================
# 10) sol2 (3.3.0) - header-only C++ bindings for Lua
# ===========================================
set(SOL2_SRC_DIR "${THIRDPARTY_DIR}/sol2-3.3.0")
# Apply patch for Clang/Emscripten compatibility in optional<T&>::emplace().
# The sentinel file prevents re-applying on subsequent cmake runs.
set(_sol2_sentinel "${SOL2_SRC_DIR}/.patched")
if(NOT EXISTS "${_sol2_sentinel}")
find_package(Git QUIET)
if(GIT_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} apply --ignore-whitespace
"${CMAKE_CURRENT_LIST_DIR}/patches/sol2-3.3.0-clang-optional.patch"
WORKING_DIRECTORY "${SOL2_SRC_DIR}"
RESULT_VARIABLE _sol2_patch_res
)
if(_sol2_patch_res EQUAL 0)
file(WRITE "${_sol2_sentinel}" "patched\n")
message(STATUS "Applied sol2 Clang optional patch")
else()
message(WARNING "sol2 patch failed (exit ${_sol2_patch_res}) — Clang/Emscripten builds may not compile")
endif()
else()
message(WARNING "Git not found — cannot apply sol2 patch automatically. "
"Apply cmake/patches/sol2-3.3.0-clang-optional.patch manually.")
endif()
endif()
if(NOT TARGET sol2_external_lib)
add_library(sol2_external_lib INTERFACE)
target_include_directories(sol2_external_lib INTERFACE "${SOL2_SRC_DIR}/include")
target_link_libraries(sol2_external_lib INTERFACE lua_static)
endif()
# ===========================================
# 11) SDL2_mixer (2.8.0) сборка из исходников
# ===========================================
set(SDL2MIXER_SRC_DIR "${THIRDPARTY_DIR}/SDL_mixer-release-2.8.0")
set(SDL2MIXER_BASE_DIR "${SDL2MIXER_SRC_DIR}/install")
set(SDL2MIXER_BASE_DIR "${SDL2MIXER_BASE_DIR}" CACHE PATH "SDL2_mixer install base directory" FORCE)
set(_have_sdl2mixer TRUE)
foreach(cfg IN LISTS BUILD_CONFIGS)
if(NOT EXISTS "${SDL2MIXER_BASE_DIR}-${cfg}/lib/SDL2_mixer.lib" AND
NOT EXISTS "${SDL2MIXER_BASE_DIR}-${cfg}/lib/SDL2_mixerd.lib")
set(_have_sdl2mixer FALSE)
endif()
endforeach()
if(NOT _have_sdl2mixer)
foreach(cfg IN LISTS BUILD_CONFIGS)
if(cfg STREQUAL "Debug")
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2d.lib")
else()
set(_SDL2_LIB "${SDL2_INSTALL_DIR}/lib/SDL2.lib")
endif()
log("Configuring SDL2_mixer (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
-G "${CMAKE_GENERATOR}"
-S "${SDL2MIXER_SRC_DIR}"
-B "${SDL2MIXER_SRC_DIR}/build-${cfg}"
-DCMAKE_INSTALL_PREFIX=${SDL2MIXER_BASE_DIR}-${cfg}
-DCMAKE_PREFIX_PATH=${SDL2_INSTALL_DIR}
-DSDL2_LIBRARY=${_SDL2_LIB}
-DSDL2_INCLUDE_DIR=${SDL2_INSTALL_DIR}/include/SDL2
-DSDL2MIXER_DEPS_SHARED=OFF
-DSDL2MIXER_VENDORED=ON
-DSDL2MIXER_SAMPLES=OFF
-DSDL2MIXER_MUSIC_CMD=OFF
-DSDL2MIXER_MOD=OFF
-DSDL2MIXER_MIDI=OFF
-DSDL2MIXER_OPUS=OFF
-DSDL2MIXER_WAVPACK=OFF
-DSDL2MIXER_MP3_MPG123=OFF
-DSDL2MIXER_MP3_DRMP3=ON
-DSDL2MIXER_FLAC_DRFLAC=ON
-DSDL2MIXER_OGG_STB=ON
-DCMAKE_DISABLE_FIND_PACKAGE_OGG=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_Vorbis=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_FLAC=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_MPG123=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_LibModPlug=TRUE
-DCMAKE_DISABLE_FIND_PACKAGE_FluidLite=TRUE
RESULT_VARIABLE _mixer_cfg_res
OUTPUT_VARIABLE _mixer_cfg_out
ERROR_VARIABLE _mixer_cfg_err
)
if(NOT _mixer_cfg_res EQUAL 0)
message(STATUS "SDL2_mixer configure stdout: ${_mixer_cfg_out}")
message(STATUS "SDL2_mixer configure stderr: ${_mixer_cfg_err}")
message(FATAL_ERROR "SDL2_mixer configure failed for ${cfg}")
endif()
log("Building SDL2_mixer (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--build "${SDL2MIXER_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _mixer_build_res
)
if(NOT _mixer_build_res EQUAL 0)
message(FATAL_ERROR "SDL2_mixer build failed for ${cfg}")
endif()
log("Installing SDL2_mixer (${cfg}) ...")
execute_process(
COMMAND ${CMAKE_COMMAND}
--install "${SDL2MIXER_SRC_DIR}/build-${cfg}" --config ${cfg}
RESULT_VARIABLE _mixer_inst_res
)
if(NOT _mixer_inst_res EQUAL 0)
message(FATAL_ERROR "SDL2_mixer install failed for ${cfg}")
endif()
endforeach()
endif()
set(_mixer_debug_lib "")
foreach(cand
"${SDL2MIXER_BASE_DIR}-Debug/lib/SDL2_mixerd.lib"
"${SDL2MIXER_BASE_DIR}-Debug/lib/SDL2_mixer.lib"
)
if(EXISTS "${cand}")
set(_mixer_debug_lib "${cand}")
break()
endif()
endforeach()
set(_mixer_release_lib "")
foreach(cand
"${SDL2MIXER_BASE_DIR}-Release/lib/SDL2_mixer.lib"
)
if(EXISTS "${cand}")
set(_mixer_release_lib "${cand}")
break()
endif()
endforeach()
if(_mixer_debug_lib STREQUAL "" OR _mixer_release_lib STREQUAL "")
message(FATAL_ERROR "SDL2_mixer libs not found in ${SDL2MIXER_BASE_DIR}-Debug/Release")
endif()
add_library(SDL2_mixer_external_lib UNKNOWN IMPORTED GLOBAL)
set_target_properties(SDL2_mixer_external_lib PROPERTIES
IMPORTED_LOCATION_DEBUG "${_mixer_debug_lib}"
IMPORTED_LOCATION_RELEASE "${_mixer_release_lib}"
INTERFACE_INCLUDE_DIRECTORIES
"$<IF:$<CONFIG:Debug>,${SDL2MIXER_BASE_DIR}-Debug/include,${SDL2MIXER_BASE_DIR}-Release/include>"
INTERFACE_LINK_LIBRARIES
"SDL2_external_lib"
)

View File

@ -1,14 +0,0 @@
--- a/include/sol/optional_implementation.hpp
+++ b/include/sol/optional_implementation.hpp
@@ -2189,7 +2189,10 @@
template <class... Args>
T& emplace(Args&&... args) noexcept {
static_assert(std::is_constructible<T, Args&&...>::value, "T must be constructible with Args");
*this = nullopt;
- this->construct(std::forward<Args>(args)...);
+ // Reference specialization stores a pointer; set it directly.
+ // construct() only exists in the non-reference specialization.
+ m_value = std::addressof(std::forward<Args>(args)...);
+ return *m_value;
}

View File

@ -0,0 +1,36 @@
cmake_minimum_required(VERSION 3.10)
project(AudioPlayer)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Use pkg-config to find Vorbis
#find_package(PkgConfig REQUIRED)
#pkg_check_modules(VORBIS REQUIRED vorbis vorbisfile)
#pkg_check_modules(OGG REQUIRED ogg)
find_package(OpenAL REQUIRED)
add_library(audioplayer
src/AudioPlayer.cpp
include/AudioPlayer.hpp
)
target_include_directories(audioplayer
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${OPENAL_INCLUDE_DIR}
${VORBIS_INCLUDE_DIRS}
${OGG_INCLUDE_DIRS}
)
target_link_libraries(audioplayer
PUBLIC
${OPENAL_LIBRARY}
${VORBIS_LIBRARIES}
${OGG_LIBRARIES}
)
# Test executable
add_executable(test_audio examples/test_audio.cpp)
target_link_libraries(test_audio PRIVATE audioplayer stdc++fs)
#git add ../../sounds

View File

@ -0,0 +1,32 @@
#include "AudioPlayer.hpp"
#include <iostream>
#include <thread>
#include <chrono>
#include <string>
int main() {
try {
AudioPlayer player;
const std::string filename = "Symphony No.6 (1st movement).ogg";
std::cout << "🔍 Looking for file: " << filename << " in sounds directory...\n";
if (!player.playFromSoundsDir(filename)) {
std::cout << "❌ Failed to play audio file\n";
return 1;
}
std::cout << "✅ Playing symphony...\n";
// Check status for 30 seconds
for (int i = 0; i < 30; ++i) {
std::cout << "📊 Status: " << (player.isPlaying() ? "Playing ▶️" : "Stopped ⏹️") << "\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 0;
} catch (const std::exception& e) {
std::cerr << "❌ Error: " << e.what() << "\n";
return 1;
}
}

View File

@ -0,0 +1,37 @@
#pragma once
#include <string>
#include <AL/al.h>
#include <AL/alc.h>
#include <vorbis/vorbisfile.h>
#include <vector>
#include <cstdint>
class AudioPlayer {
public:
AudioPlayer();
~AudioPlayer();
// Для музыки с зацикливанием (если filename пустой - продолжает играть текущую)
bool playMusic(const std::string& filename = "");
// Для одноразовых звуковых эффектов
bool playSound(const std::string& filename);
void stop();
bool isPlaying() const;
private:
ALCdevice* device;
ALCcontext* context;
ALuint musicSource; // Источник для музыки
ALuint soundSource; // Источник для звуков
ALuint musicBuffer; // Буфер для музыки
ALuint soundBuffer; // Буфер для звуков
bool playing;
std::string currentMusic; // Хранит имя текущего музыкального файла
std::vector<char> loadOgg(const std::string& filename, ALuint buffer);
std::string findFileInSounds(const std::string& filename);
bool isOggFile(const std::string& filename) const;
};

View File

@ -0,0 +1,194 @@
#include "AudioPlayer.hpp"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <cstdint>
#include <algorithm>
AudioPlayer::AudioPlayer() : device(nullptr), context(nullptr),
musicSource(0), soundSource(0), musicBuffer(0), soundBuffer(0), playing(false) {
device = alcOpenDevice(nullptr);
if (!device) {
throw std::runtime_error("Failed to open audio device");
}
context = alcCreateContext(device, nullptr);
if (!context) {
alcCloseDevice(device);
throw std::runtime_error("Failed to create audio context");
}
alcMakeContextCurrent(context);
alGenSources(1, &musicSource);
alGenSources(1, &soundSource);
alGenBuffers(1, &musicBuffer);
alGenBuffers(1, &soundBuffer);
}
AudioPlayer::~AudioPlayer() {
if (musicSource)
alDeleteSources(1, &musicSource);
if (soundSource)
alDeleteSources(1, &soundSource);
if (musicBuffer)
alDeleteBuffers(1, &musicBuffer);
if (soundBuffer)
alDeleteBuffers(1, &soundBuffer);
if (context) {
alcMakeContextCurrent(nullptr);
alcDestroyContext(context);
}
if (device)
alcCloseDevice(device);
}
bool AudioPlayer::isOggFile(const std::string& filename) const {
std::string ext = std::filesystem::path(filename).extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
return ext == ".ogg";
}
std::string AudioPlayer::findFileInSounds(const std::string& filename) {
// Primary search path - "sounds" directory next to executable
std::filesystem::path soundsDir = std::filesystem::current_path() / "sounds";
// Alternative search paths
std::vector<std::filesystem::path> altPaths = {
std::filesystem::current_path() / ".." / "sounds", // One level up
std::filesystem::current_path() / ".." / ".." / "sounds", // Two levels up
"/home/albert/gay-jam/ZeptoLabTest1/sounds" // Absolute path
};
std::cout << "🔍 Searching for \"" << filename << "\" in:\n";
std::cout << " " << soundsDir << "\n";
if (std::filesystem::exists(soundsDir / filename)) {
return (soundsDir / filename).string();
}
// Try alternative paths
for (const auto& path : altPaths) {
std::cout << " " << path << "\n";
if (std::filesystem::exists(path / filename)) {
return (path / filename).string();
}
}
throw std::runtime_error("❌ File not found: " + filename);
}
std::vector<char> AudioPlayer::loadOgg(const std::string& filename, ALuint buffer) {
FILE* file = fopen(filename.c_str(), "rb");
if (!file) {
throw std::runtime_error("Cannot open file: " + filename);
}
OggVorbis_File vf;
if (ov_open_callbacks(file, &vf, nullptr, 0, OV_CALLBACKS_DEFAULT) < 0) {
fclose(file);
throw std::runtime_error("Input not an Ogg file: " + filename);
}
vorbis_info* vi = ov_info(&vf, -1);
std::vector<char> audioData;
char data[4096];
int bitstream;
long bytes;
do {
bytes = ov_read(&vf, data, sizeof(data), 0, 2, 1, &bitstream);
if (bytes > 0) {
audioData.insert(audioData.end(), data, data + bytes);
}
} while (bytes > 0);
// Setup the buffer with the audio data
alBufferData(buffer,
(vi->channels == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16,
audioData.data(),
audioData.size(),
vi->rate);
ov_clear(&vf);
return audioData;
}
bool AudioPlayer::playMusic(const std::string& filename) {
try {
// Если filename пустой, просто проверяем играет ли музыка
if (filename.empty()) {
if (!isPlaying()) {
alSourcei(musicSource, AL_LOOPING, AL_TRUE); // Включаем зацикливание
alSourcePlay(musicSource);
}
return true;
}
// Если filename не пустой, загружаем новую музыку
if (!isOggFile(filename)) {
std::cerr << "❌ Error: Music file must be an .ogg file\n";
return false;
}
std::string fullPath = findFileInSounds(filename);
std::cout << "✅ Found music file: " << fullPath << "\n";
// Останавливаем текущую музыку
alSourceStop(musicSource);
// Загружаем и настраиваем новую музыку
loadOgg(fullPath, musicBuffer);
alSourcei(musicSource, AL_BUFFER, musicBuffer);
alSourcei(musicSource, AL_LOOPING, AL_TRUE); // Включаем зацикливание
std::cout << "▶️ Starting music playback... " << musicSource << std::endl;
std::cout << "▶️ Music buffer... " << musicBuffer << std::endl;
alSourcePlay(musicSource);
currentMusic = filename;
playing = true;
return true;
} catch (const std::exception& e) {
std::cerr << "❌ Error playing music: " << e.what() << std::endl;
return false;
}
}
bool AudioPlayer::playSound(const std::string& filename) {
try {
if (!isOggFile(filename)) {
std::cerr << "❌ Error: Sound file must be an .ogg file\n";
return false;
}
std::string fullPath = findFileInSounds(filename);
std::cout << "✅ Found sound file: " << fullPath << "\n";
// Загружаем и настраиваем звук
loadOgg(fullPath, soundBuffer);
alSourcei(soundSource, AL_BUFFER, soundBuffer);
alSourcei(soundSource, AL_LOOPING, AL_FALSE); // Выключаем зацикливание
std::cout << "▶️ Playing sound effect...\n";
alSourcePlay(soundSource);
return true;
} catch (const std::exception& e) {
std::cerr << "❌ Error playing sound: " << e.what() << std::endl;
return false;
}
}
void AudioPlayer::stop() {
alSourceStop(musicSource);
alSourceStop(soundSource);
playing = false;
}
bool AudioPlayer::isPlaying() const {
ALint state;
alGetSourcei(musicSource, AL_SOURCE_STATE, &state);
return state == AL_PLAYING;
}

View File

@ -1,323 +0,0 @@
#!/usr/bin/env python3
"""
Convert a text-based bone animation file to the BSAF binary format.
Usage:
python convert_anim_to_binary.py <input.txt> <output.bin>
Binary format (BSAF v2) -- all values little-endian:
HEADER
4 bytes magic "BSAF"
uint32 version (2)
BONES
uint32 numBones
per bone:
3 x float boneStartWorld (from HEAD_LOCAL)
float boneLength
9 x float 3x3 rotation matrix (row-major)
int32 parentIndex (-1 if none)
uint32 numChildren
numChildren x int32 childIndices
BONE NAMES (v2+)
per bone:
uint32 nameLen
nameLen bytes UTF-8 name (no terminator)
VERTICES
uint32 numVertices
numVertices x 3 x float positions
UV COORDINATES
uint32 numFaces
numFaces x 6 x float 3 UV pairs per face (u0,v0,u1,v1,u2,v2)
NORMALS
numVertices x 3 x float normals
TRIANGLES
uint32 numTriangles
numTriangles x 3 x int32 vertex indices
VERTEX WEIGHTS
per vertex (numVertices):
uint32 numGroups
numGroups x (int32 boneIndex, float weight)
ANIMATION KEYFRAMES
uint32 numKeyframes
per keyframe:
int32 frameNumber
per bone (numBones, in index order 0..N-1):
3 x float location
16 x float 4x4 matrix (row-major)
"""
import struct
import re
import sys
def parse_floats(line):
return [float(x) for x in re.findall(r'[-]?\d+\.\d+', line)]
def parse_first_int(line):
m = re.search(r'\d+', line)
if m:
return int(m.group())
raise ValueError(f"No integer found in: {line}")
def parse_children(line):
return re.findall(r"'([^']+)'", line)
def convert(input_path, output_path):
with open(input_path, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
idx = 0
def next_line():
nonlocal idx
line = lines[idx].rstrip()
idx += 1
return line
# --- Skip armature matrix (5 lines) ---
for _ in range(5):
next_line()
# --- Bone count ---
line = next_line() # "=== Armature Bones: 65"
num_bones = parse_first_int(line)
bone_names = []
bones = []
bone_parent_names = []
bone_children_names = []
for _ in range(num_bones):
bone = {}
# "Bone: mixamorig:Hips"
line = next_line()
bone_name = line[6:]
bone_names.append(bone_name)
# " HEAD_LOCAL: <Vector (x, y, z)>"
line = next_line()
bone['head'] = parse_floats(line)[:3]
# " TAIL_LOCAL: ..." -- skip
next_line()
# " Length: 0.123"
line = next_line()
bone['length'] = parse_floats(line)[0]
# 3x3 matrix (3 rows)
mat = []
for _ in range(3):
mat.extend(parse_floats(next_line()))
bone['matrix_3x3'] = mat
# " Parent: None" or " Parent: boneName"
line = next_line()
if line == " Parent: None":
bone_parent_names.append(None)
else:
bone_parent_names.append(line[10:])
# " Children: ['a', 'b'] or []"
line = next_line()
bone_children_names.append(parse_children(line))
bones.append(bone)
# Build name -> index map
name_to_idx = {name: i for i, name in enumerate(bone_names)}
# Resolve parent / child indices
for i in range(num_bones):
if bone_parent_names[i] is None:
bones[i]['parent'] = -1
else:
bones[i]['parent'] = name_to_idx[bone_parent_names[i]]
bones[i]['children'] = [name_to_idx[c] for c in bone_children_names[i]]
# --- Vertices ---
line = next_line() # "===Vertices: 5140"
num_vertices = parse_first_int(line)
vertices = []
for _ in range(num_vertices):
vertices.append(parse_floats(next_line())[:3])
# --- UV Coordinates ---
next_line() # "===UV Coordinates:"
line = next_line() # "Face count: 8602"
num_faces = parse_first_int(line)
uvs = []
for _ in range(num_faces):
next_line() # "Face N"
next_line() # "UV Count: 3"
face_uvs = []
for _ in range(3):
face_uvs.extend(parse_floats(next_line())[:2])
uvs.append(face_uvs) # 6 floats
# --- Normals ---
next_line() # "===Normals:"
normals = []
for _ in range(num_vertices):
normals.append(parse_floats(next_line())[:3])
# --- Triangles ---
line = next_line() # "===Triangles: 8602"
num_triangles = parse_first_int(line)
triangles = []
for _ in range(num_triangles):
line = next_line()
ints = [int(x) for x in re.findall(r'[-]?\d+', line)]
triangles.append(ints[:3])
# --- Vertex Weights ---
next_line() # "=== Vertex Weights ..."
vertex_weights = []
for _ in range(num_vertices):
next_line() # "Vertex N:"
line = next_line() # "Vertex groups: 2"
num_groups = parse_first_int(line)
groups = []
for _ in range(num_groups):
line = next_line()
m = re.search(r"'([^']+)'.*?([-]?\d+\.\d+)", line)
bone_name = m.group(1)
weight = float(m.group(2))
groups.append((name_to_idx[bone_name], weight))
vertex_weights.append(groups)
# --- Animation Keyframes ---
next_line() # "=== Animation Keyframes ==="
next_line() # "=== Bone Transforms per Keyframe ==="
line = next_line() # "Keyframes: 32"
num_keyframes = parse_first_int(line)
keyframes = []
for _ in range(num_keyframes):
line = next_line() # "Frame: 0"
frame_number = parse_first_int(line)
bone_data = {}
for _ in range(num_bones):
line = next_line() # " Bone: mixamorig:Hips"
bone_name = line.strip()
if bone_name.startswith("Bone: "):
bone_name = bone_name[6:]
bone_idx = name_to_idx[bone_name]
# Location
location = parse_floats(next_line())[:3]
# Rotation (skip)
next_line()
# " Matrix:" (skip header)
next_line()
# 4 rows of 4 floats
matrix = []
for _ in range(4):
matrix.extend(parse_floats(next_line()))
bone_data[bone_idx] = {
'location': location,
'matrix': matrix,
}
keyframes.append((frame_number, bone_data))
# ================================================================
# Write binary file
# ================================================================
with open(output_path, 'wb') as out:
# Header
out.write(b'BSAF')
out.write(struct.pack('<I', 2))
# Bones
out.write(struct.pack('<I', num_bones))
for i in range(num_bones):
b = bones[i]
out.write(struct.pack('<3f', *b['head']))
out.write(struct.pack('<f', b['length']))
out.write(struct.pack('<9f', *b['matrix_3x3']))
out.write(struct.pack('<i', b['parent']))
out.write(struct.pack('<I', len(b['children'])))
for c in b['children']:
out.write(struct.pack('<i', c))
# Bone names (v2+)
for name in bone_names:
name_bytes = name.encode('utf-8')
out.write(struct.pack('<I', len(name_bytes)))
out.write(name_bytes)
# Vertices
out.write(struct.pack('<I', num_vertices))
for v in vertices:
out.write(struct.pack('<3f', *v))
# UV Coordinates
out.write(struct.pack('<I', num_faces))
for uv in uvs:
out.write(struct.pack('<6f', *uv))
# Normals
for n in normals:
out.write(struct.pack('<3f', *n))
# Triangles
out.write(struct.pack('<I', num_triangles))
for t in triangles:
out.write(struct.pack('<3i', *t))
# Vertex Weights
for vw in vertex_weights:
out.write(struct.pack('<I', len(vw)))
for bone_idx, weight in vw:
out.write(struct.pack('<if', bone_idx, weight))
# Animation Keyframes
out.write(struct.pack('<I', num_keyframes))
for frame_num, bone_data in keyframes:
out.write(struct.pack('<i', frame_num))
for i in range(num_bones):
bd = bone_data[i]
out.write(struct.pack('<3f', *bd['location']))
out.write(struct.pack('<16f', *bd['matrix']))
input_size = sum(len(l) for l in lines)
import os
output_size = os.path.getsize(output_path)
print(f"Converted: {input_path} ({input_size:,} bytes text) -> {output_path} ({output_size:,} bytes binary)")
print(f" Bones: {num_bones}, Vertices: {num_vertices}, Faces: {num_faces}, "
f"Triangles: {num_triangles}, Keyframes: {num_keyframes}")
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <input.txt> <output.bin>")
sys.exit(1)
convert(sys.argv[1], sys.argv[2])

View File

@ -1,370 +0,0 @@
#!/usr/bin/env python3
"""
Convert a text-based multi-mesh bone animation file to the BSMF binary format.
Usage:
python convert_anim_to_binary_new.py <input.txt> <output.bin>
Binary format (BSMF v2) -- all values little-endian:
HEADER
4 bytes magic "BSMF"
uint32 version (2)
ARMATURE MATRIX
16 x float 4x4 matrix (row-major)
BONES
uint32 numBones
per bone:
3 x float boneStartWorld (from HEAD_LOCAL)
float boneLength
9 x float 3x3 rotation matrix (row-major)
int32 parentIndex (-1 if none)
uint32 numChildren
numChildren x int32 childIndices
BONE NAMES
per bone:
uint32 nameLen
nameLen bytes UTF-8 name (no terminator)
MESHES
uint32 numMeshes
per mesh:
uint32 nameLength
nameLength x char meshName (UTF-8, no null terminator)
VERTICES
uint32 numVertices
numVertices x 3 x float positions
UV COORDINATES
uint32 numFaces
numFaces x 6 x float 3 UV pairs per face (u0,v0,u1,v1,u2,v2)
NORMALS
numVertices x 3 x float normals
TRIANGLES
uint32 numTriangles
numTriangles x 3 x int32 vertex indices
VERTEX WEIGHTS
per vertex (numVertices):
uint32 numGroups
numGroups x (int32 boneIndex, float weight)
ANIMATION KEYFRAMES
uint32 numKeyframes
per keyframe:
int32 frameNumber
per bone (numBones, in index order 0..N-1):
3 x float location
16 x float 4x4 matrix (row-major)
"""
import struct
import re
import sys
def parse_floats(line):
return [float(x) for x in re.findall(r'[-]?\d+\.\d+', line)]
def parse_first_int(line):
m = re.search(r'\d+', line)
if m:
return int(m.group())
raise ValueError(f"No integer found in: {line}")
def parse_children(line):
return re.findall(r"'([^']+)'", line)
def convert(input_path, output_path):
with open(input_path, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
idx = 0
def next_line():
nonlocal idx
line = lines[idx].rstrip()
idx += 1
return line
# --- Armature matrix header + 4 rows ---
next_line() # "=== Armature Matrix ==="
armature_matrix = []
for _ in range(4):
armature_matrix.extend(parse_floats(next_line())[:4])
# --- Bone count ---
line = next_line() # "=== Armature Bones: 65"
num_bones = parse_first_int(line)
bone_names = []
bones = []
bone_parent_names = []
bone_children_names = []
for _ in range(num_bones):
bone = {}
# "Bone: mixamorig:Hips"
line = next_line()
bone_name = line[6:]
bone_names.append(bone_name)
# " HEAD_LOCAL: <Vector (x, y, z)>"
line = next_line()
bone['head'] = parse_floats(line)[:3]
# " TAIL_LOCAL: ..." -- skip
next_line()
# " Length: 0.123"
line = next_line()
bone['length'] = parse_floats(line)[0]
# 3x3 matrix (3 rows)
mat = []
for _ in range(3):
mat.extend(parse_floats(next_line()))
bone['matrix_3x3'] = mat
# " Parent: None" or " Parent: boneName"
line = next_line()
if line == " Parent: None":
bone_parent_names.append(None)
else:
bone_parent_names.append(line[10:])
# " Children: ['a', 'b'] or []"
line = next_line()
bone_children_names.append(parse_children(line))
bones.append(bone)
# Build name -> index map
name_to_idx = {name: i for i, name in enumerate(bone_names)}
# Resolve parent / child indices
for i in range(num_bones):
if bone_parent_names[i] is None:
bones[i]['parent'] = -1
else:
bones[i]['parent'] = name_to_idx[bone_parent_names[i]]
bones[i]['children'] = [name_to_idx[c] for c in bone_children_names[i]]
# --- Multi-mesh header ---
line = next_line() # "=== TOTAL MESHES TO EXPORT: 7 ==="
num_meshes = parse_first_int(line)
meshes = []
for _ in range(num_meshes):
# "=== Mesh Object: Name ==="
line = next_line()
m = re.match(r"===\s*Mesh Object:\s*(.+?)\s*===$", line)
if not m:
raise ValueError(f"Invalid mesh header: {line}")
mesh_name = m.group(1)
# --- Vertices ---
line = next_line() # "===Vertices: N"
num_vertices = parse_first_int(line)
vertices = []
for _ in range(num_vertices):
vertices.append(parse_floats(next_line())[:3])
# --- UV Coordinates ---
next_line() # "===UV Coordinates:"
line = next_line() # "Face count: M"
num_faces = parse_first_int(line)
uvs = []
for _ in range(num_faces):
next_line() # "Face N"
next_line() # "UV Count: 3"
face_uvs = []
for _ in range(3):
face_uvs.extend(parse_floats(next_line())[:2])
uvs.append(face_uvs)
# --- Normals ---
next_line() # "===Normals:"
normals = []
for _ in range(num_vertices):
normals.append(parse_floats(next_line())[:3])
# --- Triangles ---
line = next_line() # "===Triangles: M"
num_triangles = parse_first_int(line)
triangles = []
for _ in range(num_triangles):
line = next_line()
ints = [int(x) for x in re.findall(r'[-]?\d+', line)]
triangles.append(ints[:3])
# --- Vertex Weights ---
next_line() # "=== Vertex Weights (Max 5 bones per vertex) ==="
vertex_weights = []
for _ in range(num_vertices):
next_line() # "Vertex N:"
line = next_line() # "Vertex groups: K"
num_groups = parse_first_int(line)
groups = []
for _ in range(num_groups):
line = next_line()
m = re.search(r"'([^']+)'.*?([-]?\d+\.\d+)", line)
bone_name = m.group(1)
weight = float(m.group(2))
groups.append((name_to_idx[bone_name], weight))
vertex_weights.append(groups)
meshes.append({
'name': mesh_name,
'num_vertices': num_vertices,
'vertices': vertices,
'num_faces': num_faces,
'uvs': uvs,
'normals': normals,
'num_triangles': num_triangles,
'triangles': triangles,
'vertex_weights': vertex_weights,
})
# --- Animation Keyframes ---
next_line() # "=== Animation Keyframes ==="
next_line() # "=== Bone Transforms per Keyframe ==="
line = next_line() # "Keyframes: N"
num_keyframes = parse_first_int(line)
keyframes = []
for _ in range(num_keyframes):
line = next_line() # "Frame: N"
frame_number = parse_first_int(line)
bone_data = {}
for _ in range(num_bones):
line = next_line() # " Bone: mixamorig:Hips"
bone_name = line.strip()
if bone_name.startswith("Bone: "):
bone_name = bone_name[6:]
bone_idx = name_to_idx[bone_name]
# Location
location = parse_floats(next_line())[:3]
# Rotation (skip)
next_line()
# " Matrix:" (skip header)
next_line()
# 4 rows of 4 floats
matrix = []
for _ in range(4):
matrix.extend(parse_floats(next_line()))
bone_data[bone_idx] = {
'location': location,
'matrix': matrix,
}
keyframes.append((frame_number, bone_data))
# ================================================================
# Write binary file
# ================================================================
with open(output_path, 'wb') as out:
# Header
out.write(b'BSMF')
out.write(struct.pack('<I', 2))
# Armature matrix (16 floats, row-major)
out.write(struct.pack('<16f', *armature_matrix))
# Bones
out.write(struct.pack('<I', num_bones))
for i in range(num_bones):
b = bones[i]
out.write(struct.pack('<3f', *b['head']))
out.write(struct.pack('<f', b['length']))
out.write(struct.pack('<9f', *b['matrix_3x3']))
out.write(struct.pack('<i', b['parent']))
out.write(struct.pack('<I', len(b['children'])))
for c in b['children']:
out.write(struct.pack('<i', c))
# Bone names
for name in bone_names:
name_bytes = name.encode('utf-8')
out.write(struct.pack('<I', len(name_bytes)))
out.write(name_bytes)
# Meshes
out.write(struct.pack('<I', num_meshes))
for md in meshes:
name_bytes = md['name'].encode('utf-8')
out.write(struct.pack('<I', len(name_bytes)))
out.write(name_bytes)
# Vertices
out.write(struct.pack('<I', md['num_vertices']))
for v in md['vertices']:
out.write(struct.pack('<3f', *v))
# UV Coordinates
out.write(struct.pack('<I', md['num_faces']))
for uv in md['uvs']:
out.write(struct.pack('<6f', *uv))
# Normals
for n in md['normals']:
out.write(struct.pack('<3f', *n))
# Triangles
out.write(struct.pack('<I', md['num_triangles']))
for t in md['triangles']:
out.write(struct.pack('<3i', *t))
# Vertex weights
for vw in md['vertex_weights']:
out.write(struct.pack('<I', len(vw)))
for bone_idx, weight in vw:
out.write(struct.pack('<if', bone_idx, weight))
# Animation Keyframes
out.write(struct.pack('<I', num_keyframes))
for frame_num, bone_data in keyframes:
out.write(struct.pack('<i', frame_num))
for i in range(num_bones):
bd = bone_data[i]
out.write(struct.pack('<3f', *bd['location']))
out.write(struct.pack('<16f', *bd['matrix']))
input_size = sum(len(l) for l in lines)
import os
output_size = os.path.getsize(output_path)
print(f"Converted: {input_path} ({input_size:,} bytes text) -> {output_path} ({output_size:,} bytes binary)")
print(f" Bones: {num_bones}, Meshes: {num_meshes}, Keyframes: {num_keyframes}")
for md in meshes:
print(f" - {md['name']}: {md['num_vertices']} verts, "
f"{md['num_faces']} faces, {md['num_triangles']} tris")
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <input.txt> <output.bin>")
sys.exit(1)
convert(sys.argv[1], sys.argv[2])

View File

@ -1,63 +0,0 @@
#!/usr/bin/env python3
"""
convert_config_meshes.py - Bulk-convert 3D mesh files referenced in a game object JSON config.
Reads <src_config>, converts every .txt mesh to binary (.txt.bin via BSMF format),
and writes the updated config to <dst_config> with meshPath values pointing to the
.bin files. Works for both regular game object configs and interactive object configs.
Mesh paths in the JSON are relative to the current working directory run this
script from the project root.
Usage:
python convert_config_meshes.py <src_config.json> <dst_config.json>
"""
import json
import os
import sys
from convert_model_to_binary import convert
def convert_config(src_path: str, dst_path: str) -> None:
with open(src_path, 'r', encoding='utf-8') as f:
config = json.load(f)
objects = config.get("objects", [])
# Track already-converted paths so shared meshes are only processed once.
cache: dict[str, str | None] = {}
for obj in objects:
mesh_path = obj.get("meshPath")
if not mesh_path or not mesh_path.lower().endswith(".txt"):
continue
if mesh_path not in cache:
if not os.path.isfile(mesh_path):
print(f" WARNING: mesh not found, skipping: {mesh_path}")
cache[mesh_path] = None
else:
bin_path = mesh_path + ".bin"
convert(mesh_path, bin_path)
cache[mesh_path] = bin_path
if cache[mesh_path] is not None:
obj["meshPath"] = cache[mesh_path]
os.makedirs(os.path.dirname(os.path.abspath(dst_path)), exist_ok=True)
with open(dst_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4, ensure_ascii=False)
converted_count = sum(1 for v in cache.values() if v is not None)
print(f"Saved: {dst_path} ({converted_count} mesh(es) converted, "
f"{len(cache) - converted_count} skipped)")
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <src_config.json> <dst_config.json>")
sys.exit(1)
convert_config(sys.argv[1], sys.argv[2])

View File

@ -1,142 +0,0 @@
#!/usr/bin/env python3
"""
Convert a text-based static mesh file (.txt) to binary format (.txt.bin).
Usage:
python convert_model_to_binary.py <input.txt> [<output.bin>]
If the output path is not given it is derived by appending ".bin" to the input path,
e.g. resources/w/firebox.txt -> resources/w/firebox.txt.bin
Binary format (BSMF v1) -- all values little-endian:
HEADER
4 bytes magic "BSMF"
uint32 version (1)
uint32 numVertices
uint32 numTriangles
VERTICES (numVertices entries):
3 x float position x, y, z -- engine coordinate space
3 x float normal x, y, z -- engine coordinate space
2 x float UV u, v
TRIANGLES (numTriangles entries):
3 x uint32 vertex indices i0, i1, i2
The same Blender->engine axis swap applied by LoadFromTextFile02 is baked in here,
so the C++ binary loader can read coordinates directly without any post-processing:
engine_x = blender_y
engine_y = blender_z
engine_z = blender_x
"""
import struct
import re
import sys
import os
def _parse_floats(text):
return [float(x) for x in re.findall(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?', text)]
def _parse_ints(text):
return [int(x) for x in re.findall(r'[-]?\d+', text)]
def _swap_axes(x, y, z):
"""Blender Y-up -> engine coordinate system (mirrors LoadFromTextFile02)."""
return y, z, x
def convert(input_path, output_path):
with open(input_path, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f]
idx = 0
def next_line():
nonlocal idx
while idx < len(lines):
line = lines[idx]
idx += 1
return line
raise EOFError("Unexpected end of file while parsing: " + input_path)
# --- Vertices ---
while True:
line = next_line()
if '===Vertices' in line:
break
m = re.search(r'\d+', line)
if not m:
raise ValueError("Could not parse vertex count from: " + line)
num_vertices = int(m.group())
positions = []
normals = []
uvs = []
for i in range(num_vertices):
line = next_line()
# V N: Pos(x, y, z) Norm(nx, ny, nz) UV(u, v)
nums = _parse_floats(line)
# nums[0] = vertex index (float-parsed), then 3 pos, 3 norm, 2 uv
if len(nums) < 9:
raise ValueError(f"Malformed vertex line {i}: {line}")
px, py, pz = _swap_axes(nums[1], nums[2], nums[3])
nx, ny, nz = _swap_axes(nums[4], nums[5], nums[6])
positions.append((px, py, pz))
normals.append((nx, ny, nz))
uvs.append((nums[7], nums[8]))
# --- Triangles ---
while True:
line = next_line()
if '===Triangles' in line:
break
m = re.search(r'\d+', line)
if not m:
raise ValueError("Could not parse triangle count from: " + line)
num_triangles = int(m.group())
triangles = []
for i in range(num_triangles):
line = next_line()
ints = _parse_ints(line)
if len(ints) != 3:
raise ValueError(f"Malformed triangle line {i}: {line}")
triangles.append(tuple(ints))
# --- Write binary ---
with open(output_path, 'wb') as out:
out.write(b'BSMF')
out.write(struct.pack('<I', 1))
out.write(struct.pack('<I', num_vertices))
out.write(struct.pack('<I', num_triangles))
for i in range(num_vertices):
out.write(struct.pack('<3f', *positions[i]))
out.write(struct.pack('<3f', *normals[i]))
out.write(struct.pack('<2f', *uvs[i]))
for tri in triangles:
out.write(struct.pack('<3I', *tri))
in_size = os.path.getsize(input_path)
out_size = os.path.getsize(output_path)
print(f"Converted: {input_path} ({in_size:,} bytes text) -> {output_path} ({out_size:,} bytes binary)")
print(f" Vertices: {num_vertices}, Triangles: {num_triangles}")
if __name__ == '__main__':
if len(sys.argv) < 2 or len(sys.argv) > 3:
print(f"Usage: {sys.argv[0]} <input.txt> [<output.bin>]")
sys.exit(1)
input_path = sys.argv[1]
output_path = sys.argv[2] if len(sys.argv) == 3 else input_path + '.bin'
convert(input_path, output_path)

View File

@ -1,54 +0,0 @@
#!/usr/bin/env python3
"""
Convert an old single-mesh text animation file to the new multi-mesh text format.
The only structural difference is that the new format wraps the mesh block
between these two extra headers before the "===Vertices:" line:
=== TOTAL MESHES TO EXPORT: 1 ===
=== Mesh Object: Body ===
Usage:
python convert_old_anim_to_new.py <input.txt> <output.txt> [mesh_name]
If mesh_name is omitted, "Body" is used.
"""
import sys
def convert(input_path, output_path, mesh_name="Body"):
with open(input_path, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines()
# Find the first "===Vertices:" line -- that's where the bone block ends
# and the mesh block begins in the old format.
insert_at = None
for i, line in enumerate(lines):
if line.lstrip().startswith("===Vertices:"):
insert_at = i
break
if insert_at is None:
raise RuntimeError("Could not find '===Vertices:' line in input file")
header_lines = [
"=== TOTAL MESHES TO EXPORT: 1 ===\n",
f"=== Mesh Object: {mesh_name} ===\n",
]
out_lines = lines[:insert_at] + header_lines + lines[insert_at:]
with open(output_path, 'w', encoding='utf-8') as out:
out.writelines(out_lines)
print(f"Converted: {input_path} -> {output_path} (mesh name: {mesh_name})")
if __name__ == '__main__':
if len(sys.argv) not in (3, 4):
print(f"Usage: {sys.argv[0]} <input.txt> <output.txt> [mesh_name]")
sys.exit(1)
mesh_name = sys.argv[3] if len(sys.argv) == 4 else "Body"
convert(sys.argv[1], sys.argv[2], mesh_name)

View File

@ -1,46 +0,0 @@
.DS_STORE
node_modules
scripts/flow/*/.flowconfig
.flowconfig
*~
*.pyc
.grunt
_SpecRunner.html
__benchmarks__
build/
remote-repo/
coverage/
.module-cache
fixtures/dom/public/react-dom.js
fixtures/dom/public/react.js
test/the-files-to-test.generated.js
*.log*
chrome-user-data
*.sublime-project
*.sublime-workspace
.idea
*.iml
.vscode
.zed
*.swp
*.swo
/tmp
/.worktrees
.claude/*.local.*
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build
packages/react-devtools-extensions/chrome/*.crx
packages/react-devtools-extensions/chrome/*.pem
packages/react-devtools-extensions/firefox/build
packages/react-devtools-extensions/firefox/*.xpi
packages/react-devtools-extensions/firefox/*.pem
packages/react-devtools-extensions/shared/build
packages/react-devtools-extensions/.tempUserDataDir
packages/react-devtools-fusebox/dist
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist
resources

View File

@ -1,351 +0,0 @@
# Cutscene System
Cutscenes are defined in JSON and loaded by `CutsceneDatabase`. Each cutscene is a self-contained object with an array of animated image layers and optional subtitle lines.
The file can contain multiple cutscenes:
```json
{
"cutscenes": [
{ "id": "intro", ... },
{ "id": "ending", ... }
]
}
```
Cutscenes and dialogues are loaded from **separate files**:
```cpp
dialogueSystem.loadDatabase("resources/dialogue/uni_interior.json"); // dialogues
dialogueSystem.loadCutsceneDatabase("resources/dialogue/cutscenes.json"); // cutscenes
```
---
## Cutscene object
```json
{
"id": "intro_cutscene",
"skippable": true,
"durationMs": 8000,
"fadeOutMs": 500,
"fadeInMs": 500,
"endFadeOutMs": 500,
"endFadeInMs": 500,
"onFadeInCallback": "",
"imageSegments": [ ... ],
"lines": [ ... ]
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `id` | string | — | Unique identifier used to start the cutscene from C++ or dialogue (**required**) |
| `skippable` | bool | `true` | Whether the player can skip by holding LMB / touch |
| `durationMs` | int | `0` | Minimum content duration in ms. The cutscene will not end before this time even if all subtitle lines have finished. `0` means duration is determined solely by subtitle lines or `imageSegments.endMs` |
| `fadeOutMs` | int | `0` | Duration of the **opening fade** — game world fades to black before the cutscene images appear |
| `fadeInMs` | int | `0` | Duration of the **opening reveal** — cutscene images fade in from black after `fadeOutMs` |
| `endFadeOutMs` | int | `0` | Duration of the **closing fade** — cutscene fades to black at the end of content |
| `endFadeInMs` | int | `0` | Duration of the **closing reveal** — game world fades back in from black |
| `onFadeInCallback` | string | `""` | Lua function name called once the opening fade-in completes (fired after `fadeOutMs + fadeInMs` ms) |
| `imageSegments` | array | `[]` | Image layers with motion — see [Image segments](#image-segments) |
| `lines` | array | `[]` | Subtitle lines shown sequentially — see [Subtitle lines](#subtitle-lines) |
### Timing model
The total cutscene duration is:
```
contentDuration = max(durationMs, max(segment.endMs for all segments))
totalDuration = contentDuration + endFadeOutMs + endFadeInMs
```
The full timeline looks like this:
```
|-- fadeOutMs --|-- fadeInMs --|--- content plays (images + subtitles) ---|-- endFadeOutMs --|-- endFadeInMs --|
world→black black→images images→black black→world
```
---
## Image segments
Each entry in `imageSegments` describes one image layer: when it is visible, how it fades in/out, and how it animates from a start pose to an end pose.
```json
{
"path": "resources/cutscenes/bg_layer.png",
"width": 1280,
"height": 720,
"startMs": 0,
"endMs": 8000,
"fadeInMs": 300,
"fadeOutMs": 300,
"easing": "EaseInOutSine",
"from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 },
"to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 }
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `path` | string | — | Path to the PNG image (**required**) |
| `width` | int | `0` | Logical width used for all UV and aspect-ratio math. `0` uses the actual texture pixel width |
| `height` | int | `0` | Logical height. `0` uses the actual texture pixel height |
| `startMs` | int | `0` | Time (ms from cutscene start) when this layer becomes active |
| `endMs` | int | `0` | Time (ms) when this layer stops being active. Must be > `startMs` |
| `fadeInMs` | int | `0` | Alpha fades from 0 → 1 over this many ms after `startMs`. `0` = instant |
| `fadeOutMs` | int | `0` | Alpha fades from 1 → 0 over this many ms before `endMs`. `0` = instant |
| `easing` | string | `"Linear"` | Easing applied to the pose interpolation — see [Easing types](#easing-types) |
| `from` | pose object | center/1.0 | Pose at `startMs` — see [Image pose](#image-pose) |
| `to` | pose object | same as `from` | Pose at `endMs`. If omitted, the layer stays at `from` the whole time |
Multiple segments can be active at the same time. They are rendered **in declaration order** (first = bottom layer, last = top layer), which enables parallax layering.
---
## Image pose
A pose defines how an image is framed on screen at a given moment.
```json
{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }
```
| Property | Type | Default | Description |
|---|---|---|---|
| `centerX` | float | `0.5` | Normalized X position (0 = left edge of image, 1 = right edge) of the point that is placed at the horizontal center of the screen |
| `centerY` | float | `0.5` | Normalized Y position (0 = top edge, 1 = bottom edge) placed at the screen center |
| `scale` | float | `1.0` | Zoom level. `1.0` = the image fills the screen exactly (aspect-ratio corrected). `2.0` = zoomed in 2×, showing half the image area |
The runtime interpolates all three values independently from `from` to `to` using the chosen easing.
**Coordinate clamping:** `centerX`/`centerY` are automatically clamped so the viewport never shows area outside the image. For a zoomed-in segment (`scale > 1`) you therefore have more freedom to pan; for `scale = 1.0` the center is locked to `0.5/0.5`.
### Pose intuition
| Goal | Config |
|---|---|
| Centered, no zoom | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.0 }` |
| Slightly zoomed in on center | `{ "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }` |
| Pan left to right | `from: { "centerX": 0.3, "scale": 1.2 }``to: { "centerX": 0.7, "scale": 1.2 }` |
| Zoom out from close-up | `from: { "scale": 1.8 }``to: { "scale": 1.0 }` |
| Look at top portion | `{ "centerY": 0.2, "scale": 1.3 }` |
---
## Easing types
Controls the interpolation curve applied to pose animation between `from` and `to`.
| Value | Description |
|---|---|
| `"Linear"` | Constant speed (default) |
| `"EaseInSine"` | Slow start, fast end |
| `"EaseOutSine"` | Fast start, slow end |
| `"EaseInOutSine"` | Slow start and end, fast middle |
| `"EaseInQuad"` | Quadratic slow start |
| `"EaseOutQuad"` | Quadratic slow end |
| `"EaseInOutQuad"` | Quadratic slow start and end |
| `"EaseInCubic"` | Cubic slow start |
| `"EaseOutCubic"` | Cubic slow end |
| `"EaseInOutCubic"` | Cubic slow start and end |
For cinematic camera motion `"EaseInOutSine"` or `"EaseInOutCubic"` give the most natural feel.
---
## Subtitle lines
Lines are displayed sequentially on top of the cutscene images. Each line shows until its duration expires (or until the player advances, if `waitForConfirm` is set).
```json
{
"speaker": "Аида Дженибековна",
"text": "Здравствуйте, студенты.",
"durationMs": 3000,
"waitForConfirm": false,
"luaCallback": ""
}
```
| Property | Type | Default | Description |
|---|---|---|---|
| `speaker` | string | `""` | Speaker name shown above the subtitle text. Empty = no name bar |
| `text` | string | `""` | Subtitle text. Supports Cyrillic and any codepoint in `resources/symbols.txt` |
| `durationMs` | int | `0` | How long this line is displayed in ms. `0` = auto-computed from text length (~17 chars/sec, minimum 1500 ms) |
| `waitForConfirm` | bool | `false` | When `true`, the line waits for player input (tap/click/Enter) before advancing. No timer runs |
| `luaCallback` | string | `""` | Lua function name called when this line begins. Useful for triggering SFX, spawning effects, etc. |
Subtitle lines run on their own timer that is **independent** of the image segments. The cutscene ends when **both** subtitle lines are exhausted **and** `contentDuration` has elapsed.
---
## C++ API
### Starting a cutscene
```cpp
// Standalone cutscene (not part of a dialogue):
dialogueSystem.startCutscene("intro_cutscene");
// Skip the currently playing cutscene:
dialogueSystem.skipCutscene();
```
### Callbacks
```cpp
// Called when a cutscene begins:
dialogueSystem.setOnCutsceneStarted([]() { /* hide HUD, etc. */ });
// Called when a cutscene ends (receives the cutscene id):
dialogueSystem.setOnCutsceneFinished([](const std::string& id) {
// id == "intro_cutscene"
});
// Called when a subtitle line begins (receives luaCallback value):
dialogueSystem.setOnCutsceneLineStarted([](const std::string& fn) {
scriptEngine.callActivateFunction(fn);
});
// Called when the opening fade-in completes (receives onFadeInCallback value):
dialogueSystem.setOnCutsceneFadeInComplete([](const std::string& fn) {
scriptEngine.callActivateFunction(fn);
});
```
### Triggering from dialogue
A dialogue node of type `CutsceneStart` embeds a cutscene mid-conversation. Dialogue resumes at `next` when the cutscene ends.
```json
{
"id": "node_cutscene",
"type": "CutsceneStart",
"cutsceneId": "intro_cutscene",
"next": "node_after_cutscene"
}
```
---
## Full examples
### Minimal — static image, timed
```json
{
"id": "simple",
"durationMs": 4000,
"fadeOutMs": 300,
"fadeInMs": 300,
"endFadeOutMs": 300,
"endFadeInMs": 300,
"imageSegments": [
{
"path": "resources/cutscenes/city.png",
"width": 1280,
"height": 720,
"startMs": 0,
"endMs": 4000
}
]
}
```
### Two-layer parallax pan
Background moves slowly left-to-right; foreground character moves faster, creating depth.
```json
{
"id": "classroom_intro",
"durationMs": 8000,
"fadeOutMs": 500,
"fadeInMs": 500,
"endFadeOutMs": 500,
"endFadeInMs": 500,
"imageSegments": [
{
"path": "resources/cutscenes/classroom_bg.png",
"width": 1920,
"height": 1080,
"startMs": 0,
"endMs": 8000,
"fadeInMs": 400,
"easing": "EaseInOutSine",
"from": { "centerX": 0.4, "centerY": 0.5, "scale": 1.1 },
"to": { "centerX": 0.6, "centerY": 0.5, "scale": 1.0 }
},
{
"path": "resources/cutscenes/classroom_teacher.png",
"width": 1920,
"height": 1080,
"startMs": 0,
"endMs": 8000,
"easing": "EaseInOutSine",
"from": { "centerX": 0.35, "centerY": 0.5, "scale": 1.0 },
"to": { "centerX": 0.65, "centerY": 0.5, "scale": 1.0 }
}
],
"lines": [
{
"speaker": "Аида Дженибековна",
"text": "Здравствуйте, студенты.",
"durationMs": 3000
},
{
"speaker": "Аида Дженибековна",
"text": "Рассаживайтесь.",
"durationMs": 2500
}
]
}
```
### Zoom-in reveal with a second image appearing mid-way
```json
{
"id": "letter_reveal",
"durationMs": 7000,
"fadeOutMs": 400,
"fadeInMs": 600,
"endFadeOutMs": 600,
"endFadeInMs": 400,
"imageSegments": [
{
"path": "resources/cutscenes/desk_bg.png",
"width": 1280,
"height": 720,
"startMs": 0,
"endMs": 7000
},
{
"path": "resources/cutscenes/letter_closeup.png",
"width": 1280,
"height": 720,
"startMs": 2000,
"endMs": 7000,
"fadeInMs": 800,
"easing": "EaseOutCubic",
"from": { "centerX": 0.5, "centerY": 0.5, "scale": 2.5 },
"to": { "centerX": 0.5, "centerY": 0.5, "scale": 1.2 }
}
],
"lines": [
{
"text": "Среди бумаг на столе лежит конверт.",
"durationMs": 2500
},
{
"speaker": "Главный герой",
"text": "«Явитесь в деканат немедленно».",
"durationMs": 3000
}
]
}
```

View File

@ -1,65 +0,0 @@
{
"cutscenes": [
{
"id": "test_cutscene_01",
"background": "resources/black.png",
"durationMs": 5000,
"fadeOutMs": 500,
"fadeInMs": 500,
"endFadeOutMs": 500,
"endFadeInMs": 500,
"imageSegments": [
{
"path": "resources/w/cutscenes/cutscene1/cutscene1_wall_x.png",
"startMs": 0,
"endMs": 8000,
"fadeInMs": 0,
"width": 1280,
"height": 720,
"from": {
"centerX": 0.3, "scale": 1.2
},
"to": {
"centerX": 0.7, "scale": 1.2
},
"easing": "Linear"
},
{
"path": "resources/w/cutscenes/cutscene1/cutscene1_aida1_x.png",
"startMs": 0,
"endMs": 8000,
"width": 1280,
"height": 720,
"from": {
"centerX": 0.3,
"centerY": 0.5,
"scale": 1.0
},
"to": {
"centerX": 0.7,
"centerY": 0.5,
"scale": 1.0
}
}
],
"lines": [
{
"speaker": "Аида Дженибековна",
"text": "Здравствуйте, студенты. Кого я вижу, где вы были весь семестр?",
"durationMs": 3000
},
{
"speaker": "Аида Дженибековна",
"text": "В эпизоде \"Семетей\" трилогии \"Манас\", изменники Канчоро и Кыяз захватывают власть над кыргызами.",
"durationMs": 3000
},
{
"speaker": "Аида Дженибековна",
"text": "На сегодня лекция завершена. Домашнее задание - к практическому занятию вы должны подготовить презентации, каждый по своей теме.",
"durationMs": 2000
}
]
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cutscene Editor</title>
<script type="module" crossorigin src="/assets/index-D_5Tak8P.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B96C0g1n.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cutscene Editor</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
{
"name": "cutscene-editor",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"immer": "^10.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.2",
"vite": "^6.0.5"
}
}

View File

@ -1,6 +0,0 @@
.app {
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
}

View File

@ -1,14 +0,0 @@
import styles from './App.module.css';
import LeftPanel from './components/LeftPanel/LeftPanel';
import CenterPanel from './components/CenterPanel/CenterPanel';
import RightPanel from './components/RightPanel/RightPanel';
export default function App() {
return (
<div className={styles.app}>
<LeftPanel />
<CenterPanel />
<RightPanel />
</div>
);
}

View File

@ -1,18 +0,0 @@
.panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
.previewArea {
flex: 0 1 420px;
min-height: 120px;
display: flex;
justify-content: center;
align-items: center;
background: #111;
padding: 8px;
overflow: hidden;
}

View File

@ -1,16 +0,0 @@
import styles from './CenterPanel.module.css';
import Preview from '../Preview/Preview';
import Controls from '../Controls/Controls';
import Timeline from '../Timeline/Timeline';
export default function CenterPanel() {
return (
<div className={styles.panel}>
<div className={styles.previewArea}>
<Preview />
</div>
<Controls />
<Timeline />
</div>
);
}

View File

@ -1,94 +0,0 @@
.controls {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: #1e1e1e;
border-top: 1px solid #333;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
.btn {
background: #2d2d2d;
border: 1px solid #404040;
color: #ccc;
border-radius: 4px;
padding: 4px 10px;
font-size: 13px;
transition: background 0.1s;
min-width: 32px;
flex-shrink: 0;
}
.btn:hover:not(:disabled) { background: #3a3a3a; color: #fff; }
.btn:disabled { opacity: 0.35; cursor: default; }
.active {
color: #5ba3e0;
border-color: #4a7aaa;
}
/* ── Scrubber ───────────────────────────────────────────────── */
.scrubber {
flex: 1;
min-width: 0;
height: 4px;
-webkit-appearance: none;
appearance: none;
border-radius: 2px;
outline: none;
cursor: pointer;
border: none;
padding: 0;
/* filled portion via CSS variable set inline */
background: linear-gradient(
to right,
#5b9bd5 0%,
#5b9bd5 var(--progress, 0%),
#3a3a3a var(--progress, 0%),
#3a3a3a 100%
);
}
.scrubber:disabled {
opacity: 0.3;
cursor: default;
}
.scrubber::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #5b9bd5;
cursor: pointer;
border: 2px solid #1e1e1e;
transition: transform 0.1s;
}
.scrubber:not(:disabled)::-webkit-slider-thumb:hover {
transform: scale(1.3);
}
.scrubber::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: #5b9bd5;
cursor: pointer;
border: 2px solid #1e1e1e;
}
.scrubber::-moz-range-track {
height: 4px;
border-radius: 2px;
background: #3a3a3a;
}
.time {
flex-shrink: 0;
font-size: 11px;
color: #888;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}

View File

@ -1,67 +0,0 @@
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
import { usePlayback } from '../../hooks/usePlayback';
import styles from './Controls.module.css';
function formatMs(ms: number) {
const s = Math.floor(ms / 1000);
const frac = Math.floor((ms % 1000) / 10).toString().padStart(2, '0');
return `${s}.${frac}s`;
}
export default function Controls() {
usePlayback();
const { playState, currentTimeMs, setPlayState, setCurrentTime } = useCutsceneStore();
const cutscene = useSelectedCutscene();
const totalMs = cutscene
? Math.max(
cutscene.durationMs,
cutscene.imageSegments.reduce((m, s) => Math.max(m, s.endMs), 0)
) + cutscene.endFadeOutMs + cutscene.endFadeInMs
: 0;
function play() {
if (playState === 'stopped' || currentTimeMs >= totalMs) setCurrentTime(0);
setPlayState('playing');
}
function pause() { setPlayState('paused'); }
function stop() { setPlayState('stopped'); setCurrentTime(0); }
function handleScrub(e: React.ChangeEvent<HTMLInputElement>) {
const ms = Number(e.target.value);
setCurrentTime(ms);
if (playState === 'playing') setPlayState('paused');
}
const progress = totalMs > 0 ? currentTimeMs / totalMs : 0;
return (
<div className={styles.controls}>
<button className={styles.btn} onClick={stop} title="Stop" disabled={!cutscene}></button>
<button className={styles.btn} onClick={() => setCurrentTime(0)} title="Rewind" disabled={!cutscene}></button>
{playState === 'playing' ? (
<button className={`${styles.btn} ${styles.active}`} onClick={pause} title="Pause" disabled={!cutscene}></button>
) : (
<button className={`${styles.btn} ${styles.active}`} onClick={play} title="Play" disabled={!cutscene}></button>
)}
<input
className={styles.scrubber}
type="range"
min={0}
max={totalMs || 1}
step={16}
value={currentTimeMs}
onChange={handleScrub}
disabled={!cutscene}
style={{ '--progress': `${progress * 100}%` } as React.CSSProperties}
/>
<div className={styles.time}>
{formatMs(currentTimeMs)} / {formatMs(totalMs)}
</div>
</div>
);
}

View File

@ -1,100 +0,0 @@
.panel {
width: 200px;
min-width: 180px;
background: #252525;
border-right: 1px solid #333;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
padding: 10px 12px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
color: #888;
border-bottom: 1px solid #333;
}
.actions {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px;
border-bottom: 1px solid #333;
}
.btn {
background: #333;
color: #ddd;
border: 1px solid #444;
border-radius: 4px;
padding: 5px 8px;
text-align: center;
transition: background 0.15s;
}
.btn:hover:not(:disabled) { background: #3d3d3d; }
.btn:disabled { opacity: 0.4; cursor: default; }
.btnPrimary {
composes: btn;
background: #2a4a6e;
border-color: #3a6090;
color: #a8d0f0;
}
.btnPrimary:hover { background: #2e5480; }
.list {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.empty {
padding: 16px 12px;
color: #555;
font-size: 11px;
line-height: 1.5;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 12px;
cursor: pointer;
user-select: none;
border-left: 3px solid transparent;
transition: background 0.1s;
}
.item:hover { background: #2e2e2e; }
.selected {
background: #1e3a55;
border-left-color: #5b9bd5;
}
.itemId {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.deleteBtn {
background: none;
border: none;
color: #666;
padding: 2px 4px;
border-radius: 3px;
font-size: 10px;
opacity: 0;
transition: opacity 0.1s, color 0.1s;
}
.item:hover .deleteBtn { opacity: 1; }
.deleteBtn:hover { color: #e06c6c; }

View File

@ -1,76 +0,0 @@
import { useRef } from 'react';
import { useCutsceneStore } from '../../store/cutsceneStore';
import { parseFile, triggerDownload } from '../../utils/fileIO';
import styles from './LeftPanel.module.css';
export default function LeftPanel() {
const fileInputRef = useRef<HTMLInputElement>(null);
const { file, selectedCutsceneId, loadFile, addCutscene, deleteCutscene, selectCutscene, getExportData } = useCutsceneStore();
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const f = e.target.files?.[0];
if (!f) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const json = JSON.parse(ev.target!.result as string);
loadFile(parseFile(json));
} catch {
alert('Invalid JSON file');
}
};
reader.readAsText(f);
e.target.value = '';
}
function handleSave() {
const data = getExportData();
if (data) triggerDownload(data);
}
function handleDelete(id: string) {
if (confirm(`Delete cutscene "${id}"?`)) deleteCutscene(id);
}
return (
<div className={styles.panel}>
<div className={styles.header}>Cutscenes</div>
<div className={styles.actions}>
<button className={styles.btnPrimary} onClick={() => fileInputRef.current?.click()}>
Load JSON
</button>
<input ref={fileInputRef} type="file" accept=".json" style={{ display: 'none' }} onChange={handleFileChange} />
<button className={styles.btn} onClick={handleSave} disabled={!file}>
Save JSON
</button>
<button className={styles.btn} onClick={addCutscene}>
+ New
</button>
</div>
<div className={styles.list}>
{!file || file.cutscenes.length === 0 ? (
<div className={styles.empty}>No cutscenes. Load a JSON or create new.</div>
) : (
file.cutscenes.map(c => (
<div
key={c.id}
className={`${styles.item} ${c.id === selectedCutsceneId ? styles.selected : ''}`}
onClick={() => selectCutscene(c.id)}
>
<span className={styles.itemId}>{c.id}</span>
<button
className={styles.deleteBtn}
onClick={(e) => { e.stopPropagation(); handleDelete(c.id); }}
title="Delete"
>
</button>
</div>
))
)}
</div>
</div>
);
}

View File

@ -1,51 +0,0 @@
.wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.viewport {
position: relative;
aspect-ratio: 16 / 9;
width: 100%;
max-height: 100%;
background: #000;
overflow: hidden;
}
.empty {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #444;
font-size: 13px;
}
.subtitleBar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 10px 20px 14px;
background: linear-gradient(transparent, rgba(0,0,0,0.75));
text-align: center;
}
.speaker {
font-size: 11px;
font-weight: 600;
color: #f0c060;
margin-bottom: 4px;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
}
.text {
font-size: 14px;
color: #fff;
line-height: 1.5;
text-shadow: 0 1px 4px rgba(0,0,0,0.9);
}

View File

@ -1,85 +0,0 @@
import { useRef, useEffect, useState } from 'react';
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
import { computeSegmentState, poseToStyle } from '../../utils/rendering';
import styles from './Preview.module.css';
const LOGICAL_W = 1280;
const LOGICAL_H = 720;
function computeSubtitle(cutscene: ReturnType<typeof useSelectedCutscene>, currentMs: number) {
if (!cutscene) return null;
let elapsed = 0;
for (const line of cutscene.lines) {
const dur = line.durationMs > 0
? line.durationMs
: Math.max(1500, Math.round((line.text.length / 17) * 1000));
if (currentMs >= elapsed && currentMs < elapsed + dur) return line;
elapsed += dur;
}
return null;
}
export default function Preview() {
const containerRef = useRef<HTMLDivElement>(null);
const [containerSize, setContainerSize] = useState({ w: LOGICAL_W, h: LOGICAL_H });
const currentTimeMs = useCutsceneStore(s => s.currentTimeMs);
const cutscene = useSelectedCutscene();
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const ro = new ResizeObserver(entries => {
const e = entries[0];
if (e) setContainerSize({ w: e.contentRect.width, h: e.contentRect.height });
});
ro.observe(el);
return () => ro.disconnect();
}, []);
const subtitle = computeSubtitle(cutscene, currentTimeMs);
return (
<div className={styles.wrapper}>
<div className={styles.viewport} ref={containerRef}>
{!cutscene ? (
<div className={styles.empty}>Select or create a cutscene</div>
) : (
<>
{cutscene.imageSegments.map((seg, i) => {
const state = computeSegmentState(seg, currentTimeMs);
if (!state) return null;
const imgStyle = poseToStyle(
state.pose,
seg.width || LOGICAL_W,
seg.height || LOGICAL_H,
containerSize.w,
containerSize.h,
);
return (
<img
key={i}
src={`/${seg.path}`}
alt=""
style={{ ...imgStyle, opacity: state.alpha }}
draggable={false}
/>
);
})}
{subtitle && (
<div className={styles.subtitleBar}>
{subtitle.speaker && (
<div className={styles.speaker}>{subtitle.speaker}</div>
)}
<div className={styles.text}>{subtitle.text}</div>
</div>
)}
</>
)}
</div>
</div>
);
}

View File

@ -1,72 +0,0 @@
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
import type { Cutscene } from '../../types/cutscene';
import styles from './RightPanel.module.css';
type CutscenePatch = Partial<Omit<Cutscene, 'imageSegments' | 'lines'>>;
function NumField({ label, value, onChange, min }: {
label: string; value: number; onChange: (v: number) => void; min?: number;
}) {
return (
<div className={styles.field}>
<label>{label}</label>
<input
type="number"
value={value}
min={min}
onChange={e => onChange(Number(e.target.value))}
/>
</div>
);
}
export default function CutsceneProperties() {
const cutscene = useSelectedCutscene();
const updateCutscene = useCutsceneStore(s => s.updateCutscene);
if (!cutscene) return <div className={styles.empty}>No cutscene selected</div>;
function upd(patch: CutscenePatch) {
updateCutscene(cutscene!.id, patch);
}
return (
<div className={styles.section}>
<div className={styles.sectionTitle}>Cutscene</div>
<div className={styles.field}>
<label>ID</label>
<input
type="text"
value={cutscene.id}
onChange={e => upd({ id: e.target.value })}
/>
</div>
<div className={styles.field}>
<label>Skippable</label>
<input
type="checkbox"
checked={cutscene.skippable}
onChange={e => upd({ skippable: e.target.checked })}
/>
</div>
<NumField label="Duration (ms)" value={cutscene.durationMs} onChange={v => upd({ durationMs: v })} min={0} />
<NumField label="Fade out (ms)" value={cutscene.fadeOutMs} onChange={v => upd({ fadeOutMs: v })} min={0} />
<NumField label="Fade in (ms)" value={cutscene.fadeInMs} onChange={v => upd({ fadeInMs: v })} min={0} />
<NumField label="End fade out (ms)" value={cutscene.endFadeOutMs} onChange={v => upd({ endFadeOutMs: v })} min={0} />
<NumField label="End fade in (ms)" value={cutscene.endFadeInMs} onChange={v => upd({ endFadeInMs: v })} min={0} />
<div className={styles.field}>
<label>onFadeIn callback</label>
<input
type="text"
value={cutscene.onFadeInCallback}
onChange={e => upd({ onFadeInCallback: e.target.value })}
placeholder="lua function name"
/>
</div>
</div>
);
}

View File

@ -1,132 +0,0 @@
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
import { useShallow } from 'zustand/react/shallow';
import { AVAILABLE_IMAGES } from '../../constants/images';
import { EASING_OPTIONS } from '../../constants/easings';
import type { ImagePose } from '../../types/cutscene';
import styles from './RightPanel.module.css';
function NumField({ label, value, onChange, min, step }: {
label: string; value: number; onChange: (v: number) => void; min?: number; step?: number;
}) {
return (
<div className={styles.field}>
<label>{label}</label>
<input
type="number"
value={value}
min={min}
step={step ?? 1}
onChange={e => onChange(Number(e.target.value))}
/>
</div>
);
}
function PoseFields({ label, pose, onChange }: {
label: string;
pose: ImagePose;
onChange: (patch: Partial<ImagePose>) => void;
}) {
return (
<div className={styles.poseGroup}>
<div className={styles.poseTitle}>{label}</div>
<div className={styles.poseRow}>
<div className={styles.field}>
<label>centerX</label>
<input type="number" value={pose.centerX} step={0.01} min={0} max={1}
onChange={e => onChange({ centerX: Number(e.target.value) })} />
</div>
<div className={styles.field}>
<label>centerY</label>
<input type="number" value={pose.centerY} step={0.01} min={0} max={1}
onChange={e => onChange({ centerY: Number(e.target.value) })} />
</div>
<div className={styles.field}>
<label>scale</label>
<input type="number" value={pose.scale} step={0.05} min={0.1}
onChange={e => onChange({ scale: Number(e.target.value) })} />
</div>
</div>
</div>
);
}
export default function LayerProperties() {
const cutscene = useSelectedCutscene();
const { selectedLayerIndex, updateLayer, updateLayerFrom, updateLayerTo } = useCutsceneStore(useShallow(s => ({
selectedLayerIndex: s.selectedLayerIndex,
updateLayer: s.updateLayer,
updateLayerFrom: s.updateLayerFrom,
updateLayerTo: s.updateLayerTo,
})));
if (!cutscene || selectedLayerIndex === null) return null;
const seg = cutscene.imageSegments[selectedLayerIndex];
if (!seg) return null;
return (
<div className={styles.section}>
<div className={styles.sectionTitle}>Layer {selectedLayerIndex + 1}</div>
<div className={styles.field}>
<label>Image</label>
<select
value={AVAILABLE_IMAGES.includes(seg.path) ? seg.path : '__custom__'}
onChange={e => {
if (e.target.value !== '__custom__') updateLayer(selectedLayerIndex, { path: e.target.value });
}}
>
{AVAILABLE_IMAGES.map(img => (
<option key={img} value={img}>{img.split('/').pop()}</option>
))}
{!AVAILABLE_IMAGES.includes(seg.path) && (
<option value="__custom__">(custom)</option>
)}
</select>
</div>
<div className={styles.field}>
<label>Path (manual)</label>
<input
type="text"
value={seg.path}
onChange={e => updateLayer(selectedLayerIndex, { path: e.target.value })}
placeholder="resources/..."
/>
</div>
<div className={styles.row2}>
<NumField label="Width" value={seg.width} onChange={v => updateLayer(selectedLayerIndex, { width: v })} min={1} />
<NumField label="Height" value={seg.height} onChange={v => updateLayer(selectedLayerIndex, { height: v })} min={1} />
</div>
<div className={styles.row2}>
<NumField label="Start (ms)" value={seg.startMs} onChange={v => updateLayer(selectedLayerIndex, { startMs: v })} min={0} />
<NumField label="End (ms)" value={seg.endMs} onChange={v => updateLayer(selectedLayerIndex, { endMs: v })} min={0} />
</div>
<div className={styles.row2}>
<NumField label="Fade in (ms)" value={seg.fadeInMs} onChange={v => updateLayer(selectedLayerIndex, { fadeInMs: v })} min={0} />
<NumField label="Fade out (ms)" value={seg.fadeOutMs} onChange={v => updateLayer(selectedLayerIndex, { fadeOutMs: v })} min={0} />
</div>
<div className={styles.field}>
<label>Easing</label>
<select value={seg.easing} onChange={e => updateLayer(selectedLayerIndex, { easing: e.target.value as typeof seg.easing })}>
{EASING_OPTIONS.map(e => <option key={e} value={e}>{e}</option>)}
</select>
</div>
<PoseFields
label="From"
pose={seg.from}
onChange={patch => updateLayerFrom(selectedLayerIndex, patch)}
/>
<PoseFields
label="To"
pose={seg.to}
onChange={patch => updateLayerTo(selectedLayerIndex, patch)}
/>
</div>
);
}

View File

@ -1,208 +0,0 @@
.panel {
width: 260px;
min-width: 240px;
background: #252525;
border-left: 1px solid #333;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tabs {
display: flex;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
.tab {
flex: 1;
padding: 8px 6px;
background: none;
border: none;
color: #777;
font-size: 11px;
border-bottom: 2px solid transparent;
transition: color 0.1s;
}
.tab:hover { color: #bbb; }
.tabActive {
color: #5b9bd5;
border-bottom-color: #5b9bd5;
}
.scroll {
flex: 1;
overflow-y: auto;
padding-bottom: 20px;
}
/* ── Sections ──────────────────────────────────────────────────── */
.section {
padding: 10px;
border-bottom: 1px solid #2e2e2e;
}
.sectionTitle {
font-size: 11px;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 8px;
}
.sectionHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.addBtn {
background: #2a4a6e;
border: 1px solid #3a6090;
color: #a8d0f0;
border-radius: 3px;
padding: 2px 8px;
font-size: 11px;
}
.addBtn:hover { background: #2e5480; }
/* ── Fields ────────────────────────────────────────────────────── */
.field {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 5px;
gap: 6px;
}
.field label {
flex-shrink: 0;
width: 100px;
text-align: right;
}
.field input[type="text"],
.field input[type="number"],
.field select,
.field textarea {
flex: 1;
min-width: 0;
width: 100%;
}
.field textarea {
resize: vertical;
min-height: 40px;
}
.field input[type="checkbox"] {
width: auto;
margin: 0;
}
.row2 {
display: flex;
gap: 6px;
}
.row2 .field {
flex: 1;
flex-direction: column;
align-items: flex-start;
}
.row2 .field label {
width: auto;
text-align: left;
margin-bottom: 2px;
}
/* ── Pose groups ───────────────────────────────────────────────── */
.poseGroup {
margin-top: 8px;
background: #1e1e1e;
border-radius: 4px;
padding: 6px 8px;
}
.poseTitle {
font-size: 10px;
font-weight: 600;
color: #666;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 5px;
}
.poseRow {
display: flex;
gap: 6px;
}
.poseRow .field {
flex: 1;
flex-direction: column;
align-items: flex-start;
margin-bottom: 0;
}
.poseRow .field label {
width: auto;
text-align: left;
margin-bottom: 2px;
}
/* ── Subtitle line cards ───────────────────────────────────────── */
.lineCard {
background: #1e1e1e;
border: 1px solid #2e2e2e;
border-radius: 4px;
padding: 8px;
margin-bottom: 6px;
}
.lineHeader {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 6px;
}
.lineIndex {
font-size: 10px;
color: #666;
flex: 1;
}
.iconBtn {
background: none;
border: 1px solid #3a3a3a;
color: #888;
border-radius: 3px;
padding: 1px 5px;
font-size: 11px;
}
.iconBtn:hover:not(:disabled) { background: #333; color: #ccc; }
.iconBtn:disabled { opacity: 0.3; cursor: default; }
.iconBtnDanger {
composes: iconBtn;
color: #a05050;
border-color: #5a2a2a;
}
.iconBtnDanger:hover { background: #3a2020; color: #e06c6c; }
.emptyLines {
color: #555;
font-size: 11px;
padding: 4px 0;
}
.empty {
padding: 20px;
color: #555;
font-size: 11px;
text-align: center;
}

View File

@ -1,41 +0,0 @@
import { useCutsceneStore } from '../../store/cutsceneStore';
import { useShallow } from 'zustand/react/shallow';
import CutsceneProperties from './CutsceneProperties';
import LayerProperties from './LayerProperties';
import SubtitleLines from './SubtitleLines';
import styles from './RightPanel.module.css';
export default function RightPanel() {
const { selectedLayerIndex, selectLayer } = useCutsceneStore(useShallow(s => ({
selectedLayerIndex: s.selectedLayerIndex,
selectLayer: s.selectLayer,
})));
return (
<div className={styles.panel}>
{/* Tab strip */}
<div className={styles.tabs}>
<button
className={`${styles.tab} ${selectedLayerIndex === null ? styles.tabActive : ''}`}
onClick={() => selectLayer(null)}
>
Cutscene
</button>
{selectedLayerIndex !== null && (
<button className={`${styles.tab} ${styles.tabActive}`}>
Layer {selectedLayerIndex + 1}
</button>
)}
</div>
<div className={styles.scroll}>
{selectedLayerIndex !== null ? (
<LayerProperties />
) : (
<CutsceneProperties />
)}
<SubtitleLines />
</div>
</div>
);
}

View File

@ -1,89 +0,0 @@
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
import { useShallow } from 'zustand/react/shallow';
import styles from './RightPanel.module.css';
export default function SubtitleLines() {
const cutscene = useSelectedCutscene();
const { addLine, removeLine, moveLineUp, moveLineDown, updateLine } = useCutsceneStore(useShallow(s => ({
addLine: s.addLine,
removeLine: s.removeLine,
moveLineUp: s.moveLineUp,
moveLineDown: s.moveLineDown,
updateLine: s.updateLine,
})));
if (!cutscene) return null;
return (
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div className={styles.sectionTitle}>Subtitle Lines</div>
<button className={styles.addBtn} onClick={addLine}>+ Add</button>
</div>
{cutscene.lines.length === 0 && (
<div className={styles.emptyLines}>No lines. Click + Add.</div>
)}
{cutscene.lines.map((line, i) => (
<div key={i} className={styles.lineCard}>
<div className={styles.lineHeader}>
<span className={styles.lineIndex}>#{i + 1}</span>
<button className={styles.iconBtn} onClick={() => moveLineUp(i)} disabled={i === 0} title="Move up"></button>
<button className={styles.iconBtn} onClick={() => moveLineDown(i)} disabled={i === cutscene.lines.length - 1} title="Move down"></button>
<button className={styles.iconBtnDanger} onClick={() => removeLine(i)} title="Remove"></button>
</div>
<div className={styles.field}>
<label>Speaker</label>
<input
type="text"
value={line.speaker}
onChange={e => updateLine(i, { speaker: e.target.value })}
placeholder="(none)"
/>
</div>
<div className={styles.field}>
<label>Text</label>
<textarea
value={line.text}
rows={2}
onChange={e => updateLine(i, { text: e.target.value })}
/>
</div>
<div className={styles.row2}>
<div className={styles.field}>
<label>Duration (ms)</label>
<input
type="number"
value={line.durationMs}
min={0}
onChange={e => updateLine(i, { durationMs: Number(e.target.value) })}
/>
</div>
<div className={styles.field}>
<label>Wait confirm</label>
<input
type="checkbox"
checked={line.waitForConfirm}
onChange={e => updateLine(i, { waitForConfirm: e.target.checked })}
/>
</div>
</div>
<div className={styles.field}>
<label>Lua callback</label>
<input
type="text"
value={line.luaCallback}
onChange={e => updateLine(i, { luaCallback: e.target.value })}
placeholder="function name"
/>
</div>
</div>
))}
</div>
);
}

View File

@ -1,192 +0,0 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
background: #1a1a1a;
border-top: 1px solid #333;
overflow: hidden;
}
.toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: #222;
border-bottom: 1px solid #333;
flex-shrink: 0;
}
.tbBtn {
background: #2d2d2d;
border: 1px solid #404040;
color: #bbb;
border-radius: 3px;
padding: 3px 8px;
font-size: 11px;
transition: background 0.1s;
}
.tbBtn:hover:not(:disabled) { background: #3a3a3a; color: #fff; }
.tbBtn:disabled { opacity: 0.3; cursor: default; }
.spacer { flex: 1; }
.zoomLabel { font-size: 11px; color: #666; margin-right: 2px; }
.scroll {
flex: 1;
overflow: auto;
position: relative;
cursor: default;
}
/* ── Ruler ────────────────────────────────────────────────────── */
.ruler {
display: flex;
height: 22px;
background: #212121;
border-bottom: 1px solid #333;
position: sticky;
top: 0;
z-index: 10;
user-select: none;
cursor: crosshair;
}
.tick {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: #444;
}
.tickLabel {
position: absolute;
top: 3px;
left: 3px;
font-size: 9px;
color: #666;
white-space: nowrap;
}
/* ── Layers area ──────────────────────────────────────────────── */
.layersArea {
position: relative;
}
.playhead {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: #e05050;
z-index: 20;
pointer-events: none;
}
.gridLine {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
background: rgba(255,255,255,0.04);
pointer-events: none;
}
/* ── Layer row ────────────────────────────────────────────────── */
.row {
position: absolute;
left: 0;
right: 0;
height: 32px;
display: flex;
align-items: center;
border-bottom: 1px solid #2a2a2a;
cursor: pointer;
transition: background 0.1s;
}
.row:hover { background: rgba(255,255,255,0.03); }
.rowSelected { background: rgba(91,155,213,0.08); }
.rowLabel {
position: absolute;
left: 0;
width: 140px;
height: 100%;
display: flex;
align-items: center;
padding: 0 8px;
background: #1e1e1e;
border-right: 1px solid #2a2a2a;
z-index: 5;
overflow: hidden;
flex-shrink: 0;
}
.layerName {
font-size: 11px;
color: #bbb;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ── Segment bar ──────────────────────────────────────────────── */
.bar {
position: absolute;
height: 22px;
border-radius: 3px;
cursor: grab;
top: 5px;
border: 1px solid rgba(255,255,255,0.15);
box-sizing: border-box;
min-width: 4px;
}
.bar:active { cursor: grabbing; }
.handle {
position: absolute;
top: 0;
bottom: 0;
width: 6px;
cursor: ew-resize;
z-index: 2;
}
.handleLeft { left: 0; border-radius: 3px 0 0 3px; background: rgba(255,255,255,0.15); }
.handleRight { right: 0; border-radius: 0 3px 3px 0; background: rgba(255,255,255,0.15); }
/* ── Subtitle row ─────────────────────────────────────────────── */
.subtitleRow {
position: absolute;
left: 0;
right: 0;
height: 14px;
border-bottom: 1px solid #2a2a2a;
}
.subtitleLabel {
position: absolute;
left: 0;
width: 140px;
height: 100%;
background: #1e1e1e;
border-right: 1px solid #2a2a2a;
display: flex;
align-items: center;
padding: 0 8px;
font-size: 9px;
color: #555;
z-index: 5;
}
.subtitleBlock {
position: absolute;
top: 2px;
height: 10px;
background: #8a6040;
border-radius: 2px;
border: 1px solid #aa7050;
opacity: 0.8;
}

View File

@ -1,285 +0,0 @@
import { useRef, useCallback, useState, useEffect } from 'react';
import { useCutsceneStore, useSelectedCutscene } from '../../store/cutsceneStore';
import { useShallow } from 'zustand/react/shallow';
import styles from './Timeline.module.css';
const LABEL_WIDTH = 140;
const ROW_HEIGHT = 32;
const SUBTITLE_ROW_HEIGHT = 14;
const MIN_ZOOM = 20; // px per second
const MAX_ZOOM = 400;
const LAYER_COLORS = ['#4a7fc1', '#c17a4a', '#4ac17a', '#c14a7a', '#7a4ac1', '#c1b44a', '#4ac1c1'];
function layerColor(i: number) { return LAYER_COLORS[i % LAYER_COLORS.length]; }
function basename(path: string) {
return path.split('/').pop() ?? path;
}
export default function Timeline() {
const cutscene = useSelectedCutscene();
const { selectedLayerIndex, currentTimeMs, playState } = useCutsceneStore(useShallow(s => ({
selectedLayerIndex: s.selectedLayerIndex,
currentTimeMs: s.currentTimeMs,
playState: s.playState,
})));
const { selectLayer, addLayer, removeLayer, moveLayerUp, moveLayerDown, updateLayer, setCurrentTime, setPlayState } = useCutsceneStore(useShallow(s => ({
selectLayer: s.selectLayer,
addLayer: s.addLayer,
removeLayer: s.removeLayer,
moveLayerUp: s.moveLayerUp,
moveLayerDown: s.moveLayerDown,
updateLayer: s.updateLayer,
setCurrentTime: s.setCurrentTime,
setPlayState: s.setPlayState,
})));
const [pxPerSec, setPxPerSec] = useState(60);
const scrollRef = useRef<HTMLDivElement>(null);
const isDraggingPlayhead = useRef(false);
const dragState = useRef<{
type: 'move' | 'left' | 'right';
layerIndex: number;
startX: number;
startMs: number;
endMs: number;
durationMs: number;
} | null>(null);
const msToX = useCallback((ms: number) => (ms / 1000) * pxPerSec, [pxPerSec]);
const xToMs = useCallback((x: number) => Math.max(0, Math.round((x / pxPerSec) * 1000)), [pxPerSec]);
const totalMs = cutscene
? Math.max(
cutscene.durationMs,
cutscene.imageSegments.reduce((m, s) => Math.max(m, s.endMs), 0),
10000,
) + 2000
: 12000;
const rulerWidth = Math.ceil(msToX(totalMs));
// Ruler ticks
const tickStepMs = pxPerSec >= 100 ? 500 : pxPerSec >= 50 ? 1000 : 2000;
const labelStepMs = pxPerSec >= 100 ? 1000 : pxPerSec >= 50 ? 2000 : 4000;
const ticks: number[] = [];
for (let ms = 0; ms <= totalMs; ms += tickStepMs) ticks.push(ms);
// Scroll wheel zoom
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
function onWheel(e: WheelEvent) {
if (!e.ctrlKey && !e.metaKey) return;
e.preventDefault();
setPxPerSec(prev => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, prev * (e.deltaY < 0 ? 1.15 : 0.87))));
}
el.addEventListener('wheel', onWheel, { passive: false });
return () => el.removeEventListener('wheel', onWheel);
}, []);
// Auto-scroll playhead into view while playing
useEffect(() => {
if (playState !== 'playing') return;
const el = scrollRef.current;
if (!el) return;
const x = msToX(currentTimeMs) + LABEL_WIDTH;
const { scrollLeft, clientWidth } = el;
if (x > scrollLeft + clientWidth - 40) {
el.scrollLeft = x - clientWidth + 80;
}
}, [currentTimeMs, playState, msToX]);
// ── Playhead drag ──────────────────────────────────────────────────────────
function onRulerMouseDown(e: React.MouseEvent) {
if (e.button !== 0) return;
isDraggingPlayhead.current = true;
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const x = e.clientX - rect.left - LABEL_WIDTH + (scrollRef.current?.scrollLeft ?? 0);
setCurrentTime(xToMs(x));
if (playState === 'playing') setPlayState('paused');
function onMove(ev: MouseEvent) {
const x2 = ev.clientX - rect.left - LABEL_WIDTH + (scrollRef.current?.scrollLeft ?? 0);
setCurrentTime(xToMs(x2));
}
function onUp() {
isDraggingPlayhead.current = false;
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
}
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
}
// ── Segment bar drag ───────────────────────────────────────────────────────
function onBarMouseDown(e: React.MouseEvent, layerIndex: number, type: 'move' | 'left' | 'right') {
e.preventDefault();
e.stopPropagation();
selectLayer(layerIndex);
const seg = cutscene!.imageSegments[layerIndex];
dragState.current = {
type,
layerIndex,
startX: e.clientX,
startMs: seg.startMs,
endMs: seg.endMs,
durationMs: seg.endMs - seg.startMs,
};
function onMove(ev: MouseEvent) {
if (!dragState.current) return;
const dx = ev.clientX - dragState.current.startX;
const dMs = Math.round((dx / pxPerSec) * 1000);
const { type, layerIndex: li, startMs, endMs, durationMs } = dragState.current;
if (type === 'move') {
const newStart = Math.max(0, startMs + dMs);
updateLayer(li, { startMs: newStart, endMs: newStart + durationMs });
} else if (type === 'left') {
const newStart = Math.max(0, Math.min(endMs - 100, startMs + dMs));
updateLayer(li, { startMs: newStart });
} else {
const newEnd = Math.max(startMs + 100, endMs + dMs);
updateLayer(li, { endMs: newEnd });
}
}
function onUp() {
dragState.current = null;
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
}
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
}
const playheadX = msToX(currentTimeMs) + LABEL_WIDTH;
// Subtitle timing for display
let subtitleBlocks: { x: number; w: number; text: string }[] = [];
if (cutscene) {
let elapsed = 0;
for (const line of cutscene.lines) {
const dur = line.durationMs > 0
? line.durationMs
: Math.max(1500, Math.round((line.text.length / 17) * 1000));
subtitleBlocks.push({ x: msToX(elapsed), w: msToX(dur), text: line.text || '…' });
elapsed += dur;
}
}
const layerCount = cutscene?.imageSegments.length ?? 0;
const totalHeight = layerCount * ROW_HEIGHT + SUBTITLE_ROW_HEIGHT + 20;
return (
<div className={styles.container}>
{/* Toolbar */}
<div className={styles.toolbar}>
<button className={styles.tbBtn} onClick={addLayer} disabled={!cutscene} title="Add layer">+ Layer</button>
{selectedLayerIndex !== null && cutscene && (
<>
<button className={styles.tbBtn} onClick={() => moveLayerUp(selectedLayerIndex!)} disabled={selectedLayerIndex! <= 0} title="Move up"></button>
<button className={styles.tbBtn} onClick={() => moveLayerDown(selectedLayerIndex!)} disabled={selectedLayerIndex! >= layerCount - 1} title="Move down"></button>
<button className={styles.tbBtn} onClick={() => removeLayer(selectedLayerIndex!)} title="Remove layer" style={{ color: '#e06c6c' }}> Layer</button>
</>
)}
<div className={styles.spacer} />
<span className={styles.zoomLabel}>Zoom:</span>
<button className={styles.tbBtn} onClick={() => setPxPerSec(p => Math.max(MIN_ZOOM, p * 0.75))}></button>
<button className={styles.tbBtn} onClick={() => setPxPerSec(p => Math.min(MAX_ZOOM, p * 1.33))}>+</button>
<button className={styles.tbBtn} onClick={() => setPxPerSec(60)}>Reset</button>
</div>
{/* Scrollable area */}
<div className={styles.scroll} ref={scrollRef}>
{/* Ruler */}
<div
className={styles.ruler}
style={{ width: LABEL_WIDTH + rulerWidth }}
onMouseDown={onRulerMouseDown}
>
<div style={{ width: LABEL_WIDTH, flexShrink: 0 }} />
<div style={{ position: 'relative', flex: 1 }}>
{ticks.map(ms => (
<div
key={ms}
className={styles.tick}
style={{ left: msToX(ms) }}
>
{ms % labelStepMs === 0 && (
<span className={styles.tickLabel}>{ms / 1000}s</span>
)}
</div>
))}
</div>
</div>
{/* Layers + playhead overlay */}
<div
className={styles.layersArea}
style={{ width: LABEL_WIDTH + rulerWidth, minHeight: totalHeight }}
>
{/* Playhead */}
<div className={styles.playhead} style={{ left: playheadX }} />
{/* Grid lines */}
{ticks.filter(ms => ms % labelStepMs === 0).map(ms => (
<div key={ms} className={styles.gridLine} style={{ left: LABEL_WIDTH + msToX(ms) }} />
))}
{/* Layer rows */}
{cutscene?.imageSegments.map((seg, i) => {
const isSelected = selectedLayerIndex === i;
const color = layerColor(i);
const barX = LABEL_WIDTH + msToX(seg.startMs);
const barW = Math.max(4, msToX(seg.endMs) - msToX(seg.startMs));
return (
<div
key={i}
className={`${styles.row} ${isSelected ? styles.rowSelected : ''}`}
style={{ top: i * ROW_HEIGHT }}
onClick={() => selectLayer(i)}
>
{/* Label */}
<div className={styles.rowLabel} style={{ borderLeft: `3px solid ${color}` }}>
<span className={styles.layerName}>{basename(seg.path) || `Layer ${i + 1}`}</span>
</div>
{/* Bar */}
<div
className={styles.bar}
style={{ left: barX, width: barW, background: color + (isSelected ? 'cc' : '88') }}
onMouseDown={e => onBarMouseDown(e, i, 'move')}
>
<div className={`${styles.handle} ${styles.handleLeft}`}
onMouseDown={e => onBarMouseDown(e, i, 'left')} />
<div className={`${styles.handle} ${styles.handleRight}`}
onMouseDown={e => onBarMouseDown(e, i, 'right')} />
</div>
</div>
);
})}
{/* Subtitle blocks row */}
{cutscene && (
<div
className={styles.subtitleRow}
style={{ top: layerCount * ROW_HEIGHT }}
>
<div className={styles.subtitleLabel}>Subtitles</div>
{subtitleBlocks.map((b, i) => (
<div
key={i}
className={styles.subtitleBlock}
style={{ left: LABEL_WIDTH + b.x, width: Math.max(2, b.w) }}
title={b.text}
/>
))}
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -1,25 +0,0 @@
import type { EasingType } from '../types/cutscene';
export const EASING_OPTIONS: EasingType[] = [
'Linear',
'EaseInSine', 'EaseOutSine', 'EaseInOutSine',
'EaseInQuad', 'EaseOutQuad', 'EaseInOutQuad',
'EaseInCubic', 'EaseOutCubic', 'EaseInOutCubic',
];
export function applyEasing(t: number, easing: EasingType): number {
const pi = Math.PI;
switch (easing) {
case 'Linear': return t;
case 'EaseInSine': return 1 - Math.cos((t * pi) / 2);
case 'EaseOutSine': return Math.sin((t * pi) / 2);
case 'EaseInOutSine': return -(Math.cos(pi * t) - 1) / 2;
case 'EaseInQuad': return t * t;
case 'EaseOutQuad': return 1 - (1 - t) * (1 - t);
case 'EaseInOutQuad': return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
case 'EaseInCubic': return t * t * t;
case 'EaseOutCubic': return 1 - Math.pow(1 - t, 3);
case 'EaseInOutCubic':return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
default: return t;
}
}

View File

@ -1,15 +0,0 @@
export const AVAILABLE_IMAGES: string[] = [
'resources/w/cutscenes/cutscene2/cs2_background.png',
'resources/w/cutscenes/cutscene2/cs2_books.png',
'resources/w/cutscenes/cutscene2/cs2_chair.png',
'resources/w/cutscenes/cutscene2/cs2_gg001.png',
'resources/w/cutscenes/cutscene2/cs2_gg002.png',
'resources/w/cutscenes/cutscene2/cs2_gg003.png',
'resources/w/cutscenes/cutscene2/cs2_gg004.png',
'resources/w/cutscenes/cutscene3/cs2_foreground.png',
'resources/black.png',
'resources/w/cutscenes/cutscene_exit_darklands/img.png',
'resources/w/cutscenes/cutscene_exit_darklands/img2.png',
'resources/w/cutscenes/cutscene3/img.png',
'resources/w/white.png',
];

View File

@ -1,52 +0,0 @@
import { useEffect, useRef } from 'react';
import { useCutsceneStore, useSelectedCutscene } from '../store/cutsceneStore';
export function usePlayback() {
const { playState, currentTimeMs, setPlayState, setCurrentTime } = useCutsceneStore();
const cutscene = useSelectedCutscene();
const rafRef = useRef<number | null>(null);
const lastTsRef = useRef<number | null>(null);
useEffect(() => {
if (playState !== 'playing') {
lastTsRef.current = null;
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
return;
}
function totalDuration() {
if (!cutscene) return 5000;
const segMax = cutscene.imageSegments.reduce((m, s) => Math.max(m, s.endMs), 0);
const content = Math.max(cutscene.durationMs, segMax);
return content + cutscene.endFadeOutMs + cutscene.endFadeInMs;
}
function tick(ts: number) {
if (lastTsRef.current === null) lastTsRef.current = ts;
const delta = ts - lastTsRef.current;
lastTsRef.current = ts;
const newTime = useCutsceneStore.getState().currentTimeMs + delta;
const total = totalDuration();
if (newTime >= total) {
setCurrentTime(total);
setPlayState('stopped');
return;
}
setCurrentTime(newTime);
rafRef.current = requestAnimationFrame(tick);
}
rafRef.current = requestAnimationFrame(tick);
return () => {
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
};
}, [playState, cutscene, setPlayState, setCurrentTime]);
return null;
}

View File

@ -1,52 +0,0 @@
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body, #root {
height: 100%;
width: 100%;
overflow: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 13px;
background: #1a1a1a;
color: #e0e0e0;
}
button {
cursor: pointer;
font-family: inherit;
font-size: 12px;
}
input, select, textarea {
font-family: inherit;
font-size: 12px;
background: #2a2a2a;
color: #e0e0e0;
border: 1px solid #444;
border-radius: 3px;
padding: 3px 6px;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #5b9bd5;
}
label {
color: #aaa;
font-size: 11px;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track { background: #1a1a1a; }
::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #666; }

View File

@ -1,10 +0,0 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -1,242 +0,0 @@
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import type { CutsceneFile, Cutscene, ImageSegment, SubtitleLine, ImagePose } from '../types/cutscene';
export type PlayState = 'stopped' | 'playing' | 'paused';
interface CutsceneStore {
file: CutsceneFile | null;
selectedCutsceneId: string | null;
selectedLayerIndex: number | null;
playState: PlayState;
currentTimeMs: number;
// File I/O
loadFile: (file: CutsceneFile) => void;
getExportData: () => CutsceneFile | null;
// Cutscene CRUD
addCutscene: () => void;
deleteCutscene: (id: string) => void;
selectCutscene: (id: string | null) => void;
updateCutscene: (id: string, patch: Partial<Omit<Cutscene, 'imageSegments' | 'lines'>>) => void;
// Layer CRUD
selectLayer: (index: number | null) => void;
addLayer: () => void;
removeLayer: (index: number) => void;
moveLayerUp: (index: number) => void;
moveLayerDown: (index: number) => void;
updateLayer: (index: number, patch: Partial<ImageSegment>) => void;
updateLayerFrom: (index: number, patch: Partial<ImagePose>) => void;
updateLayerTo: (index: number, patch: Partial<ImagePose>) => void;
// Subtitle lines
addLine: () => void;
removeLine: (index: number) => void;
moveLineUp: (index: number) => void;
moveLineDown: (index: number) => void;
updateLine: (index: number, patch: Partial<SubtitleLine>) => void;
// Playback
setPlayState: (state: PlayState) => void;
setCurrentTime: (ms: number) => void;
}
function newCutscene(id: string): Cutscene {
return {
id,
skippable: true,
durationMs: 5000,
fadeOutMs: 500,
fadeInMs: 500,
endFadeOutMs: 500,
endFadeInMs: 500,
onFadeInCallback: '',
imageSegments: [],
lines: [],
};
}
function newSegment(): ImageSegment {
return {
path: '',
width: 1280,
height: 720,
startMs: 0,
endMs: 5000,
fadeInMs: 0,
fadeOutMs: 0,
easing: 'Linear',
from: { centerX: 0.5, centerY: 0.5, scale: 1.0 },
to: { centerX: 0.5, centerY: 0.5, scale: 1.0 },
};
}
function newLine(): SubtitleLine {
return {
speaker: '',
text: '',
durationMs: 3000,
waitForConfirm: false,
luaCallback: '',
};
}
function getSelected(file: CutsceneFile | null, id: string | null): Cutscene | null {
if (!file || !id) return null;
return file.cutscenes.find(c => c.id === id) ?? null;
}
export const useCutsceneStore = create<CutsceneStore>()(
immer((set, get) => ({
file: null,
selectedCutsceneId: null,
selectedLayerIndex: null,
playState: 'stopped',
currentTimeMs: 0,
loadFile: (file) => set(s => {
s.file = file;
s.selectedCutsceneId = file.cutscenes[0]?.id ?? null;
s.selectedLayerIndex = null;
s.playState = 'stopped';
s.currentTimeMs = 0;
}),
getExportData: () => get().file,
addCutscene: () => set(s => {
if (!s.file) s.file = { cutscenes: [] };
let base = 'cutscene_new';
let n = 1;
const ids = new Set(s.file.cutscenes.map(c => c.id));
while (ids.has(`${base}_${n}`)) n++;
const id = `${base}_${n}`;
s.file.cutscenes.push(newCutscene(id));
s.selectedCutsceneId = id;
s.selectedLayerIndex = null;
}),
deleteCutscene: (id) => set(s => {
if (!s.file) return;
const idx = s.file.cutscenes.findIndex(c => c.id === id);
if (idx === -1) return;
s.file.cutscenes.splice(idx, 1);
if (s.selectedCutsceneId === id) {
s.selectedCutsceneId = s.file.cutscenes[0]?.id ?? null;
s.selectedLayerIndex = null;
}
}),
selectCutscene: (id) => set(s => {
s.selectedCutsceneId = id;
s.selectedLayerIndex = null;
s.playState = 'stopped';
s.currentTimeMs = 0;
}),
updateCutscene: (id, patch) => set(s => {
if (!s.file) return;
const c = s.file.cutscenes.find(c => c.id === id);
if (!c) return;
Object.assign(c, patch);
}),
selectLayer: (index) => set(s => { s.selectedLayerIndex = index; }),
addLayer: () => set(s => {
const cutscene = getSelected(s.file, s.selectedCutsceneId);
if (!cutscene) return;
cutscene.imageSegments.push(newSegment());
s.selectedLayerIndex = cutscene.imageSegments.length - 1;
}),
removeLayer: (index) => set(s => {
const cutscene = getSelected(s.file, s.selectedCutsceneId);
if (!cutscene) return;
cutscene.imageSegments.splice(index, 1);
if (s.selectedLayerIndex === index) s.selectedLayerIndex = null;
else if (s.selectedLayerIndex !== null && s.selectedLayerIndex > index) s.selectedLayerIndex--;
}),
moveLayerUp: (index) => set(s => {
const cutscene = getSelected(s.file, s.selectedCutsceneId);
if (!cutscene || index <= 0) return;
const segs = cutscene.imageSegments;
[segs[index - 1], segs[index]] = [segs[index], segs[index - 1]];
if (s.selectedLayerIndex === index) s.selectedLayerIndex = index - 1;
else if (s.selectedLayerIndex === index - 1) s.selectedLayerIndex = index;
}),
moveLayerDown: (index) => set(s => {
const cutscene = getSelected(s.file, s.selectedCutsceneId);
if (!cutscene) return;
const segs = cutscene.imageSegments;
if (index >= segs.length - 1) return;
[segs[index], segs[index + 1]] = [segs[index + 1], segs[index]];
if (s.selectedLayerIndex === index) s.selectedLayerIndex = index + 1;
else if (s.selectedLayerIndex === index + 1) s.selectedLayerIndex = index;
}),
updateLayer: (index, patch) => set(s => {
const cutscene = getSelected(s.file, s.selectedCutsceneId);
if (!cutscene) return;
Object.assign(cutscene.imageSegments[index], patch);
}),
updateLayerFrom: (index, patch) => set(s => {
const cutscene = getSelected(s.file, s.selectedCutsceneId);
if (!cutscene) return;
Object.assign(cutscene.imageSegments[index].from, patch);
}),
updateLayerTo: (index, patch) => set(s => {
const cutscene = getSelected(s.file, s.selectedCutsceneId);
if (!cutscene) return;
Object.assign(cutscene.imageSegments[index].to, patch);
}),
addLine: () => set(s => {
const cutscene = getSelected(s.file, s.selectedCutsceneId);
if (!cutscene) return;
cutscene.lines.push(newLine());
}),
removeLine: (index) => set(s => {
const cutscene = getSelected(s.file, s.selectedCutsceneId);
if (!cutscene) return;
cutscene.lines.splice(index, 1);
}),
moveLineUp: (index) => set(s => {
const cutscene = getSelected(s.file, s.selectedCutsceneId);
if (!cutscene || index <= 0) return;
const lines = cutscene.lines;
[lines[index - 1], lines[index]] = [lines[index], lines[index - 1]];
}),
moveLineDown: (index) => set(s => {
const cutscene = getSelected(s.file, s.selectedCutsceneId);
if (!cutscene) return;
const lines = cutscene.lines;
if (index >= lines.length - 1) return;
[lines[index], lines[index + 1]] = [lines[index + 1], lines[index]];
}),
updateLine: (index, patch) => set(s => {
const cutscene = getSelected(s.file, s.selectedCutsceneId);
if (!cutscene) return;
Object.assign(cutscene.lines[index], patch);
}),
setPlayState: (state) => set(s => { s.playState = state; }),
setCurrentTime: (ms) => set(s => { s.currentTimeMs = ms; }),
}))
);
export function useSelectedCutscene() {
return useCutsceneStore(s =>
s.file?.cutscenes.find(c => c.id === s.selectedCutsceneId) ?? null
);
}

View File

@ -1,49 +0,0 @@
export type EasingType =
| 'Linear'
| 'EaseInSine' | 'EaseOutSine' | 'EaseInOutSine'
| 'EaseInQuad' | 'EaseOutQuad' | 'EaseInOutQuad'
| 'EaseInCubic' | 'EaseOutCubic' | 'EaseInOutCubic';
export interface ImagePose {
centerX: number;
centerY: number;
scale: number;
}
export interface ImageSegment {
path: string;
width: number;
height: number;
startMs: number;
endMs: number;
fadeInMs: number;
fadeOutMs: number;
easing: EasingType;
from: ImagePose;
to: ImagePose;
}
export interface SubtitleLine {
speaker: string;
text: string;
durationMs: number;
waitForConfirm: boolean;
luaCallback: string;
}
export interface Cutscene {
id: string;
skippable: boolean;
durationMs: number;
fadeOutMs: number;
fadeInMs: number;
endFadeOutMs: number;
endFadeInMs: number;
onFadeInCallback: string;
imageSegments: ImageSegment[];
lines: SubtitleLine[];
}
export interface CutsceneFile {
cutscenes: Cutscene[];
}

View File

@ -1,76 +0,0 @@
import type { CutsceneFile, Cutscene, ImageSegment, SubtitleLine } from '../types/cutscene';
function parseSegment(raw: Record<string, unknown>): ImageSegment {
const from = (raw.from as Record<string, number> | undefined) ?? {};
const to = (raw.to as Record<string, number> | undefined) ?? {};
return {
path: String(raw.path ?? ''),
width: Number(raw.width ?? 1280),
height: Number(raw.height ?? 720),
startMs: Number(raw.startMs ?? 0),
endMs: Number(raw.endMs ?? 5000),
fadeInMs: Number(raw.fadeInMs ?? 0),
fadeOutMs: Number(raw.fadeOutMs ?? 0),
easing: String(raw.easing ?? 'Linear') as ImageSegment['easing'],
from: {
centerX: Number(from.centerX ?? 0.5),
centerY: Number(from.centerY ?? 0.5),
scale: Number(from.scale ?? 1.0),
},
to: {
centerX: Number(to.centerX ?? from.centerX ?? 0.5),
centerY: Number(to.centerY ?? from.centerY ?? 0.5),
scale: Number(to.scale ?? from.scale ?? 1.0),
},
};
}
function parseLine(raw: Record<string, unknown>): SubtitleLine {
return {
speaker: String(raw.speaker ?? ''),
text: String(raw.text ?? ''),
durationMs: Number(raw.durationMs ?? 0),
waitForConfirm: Boolean(raw.waitForConfirm ?? false),
luaCallback: String(raw.luaCallback ?? ''),
};
}
function parseCutscene(raw: Record<string, unknown>): Cutscene {
const segments = Array.isArray(raw.imageSegments)
? (raw.imageSegments as Record<string, unknown>[]).map(parseSegment)
: [];
const lines = Array.isArray(raw.lines)
? (raw.lines as Record<string, unknown>[]).map(parseLine)
: [];
return {
id: String(raw.id ?? 'untitled'),
skippable: raw.skippable !== false,
durationMs: Number(raw.durationMs ?? 0),
fadeOutMs: Number(raw.fadeOutMs ?? 0),
fadeInMs: Number(raw.fadeInMs ?? 0),
endFadeOutMs: Number(raw.endFadeOutMs ?? 0),
endFadeInMs: Number(raw.endFadeInMs ?? 0),
onFadeInCallback: String(raw.onFadeInCallback ?? ''),
imageSegments: segments,
lines,
};
}
export function parseFile(json: unknown): CutsceneFile {
const raw = json as Record<string, unknown>;
const cutscenes = Array.isArray(raw.cutscenes)
? (raw.cutscenes as Record<string, unknown>[]).map(parseCutscene)
: [];
return { cutscenes };
}
export function triggerDownload(file: CutsceneFile, filename = 'cutscenes.json') {
const json = JSON.stringify(file, null, 4);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}

View File

@ -1,79 +0,0 @@
import type { ImageSegment, ImagePose } from '../types/cutscene';
import { applyEasing } from '../constants/easings';
function clamp(v: number, min: number, max: number) {
return Math.max(min, Math.min(max, v));
}
function lerp(a: number, b: number, t: number) {
return a + (b - a) * t;
}
export interface SegmentRenderState {
alpha: number;
pose: ImagePose;
}
export function computeSegmentState(seg: ImageSegment, currentMs: number): SegmentRenderState | null {
if (currentMs < seg.startMs || currentMs > seg.endMs) return null;
const duration = seg.endMs - seg.startMs;
const localMs = currentMs - seg.startMs;
const t = duration > 0 ? clamp(localMs / duration, 0, 1) : 1;
const tEased = applyEasing(t, seg.easing);
const pose: ImagePose = {
centerX: lerp(seg.from.centerX, seg.to.centerX, tEased),
centerY: lerp(seg.from.centerY, seg.to.centerY, tEased),
scale: lerp(seg.from.scale, seg.to.scale, tEased),
};
let alpha = 1;
if (seg.fadeInMs > 0 && localMs < seg.fadeInMs) {
alpha = localMs / seg.fadeInMs;
} else if (seg.fadeOutMs > 0 && localMs > duration - seg.fadeOutMs) {
alpha = (duration - localMs) / seg.fadeOutMs;
}
return { alpha: clamp(alpha, 0, 1), pose };
}
/**
* Returns CSS for an absolutely-positioned <img> inside the viewport div.
*
* The image is stretched (object-fit: fill) to exactly logicalW × logicalH
* logical pixels on screen matching how the game engine maps the full
* texture quad to those dimensions, regardless of the file's natural size.
*
* At scale=1 the logical area fills the viewport (aspect-ratio corrected).
* The viewport's own overflow:hidden clips anything that extends beyond.
*/
export function poseToStyle(
pose: ImagePose,
logicalW: number, // segment.width (e.g. 1280)
logicalH: number, // segment.height (e.g. 720)
containerW: number,
containerH: number,
): React.CSSProperties {
const baseScale = Math.max(containerW / logicalW, containerH / logicalH);
const renderW = logicalW * baseScale * pose.scale;
const renderH = logicalH * baseScale * pose.scale;
const maxOffsetX = Math.max(0, (renderW - containerW) / 2);
const maxOffsetY = Math.max(0, (renderH - containerH) / 2);
const offsetX = clamp((0.5 - pose.centerX) * renderW, -maxOffsetX, maxOffsetX);
// Y axis is inverted: centerY=0 → bottom of image, centerY=1 → top (Y-up convention)
const offsetY = clamp((pose.centerY - 0.5) * renderH, -maxOffsetY, maxOffsetY);
return {
position: 'absolute',
top: '50%',
left: '50%',
width: renderW,
height: renderH,
// Default object-fit (fill) stretches the full texture to these logical
// dimensions, exactly as the game engine renders the quad.
objectFit: 'fill',
transform: `translate(calc(-50% + ${offsetX}px), calc(-50% + ${offsetY}px))`,
};
}

View File

@ -1,6 +0,0 @@
/// <reference types="vite/client" />
declare module '*.module.css' {
const classes: Record<string, string>;
export default classes;
}

View File

@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@ -1,26 +0,0 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import fs from 'fs';
import path from 'path';
export default defineConfig({
plugins: [
react(),
{
name: 'serve-resources',
configureServer(server) {
server.middlewares.use('/resources', (req, res, next) => {
const filePath = path.join(process.cwd(), 'resources', decodeURIComponent(req.url ?? ''));
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
const ext = path.extname(filePath).toLowerCase();
const mime = ext === '.png' ? 'image/png' : ext === '.jpg' ? 'image/jpeg' : 'application/octet-stream';
res.setHeader('Content-Type', mime);
fs.createReadStream(filePath).pipe(res as import('stream').Writable);
} else {
next();
}
});
},
},
],
});

View File

@ -1,43 +0,0 @@
.DS_STORE
node_modules
scripts/flow/*/.flowconfig
.flowconfig
*~
*.pyc
.grunt
_SpecRunner.html
__benchmarks__
build/
remote-repo/
coverage/
.module-cache
fixtures/dom/public/react-dom.js
fixtures/dom/public/react.js
test/the-files-to-test.generated.js
*.log*
chrome-user-data
*.sublime-project
*.sublime-workspace
.idea
*.iml
.vscode
.zed
*.swp
*.swo
/tmp
/.worktrees
.claude/*.local.*
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build
packages/react-devtools-extensions/chrome/*.crx
packages/react-devtools-extensions/chrome/*.pem
packages/react-devtools-extensions/firefox/build
packages/react-devtools-extensions/firefox/*.xpi
packages/react-devtools-extensions/firefox/*.pem
packages/react-devtools-extensions/shared/build
packages/react-devtools-extensions/.tempUserDataDir
packages/react-devtools-fusebox/dist
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist

View File

@ -1,711 +0,0 @@
{
"dialogues": [
{
"id": "dialog_start001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Новый день! Я проснулся, позавтракал и готов поехать в универ! Надо проверить телефон, и не забыть взять свою записную книжку.",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_phone001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Я не буду никуда идти без своего телефона и записной книжки!",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_chat_parents001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Отец",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Бекзат, сынок, мы c мамой тебе отправили немного денег, постарайся прожить на эти деньги до конца недели!",
"next": "line_2",
"chatBubble": "in"
},
{
"id": "line_2",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Спасибо!",
"next": "end_1",
"chatBubble": "out"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_chat_news001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Отец",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Жители Бишкека все чаще жалуются на депрессию и апатию. Смотрите свежее видео об этом на нашем канале!",
"next": "end_1",
"chatBubble": "in"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_chat_aiperi001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековой? Я тогда еще приносила скатерть, тарелки и нож для торта. И я до сих пор не получила назад ничего.",
"next": "line_2",
"chatBubble": "in"
},
{
"id": "line_2",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Скатерть и тарелки вроде бы лежат в студзоне.",
"next": "line_3",
"chatBubble": "out"
},
{
"id": "line_3",
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "А нож?",
"next": "line_4",
"chatBubble": "in"
},
{
"id": "line_4",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Нож, наверное, так и остался в учительской.",
"next": "line_5",
"chatBubble": "out"
},
{
"id": "line_5",
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "А давай не \"наверное\"?",
"next": "line_6",
"chatBubble": "in"
},
{
"id": "line_6",
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "А давай ты приедешь в универ, зайдешь в учительскую, заберешь нож и отдашь мне?",
"next": "line_7",
"chatBubble": "in"
},
{
"id": "line_7",
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "У вас сегодня как раз Аида ведет лекцию. После лекции попросишь у нее ключи от учительской и заберешь нож.",
"next": "line_8",
"chatBubble": "in"
},
{
"id": "line_8",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Почему ты сама не можешь забрать?",
"next": "line_9",
"chatBubble": "out"
},
{
"id": "line_9",
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Ты же знаешь, если я встречу Аиду, она 100% даст мне какое-нибудь сложное задание.",
"next": "line_10",
"chatBubble": "in"
},
{
"id": "line_10",
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "И потом, это ты у меня брал нож, с чего я должна ходить искать его по всему универу?",
"next": "line_11",
"chatBubble": "in"
},
{
"id": "line_11",
"type": "Line",
"speaker": "Айпери",
"portrait": "resources/dialogue/portrait_phone.png",
"text": "Так что жду тебя в универе! Не вздумай прогулять!",
"next": "end_1",
"chatBubble": "in",
"questUnlock" : "aiperi_knife",
"luaCallback" : "on_aiperi_dialog_over",
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_no_sleep001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Я сейчас не хочу спать.",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_phone_pickup001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Отлично, вот и мой телефон! Надо проверить новые сообщения.",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "door_bathroom_dialog001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Здесь у меня душ и туалет.",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "door_bathroom_alik_dialog001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Я не буду лезть в ванную комнату к Алику.",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "door_locked_dialog001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Дверь закрыта. Кажется, сюда все еще никто не заселился.",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_journal_pickup001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Возьму журнал с собой! Там все мои записи.",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_taxi001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Прежде чем выходить наружу, я должен заказать такси до универа.",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_taxi002",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Я заказал такси до универа, машина уже ждет!",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_taxi004",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Я уже заказал такси, машина уже ждет!",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_second_floor001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "На втором этаже женское общежитие, мне там делать нечего.",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_female_student001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бермет",
"portrait": "resources/dialogue/portrait_student_girl.png",
"text": "Бекзат отстань!",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_female_student002",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Алтынай",
"portrait": "resources/dialogue/portrait_student_girl.png",
"text": "Бекзат ты почему на пары не ходишь?!",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_alik001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "Привет Бекзат! Давно я не видел тебя на парах!",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "door_alik_dialog001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Тук тук!",
"next": "line_2"
},
{
"id": "line_2",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "Заходи!",
"luaCallback" : "on_alik_room_enter",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_alik002",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "Привет Бекзат!",
"next": "line_2"
},
{
"id": "line_2",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Привет Алик! Разговор есть.",
"next": "line_3"
},
{
"id": "line_3",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "С тобой на курсе училась Бегимай, ты ее помнишь?",
"next": "line_4"
},
{
"id": "line_4",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "Конечно помню! Я тебе даже больше расскажу.",
"next": "line_5"
},
{
"id": "line_5",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "В тот день она принесла свою курсовую, чтобы сдать.",
"next": "line_6"
},
{
"id": "line_6",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "Но в тот день в учительской происходила генеральная уборка.",
"next": "line_7"
},
{
"id": "line_7",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "И получилось так, что ее курсовая оказалась в стопке бумаг на выброс.",
"next": "line_8"
},
{
"id": "line_8",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "Курсовая работа пропала, Бегимай получила за нее ноль баллов, и не прошла отбор в Германию.",
"next": "line_9"
},
{
"id": "line_9",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "Поэтому с горя она выпрыгнула из окна лекционного зала и убилась.",
"next": "line_10"
},
{
"id": "line_10",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "А ты откуда все это знаешь?",
"next": "line_11"
},
{
"id": "line_11",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "Я видел как ее курсовую уносили вместе с другой макулатурой из учительской.",
"next": "line_12"
},
{
"id": "line_12",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "И где сейчас ее курсовая работа?",
"next": "line_13"
},
{
"id": "line_13",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "За зданием универа есть контейнер с кучей бумажного мусора и макулатурой.",
"next": "line_14"
},
{
"id": "line_14",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "Скорее всего, курсовая до сих пор лежит где-то там.",
"next": "line_15"
},
{
"id": "line_15",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Спасибо Алик! Ты мне очень помог.",
"next": "line_16"
},
{
"id": "line_16",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "Да без проблем! Обращайся если что.",
"objectiveComplete" : "ghost_lore.ghost_lore_alik",
"objectiveVisible": "ghost_lore.ghost_lore_alik",
"questUnlock": "ghost_coursework",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_alik003",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Алик",
"portrait": "resources/dialogue/portrait_student_boy.png",
"text": "Привет Бекзат! Надеюсь ты нашел то что ищешь.",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_video001",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Ого, пока я залипал в телефоне, уже наступила ночь!",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_video002",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Я не буду сейчас смотреть видеоролики, давай лучше пойдем спать.",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
},
{
"id": "dialog_video003",
"start": "line_1",
"nodes": [
{
"id": "line_1",
"type": "Line",
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Мне некогда деградировать, мне нужно сегодня 100% быть на лекции!",
"next": "end_1"
},
{
"id": "end_1",
"type": "End"
}
]
}
],
"cutscenes": [{
"id": "sleep_cutscene001",
"background": "resources/test_cutscene001.png",
"onFadeInCallback": "on_sleep_cutscene",
"durationMs": 5000,
"fadeOutMs": 500,
"fadeInMs": 500,
"endFadeOutMs": 500,
"endFadeInMs": 500,
"cameraTrack": [
{
"durationMs": 3000,
"from": { "focusX": 0.3, "focusY": 0.50, "zoom": 1.10, "rotationDeg": 0.0 },
"to": { "focusX": 0.7, "focusY": 0.50, "zoom": 1.00, "rotationDeg": 0.0 },
"easing": "EaseInOutSine"
},
{
"durationMs": 3000,
"from": { "focusX": 0.3, "focusY": 0.50, "zoom": 1.0, "rotationDeg": 0.0 },
"to": { "focusX": 0.7, "focusY": 0.50, "zoom": 1.1, "rotationDeg": 0.0 },
"easing": "EaseInOutCubic"
}
],
"lines": [
{
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "Я завалился спать и уснул.",
"durationMs": 3000
},
{
"speaker": "Бекзат",
"portrait": "resources/dialogue/portrait_hero_neutral.png",
"text": "И я проспал аж до обеда.",
"durationMs": 3000
}
]
}
]
}

Some files were not shown because too many files have changed in this diff Show More