Compare commits
487 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a11c4614ea | ||
|
|
5cbe7a345e | ||
|
|
2add42c1cb | ||
|
|
4039c24372 | ||
|
|
7df68febca | ||
|
|
5ecf5fb9d7 | ||
|
|
c3f449b999 | ||
|
|
8a0a8390b2 | ||
|
|
d39f5ca68b | ||
|
|
1c20333f30 | ||
|
|
1d8e103690 | ||
|
|
4c9ecb31d8 | ||
|
|
2e749ed26e | ||
|
|
fc93eb6680 | ||
|
|
5b21ba615e | ||
|
|
02ba86331b | ||
|
|
6a79e8e41e | ||
|
|
6406c4f258 | ||
|
|
9f812f0318 | ||
|
|
adf9871861 | ||
|
|
825a56a4a4 | ||
|
|
e5e3d7bb28 | ||
|
|
e773b51985 | ||
|
|
0975f3b9cd | ||
|
|
e9c888e2bf | ||
|
|
a743f653dd | ||
|
|
3d3522b905 | ||
|
|
b6cd17449f | ||
|
|
159fac02f0 | ||
|
|
9f17d51fc8 | ||
|
|
ca73148f4b | ||
|
|
0c24d52e6d | ||
|
|
beb4fee0d8 | ||
|
|
a7d17d9afd | ||
|
|
76287dda46 | ||
|
|
9da0ae1016 | ||
|
|
804e34ce12 | ||
|
|
34a55611d5 | ||
|
|
35a2a91178 | ||
|
|
38e45d67b8 | ||
|
|
33c2b313af | ||
|
|
92929118d5 | ||
|
|
9d03b52c2a | ||
|
|
d4365728fe | ||
|
|
79413e7319 | ||
|
|
8e745ce0cd | ||
|
|
7dadfd8ed8 | ||
|
|
07f45ad1e1 | ||
|
|
17d903fd52 | ||
|
|
2d3aa22521 | ||
|
|
5cde7078f9 | ||
|
|
44f5136f1d | ||
|
|
1c2c80f0ea | ||
|
|
124208f2a4 | ||
|
|
c6ba8bd008 | ||
|
|
a40a582b5a | ||
|
|
cdaebc3c0c | ||
|
|
e7dc3279ae | ||
|
|
08eac60657 | ||
|
|
ef5b142425 | ||
|
|
e2d9105b02 | ||
|
|
9d8ca865c7 | ||
|
|
794a820be9 | ||
|
|
a98f71676d | ||
|
|
bd994a8690 | ||
|
|
2d8f80b675 | ||
|
|
8a6366098c | ||
|
|
f7e63f933b | ||
|
|
5516d02e2c | ||
|
|
9aeb0b1194 | ||
|
|
40e7e0ee44 | ||
|
|
763ba9eeac | ||
|
|
bdad5fa107 | ||
|
|
1b172460e0 | ||
|
|
b41e312511 | ||
|
|
8c42179576 | ||
|
|
0b4df8ab2d | ||
|
|
6b39cfca3b | ||
|
|
b8d79c7b7b | ||
|
|
84f9fa37a8 | ||
|
|
7db9ca8f70 | ||
|
|
51977a9eea | ||
|
|
130e4411cb | ||
|
|
d7db0faea3 | ||
|
|
901142385d | ||
|
|
2807d82d51 | ||
|
|
b01e970554 | ||
|
|
e0968b5522 | ||
|
|
0ca00f965d | ||
|
|
0eb9d0be25 | ||
|
|
c891161d69 | ||
|
|
939136c705 | ||
|
|
bba493ee7c | ||
|
|
9f4d60036d | ||
|
|
020f31e1e4 | ||
|
|
80825e3220 | ||
|
|
b80e3fdc5e | ||
|
|
5d427af9f7 | ||
|
|
db456bde09 | ||
|
|
fff5a9d4aa | ||
|
|
2b32fce5e0 | ||
|
|
7031a21922 | ||
|
|
dee9615483 | ||
|
|
a4032d769e | ||
|
|
a9f4025333 | ||
|
|
f52b6872b0 | ||
|
|
87fb6d4655 | ||
|
|
80133aa8c8 | ||
|
|
0ed7021e90 | ||
|
|
b94f02bd77 | ||
|
|
77c8471636 | ||
|
|
dcdeae75bc | ||
|
|
619de45221 | ||
|
|
cfbd29d5b1 | ||
|
|
6349859e66 | ||
|
|
54cc118df7 | ||
|
|
3ba8a2f44b | ||
|
|
2e5988f8f4 | ||
|
|
7011bbb754 | ||
|
|
c991b5dbc1 | ||
|
|
f61a12f874 | ||
|
|
0cc384028f | ||
|
|
597ed04fdc | ||
|
|
e96c9f171f | ||
|
|
1a00e3d4a2 | ||
|
|
8f7797789d | ||
|
|
0e27521bea | ||
|
|
1fc8120ee0 | ||
|
|
9221ab68ac | ||
|
|
1d3bea452c | ||
|
|
59e0b2d77a | ||
|
|
c8d743b932 | ||
|
|
83b92e9881 | ||
|
|
ed8d6f2e92 | ||
|
|
e3542b59f7 | ||
|
|
442d3a7945 | ||
|
|
e6bfe85e2e | ||
|
|
1b1ceac2fe | ||
|
|
7a02665305 | ||
|
|
8694b0087d | ||
|
|
e7ae148ead | ||
|
|
aaae368a07 | ||
|
|
65d19c577a | ||
|
|
89cbc293fc | ||
|
|
60b0ba69ba | ||
|
|
4b868f47c2 | ||
|
|
22b0e1992f | ||
|
|
8552725dfc | ||
|
|
3d4560bb51 | ||
|
|
5f7e66f765 | ||
|
|
03681bedaf | ||
|
|
a0550daf4b | ||
|
|
9425f9b19a | ||
|
|
cdab525858 | ||
|
|
3f5c4a381b | ||
|
|
31130ec248 | ||
|
|
c659293bf8 | ||
|
|
9adcde5c05 | ||
|
|
90c57932ec | ||
|
|
dda9f34277 | ||
|
|
586ffc99b6 | ||
|
|
1eee415b41 | ||
|
|
80af0e61ae | ||
|
|
0da1ae124c | ||
|
|
6c8fcf17b6 | ||
|
|
cfafcfc944 | ||
|
|
26f596a149 | ||
|
|
e1a191634c | ||
|
|
84c8a7a27a | ||
|
|
8c200974c2 | ||
|
|
715f2e8c31 | ||
|
|
ffd5bd01a4 | ||
|
|
3a73f3fbde | ||
|
|
2659951d8e | ||
|
|
27b4b19df4 | ||
|
|
21bf3f0710 | ||
|
|
49501e32ce | ||
|
|
4044acefb2 | ||
|
|
c086fe1ef0 | ||
|
|
4e9eef1863 | ||
|
|
b973d714cc | ||
|
|
e618c91415 | ||
|
|
0f2b2e55af | ||
|
|
774397f690 | ||
|
|
d6417eba1b | ||
|
|
2cae9998a0 | ||
|
|
efb85f7328 | ||
|
|
001077a1d6 | ||
|
|
be05643b70 | ||
|
|
72e05cdc28 | ||
|
|
13ef92160d | ||
|
|
a479fa4ac1 | ||
|
|
d4bc73f732 | ||
|
|
8214342248 | ||
|
|
7e94009217 | ||
|
|
fb7b42ddf9 | ||
|
|
6149182f02 | ||
|
|
d24748df1e | ||
|
|
be6a8cca9e | ||
|
|
e04fcbb9bd | ||
|
|
519d780b3c | ||
|
|
16d250a51d | ||
|
|
5231b50aab | ||
| 8b0a18a6cf | |||
|
|
4de8f78eca | ||
|
|
5dd964c740 | ||
|
|
b98631a8c0 | ||
| d78286a9c2 | |||
|
|
635960007c | ||
| dbf0af2cb2 | |||
| cebbb8bb5d | |||
|
|
2ee288cbe0 | ||
|
|
348aa632dd | ||
|
|
3495d28edf | ||
|
|
c7b3f04cd4 | ||
|
|
eac914e073 | ||
|
|
44cc0fba67 | ||
| 0a073243ad | |||
|
|
393ebcd831 | ||
|
|
2bb7da2e37 | ||
|
|
b4b41e8620 | ||
|
|
3241ab3735 | ||
|
|
c655678486 | ||
|
|
4d9a5b0a6e | ||
|
|
52c4ad8ee0 | ||
|
|
fe01a5d1b2 | ||
|
|
b6e0422d71 | ||
|
|
3778a603f2 | ||
|
|
e310822462 | ||
|
|
3d3c3b2a36 | ||
|
|
b5bcd3a812 | ||
| 16e7580418 | |||
| be4dcd4cf9 | |||
|
|
a1dda3fd50 | ||
|
|
92ba3f2b60 | ||
|
|
f2f56125f0 | ||
|
|
2ef2e456f7 | ||
|
|
83cceecccd | ||
|
|
24851a89e8 | ||
|
|
d47cfd1b6b | ||
|
|
71346b4227 | ||
|
|
08e1174eaf | ||
|
|
cc2eba1352 | ||
|
|
cfee60d71a | ||
|
|
dc76986cd3 | ||
|
|
61665434ab | ||
| 2494a44e4a | |||
|
|
d8795ec076 | ||
| ceebb13719 | |||
|
|
a47306533e | ||
|
|
f708843747 | ||
|
|
a589765b7e | ||
| 4eaefd25d4 | |||
|
|
eff7322140 | ||
|
|
44dd09fcb5 | ||
|
|
de2e18e38f | ||
|
|
984d7489d5 | ||
|
|
f7e6f063d1 | ||
|
|
db8b9b7a18 | ||
|
|
493eb5a5d7 | ||
|
|
422ff1fbe3 | ||
| 6465549c3b | |||
|
|
34759326b9 | ||
| 7125674308 | |||
|
|
bc2997d86f | ||
|
|
943b6b4d21 | ||
|
|
b3fda88784 | ||
|
|
e1b491be73 | ||
|
|
178f69967b | ||
|
|
90e2a369f4 | ||
|
|
403b93e7f5 | ||
|
|
63c9d7b35b | ||
|
|
995e2c8e5b | ||
|
|
2e4a7ab467 | ||
|
|
a96d0b207a | ||
|
|
7840bfdb4e | ||
|
|
56ce826205 | ||
|
|
5364b97235 | ||
|
|
beac06fd45 | ||
|
|
ca98fea2bd | ||
|
|
a4d5017270 | ||
|
|
45826ab5b5 | ||
|
|
e5296a41b9 | ||
|
|
18674efd8e | ||
|
|
5e7c050c68 | ||
|
|
c88784a669 | ||
|
|
181132e092 | ||
|
|
6fe10eacfc | ||
|
|
9e692c9ec3 | ||
|
|
785b96ce82 | ||
|
|
a8ded217df | ||
|
|
3e8dba36a4 | ||
|
|
84a5e888a0 | ||
|
|
86a9f38c3b | ||
|
|
24aa7007bb | ||
|
|
59b5bea540 | ||
|
|
9da3cc4401 | ||
|
|
4a542fd6c8 | ||
|
|
a06cb77a66 | ||
|
|
3c2c06ca40 | ||
|
|
928600acd4 | ||
|
|
7ccda081b0 | ||
|
|
41f370f2ee | ||
|
|
4af20c65e6 | ||
|
|
ac551122d9 | ||
|
|
8528b4dc90 | ||
|
|
c35e093cc5 | ||
|
|
a8c76dee3c | ||
|
|
e84a094ad9 | ||
|
|
b0b7391939 | ||
|
|
3e2632c5b5 | ||
|
|
70ef730e86 | ||
|
|
92b288d978 | ||
|
|
cca1d0ece2 | ||
|
|
8effdd5dfa | ||
|
|
e78429b600 | ||
| 05a13a40e7 | |||
| ef7617701e | |||
| 2f50dc1522 | |||
| 9eafcd27fb | |||
|
|
3879eb1d75 | ||
| 78046e5e2d | |||
| 26233f934f | |||
|
|
167c04b107 | ||
|
|
5fcb1d1234 | ||
|
|
70a617b688 | ||
|
|
bb0f584bf6 | ||
|
|
6b4b549b3c | ||
|
|
ffbecbbcde | ||
|
|
2728ca9646 | ||
|
|
7e5aa22fee | ||
|
|
c20e82634a | ||
|
|
9f82e7a2e6 | ||
|
|
7418bbbe27 | ||
|
|
be2aa76f8b | ||
|
|
e8f1786a2a | ||
|
|
e30f8a4c70 | ||
|
|
f8642efa23 | ||
| 5da5a754fe | |||
| fb1b0a1c2e | |||
|
|
74c2f786a1 | ||
|
|
5216965496 | ||
|
|
ad7294ceea | ||
|
|
3893038d9a | ||
|
|
cb2e8318e7 | ||
|
|
5cca2413fe | ||
|
|
db4254e205 | ||
| fa3a4c66c9 | |||
|
|
84006380db | ||
|
|
f6cc30a30c | ||
|
|
d883b58260 | ||
|
|
b59a10b7e6 | ||
|
|
5b57696acf | ||
|
|
1265d87bc5 | ||
|
|
46d7a2a25d | ||
|
|
0babfff28b | ||
|
|
ed6e1bacc7 | ||
|
|
9c39782729 | ||
|
|
efad2dde3e | ||
|
|
84d8ee7eee | ||
|
|
7693237aa5 | ||
| 528c94e921 | |||
|
|
a8700af1d0 | ||
|
|
9793c5bf06 | ||
| b56fafa0e0 | |||
|
|
22ee99418d | ||
| 4eda57b4e4 | |||
|
|
2ffd8124f2 | ||
|
|
210c191d41 | ||
|
|
251a59ddbe | ||
|
|
5df2216da6 | ||
|
|
2c1c077611 | ||
|
|
629c9ba7b1 | ||
|
|
a59bcc0c4b | ||
|
|
cf1ec1e76f | ||
|
|
8a374a4231 | ||
|
|
da2b6e5577 | ||
| aa9590e2f4 | |||
| a1d54b01b0 | |||
|
|
65a2066727 | ||
|
|
c49c4626d0 | ||
|
|
ec156bd679 | ||
|
|
7b1d9363a0 | ||
|
|
82ca994fac | ||
|
|
0d2f705d42 | ||
| e799b4ec79 | |||
| c8076b6018 | |||
|
|
88f68b20ea | ||
| d249f17d22 | |||
| bee3545196 | |||
| e050d7d0ec | |||
| e18484ea62 | |||
|
|
204b79bf06 | ||
|
|
50232c0816 | ||
|
|
e8fb14b809 | ||
|
|
d425167dce | ||
| f41228acf8 | |||
|
|
cacc18dc7e | ||
|
|
96d879076a | ||
|
|
8f3528f9a1 | ||
|
|
67c0696061 | ||
|
|
1a7e424085 | ||
|
|
6aff22ec53 | ||
|
|
579f4886d1 | ||
|
|
64385ba15c | ||
|
|
3c55b59c8d | ||
|
|
e0a60dcd6f | ||
|
|
f967807296 | ||
|
|
fbc420e894 | ||
|
|
ff95163bcd | ||
|
|
e0453db063 | ||
| 0f415b6ed6 | |||
|
|
1348efb5fe | ||
|
|
ea26d6fb23 | ||
|
|
52a52ef960 | ||
|
|
90ad3392cb | ||
|
|
dc5c76f888 | ||
|
|
483757f8ca | ||
|
|
bdce619e68 | ||
|
|
dca38c986d | ||
|
|
13af968a04 | ||
|
|
4cc6971542 | ||
|
|
8d58e41459 | ||
|
|
dffbc6029d | ||
|
|
988e0fefa8 | ||
|
|
f0ad32476e | ||
|
|
deedec7833 | ||
|
|
cda1c0de20 | ||
|
|
ec30cbe810 | ||
|
|
d610f1dea1 | ||
|
|
c347545d0e | ||
|
|
a08ee412a6 | ||
|
|
528e7ae5a6 | ||
|
|
8089bcc0f3 | ||
|
|
d1cab1f3b3 | ||
|
|
9fc0adf210 | ||
|
|
197423b572 | ||
|
|
112684b40a | ||
|
|
648acf8938 | ||
|
|
b055faf882 | ||
|
|
723b973c7f | ||
|
|
6961152d34 | ||
|
|
34fee1005a | ||
|
|
8168734fbb | ||
|
|
144978bfa2 | ||
|
|
9bfe2bef89 | ||
|
|
c553780b6e | ||
|
|
570c96a628 | ||
|
|
c9178cff3a | ||
|
|
2b40c13c9d | ||
|
|
592e008914 | ||
|
|
0a5a3b653d | ||
|
|
ff3946a019 | ||
|
|
f197dfcf02 | ||
|
|
1af829bf49 | ||
|
|
7287f4b619 | ||
|
|
14b43239b5 | ||
|
|
d57112be35 | ||
|
|
7fc46d36a1 | ||
|
|
bee2fff538 | ||
|
|
29834d528a | ||
|
|
068c319890 | ||
|
|
53ea1f768c | ||
|
|
b369c52270 | ||
|
|
01d362615f | ||
|
|
e690d0f8e2 | ||
|
|
854a425d85 | ||
|
|
0464bed4cf | ||
|
|
7c272a2ee6 | ||
|
|
db25abc305 | ||
|
|
57ff27b0f6 | ||
|
|
a91d7c04e8 | ||
|
|
16ba536481 | ||
|
|
7a1961b5d1 | ||
|
|
af550f56a7 | ||
|
|
27fb7a448c | ||
|
|
2b6af91992 | ||
|
|
4afb9d6f4f | ||
|
|
fe361885c0 | ||
|
|
3887208b8d | ||
|
|
4c837eff0a | ||
|
|
8f9a18d960 | ||
|
|
bc49540a95 | ||
|
|
d2605d9108 | ||
|
|
115cbbb7fa | ||
| ae1ca71a20 | |||
|
|
b855cff0e6 |
7
.gitattributes
vendored
7
.gitattributes
vendored
@ -1,3 +1,10 @@
|
|||||||
*.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
15
.gitignore
vendored
@ -400,4 +400,17 @@ jumpingbird.*
|
|||||||
jumpingbird.data
|
jumpingbird.data
|
||||||
build
|
build
|
||||||
build-emcmake
|
build-emcmake
|
||||||
thirdparty1
|
thirdparty
|
||||||
|
|
||||||
|
proj-web/build
|
||||||
|
proj-windows/build
|
||||||
|
public
|
||||||
|
web_resources/
|
||||||
|
pc_resources/
|
||||||
|
resources_hd/
|
||||||
|
web_resources_x2/
|
||||||
|
android_resources/
|
||||||
|
|
||||||
|
.artifacts/
|
||||||
|
|
||||||
|
*.zip
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
#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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
#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
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
#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
|
|
||||||
@ -1,719 +0,0 @@
|
|||||||
#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];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
#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);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
532
CMakeLists.txt
532
CMakeLists.txt
@ -1,532 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
# Установка проекта по умолчанию для 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()
|
|
||||||
351
CUTSCENES.md
Normal file
351
CUTSCENES.md
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
# 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -1,39 +0,0 @@
|
|||||||
#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 = 6.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,0};
|
|
||||||
|
|
||||||
float Environment::shipVelocity = 0.f;
|
|
||||||
|
|
||||||
|
|
||||||
} // namespace ZL
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
#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;
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace ZL
|
|
||||||
612
Game.cpp
612
Game.cpp
@ -1,612 +0,0 @@
|
|||||||
#include "Game.h"
|
|
||||||
#include "AnimatedModel.h"
|
|
||||||
#include "BoneAnimatedModel.h"
|
|
||||||
#include "Utils.h"
|
|
||||||
#include "OpenGlExtensions.h"
|
|
||||||
#include <iostream>
|
|
||||||
#include "TextureManager.h"
|
|
||||||
#include "TextModel.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);
|
|
||||||
|
|
||||||
// Ãåíåðèðóåì ÷åòûðå ñëó÷àéíûõ ÷èñëà èç íîðìàëüíîãî ðàñïðåäåëåíèÿ N(0, 1).
|
|
||||||
// Íîðìàëèçàöèÿ ýòîãî âåêòîðà äàåò ðàâíîìåðíîå ðàñïðåäåëåíèå ïî 4D-ñôåðå (ò.å. êâàòåðíèîí åäèíè÷íîé äëèíû).
|
|
||||||
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;
|
|
||||||
boxCoordsArr.reserve(N); // Ðåçåðâèðóåì ïàìÿòü
|
|
||||||
|
|
||||||
// 1. Èíèöèàëèçàöèÿ ãåíåðàòîðà ïñåâäîñëó÷àéíûõ ÷èñåë
|
|
||||||
// Èñïîëüçóåì Mersenne Twister (mt19937) êàê âûñîêîêà÷åñòâåííûé ãåíåðàòîð
|
|
||||||
std::random_device rd;
|
|
||||||
std::mt19937 gen(rd());
|
|
||||||
|
|
||||||
// 2. Îïðåäåëåíèå ðàâíîìåðíîãî ðàñïðåäåëåíèÿ äëÿ êîîðäèíàò [MIN_COORD, MAX_COORD]
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
// 2. Ãåíåðèðóåì ñëó÷àéíûé êâàòåðíèîí
|
|
||||||
Vector4f randomQuat = generateRandomQuaternion(gen);
|
|
||||||
|
|
||||||
// 3. Ïðåîáðàçóåì åãî â ìàòðèöó âðàùåíèÿ
|
|
||||||
Matrix3f randomMatrix = QuatToMatrix(randomQuat);
|
|
||||||
|
|
||||||
// 4. Äîáàâëÿåì îáúåêò ñ íîâîé ñëó÷àéíîé ìàòðèöåé
|
|
||||||
boxCoordsArr.emplace_back(BoxCoords{ newPos, randomMatrix });
|
|
||||||
generatedCount++;
|
|
||||||
}
|
|
||||||
attempts++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Åñëè ïðåâûøåíî ìàêñèìàëüíîå êîëè÷åñòâî ïîïûòîê, âûõîäèì èç öèêëà,
|
|
||||||
// ÷òîáû èçáåæàòü çàâèñàíèÿ, åñëè N ñëèøêîì âåëèêî èëè äèàïàçîí ñëèøêîì ìàë.
|
|
||||||
if (!accepted) {
|
|
||||||
std::cerr << "Ïðåäóïðåæäåíèå: Íå óäàëîñü ñãåíåðèðîâàòü " << N << " îáúåêòîâ. Ñãåíåðèðîâàíî: " << generatedCount << std::endl;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return boxCoordsArr;
|
|
||||||
}
|
|
||||||
|
|
||||||
Game::Game()
|
|
||||||
: window(nullptr)
|
|
||||||
, glContext(nullptr)
|
|
||||||
, newTickCount(0)
|
|
||||||
, lastTickCount(0)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
#else
|
|
||||||
renderer.shaderManager.AddShaderFromFiles("default", "./shaders/default.vertex", "./shaders/default_desktop.fragment", CONST_ZIP_FILE);
|
|
||||||
renderer.shaderManager.AddShaderFromFiles("defaultColor", "./shaders/defaultColor.vertex", "./shaders/defaultColor_desktop.fragment", CONST_ZIP_FILE);
|
|
||||||
renderer.shaderManager.AddShaderFromFiles("env", "./shaders/env.vertex", "./shaders/env_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.png", CONST_ZIP_FILE));
|
|
||||||
spaceshipBase = LoadFromTextFile02("./resources/spaceship005.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].RefreshVBO();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void Game::drawCubemap()
|
|
||||||
{
|
|
||||||
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(envShaderName);
|
|
||||||
renderer.RenderUniform1i(textureUniformName, 0);
|
|
||||||
renderer.EnableVertexAttribArray(vPositionName);
|
|
||||||
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
|
|
||||||
static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
|
|
||||||
1, 1000);
|
|
||||||
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),
|
|
||||||
1, 1000);
|
|
||||||
renderer.PushMatrix();
|
|
||||||
|
|
||||||
renderer.LoadIdentity();
|
|
||||||
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
|
|
||||||
|
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
|
|
||||||
renderer.DrawVertexRenderStruct(spaceship);
|
|
||||||
|
|
||||||
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),
|
|
||||||
1, 1000);
|
|
||||||
|
|
||||||
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(boxCoordsArr[i].pos);
|
|
||||||
renderer.RotateMatrix(boxCoordsArr[i].m);
|
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, boxTexture->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 - 0.2) * (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, 0.5f, 1.0f, 1.0f);
|
|
||||||
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
|
|
||||||
|
|
||||||
glViewport(0, 0, Environment::width, Environment::height);
|
|
||||||
|
|
||||||
CheckGlError();
|
|
||||||
|
|
||||||
drawCubemap();
|
|
||||||
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);
|
|
||||||
|
|
||||||
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 && uiX <= volumeBarMaxX &&
|
|
||||||
uiY >= volumeBarMinY && uiY <= volumeBarMaxY) {
|
|
||||||
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 += 1.f;
|
|
||||||
}
|
|
||||||
if (event.key.keysym.sym == SDLK_k)
|
|
||||||
{
|
|
||||||
Environment::shipVelocity -= 1.f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace ZL
|
|
||||||
83
Game.h
83
Game.h
@ -1,83 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "OpenGlExtensions.h"
|
|
||||||
#include "Renderer.h"
|
|
||||||
#include "Environment.h"
|
|
||||||
#include "TextureManager.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();
|
|
||||||
void drawShip();
|
|
||||||
void drawBoxes();
|
|
||||||
void drawUI();
|
|
||||||
|
|
||||||
SDL_Window* window;
|
|
||||||
SDL_GLContext glContext;
|
|
||||||
Renderer renderer;
|
|
||||||
|
|
||||||
size_t newTickCount;
|
|
||||||
size_t lastTickCount;
|
|
||||||
|
|
||||||
static const size_t CONST_TIMER_INTERVAL = 10;
|
|
||||||
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
|
|
||||||
|
|
||||||
std::shared_ptr<Texture> spaceshipTexture;
|
|
||||||
std::shared_ptr<Texture> cubemapTexture;
|
|
||||||
VertexDataStruct spaceshipBase;
|
|
||||||
VertexRenderStruct spaceship;
|
|
||||||
|
|
||||||
VertexRenderStruct cubemap;
|
|
||||||
|
|
||||||
std::shared_ptr<Texture> boxTexture;
|
|
||||||
VertexDataStruct boxBase;
|
|
||||||
|
|
||||||
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 = 1.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();
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace ZL
|
|
||||||
@ -1,335 +0,0 @@
|
|||||||
#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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,160 +0,0 @@
|
|||||||
#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();
|
|
||||||
}
|
|
||||||
260
PATHFINDING.md
Normal file
260
PATHFINDING.md
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
# 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 |
|
||||||
62
Readme.md
62
Readme.md
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
download from https://cmake.org/download/
|
download from https://cmake.org/download/
|
||||||
|
|
||||||
|
|
||||||
Windows x64 Installer: cmake-4.2.0-windows-x86_64.msi
|
Windows x64 Installer: cmake-4.2.0-windows-x86_64.msi
|
||||||
|
|
||||||
|
|
||||||
@ -125,6 +126,18 @@ $(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
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -153,6 +166,12 @@ 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
|
||||||
|
|
||||||
@ -165,4 +184,45 @@ 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
|
||||||
|
|
||||||
|
```
|
||||||
|
|||||||
791
Renderer.cpp
791
Renderer.cpp
@ -1,791 +0,0 @@
|
|||||||
#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::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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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!");
|
|
||||||
}
|
|
||||||
|
|
||||||
ProjectionModelViewMatrix = ProjectionMatrixStack.top() * ModelviewMatrixStack.top();
|
|
||||||
|
|
||||||
static const std::string ProjectionModelViewMatrixName = "ProjectionModelViewMatrix";
|
|
||||||
|
|
||||||
//static const std::string ProjectionMatrixName = "ProjectionMatrix";
|
|
||||||
|
|
||||||
RenderUniformMatrix4fv(ProjectionModelViewMatrixName, false, &ProjectionModelViewMatrix.m[0]);
|
|
||||||
|
|
||||||
//RenderUniformMatrix4fv(ProjectionMatrixName, false, &ProjectionMatrixStack.top().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::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::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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
141
Renderer.h
141
Renderer.h
@ -1,141 +0,0 @@
|
|||||||
#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 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();
|
|
||||||
|
|
||||||
void SetMatrix();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void EnableVertexAttribArray(const std::string& attribName);
|
|
||||||
|
|
||||||
void DisableVertexAttribArray(const std::string& attribName);
|
|
||||||
|
|
||||||
|
|
||||||
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 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);
|
|
||||||
|
|
||||||
};
|
|
||||||
@ -1,212 +0,0 @@
|
|||||||
#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;
|
|
||||||
|
|
||||||
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, &infoLogLength, infoLog);
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
#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();
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
390
TextModel.cpp
390
TextModel.cpp
@ -1,390 +0,0 @@
|
|||||||
#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
12
TextModel.h
@ -1,12 +0,0 @@
|
|||||||
#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 = "");
|
|
||||||
}
|
|
||||||
@ -1,430 +0,0 @@
|
|||||||
#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
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
#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
Normal file
527
UI.md
Normal file
@ -0,0 +1,527 @@
|
|||||||
|
# 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
105
Utils.cpp
@ -1,105 +0,0 @@
|
|||||||
#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
20
Utils.h
@ -1,20 +0,0 @@
|
|||||||
#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);
|
|
||||||
|
|
||||||
}
|
|
||||||
757
ZLMath.cpp
757
ZLMath.cpp
@ -1,757 +0,0 @@
|
|||||||
#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 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
144
ZLMath.h
144
ZLMath.h
@ -1,144 +0,0 @@
|
|||||||
#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]);
|
|
||||||
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]);
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Îïåðàòîð âû÷èòàíèÿ
|
|
||||||
/*Vector3f operator-(const Vector3f& other) const {
|
|
||||||
return Vector3f(v[0] - other.v[0], v[1] - other.v[1], v[2] - other.v[2]);
|
|
||||||
}*/
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
struct Vector2f
|
|
||||||
{
|
|
||||||
std::array<float, 2> v = {0.f, 0.f};
|
|
||||||
};
|
|
||||||
|
|
||||||
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 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);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
};
|
|
||||||
65
blender scripts/blender_join_anims.py
Normal file
65
blender scripts/blender_join_anims.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
192
blender scripts/bone_anim_script01.py
Normal file
192
blender scripts/bone_anim_script01.py
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
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'")
|
||||||
228
blender scripts/export_anim_001.py
Normal file
228
blender scripts/export_anim_001.py
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
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'")
|
||||||
111
blender scripts/generate_tree001.py
Normal file
111
blender scripts/generate_tree001.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
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
58
cmake/FetchDependencies.cmake
Normal file
58
cmake/FetchDependencies.cmake
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# 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")
|
||||||
28
cmake/FetchDependenciesLinux.cmake
Normal file
28
cmake/FetchDependenciesLinux.cmake
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# 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")
|
||||||
728
cmake/ThirdParty.cmake
Normal file
728
cmake/ThirdParty.cmake
Normal file
@ -0,0 +1,728 @@
|
|||||||
|
# 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"
|
||||||
|
)
|
||||||
14
cmake/patches/sol2-3.3.0-clang-optional.patch
Normal file
14
cmake/patches/sol2-3.3.0-clang-optional.patch
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
--- 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;
|
||||||
|
}
|
||||||
@ -1,36 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
#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;
|
|
||||||
};
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
323
convert_anim_to_binary.py
Normal file
323
convert_anim_to_binary.py
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
#!/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])
|
||||||
370
convert_anim_to_binary_new.py
Normal file
370
convert_anim_to_binary_new.py
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
#!/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])
|
||||||
63
convert_config_meshes.py
Normal file
63
convert_config_meshes.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#!/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])
|
||||||
142
convert_model_to_binary.py
Normal file
142
convert_model_to_binary.py
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
#!/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)
|
||||||
54
convert_old_anim_to_new.py
Normal file
54
convert_old_anim_to_new.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#!/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)
|
||||||
46
cutsceneEditor/.gitignore
vendored
Normal file
46
cutsceneEditor/.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
.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
|
||||||
|
|
||||||
351
cutsceneEditor/CUTSCENES.md
Normal file
351
cutsceneEditor/CUTSCENES.md
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
# 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
65
cutsceneEditor/cutscene_example.json
Normal file
65
cutsceneEditor/cutscene_example.json
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1
cutsceneEditor/dist/assets/index-B96C0g1n.css
vendored
Normal file
1
cutsceneEditor/dist/assets/index-B96C0g1n.css
vendored
Normal file
File diff suppressed because one or more lines are too long
40
cutsceneEditor/dist/assets/index-D_5Tak8P.js
vendored
Normal file
40
cutsceneEditor/dist/assets/index-D_5Tak8P.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
cutsceneEditor/dist/index.html
vendored
Normal file
13
cutsceneEditor/dist/index.html
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!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>
|
||||||
12
cutsceneEditor/index.html
Normal file
12
cutsceneEditor/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!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>
|
||||||
1890
cutsceneEditor/package-lock.json
generated
Normal file
1890
cutsceneEditor/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
cutsceneEditor/package.json
Normal file
24
cutsceneEditor/package.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
cutsceneEditor/src/App.module.css
Normal file
6
cutsceneEditor/src/App.module.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.app {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
14
cutsceneEditor/src/App.tsx
Normal file
14
cutsceneEditor/src/App.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
16
cutsceneEditor/src/components/CenterPanel/CenterPanel.tsx
Normal file
16
cutsceneEditor/src/components/CenterPanel/CenterPanel.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
cutsceneEditor/src/components/Controls/Controls.module.css
Normal file
94
cutsceneEditor/src/components/Controls/Controls.module.css
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
67
cutsceneEditor/src/components/Controls/Controls.tsx
Normal file
67
cutsceneEditor/src/components/Controls/Controls.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
100
cutsceneEditor/src/components/LeftPanel/LeftPanel.module.css
Normal file
100
cutsceneEditor/src/components/LeftPanel/LeftPanel.module.css
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
.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; }
|
||||||
76
cutsceneEditor/src/components/LeftPanel/LeftPanel.tsx
Normal file
76
cutsceneEditor/src/components/LeftPanel/LeftPanel.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
cutsceneEditor/src/components/Preview/Preview.module.css
Normal file
51
cutsceneEditor/src/components/Preview/Preview.module.css
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
.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);
|
||||||
|
}
|
||||||
85
cutsceneEditor/src/components/Preview/Preview.tsx
Normal file
85
cutsceneEditor/src/components/Preview/Preview.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
cutsceneEditor/src/components/RightPanel/LayerProperties.tsx
Normal file
132
cutsceneEditor/src/components/RightPanel/LayerProperties.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
208
cutsceneEditor/src/components/RightPanel/RightPanel.module.css
Normal file
208
cutsceneEditor/src/components/RightPanel/RightPanel.module.css
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
41
cutsceneEditor/src/components/RightPanel/RightPanel.tsx
Normal file
41
cutsceneEditor/src/components/RightPanel/RightPanel.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
cutsceneEditor/src/components/RightPanel/SubtitleLines.tsx
Normal file
89
cutsceneEditor/src/components/RightPanel/SubtitleLines.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
192
cutsceneEditor/src/components/Timeline/Timeline.module.css
Normal file
192
cutsceneEditor/src/components/Timeline/Timeline.module.css
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
285
cutsceneEditor/src/components/Timeline/Timeline.tsx
Normal file
285
cutsceneEditor/src/components/Timeline/Timeline.tsx
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
cutsceneEditor/src/constants/easings.ts
Normal file
25
cutsceneEditor/src/constants/easings.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
cutsceneEditor/src/constants/images.ts
Normal file
15
cutsceneEditor/src/constants/images.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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',
|
||||||
|
];
|
||||||
52
cutsceneEditor/src/hooks/usePlayback.ts
Normal file
52
cutsceneEditor/src/hooks/usePlayback.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
52
cutsceneEditor/src/index.css
Normal file
52
cutsceneEditor/src/index.css
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
*, *::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; }
|
||||||
10
cutsceneEditor/src/main.tsx
Normal file
10
cutsceneEditor/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
242
cutsceneEditor/src/store/cutsceneStore.ts
Normal file
242
cutsceneEditor/src/store/cutsceneStore.ts
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
49
cutsceneEditor/src/types/cutscene.ts
Normal file
49
cutsceneEditor/src/types/cutscene.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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[];
|
||||||
|
}
|
||||||
76
cutsceneEditor/src/utils/fileIO.ts
Normal file
76
cutsceneEditor/src/utils/fileIO.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
79
cutsceneEditor/src/utils/rendering.ts
Normal file
79
cutsceneEditor/src/utils/rendering.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
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))`,
|
||||||
|
};
|
||||||
|
}
|
||||||
6
cutsceneEditor/src/vite-env.d.ts
vendored
Normal file
6
cutsceneEditor/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: Record<string, string>;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
20
cutsceneEditor/tsconfig.json
Normal file
20
cutsceneEditor/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
26
cutsceneEditor/vite.config.ts
Normal file
26
cutsceneEditor/vite.config.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
43
dialogEditor/.gitignore
vendored
Normal file
43
dialogEditor/.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.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
|
||||||
711
dialogEditor/dorm_dialogues.json
Normal file
711
dialogEditor/dorm_dialogues.json
Normal file
@ -0,0 +1,711 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
12
dialogEditor/index.html
Normal file
12
dialogEditor/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Dialogue Editor</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2157
dialogEditor/package-lock.json
generated
Normal file
2157
dialogEditor/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
dialogEditor/package.json
Normal file
26
dialogEditor/package.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "dialogue-editor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
|
"@xyflow/react": "^12.3.6",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"zustand": "^5.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/dagre": "^0.7.52",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
501
dialogEditor/sample_dialogues.json
Normal file
501
dialogEditor/sample_dialogues.json
Normal file
@ -0,0 +1,501 @@
|
|||||||
|
{
|
||||||
|
"dialogues": [
|
||||||
|
{
|
||||||
|
"id": "dialog_student",
|
||||||
|
"start": "line_1",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "line_1",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Студент",
|
||||||
|
"portrait": "resources/w/avatar_student.png",
|
||||||
|
"text": "В университете завелись призраки, мне страшно ходить на занятия.",
|
||||||
|
"next": "line_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_2",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Hero",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Можешь рассказать подробнее?",
|
||||||
|
"next": "line_3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_3",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Студент",
|
||||||
|
"portrait": "resources/w/avatar_student.png",
|
||||||
|
"text": "Спроси у Мухтара байке, он все знает.",
|
||||||
|
"next": "line_4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_4",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Hero",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Хорошо.",
|
||||||
|
"next": "end_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dialog_mukhtar",
|
||||||
|
"start": "line_1",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "line_1",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Мухтар байке",
|
||||||
|
"portrait": "resources/w/avatar_unknown.png",
|
||||||
|
"text": "Здравствуй, мы давно тебя ждем! Ты поможешь нам избавиться от призраков?",
|
||||||
|
"next": "line_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_2",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Hero",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Где их найти?",
|
||||||
|
"next": "line_3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_3",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Мухтар байке",
|
||||||
|
"portrait": "resources/w/avatar_unknown.png",
|
||||||
|
"text": "Заходи в здание универа и поднимайся на второй этаж.",
|
||||||
|
"next": "line_4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_4",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Мухтар байке",
|
||||||
|
"portrait": "resources/w/avatar_unknown.png",
|
||||||
|
"text": "Ты их встретишь прямо там.",
|
||||||
|
"next": "line_5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_4",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Hero",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Хорошо, я скоро вернусь!",
|
||||||
|
"next": "end_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dialog_female_student",
|
||||||
|
"start": "line_1",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "line_1",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Студентка",
|
||||||
|
"portrait": "resources/w/avatar_girl.png",
|
||||||
|
"text": "С этими призраками совсем невозможно ходить на лекции!",
|
||||||
|
"next": "end_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test_line_dialogue",
|
||||||
|
"start": "line_1",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "line_1",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Ghost",
|
||||||
|
"portrait": "resources/ghost_avatar.png",
|
||||||
|
"text": "Наконец-то ты пришел.",
|
||||||
|
"next": "line_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_2",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Hero",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Ты сделан из дыма?",
|
||||||
|
"next": "line_3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_3",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Ghost",
|
||||||
|
"portrait": "resources/ghost_avatar.png",
|
||||||
|
"text": "Ты думаешь, это смешно?",
|
||||||
|
"next": "line_4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_4",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Hero",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Я думаю что ты пахнешь как выхлоп от Камаза.",
|
||||||
|
"next": "end_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ghost_choice_dialogue",
|
||||||
|
"start": "line_1",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "line_1",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Беспокойный Призрак",
|
||||||
|
"portrait": "resources/w/avatar_ghost.png",
|
||||||
|
"text": "Нечасто я вижу смертных, готовых разговаривать со мной.",
|
||||||
|
"next": "choice_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "choice_1",
|
||||||
|
"type": "Choice",
|
||||||
|
"speaker": "Hero",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"id": "main_1",
|
||||||
|
"kind": "Main",
|
||||||
|
"text": "Не мешай студентам учиться!",
|
||||||
|
"next": "line_goods"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "optional_1",
|
||||||
|
"kind": "Optional",
|
||||||
|
"text": "Почему ты появился здесь?",
|
||||||
|
"next": "line_who"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_goods",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Беспокойный Призрак",
|
||||||
|
"portrait": "resources/w/avatar_ghost.png",
|
||||||
|
"text": "Это моя месть студентам за то что они призвали меня.",
|
||||||
|
"next": "end_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_who",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Беспокойный Призрак",
|
||||||
|
"portrait": "resources/w/avatar_ghost.png",
|
||||||
|
"text": "Группа студентов совершила ритуал и призвала меня сюда. Пока проклятие не спадет, я всегда буду здесь обитать.",
|
||||||
|
"next": "choice_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test_condition_dialogue",
|
||||||
|
"start": "set_flag_1",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "set_flag_1",
|
||||||
|
"type": "SetFlag",
|
||||||
|
"effects": [
|
||||||
|
{ "flag": "met_ghost", "value": 1 }
|
||||||
|
],
|
||||||
|
"next": "condition_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "condition_1",
|
||||||
|
"type": "Condition",
|
||||||
|
"conditions": [
|
||||||
|
{ "flag": "met_ghost", "op": "Equals", "value": 1 }
|
||||||
|
],
|
||||||
|
"trueNext": "line_true",
|
||||||
|
"falseNext": "line_false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_true",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Ghost",
|
||||||
|
"portrait": "resources/ghost_avatar.png",
|
||||||
|
"text": "Now you know who I am.",
|
||||||
|
"next": "end_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_false",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Ghost",
|
||||||
|
"portrait": "resources/ghost_avatar.png",
|
||||||
|
"text": "You should not hear this line.",
|
||||||
|
"next": "end_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test_cutscene_dialogue",
|
||||||
|
"start": "cutscene_start",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "cutscene_start",
|
||||||
|
"type": "CutsceneStart",
|
||||||
|
"cutsceneId": "test_cutscene_01",
|
||||||
|
"next": "end_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test_silent_cutscene_dialogue",
|
||||||
|
"start": "cutscene_start",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "cutscene_start",
|
||||||
|
"type": "CutsceneStart",
|
||||||
|
"cutsceneId": "test_cutscene_silent_01",
|
||||||
|
"next": "end_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test_cutscene_pan_dialogue",
|
||||||
|
"start": "cutscene_start",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "cutscene_start",
|
||||||
|
"type": "CutsceneStart",
|
||||||
|
"cutsceneId": "test_cutscene_pan_01",
|
||||||
|
"next": "end_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test_cutscene_pan_dialogue_silent",
|
||||||
|
"start": "cutscene_start",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "cutscene_start",
|
||||||
|
"type": "CutsceneStart",
|
||||||
|
"cutsceneId": "test_cutscene_pan_02",
|
||||||
|
"next": "end_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dialog_aida",
|
||||||
|
"start": "line_1",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "line_1",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Асель Дженибековна",
|
||||||
|
"portrait": "resources/w/avatar_teacher.png",
|
||||||
|
"text": "Молодой человек, у меня обед! Я принимаю лабораторные работы только после двух!",
|
||||||
|
"next": "line_2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_2",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Hero",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Хорошо, Асель Дженибековна.",
|
||||||
|
"next": "end_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cutscenes": [
|
||||||
|
{
|
||||||
|
"id": "test_cutscene_01",
|
||||||
|
"background": "resources/first_cutscene.png",
|
||||||
|
"durationMs": 6800,
|
||||||
|
"cameraTrack": [
|
||||||
|
{
|
||||||
|
"durationMs": 2400,
|
||||||
|
"from": { "focusX": 0.50, "focusY": 0.55, "zoom": 1.00, "rotationDeg": 0.0 },
|
||||||
|
"to": { "focusX": 0.63, "focusY": 0.58, "zoom": 1.16, "rotationDeg": -1.0 },
|
||||||
|
"easing": "EaseInOutSine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"durationMs": 2200,
|
||||||
|
"from": { "focusX": 0.63, "focusY": 0.58, "zoom": 1.16, "rotationDeg": -1.0 },
|
||||||
|
"to": { "focusX": 0.74, "focusY": 0.52, "zoom": 1.30, "rotationDeg": -2.4 },
|
||||||
|
"easing": "EaseInOutCubic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"durationMs": 2200,
|
||||||
|
"from": { "focusX": 0.74, "focusY": 0.52, "zoom": 1.30, "rotationDeg": -2.4 },
|
||||||
|
"to": { "focusX": 0.58, "focusY": 0.46, "zoom": 1.10, "rotationDeg": -0.6 },
|
||||||
|
"easing": "EaseOutSine"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"speaker": "Narrator",
|
||||||
|
"portrait": "resources/hero.png",
|
||||||
|
"text": "The air in the room turned cold.",
|
||||||
|
"durationMs": 2200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"speaker": "Ghost",
|
||||||
|
"portrait": "resources/w/avatar_ghost.png",
|
||||||
|
"text": "Some memories never fade.",
|
||||||
|
"durationMs": 2600,
|
||||||
|
"background": "resources/loading.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test_cutscene_silent_01",
|
||||||
|
"background": "resources/first_cutscene.png",
|
||||||
|
"durationMs": 5200,
|
||||||
|
"cameraTrack": [
|
||||||
|
{
|
||||||
|
"durationMs": 2600,
|
||||||
|
"from": { "focusX": 0.40, "focusY": 0.54, "zoom": 1.00, "rotationDeg": 0.0 },
|
||||||
|
"to": { "focusX": 0.58, "focusY": 0.54, "zoom": 1.22, "rotationDeg": 0.8 },
|
||||||
|
"easing": "EaseInOutSine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"durationMs": 2600,
|
||||||
|
"from": { "focusX": 0.58, "focusY": 0.54, "zoom": 1.22, "rotationDeg": 0.8 },
|
||||||
|
"to": { "focusX": 0.72, "focusY": 0.48, "zoom": 1.34, "rotationDeg": -0.5 },
|
||||||
|
"easing": "EaseOutCubic"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lines": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test_cutscene_pan_01",
|
||||||
|
"background": "resources/first_cutscene.png",
|
||||||
|
"durationMs": 12000,
|
||||||
|
"cameraTrack": [
|
||||||
|
{
|
||||||
|
"durationMs": 1200,
|
||||||
|
"from": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
|
||||||
|
"to": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
|
||||||
|
"easing": "Linear"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"durationMs": 2500,
|
||||||
|
"from": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
|
||||||
|
"to": { "anchor": "TopLeft", "zoom": 1.55, "rotationDeg": 0.0 },
|
||||||
|
"easing": "EaseInOutSine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"durationMs": 2600,
|
||||||
|
"from": { "anchor": "TopLeft", "zoom": 1.55, "rotationDeg": 0.0 },
|
||||||
|
"to": { "anchor": "TopRight", "zoom": 1.55, "rotationDeg": 0.0 },
|
||||||
|
"easing": "EaseInOutSine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"durationMs": 1800,
|
||||||
|
"from": { "anchor": "TopRight", "zoom": 1.55, "rotationDeg": 0.0 },
|
||||||
|
"to": { "anchor": "BottomRight", "zoom": 1.72, "rotationDeg": 0.0 },
|
||||||
|
"easing": "EaseInCubic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"durationMs": 3900,
|
||||||
|
"from": { "anchor": "BottomRight", "zoom": 1.72, "rotationDeg": 0.0 },
|
||||||
|
"to": { "anchor": "BottomLeft", "zoom": 1.55, "rotationDeg": 0.0 },
|
||||||
|
"easing": "EaseInOutSine"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"speaker": "Narrator",
|
||||||
|
"portrait": "resources/hero.png",
|
||||||
|
"text": "The memory begins in silence.",
|
||||||
|
"durationMs": 2200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"speaker": "Narrator",
|
||||||
|
"portrait": "resources/hero.png",
|
||||||
|
"text": "Something is drawing your eyes across the whole scene.",
|
||||||
|
"durationMs": 2800
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"speaker": "Ghost",
|
||||||
|
"portrait": "resources/ghost_avatar.png",
|
||||||
|
"text": "Do not look away.",
|
||||||
|
"durationMs": 2400
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "test_cutscene_pan_02",
|
||||||
|
"background": "resources/first_cutscene.png",
|
||||||
|
"durationMs": 12000,
|
||||||
|
"cameraTrack": [
|
||||||
|
{
|
||||||
|
"durationMs": 1200,
|
||||||
|
"from": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
|
||||||
|
"to": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
|
||||||
|
"easing": "Linear"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"durationMs": 2500,
|
||||||
|
"from": { "anchor": "Center", "zoom": 1.00, "rotationDeg": 0.0 },
|
||||||
|
"to": { "anchor": "TopLeft", "zoom": 1.55, "rotationDeg": 0.0 },
|
||||||
|
"easing": "EaseInOutSine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"durationMs": 2600,
|
||||||
|
"from": { "anchor": "TopLeft", "zoom": 1.55, "rotationDeg": 0.0 },
|
||||||
|
"to": { "anchor": "TopRight", "zoom": 1.55, "rotationDeg": 0.0 },
|
||||||
|
"easing": "EaseInOutSine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"durationMs": 1800,
|
||||||
|
"from": { "anchor": "TopRight", "zoom": 1.55, "rotationDeg": 0.0 },
|
||||||
|
"to": { "anchor": "BottomRight", "zoom": 1.72, "rotationDeg": 0.0 },
|
||||||
|
"easing": "EaseInCubic"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"durationMs": 3900,
|
||||||
|
"from": { "anchor": "BottomRight", "zoom": 1.72, "rotationDeg": 0.0 },
|
||||||
|
"to": { "anchor": "BottomLeft", "zoom": 1.55, "rotationDeg": 0.0 },
|
||||||
|
"easing": "EaseInOutSine"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lines": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5
dialogEditor/src/App.module.css
Normal file
5
dialogEditor/src/App.module.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.app {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
19
dialogEditor/src/App.tsx
Normal file
19
dialogEditor/src/App.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { LeftPanel } from './components/LeftPanel/LeftPanel';
|
||||||
|
import { GraphPanel } from './components/GraphPanel/GraphPanel';
|
||||||
|
import { RightPanel } from './components/RightPanel/RightPanel';
|
||||||
|
import { PlayModeOverlay } from './components/PlayMode/PlayModeOverlay';
|
||||||
|
import { useDialogueStore } from './store/dialogueStore';
|
||||||
|
import styles from './App.module.css';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const playModeActive = useDialogueStore(s => s.playModeActive);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.app}>
|
||||||
|
<LeftPanel />
|
||||||
|
<GraphPanel />
|
||||||
|
<RightPanel />
|
||||||
|
{playModeActive && <PlayModeOverlay />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
dialogEditor/src/components/GraphPanel/GraphPanel.module.css
Normal file
26
dialogEditor/src/components/GraphPanel/GraphPanel.module.css
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
.container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #1e1e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #1e1e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyMsg {
|
||||||
|
color: #6c7086;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
185
dialogEditor/src/components/GraphPanel/GraphPanel.tsx
Normal file
185
dialogEditor/src/components/GraphPanel/GraphPanel.tsx
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
Background,
|
||||||
|
Controls,
|
||||||
|
MiniMap,
|
||||||
|
NodeChange,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
Connection,
|
||||||
|
MarkerType,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
import { useDialogueStore } from '../../store/dialogueStore';
|
||||||
|
import { nodeTypes } from '../nodes/nodeTypes';
|
||||||
|
import { DialogueNode, ChoiceNode } from '../../types/dialogue';
|
||||||
|
import { GraphToolbar } from './GraphToolbar';
|
||||||
|
import styles from './GraphPanel.module.css';
|
||||||
|
|
||||||
|
function buildEdges(nodes: DialogueNode[]): Edge[] {
|
||||||
|
const edges: Edge[] = [];
|
||||||
|
for (const node of nodes) {
|
||||||
|
const base = {
|
||||||
|
markerEnd: { type: MarkerType.ArrowClosed, color: '#6c7086' },
|
||||||
|
style: { stroke: '#6c7086', strokeWidth: 1.5 },
|
||||||
|
animated: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (node.type === 'Line' || node.type === 'SetFlag' || node.type === 'CutsceneStart') {
|
||||||
|
if (node.next) {
|
||||||
|
edges.push({ ...base, id: `${node.id}->source`, source: node.id, target: node.next, sourceHandle: 'source', targetHandle: 'target' });
|
||||||
|
}
|
||||||
|
} else if (node.type === 'Choice') {
|
||||||
|
for (const choice of node.choices) {
|
||||||
|
if (choice.next) {
|
||||||
|
edges.push({ ...base, id: `${node.id}->${choice.id}`, source: node.id, target: choice.next, sourceHandle: choice.id, targetHandle: 'target', label: choice.text.slice(0, 20), labelStyle: { fontSize: 10, fill: '#cdd6f4' }, labelBgStyle: { fill: '#181825' } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (node.type === 'Condition') {
|
||||||
|
if (node.trueNext) {
|
||||||
|
edges.push({ ...base, id: `${node.id}->true`, source: node.id, target: node.trueNext, sourceHandle: 'true', targetHandle: 'target', label: 'TRUE', labelStyle: { fontSize: 10, fill: '#a6e3a1' }, labelBgStyle: { fill: '#181825' }, style: { ...base.style, stroke: '#a6e3a1' }, markerEnd: { type: MarkerType.ArrowClosed, color: '#a6e3a1' } });
|
||||||
|
}
|
||||||
|
if (node.falseNext) {
|
||||||
|
edges.push({ ...base, id: `${node.id}->false`, source: node.id, target: node.falseNext, sourceHandle: 'false', targetHandle: 'target', label: 'FALSE', labelStyle: { fontSize: 10, fill: '#f38ba8' }, labelBgStyle: { fill: '#181825' }, style: { ...base.style, stroke: '#f38ba8' }, markerEnd: { type: MarkerType.ArrowClosed, color: '#f38ba8' } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GraphPanel() {
|
||||||
|
const {
|
||||||
|
file,
|
||||||
|
selectedDialogueId,
|
||||||
|
selectedNodeId,
|
||||||
|
positions,
|
||||||
|
selectNode,
|
||||||
|
setNodePosition,
|
||||||
|
applyAutoLayout,
|
||||||
|
updateNode,
|
||||||
|
} = useDialogueStore();
|
||||||
|
|
||||||
|
const dialogue = file?.dialogues.find(d => d.id === selectedDialogueId);
|
||||||
|
const dialoguePositions = positions[selectedDialogueId ?? ''] ?? {};
|
||||||
|
|
||||||
|
const rfNodes: Node[] = useMemo(() => {
|
||||||
|
if (!dialogue) return [];
|
||||||
|
|
||||||
|
return dialogue.nodes.map(node => ({
|
||||||
|
id: node.id,
|
||||||
|
type: node.type,
|
||||||
|
data: node as unknown as Record<string, unknown>,
|
||||||
|
position: dialoguePositions[node.id] ?? { x: 0, y: 0 },
|
||||||
|
selected: node.id === selectedNodeId,
|
||||||
|
}));
|
||||||
|
}, [dialogue, dialoguePositions, selectedNodeId]);
|
||||||
|
|
||||||
|
const rfEdges: Edge[] = useMemo(() => {
|
||||||
|
if (!dialogue) return [];
|
||||||
|
return buildEdges(dialogue.nodes);
|
||||||
|
}, [dialogue]);
|
||||||
|
|
||||||
|
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||||
|
if (!selectedDialogueId) return;
|
||||||
|
for (const change of changes) {
|
||||||
|
if (change.type === 'position' && change.position) {
|
||||||
|
setNodePosition(selectedDialogueId, change.id, change.position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedDialogueId, setNodePosition]);
|
||||||
|
|
||||||
|
const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => {
|
||||||
|
selectNode(node.id);
|
||||||
|
}, [selectNode]);
|
||||||
|
|
||||||
|
const onPaneClick = useCallback(() => {
|
||||||
|
selectNode(null);
|
||||||
|
}, [selectNode]);
|
||||||
|
|
||||||
|
const onConnect = useCallback((connection: Connection) => {
|
||||||
|
if (!selectedDialogueId || !connection.source || !connection.target) return;
|
||||||
|
const dialogue = file?.dialogues.find(d => d.id === selectedDialogueId);
|
||||||
|
if (!dialogue) return;
|
||||||
|
const sourceNode = dialogue.nodes.find(n => n.id === connection.source);
|
||||||
|
if (!sourceNode) return;
|
||||||
|
|
||||||
|
const handle = connection.sourceHandle;
|
||||||
|
|
||||||
|
if (sourceNode.type === 'Line' || sourceNode.type === 'SetFlag' || sourceNode.type === 'CutsceneStart') {
|
||||||
|
updateNode(selectedDialogueId, sourceNode.id, { next: connection.target } as Partial<DialogueNode>);
|
||||||
|
} else if (sourceNode.type === 'Condition') {
|
||||||
|
if (handle === 'true') {
|
||||||
|
updateNode(selectedDialogueId, sourceNode.id, { trueNext: connection.target } as Partial<DialogueNode>);
|
||||||
|
} else if (handle === 'false') {
|
||||||
|
updateNode(selectedDialogueId, sourceNode.id, { falseNext: connection.target } as Partial<DialogueNode>);
|
||||||
|
}
|
||||||
|
} else if (sourceNode.type === 'Choice') {
|
||||||
|
const choices = (sourceNode as ChoiceNode).choices.map(c =>
|
||||||
|
c.id === handle ? { ...c, next: connection.target! } : c
|
||||||
|
);
|
||||||
|
updateNode(selectedDialogueId, sourceNode.id, { choices } as Partial<DialogueNode>);
|
||||||
|
}
|
||||||
|
}, [selectedDialogueId, file, updateNode]);
|
||||||
|
|
||||||
|
// Trigger auto-layout when dialogue is first loaded with no positions
|
||||||
|
useEffect(() => {
|
||||||
|
if (dialogue && dialogue.nodes.length > 0 && !Object.keys(dialoguePositions).length && selectedDialogueId) {
|
||||||
|
applyAutoLayout(selectedDialogueId);
|
||||||
|
}
|
||||||
|
}, [selectedDialogueId]);
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
<div className={styles.emptyMsg}>Load a dialogue JSON file to get started</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dialogue) {
|
||||||
|
return (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
<div className={styles.emptyMsg}>Select a dialogue from the left panel</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<GraphToolbar />
|
||||||
|
<div className={styles.flow}>
|
||||||
|
<ReactFlow
|
||||||
|
key={selectedDialogueId}
|
||||||
|
nodes={rfNodes}
|
||||||
|
edges={rfEdges}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
|
onPaneClick={onPaneClick}
|
||||||
|
fitView
|
||||||
|
fitViewOptions={{ padding: 0.2 }}
|
||||||
|
deleteKeyCode={null}
|
||||||
|
proOptions={{ hideAttribution: true }}
|
||||||
|
>
|
||||||
|
<Background color="#313244" gap={20} />
|
||||||
|
<Controls />
|
||||||
|
<MiniMap
|
||||||
|
nodeColor={(n) => {
|
||||||
|
switch (n.type) {
|
||||||
|
case 'Line': return '#1e66f5';
|
||||||
|
case 'Choice': return '#df8e1d';
|
||||||
|
case 'Condition': return '#8839ef';
|
||||||
|
case 'SetFlag': return '#179299';
|
||||||
|
case 'CutsceneStart': return '#4a4a5a';
|
||||||
|
case 'End': return '#f38ba8';
|
||||||
|
default: return '#6c7086';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ background: '#181825', border: '1px solid #313244' }}
|
||||||
|
/>
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
101
dialogEditor/src/components/GraphPanel/GraphToolbar.module.css
Normal file
101
dialogEditor/src/components/GraphPanel/GraphToolbar.module.css
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #181825;
|
||||||
|
border-bottom: 1px solid #313244;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6c7086;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: #313244;
|
||||||
|
color: #cdd6f4;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #45475a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPlay {
|
||||||
|
background: #40a02b;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPlay:hover {
|
||||||
|
background: #37872b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
width: 1px;
|
||||||
|
height: 20px;
|
||||||
|
background: #313244;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #cdd6f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileBanner {
|
||||||
|
background: #2a1f00;
|
||||||
|
color: #f9e2af;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #f9e2af44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flagPill {
|
||||||
|
background: rgba(137, 180, 250, 0.15);
|
||||||
|
color: #89b4fa;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnReset {
|
||||||
|
background: #45475a;
|
||||||
|
color: #f9e2af;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnReset:hover {
|
||||||
|
background: #585b70;
|
||||||
|
}
|
||||||
104
dialogEditor/src/components/GraphPanel/GraphToolbar.tsx
Normal file
104
dialogEditor/src/components/GraphPanel/GraphToolbar.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { useDialogueStore } from '../../store/dialogueStore';
|
||||||
|
import { makeNodeId } from '../../utils/idGen';
|
||||||
|
import { DialogueNode, NodeType } from '../../types/dialogue';
|
||||||
|
import { MAIN_CHARACTER } from '../../constants/characters';
|
||||||
|
import styles from './GraphToolbar.module.css';
|
||||||
|
|
||||||
|
const NODE_DEFAULTS: Record<string, () => Omit<DialogueNode, 'id'>> = {
|
||||||
|
Line: () => ({
|
||||||
|
type: 'Line',
|
||||||
|
speaker: MAIN_CHARACTER,
|
||||||
|
portrait: 'resources/dialogue/portrait_hero_neutral.png',
|
||||||
|
text: '',
|
||||||
|
next: '',
|
||||||
|
}),
|
||||||
|
Choice: () => ({
|
||||||
|
type: 'Choice',
|
||||||
|
speaker: MAIN_CHARACTER,
|
||||||
|
portrait: 'resources/dialogue/portrait_hero_neutral.png',
|
||||||
|
text: '',
|
||||||
|
choices: [],
|
||||||
|
}),
|
||||||
|
Condition: () => ({
|
||||||
|
type: 'Condition',
|
||||||
|
conditions: [],
|
||||||
|
trueNext: '',
|
||||||
|
falseNext: '',
|
||||||
|
}),
|
||||||
|
SetFlag: () => ({
|
||||||
|
type: 'SetFlag',
|
||||||
|
effects: [],
|
||||||
|
next: '',
|
||||||
|
}),
|
||||||
|
CutsceneStart: () => ({
|
||||||
|
type: 'CutsceneStart',
|
||||||
|
cutsceneId: '',
|
||||||
|
next: '',
|
||||||
|
}),
|
||||||
|
End: () => ({ type: 'End' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GraphToolbar() {
|
||||||
|
const { file, selectedDialogueId, selectedNodeId, addNode, applyAutoLayout, startPlay, setDialogueMobileMode, persistentFlags, resetFlags } = useDialogueStore();
|
||||||
|
|
||||||
|
const dialogue = file?.dialogues.find(d => d.id === selectedDialogueId);
|
||||||
|
if (!dialogue) return null;
|
||||||
|
|
||||||
|
function handleAddNode(type: NodeType) {
|
||||||
|
if (!selectedDialogueId || !dialogue) return;
|
||||||
|
const existingIds = new Set(dialogue.nodes.map(n => n.id));
|
||||||
|
const id = makeNodeId(type, existingIds);
|
||||||
|
const node = { id, ...NODE_DEFAULTS[type]() } as DialogueNode;
|
||||||
|
addNode(selectedDialogueId, node, selectedNodeId ?? undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.toolbar}>
|
||||||
|
<div className={styles.group}>
|
||||||
|
<span className={styles.label}>Add:</span>
|
||||||
|
{(['Line', 'Choice', 'Condition', 'SetFlag', 'CutsceneStart', 'End'] as NodeType[]).map(type => (
|
||||||
|
<button key={type} className={styles.btn} onClick={() => handleAddNode(type)}>
|
||||||
|
{type}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={styles.separator} />
|
||||||
|
<div className={styles.group}>
|
||||||
|
<button className={styles.btn} onClick={() => applyAutoLayout(selectedDialogueId!)}>
|
||||||
|
⬡ Auto Layout
|
||||||
|
</button>
|
||||||
|
<button className={[styles.btn, styles.btnPlay].join(' ')} onClick={startPlay}>
|
||||||
|
▶ Play
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.separator} />
|
||||||
|
<div className={styles.group}>
|
||||||
|
<label className={styles.toggle}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!dialogue.mobileMode}
|
||||||
|
onChange={e => setDialogueMobileMode(selectedDialogueId!, e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>📱 Mobile</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{dialogue.mobileMode && (
|
||||||
|
<div className={styles.mobileBanner}>
|
||||||
|
Mobile mode — portraits will be overridden on export
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{Object.keys(persistentFlags).length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className={styles.separator} />
|
||||||
|
<div className={styles.group}>
|
||||||
|
<span className={styles.label}>🚩 Flags:</span>
|
||||||
|
{Object.entries(persistentFlags).map(([k, v]) => (
|
||||||
|
<span key={k} className={styles.flagPill}>{k}={v}</span>
|
||||||
|
))}
|
||||||
|
<button className={styles.btnReset} onClick={resetFlags}>Reset</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
185
dialogEditor/src/components/LeftPanel/LeftPanel.module.css
Normal file
185
dialogEditor/src/components/LeftPanel/LeftPanel.module.css
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
.panel {
|
||||||
|
width: 220px;
|
||||||
|
min-width: 220px;
|
||||||
|
background: #181825;
|
||||||
|
border-right: 1px solid #313244;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 12px 12px 6px;
|
||||||
|
border-bottom: 1px solid #313244;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #cdd6f4;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileName {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #6c7086;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileButtons {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid #313244;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
background: #313244;
|
||||||
|
color: #cdd6f4;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #45475a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSave {
|
||||||
|
background: #1e66f5;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSave:hover {
|
||||||
|
background: #2779e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnNew {
|
||||||
|
background: #40a02b;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnNew:hover {
|
||||||
|
background: #37872b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorMsg {
|
||||||
|
background: #2a0e14;
|
||||||
|
color: #f38ba8;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-top: 1px solid #f38ba8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyMsg {
|
||||||
|
padding: 16px 12px;
|
||||||
|
color: #6c7086;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogueItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 1px 4px;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogueItem:hover {
|
||||||
|
background: #313244;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogueItem.active {
|
||||||
|
background: #1e3a5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogueId {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: monospace;
|
||||||
|
color: #cdd6f4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errDot { color: #f38ba8; font-size: 9px; }
|
||||||
|
.warnDot { color: #f9e2af; font-size: 9px; }
|
||||||
|
.mobileDot { font-size: 10px; }
|
||||||
|
|
||||||
|
.copyBtn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #6c7086;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyBtn:hover {
|
||||||
|
color: #89b4fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteBtn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #6c7086;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteBtn:hover {
|
||||||
|
color: #f38ba8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 8px;
|
||||||
|
border-top: 1px solid #313244;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createForm {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createInput {
|
||||||
|
flex: 1;
|
||||||
|
background: #313244;
|
||||||
|
border: 1px solid #45475a;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #cdd6f4;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
min-width: 0;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.createInput:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #89b4fa;
|
||||||
|
}
|
||||||
156
dialogEditor/src/components/LeftPanel/LeftPanel.tsx
Normal file
156
dialogEditor/src/components/LeftPanel/LeftPanel.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { useDialogueStore } from '../../store/dialogueStore';
|
||||||
|
import { parseDialogueFile } from '../../utils/fileIO';
|
||||||
|
import { useValidation } from '../../hooks/useValidation';
|
||||||
|
import styles from './LeftPanel.module.css';
|
||||||
|
|
||||||
|
export function LeftPanel() {
|
||||||
|
const { file, fileName, selectedDialogueId, loadFile, selectDialogue, createDialogue, deleteDialogue, duplicateDialogue, exportFile } = useDialogueStore();
|
||||||
|
const { issuesByNodeId } = useValidation();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [newId, setNewId] = useState('');
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const f = e.target.files?.[0];
|
||||||
|
if (!f) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (ev) => {
|
||||||
|
try {
|
||||||
|
const parsed = parseDialogueFile(ev.target?.result as string);
|
||||||
|
loadFile(parsed, f.name);
|
||||||
|
setError('');
|
||||||
|
} catch (err) {
|
||||||
|
setError(String(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(f);
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreate() {
|
||||||
|
if (!newId.trim()) return;
|
||||||
|
if (file?.dialogues.some(d => d.id === newId.trim())) {
|
||||||
|
setError(`ID "${newId.trim()}" already exists`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createDialogue(newId.trim());
|
||||||
|
setNewId('');
|
||||||
|
setCreating(false);
|
||||||
|
setError('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function dialogueHasIssues(dialogueId: string) {
|
||||||
|
// Simple check: are there any issues for nodes in this dialogue?
|
||||||
|
// We only have current dialogue issues so we check if this is selected
|
||||||
|
return selectedDialogueId === dialogueId && Object.keys(issuesByNodeId).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dialogueHasErrors(dialogueId: string) {
|
||||||
|
return selectedDialogueId === dialogueId &&
|
||||||
|
Object.values(issuesByNodeId).some(list => list.some(i => i.severity === 'error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.panel}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<span className={styles.title}>Dialogues</span>
|
||||||
|
{file && <span className={styles.fileName}>{fileName}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.fileButtons}>
|
||||||
|
<button className={styles.btn} onClick={() => fileInputRef.current?.click()}>
|
||||||
|
📂 Load
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
{file && (
|
||||||
|
<button className={[styles.btn, styles.btnSave].join(' ')} onClick={exportFile}>
|
||||||
|
💾 Save
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className={styles.errorMsg}>{error}</div>}
|
||||||
|
|
||||||
|
<div className={styles.list}>
|
||||||
|
{!file && (
|
||||||
|
<div className={styles.emptyMsg}>Load a JSON file to start</div>
|
||||||
|
)}
|
||||||
|
{file?.dialogues.map(d => {
|
||||||
|
const hasErr = dialogueHasErrors(d.id);
|
||||||
|
const hasWarn = !hasErr && dialogueHasIssues(d.id);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={d.id}
|
||||||
|
className={[
|
||||||
|
styles.dialogueItem,
|
||||||
|
d.id === selectedDialogueId ? styles.active : '',
|
||||||
|
].join(' ')}
|
||||||
|
onClick={() => selectDialogue(d.id)}
|
||||||
|
>
|
||||||
|
<span className={styles.dialogueId}>
|
||||||
|
{hasErr && <span className={styles.errDot} title="Has errors">●</span>}
|
||||||
|
{hasWarn && <span className={styles.warnDot} title="Has warnings">●</span>}
|
||||||
|
{d.mobileMode && <span className={styles.mobileDot} title="Mobile mode">📱</span>}
|
||||||
|
{d.id}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className={styles.copyBtn}
|
||||||
|
title="Duplicate dialogue"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
duplicateDialogue(d.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⧉
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.deleteBtn}
|
||||||
|
title="Delete dialogue"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (confirm(`Delete "${d.id}"?`)) deleteDialogue(d.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footer}>
|
||||||
|
{creating ? (
|
||||||
|
<div className={styles.createForm}>
|
||||||
|
<input
|
||||||
|
className={styles.createInput}
|
||||||
|
value={newId}
|
||||||
|
onChange={e => setNewId(e.target.value)}
|
||||||
|
placeholder="dialogue_id"
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter') handleCreate();
|
||||||
|
if (e.key === 'Escape') { setCreating(false); setNewId(''); }
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button className={[styles.btn, styles.btnSave].join(' ')} onClick={handleCreate}>✓</button>
|
||||||
|
<button className={styles.btn} onClick={() => { setCreating(false); setNewId(''); }}>✗</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
file && (
|
||||||
|
<button className={[styles.btn, styles.btnNew].join(' ')} onClick={() => setCreating(true)}>
|
||||||
|
+ New Dialogue
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user