mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-20 17:54:52 +02:00
Compare commits
1076 Commits
improve-ba
...
babylon-pu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac06c1700a | ||
|
|
2d0fb674d5 | ||
|
|
61b8b0aac3 | ||
|
|
bd6e443cec | ||
|
|
897199a807 | ||
|
|
1f64579516 | ||
|
|
84203630a1 | ||
|
|
561e0c3813 | ||
|
|
169d655717 | ||
|
|
ade652dd3e | ||
|
|
138353c84d | ||
|
|
4b30bfeb3c | ||
|
|
aae2039df2 | ||
|
|
850e156687 | ||
|
|
844196cfea | ||
|
|
1925840f3f | ||
|
|
d5df006463 | ||
|
|
2593652510 | ||
|
|
2e22b34c40 | ||
|
|
8d3c3284b5 | ||
|
|
7d7a125299 | ||
|
|
aac51a171c | ||
|
|
908e0022e9 | ||
|
|
b56a2a8bf7 | ||
|
|
61de0fb279 | ||
|
|
b43ff3d820 | ||
|
|
23b2f4c7a0 | ||
|
|
827895b548 | ||
|
|
8b0dd1ea68 | ||
|
|
ec3517ba58 | ||
|
|
c24515044e | ||
|
|
f55adc19d3 | ||
|
|
32af098cd9 | ||
|
|
cfea88dce3 | ||
|
|
3a97e26f80 | ||
|
|
7bcb1b1732 | ||
|
|
8684cf14cc | ||
|
|
686a847697 | ||
|
|
0294ef74e4 | ||
|
|
42af31fbbd | ||
|
|
b9ea42531a | ||
|
|
39c4618a35 | ||
|
|
b025ce0ae6 | ||
|
|
07e660df01 | ||
|
|
e07eed4a4b | ||
|
|
19ad95cfa6 | ||
|
|
8f5f595073 | ||
|
|
6a536d5c92 | ||
|
|
c1e6e6a871 | ||
|
|
039bce590f | ||
|
|
24dac68475 | ||
|
|
8217a3a42c | ||
|
|
dbcb856638 | ||
|
|
dbbb197816 | ||
|
|
40263b0af5 | ||
|
|
b978df8990 | ||
|
|
3a0da02b50 | ||
|
|
af7d1a763a | ||
|
|
a7e16c09dd | ||
|
|
351872f47b | ||
|
|
e3c6c3025a | ||
|
|
273bc59cdb | ||
|
|
7a3e27d653 | ||
|
|
6a8aacfb95 | ||
|
|
99659a89b4 | ||
|
|
4591b940c3 | ||
|
|
f01e9aa2ac | ||
|
|
bba1605801 | ||
|
|
6d6dbb2584 | ||
|
|
51a08bbf34 | ||
|
|
7fa8bcc9fd | ||
|
|
9d5818f7b5 | ||
|
|
bb028fda00 | ||
|
|
56ae76e994 | ||
|
|
69bc12201e | ||
|
|
313e730fbb | ||
|
|
c14986b43d | ||
|
|
69c75fc464 | ||
|
|
1b13c945ff | ||
|
|
6e763d4daa | ||
|
|
d1e5b04a10 | ||
|
|
7fec78b70e | ||
|
|
c962cd33b3 | ||
|
|
814bf357ca | ||
|
|
222d914544 | ||
|
|
b81b9e1da7 | ||
|
|
52cb23da63 | ||
|
|
6b4c2c9141 | ||
|
|
7af9069962 | ||
|
|
8f9be8c735 | ||
|
|
cdbf5c843f | ||
|
|
27fff648b6 | ||
|
|
5cdc5f53b4 | ||
|
|
c67e231ff5 | ||
|
|
f564b7a29b | ||
|
|
1eea31aacd | ||
|
|
5da458ca54 | ||
|
|
c343ccbdff | ||
|
|
f8a981a1fc | ||
|
|
72df3d29ce | ||
|
|
75454b8075 | ||
|
|
559846fd7c | ||
|
|
8c1fa69800 | ||
|
|
810d437a2c | ||
|
|
3fca2cb664 | ||
|
|
e95552b264 | ||
|
|
781f6135fd | ||
|
|
e90830c738 | ||
|
|
ab32ab6746 | ||
|
|
86c4f3072f | ||
|
|
8bb8b2fc97 | ||
|
|
fad53328be | ||
|
|
0a6bfce548 | ||
|
|
5998b01ffc | ||
|
|
9a91170839 | ||
|
|
a0aa64cd9f | ||
|
|
95864071a0 | ||
|
|
ff9d997cec | ||
|
|
c6d0855a2e | ||
|
|
624b858758 | ||
|
|
d722b8e9d7 | ||
|
|
fd02ea906f | ||
|
|
aed9a4a1b4 | ||
|
|
ace51f7605 | ||
|
|
c10e1c4b31 | ||
|
|
4c80e92522 | ||
|
|
71c3f921cc | ||
|
|
6bc9eb9d84 | ||
|
|
fbf3c02392 | ||
|
|
0c8b1055d4 | ||
|
|
0e7b517554 | ||
|
|
5cefdd224b | ||
|
|
1a5a4c834f | ||
|
|
2df145c458 | ||
|
|
43958fc70d | ||
|
|
8f94b04edc | ||
|
|
c7d07d0364 | ||
|
|
5d974e8242 | ||
|
|
807ade4c37 | ||
|
|
2efd52f400 | ||
|
|
f0375aa420 | ||
|
|
04c2703ace | ||
|
|
65022ed342 | ||
|
|
0d025d99f2 | ||
|
|
e68847e563 | ||
|
|
409e9f744c | ||
|
|
9f81a88466 | ||
|
|
564d2446cb | ||
|
|
cb605a3978 | ||
|
|
4f89010aca | ||
|
|
8933ac119a | ||
|
|
982897f1b5 | ||
|
|
dd69662603 | ||
|
|
9485b3fba9 | ||
|
|
245f673a78 | ||
|
|
71bb388b8b | ||
|
|
39f2614f9a | ||
|
|
46efad5b12 | ||
|
|
fda845cb63 | ||
|
|
cccc66bbbf | ||
|
|
32822d6d07 | ||
|
|
7bca3ca179 | ||
|
|
6961f277b5 | ||
|
|
eb33c8a93f | ||
|
|
43232d5baa | ||
|
|
b8f67a3e69 | ||
|
|
748b571856 | ||
|
|
f0bf3cda75 | ||
|
|
bf28b303d7 | ||
|
|
1a064ea430 | ||
|
|
68431f62f1 | ||
|
|
2c8abf8239 | ||
|
|
822b567742 | ||
|
|
a89475e820 | ||
|
|
2d8c6ca585 | ||
|
|
fc6a840132 | ||
|
|
9a44c69177 | ||
|
|
0be946a10b | ||
|
|
a201f72c8a | ||
|
|
83a4281a30 | ||
|
|
7546bf7dc2 | ||
|
|
2942234778 | ||
|
|
540e20864c | ||
|
|
6ee6894184 | ||
|
|
2a880164f8 | ||
|
|
c002752d65 | ||
|
|
05e1a43a1c | ||
|
|
ec79d6bee7 | ||
|
|
a3b6dc0899 | ||
|
|
2fffa94eb2 | ||
|
|
0fade25574 | ||
|
|
aa4fa9c54e | ||
|
|
fdba915288 | ||
|
|
773077470e | ||
|
|
3eafdbe57a | ||
|
|
8138fd2c34 | ||
|
|
ce72d10491 | ||
|
|
f8851c0f2e | ||
|
|
d3a7dc0ea5 | ||
|
|
32bdb77e97 | ||
|
|
8ac3c77ae1 | ||
|
|
8611d55ffc | ||
|
|
14b648faa6 | ||
|
|
c6a755c0a7 | ||
|
|
1154bb5370 | ||
|
|
1701e993cf | ||
|
|
99f998cf60 | ||
|
|
05a8dcf897 | ||
|
|
90ea7902eb | ||
|
|
644c16aee6 | ||
|
|
ce5857b265 | ||
|
|
fbae160442 | ||
|
|
2daad2223d | ||
|
|
6cbae336d0 | ||
|
|
47f9f38a40 | ||
|
|
73d6764a0b | ||
|
|
c85ee681bb | ||
|
|
61d5a5620a | ||
|
|
2873768cd2 | ||
|
|
4c957ecd9b | ||
|
|
8c61fc2be0 | ||
|
|
57057f56f5 | ||
|
|
985dd72a7f | ||
|
|
dfbe765baa | ||
|
|
555fa80709 | ||
|
|
7546e7c800 | ||
|
|
ce36de7e4f | ||
|
|
1cb96731e6 | ||
|
|
4bf0ba3716 | ||
|
|
d2e48b707b | ||
|
|
8e70550c84 | ||
|
|
199c0f533b | ||
|
|
ad44cc3446 | ||
|
|
8f73c22df3 | ||
|
|
20eb342eff | ||
|
|
5d17b557c1 | ||
|
|
43cb18b3e9 | ||
|
|
75dfcf48b6 | ||
|
|
9e9ae54c26 | ||
|
|
e78694f11b | ||
|
|
5397beb965 | ||
|
|
92bc50cb90 | ||
|
|
45d8b656cb | ||
|
|
a5e0c594b2 | ||
|
|
646b0ca041 | ||
|
|
de27cc92dc | ||
|
|
09d5502eb6 | ||
|
|
dc6ac2529f | ||
|
|
a766609963 | ||
|
|
e1aa62d2b7 | ||
|
|
c62d92d88f | ||
|
|
1a456998bc | ||
|
|
14fe51c0a4 | ||
|
|
f203ebe4d5 | ||
|
|
de979d3a98 | ||
|
|
7c2e3adf25 | ||
|
|
e24cf73a83 | ||
|
|
d3eb3867bd | ||
|
|
fcfd3331dd | ||
|
|
5aa1f0d562 | ||
|
|
491510a11e | ||
|
|
e0382cdf86 | ||
|
|
9ae6a9f426 | ||
|
|
a8ca0cf9ed | ||
|
|
45fc8994f0 | ||
|
|
85ae7c7efc | ||
|
|
2867f03f50 | ||
|
|
1cd75fe5a8 | ||
|
|
7f2286f1fd | ||
|
|
840e2c3744 | ||
|
|
af1dbd3a71 | ||
|
|
82cdaeb56e | ||
|
|
ea732458ab | ||
|
|
b6e92cdef9 | ||
|
|
11b699a180 | ||
|
|
e90a53a851 | ||
|
|
1469f890dd | ||
|
|
6c82e0fef8 | ||
|
|
6ee52259c4 | ||
|
|
414b522d0a | ||
|
|
a95f3c9467 | ||
|
|
ecb2ae379e | ||
|
|
2e34e6f454 | ||
|
|
bee415625a | ||
|
|
d0d3aef76c | ||
|
|
93a46ae2d6 | ||
|
|
8b5d006248 | ||
|
|
bcc965a19c | ||
|
|
2c5ce5bfae | ||
|
|
170980a0a6 | ||
|
|
216fb56a4b | ||
|
|
8defcd9f9f | ||
|
|
d38f04c97f | ||
|
|
ecedd71192 | ||
|
|
b582895ac6 | ||
|
|
267dc5fb64 | ||
|
|
ab02a99a0e | ||
|
|
03481322a9 | ||
|
|
fb130466ae | ||
|
|
43f20e44c3 | ||
|
|
2966d2a862 | ||
|
|
8f23991723 | ||
|
|
bfdda83a6d | ||
|
|
ddce5ff526 | ||
|
|
8dd62e57e2 | ||
|
|
0b17663eeb | ||
|
|
41c5e7242d | ||
|
|
ef8f593de8 | ||
|
|
410cc8ac50 | ||
|
|
a905eeef03 | ||
|
|
ebb74eff92 | ||
|
|
b69efeeb79 | ||
|
|
1a57b8177b | ||
|
|
0e300a9795 | ||
|
|
13d35f8124 | ||
|
|
b53e4f6742 | ||
|
|
9d102c2a70 | ||
|
|
84f389aebd | ||
|
|
8072dbef8b | ||
|
|
8195f5257f | ||
|
|
b8f8ac031c | ||
|
|
57f1adb402 | ||
|
|
48ce2e09ab | ||
|
|
dca5450340 | ||
|
|
202c9e8f25 | ||
|
|
8e1e69e60e | ||
|
|
f3d0edf546 | ||
|
|
08dfd23c19 | ||
|
|
b231357ae8 | ||
|
|
102cf03213 | ||
|
|
d1da15128e | ||
|
|
0d96f418ab | ||
|
|
b879f2f1e3 | ||
|
|
fb77eb4349 | ||
|
|
79ad7e274c | ||
|
|
4890673013 | ||
|
|
1f204f572f | ||
|
|
9976588025 | ||
|
|
414a28fb19 | ||
|
|
3605ffdafc | ||
|
|
179c9fc70a | ||
|
|
f3a7f10319 | ||
|
|
17333fd7e5 | ||
|
|
14f4d2c228 | ||
|
|
18b4210eef | ||
|
|
54c2c4dd53 | ||
|
|
42da479026 | ||
|
|
adc487ef78 | ||
|
|
b08b3a2500 | ||
|
|
4324b6def2 | ||
|
|
21fe0f5e67 | ||
|
|
3ede04c563 | ||
|
|
b750d69065 | ||
|
|
0c4b36e2d1 | ||
|
|
5cb9474494 | ||
|
|
3be075d281 | ||
|
|
d7c94fbf86 | ||
|
|
c6d7aa7be8 | ||
|
|
d56c6dfe57 | ||
|
|
f9be5d8c47 | ||
|
|
ac023668a7 | ||
|
|
59c9b86842 | ||
|
|
c4adcde114 | ||
|
|
db1b5e9ce9 | ||
|
|
a1cb4b8304 | ||
|
|
fd04c5f2fc | ||
|
|
7be7465703 | ||
|
|
626ae675bc | ||
|
|
2a1cd5c197 | ||
|
|
a9dd5fd5bf | ||
|
|
beb5d1dec5 | ||
|
|
d451ce8c36 | ||
|
|
f0f78a11cb | ||
|
|
69dd2675fa | ||
|
|
eb7691e3ba | ||
|
|
db90e4ebc0 | ||
|
|
29491997ea | ||
|
|
015e6d1c81 | ||
|
|
49ee15dd9a | ||
|
|
e6b5758d54 | ||
|
|
210368d597 | ||
|
|
203f29afb9 | ||
|
|
fa0eac34c2 | ||
|
|
d4fa5cf7ca | ||
|
|
479e9af17e | ||
|
|
f03af71dc0 | ||
|
|
6d94f00ecf | ||
|
|
9e848f3135 | ||
|
|
b16b158372 | ||
|
|
39525c66c2 | ||
|
|
d3dc9bc86c | ||
|
|
444d862eac | ||
|
|
74e9851511 | ||
|
|
a8586fe224 | ||
|
|
d132fdfc04 | ||
|
|
3ba902c2b6 | ||
|
|
50f7c74259 | ||
|
|
9b7c908c68 | ||
|
|
e7290c0486 | ||
|
|
db22eddd1e | ||
|
|
a8cea0622d | ||
|
|
5bf1b5569f | ||
|
|
d1cb2c5bc7 | ||
|
|
a49697042a | ||
|
|
4656d93358 | ||
|
|
2e5a02a85a | ||
|
|
8421ec75da | ||
|
|
dcd2160294 | ||
|
|
3833469955 | ||
|
|
0adfbc8d51 | ||
|
|
a931079896 | ||
|
|
18c08f52f1 | ||
|
|
8975449538 | ||
|
|
4e149a642d | ||
|
|
09d133242d | ||
|
|
a8db20259b | ||
|
|
d1eda166de | ||
|
|
7df4b729e9 | ||
|
|
3ed6148f6a | ||
|
|
ebe5739ce3 | ||
|
|
9bc404a8f5 | ||
|
|
90ff3d79d1 | ||
|
|
6fb49ab88d | ||
|
|
722d09b1ae | ||
|
|
6d6ae6728c | ||
|
|
ce2e74f3ca | ||
|
|
b34e957c25 | ||
|
|
157b4673fd | ||
|
|
ac1a19e95c | ||
|
|
5639324077 | ||
|
|
a471fe16fa | ||
|
|
6cf90fd714 | ||
|
|
c7c785ad2a | ||
|
|
bde64b5b1f | ||
|
|
30f7727e33 | ||
|
|
5d36da17fe | ||
|
|
7bcda08339 | ||
|
|
a15575078f | ||
|
|
56d813a184 | ||
|
|
8f5c09daa1 | ||
|
|
c2d5a33400 | ||
|
|
6f3f4e7ef1 | ||
|
|
49f21d7423 | ||
|
|
92a6086e21 | ||
|
|
9d1c2d52d2 | ||
|
|
f97a6c6d55 | ||
|
|
0f69a284c6 | ||
|
|
1427d887dd | ||
|
|
a4c9aff8a9 | ||
|
|
5db8ccec74 | ||
|
|
70ebc0d32c | ||
|
|
d885627350 | ||
|
|
4a77db7866 | ||
|
|
e50d4fa8ab | ||
|
|
27578f2688 | ||
|
|
fc97ba41af | ||
|
|
a3610ae6c4 | ||
|
|
fc615daad3 | ||
|
|
0321edb1ac | ||
|
|
8593737886 | ||
|
|
35ad1d758e | ||
|
|
110e5daa6f | ||
|
|
5de191f01a | ||
|
|
80c2b1fa65 | ||
|
|
c8441da835 | ||
|
|
fa2b1d6096 | ||
|
|
18a4da4ad7 | ||
|
|
52e9395fab | ||
|
|
d05d7938a4 | ||
|
|
7846e8efb8 | ||
|
|
e09f832fad | ||
|
|
515f6d9790 | ||
|
|
657159da45 | ||
|
|
e88188cd6d | ||
|
|
dcb834ed41 | ||
|
|
5a7960d0a9 | ||
|
|
11e55d8fe8 | ||
|
|
836de1bb28 | ||
|
|
58e617af6d | ||
|
|
f44d566933 | ||
|
|
72fbc4bc9c | ||
|
|
746c16aecc | ||
|
|
7bef2cd8e0 | ||
|
|
aed73eb074 | ||
|
|
a756ca6ffb | ||
|
|
f19040888a | ||
|
|
634cdf5e1e | ||
|
|
c09d445215 | ||
|
|
1d71c0c6dd | ||
|
|
130a43f39a | ||
|
|
9c25c44a8a | ||
|
|
76ce6c84c0 | ||
|
|
24caff71e1 | ||
|
|
036b8ea320 | ||
|
|
f98394fc60 | ||
|
|
f52ac6351d | ||
|
|
eb1357026d | ||
|
|
1b5be37f9b | ||
|
|
42c659c580 | ||
|
|
ec32cad19f | ||
|
|
0910c47612 | ||
|
|
88e7303779 | ||
|
|
3a5532211b | ||
|
|
5b945278f9 | ||
|
|
0db2e5a42f | ||
|
|
8e1c5673b8 | ||
|
|
a763c396bd | ||
|
|
f5dae1d4c8 | ||
|
|
17697ba6ec | ||
|
|
a77987ab28 | ||
|
|
47d9e92776 | ||
|
|
af928ffe93 | ||
|
|
4adca586ed | ||
|
|
e703705d60 | ||
|
|
be434949a4 | ||
|
|
bbff43e9e6 | ||
|
|
5c28ee0536 | ||
|
|
7e41d17c6a | ||
|
|
1cd6d01fdd | ||
|
|
3263f4bcc0 | ||
|
|
58feedb53d | ||
|
|
e62c85a971 | ||
|
|
585d727297 | ||
|
|
abd6c85b41 | ||
|
|
9551a3d01a | ||
|
|
d281a81200 | ||
|
|
82741c2d61 | ||
|
|
b00880c21f | ||
|
|
df5d5d23cc | ||
|
|
35f6cac9f6 | ||
|
|
7ddcbf5e94 | ||
|
|
634bae3c49 | ||
|
|
0b50aa9d13 | ||
|
|
e594ad9c6f | ||
|
|
d105c707ec | ||
|
|
93f24c5b8f | ||
|
|
7c170a21e5 | ||
|
|
1de4440dbd | ||
|
|
e6ce36178c | ||
|
|
8a6e925297 | ||
|
|
eeda7e7002 | ||
|
|
abfa67965e | ||
|
|
2c6560cc71 | ||
|
|
4984146f6e | ||
|
|
c36dfc6643 | ||
|
|
9184f0d7b9 | ||
|
|
4c659c3129 | ||
|
|
10926e5525 | ||
|
|
8d3a5a6503 | ||
|
|
924c517bb5 | ||
|
|
54c339d89c | ||
|
|
ce98d4244b | ||
|
|
4e4b56699b | ||
|
|
fab7667b0a | ||
|
|
942e32f4de | ||
|
|
3897b044fe | ||
|
|
c47a0c33cf | ||
|
|
2b849685a1 | ||
|
|
bad29c7604 | ||
|
|
b7b24a2140 | ||
|
|
20d9a03bb3 | ||
|
|
09d58a3ecf | ||
|
|
94e8050455 | ||
|
|
2f039ce2e9 | ||
|
|
61fd35bc97 | ||
|
|
2d36ccf1b2 | ||
|
|
174221fdc4 | ||
|
|
08a0f03a45 | ||
|
|
de795a48e8 | ||
|
|
af8a0bdf12 | ||
|
|
b734ab3419 | ||
|
|
595e66c423 | ||
|
|
7b0e839661 | ||
|
|
6f79420fdc | ||
|
|
b41bad4188 | ||
|
|
2f34f1c6f1 | ||
|
|
7b80da7737 | ||
|
|
e65e3b4569 | ||
|
|
0ef489513a | ||
|
|
a92cae8e09 | ||
|
|
2619509d69 | ||
|
|
da8945dc23 | ||
|
|
64995bebc5 | ||
|
|
c2dde53c1c | ||
|
|
efaf7bdd95 | ||
|
|
3dcbfc0168 | ||
|
|
e96e88b1ce | ||
|
|
493a2eb50c | ||
|
|
fc4d769e1e | ||
|
|
0f0bc9b54f | ||
|
|
ead90471c4 | ||
|
|
461e083454 | ||
|
|
79fe0fbd05 | ||
|
|
db9d0090b7 | ||
|
|
1f81960640 | ||
|
|
5d389732d9 | ||
|
|
b300f9620c | ||
|
|
ddafd9e517 | ||
|
|
2b9f593e03 | ||
|
|
5f3f3d715a | ||
|
|
de62fa3b59 | ||
|
|
32cba5b979 | ||
|
|
ab90824b9c | ||
|
|
ebec026508 | ||
|
|
8ddb2cbe75 | ||
|
|
97439d7718 | ||
|
|
60a2cd9306 | ||
|
|
62be4a258e | ||
|
|
ae4a174de5 | ||
|
|
1e15503000 | ||
|
|
61ac82d297 | ||
|
|
f18b3467d9 | ||
|
|
dec440b6cc | ||
|
|
26ac4f7732 | ||
|
|
b7b3c07a96 | ||
|
|
2040827615 | ||
|
|
2b456fec47 | ||
|
|
b21ad59db3 | ||
|
|
f04799a4f5 | ||
|
|
28dec6b0a3 | ||
|
|
714bff0835 | ||
|
|
772608ae99 | ||
|
|
fe3b3704ba | ||
|
|
d6caef7ee7 | ||
|
|
83a15f74ef | ||
|
|
27addb49cf | ||
|
|
6aa741f8d4 | ||
|
|
e224bdf5e4 | ||
|
|
9fe2044f53 | ||
|
|
b1aef2d308 | ||
|
|
79a063f692 | ||
|
|
c3c36b06c2 | ||
|
|
358f0c0a6f | ||
|
|
6041db87e8 | ||
|
|
ff4af812b5 | ||
|
|
c2428ca3cc | ||
|
|
e402057d3b | ||
|
|
3811de2283 | ||
|
|
421d466921 | ||
|
|
a211d0df51 | ||
|
|
c0690c9b80 | ||
|
|
8b9164a8c3 | ||
|
|
5df01bae9a | ||
|
|
f8e093466d | ||
|
|
664ca528fe | ||
|
|
aaab1e7260 | ||
|
|
a85f05ca29 | ||
|
|
3253d30073 | ||
|
|
02d365e27b | ||
|
|
a4be9b2078 | ||
|
|
47cd092380 | ||
|
|
d1555d5423 | ||
|
|
acd9b94b49 | ||
|
|
4b135bccd8 | ||
|
|
109fdd2ff3 | ||
|
|
aa5275137e | ||
|
|
669286c1d8 | ||
|
|
623b4f087f | ||
|
|
02c6e1b876 | ||
|
|
6cca5706f7 | ||
|
|
ebdf627b19 | ||
|
|
ae463cde5e | ||
|
|
6350890e9f | ||
|
|
77fc803612 | ||
|
|
e87752a07c | ||
|
|
a7c8a3d6d1 | ||
|
|
2175a3a18f | ||
|
|
3d48146b92 | ||
|
|
4f9aded205 | ||
|
|
e634fb1456 | ||
|
|
2b0d0d4533 | ||
|
|
4ac9da7f1f | ||
|
|
08e0ecf99b | ||
|
|
007a2481ef | ||
|
|
61eea5799b | ||
|
|
1fe9117944 | ||
|
|
4a16a71fa2 | ||
|
|
85701fcb6d | ||
|
|
cd918817d9 | ||
|
|
24c0504cb0 | ||
|
|
750a48df62 | ||
|
|
ead79ab275 | ||
|
|
124079f80a | ||
|
|
ac2c6b93ce | ||
|
|
b0d4ab371b | ||
|
|
a3c3f7ba91 | ||
|
|
12e1b86b53 | ||
|
|
7fd9ac1cc8 | ||
|
|
da8a7abcde | ||
|
|
6275196101 | ||
|
|
7eb8723082 | ||
|
|
d730c26fcc | ||
|
|
56c2e1c989 | ||
|
|
77acce78dd | ||
|
|
db5f64b097 | ||
|
|
ddfd9f46f3 | ||
|
|
4254268f8d | ||
|
|
5104bafe95 | ||
|
|
c426f95bee | ||
|
|
c9ae842258 | ||
|
|
6a7e05d00a | ||
|
|
af5bdb4296 | ||
|
|
cf46a4af1e | ||
|
|
83daf43f49 | ||
|
|
3db0b8a1fd | ||
|
|
cf9349f29c | ||
|
|
7e0b5ff8be | ||
|
|
28030ea3fa | ||
|
|
357aeff407 | ||
|
|
2f80442b99 | ||
|
|
3bad686c71 | ||
|
|
240c055b45 | ||
|
|
02eb8cfe1c | ||
|
|
87e2a046b7 | ||
|
|
4ce42a02c1 | ||
|
|
f61b2504cc | ||
|
|
1b119c49a1 | ||
|
|
0ae3eb0721 | ||
|
|
205d2c3343 | ||
|
|
b6e269d140 | ||
|
|
b9335bc314 | ||
|
|
ae92f75345 | ||
|
|
2ff307fe42 | ||
|
|
60bdd30681 | ||
|
|
c877210828 | ||
|
|
e85d1c6139 | ||
|
|
2094e82a30 | ||
|
|
f86c5fb3b2 | ||
|
|
6b95d07930 | ||
|
|
886f64055d | ||
|
|
4740c76128 | ||
|
|
baad4ae929 | ||
|
|
e989c4b1a5 | ||
|
|
8f133d3fed | ||
|
|
f26ebef565 | ||
|
|
7f46bd4928 | ||
|
|
abe22da9ed | ||
|
|
ebc9a60237 | ||
|
|
272c267ea0 | ||
|
|
21e6ac6678 | ||
|
|
2685e254f1 | ||
|
|
7d5c2052eb | ||
|
|
e8d43032cc | ||
|
|
4ad5234325 | ||
|
|
6582087b2f | ||
|
|
dc689a8c22 | ||
|
|
fa8bdf55be | ||
|
|
87828dc0ad | ||
|
|
9fe161ec7c | ||
|
|
938dc5ce40 | ||
|
|
5049857e81 | ||
|
|
58cf1414fc | ||
|
|
6563eb9b8f | ||
|
|
69ac19d018 | ||
|
|
eeae06014a | ||
|
|
d808d20f8e | ||
|
|
d775fa6360 | ||
|
|
5f4914e6dc | ||
|
|
8427685f7b | ||
|
|
7dd05a3d60 | ||
|
|
55747bdb99 | ||
|
|
909c0ae156 | ||
|
|
4c0b78a1ac | ||
|
|
62a9795685 | ||
|
|
04ff23c44f | ||
|
|
ed119dfeb8 | ||
|
|
c341ad21db | ||
|
|
11119ab046 | ||
|
|
dc59fd4edb | ||
|
|
887f548985 | ||
|
|
a627b58e85 | ||
|
|
57fbebaea5 | ||
|
|
6386ebe18e | ||
|
|
60bf7c6b1f | ||
|
|
6fe0e97ac6 | ||
|
|
bf72fb0d9a | ||
|
|
85af12d35a | ||
|
|
7970fed6e1 | ||
|
|
03c0b48e1e | ||
|
|
45c851bb5b | ||
|
|
9b3424c5d3 | ||
|
|
9d9b47daca | ||
|
|
1d8f03e199 | ||
|
|
5e1c0d1064 | ||
|
|
a6b7150371 | ||
|
|
7701bca55b | ||
|
|
547da71743 | ||
|
|
25ed41ba64 | ||
|
|
5e4d128f68 | ||
|
|
8ec7da0cf4 | ||
|
|
970efb3440 | ||
|
|
58acbac7ef | ||
|
|
5736b43149 | ||
|
|
682c5417cb | ||
|
|
127600a5d4 | ||
|
|
61386f699e | ||
|
|
3283ec410a | ||
|
|
6fd8c8c908 | ||
|
|
5b6fa78748 | ||
|
|
64fd4b7c0a | ||
|
|
3356cf36d3 | ||
|
|
e2e4e83e6f | ||
|
|
8030e4f3fa | ||
|
|
a00d80e30c | ||
|
|
82373f24d9 | ||
|
|
fb0c089a16 | ||
|
|
c04c6502be | ||
|
|
6b4310c91d | ||
|
|
662de635aa | ||
|
|
af8d14a139 | ||
|
|
afd731797e | ||
|
|
d51d1191c5 | ||
|
|
f4a060b9a8 | ||
|
|
f6677aa02c | ||
|
|
0d966d8ded | ||
|
|
a54a8a10ad | ||
|
|
602c8d8be1 | ||
|
|
bdbbe83421 | ||
|
|
50575a272c | ||
|
|
3b80a96412 | ||
|
|
fa979f1fed | ||
|
|
38bf5fc0bf | ||
|
|
78911b24de | ||
|
|
83b5305671 | ||
|
|
07d57b0edd | ||
|
|
bd0e6f3268 | ||
|
|
0c808aa23d | ||
|
|
68eebb9d76 | ||
|
|
b318cff137 | ||
|
|
614e8a7254 | ||
|
|
93f62f6054 | ||
|
|
ca49618fdb | ||
|
|
fdd360f490 | ||
|
|
df5df25c80 | ||
|
|
c454b8922b | ||
|
|
ba4d495b42 | ||
|
|
4ed1d56f03 | ||
|
|
49823f9ec3 | ||
|
|
85dea8b49d | ||
|
|
f2be8a2169 | ||
|
|
7121cd1ea9 | ||
|
|
e79532e50a | ||
|
|
17f97bab7b | ||
|
|
f13ada97a7 | ||
|
|
0db754bdd6 | ||
|
|
74dfbb7a74 | ||
|
|
2a583509ab | ||
|
|
4813ea5afc | ||
|
|
65b6821d4f | ||
|
|
8a165321b7 | ||
|
|
b89fc36cd0 | ||
|
|
f5ebbbca50 | ||
|
|
81414a18d3 | ||
|
|
6bf4feaef5 | ||
|
|
4efe4fb519 | ||
|
|
3873eb0cd7 | ||
|
|
df092dd120 | ||
|
|
654c2c5b05 | ||
|
|
f367bc37f8 | ||
|
|
e8d8242c09 | ||
|
|
2ecaccedab | ||
|
|
4a7614f903 | ||
|
|
d3da77c307 | ||
|
|
1c610ce825 | ||
|
|
d5a5b04468 | ||
|
|
b91409f5c6 | ||
|
|
74c67b843e | ||
|
|
913e35442c | ||
|
|
7a939dc5b3 | ||
|
|
71179b8b24 | ||
|
|
c5a9c08814 | ||
|
|
ea8df304c9 | ||
|
|
a01bbf828d | ||
|
|
d2807e974c | ||
|
|
d6f41f9c51 | ||
|
|
b2f2e9e75d | ||
|
|
ee55a0a6cc | ||
|
|
df184c8fdf | ||
|
|
4dd3bfc208 | ||
|
|
c677dd6566 | ||
|
|
de53f475c5 | ||
|
|
b2eed6b82a | ||
|
|
6eefe6899c | ||
|
|
8619d0156e | ||
|
|
9bc0d7b361 | ||
|
|
9ffd8d777e | ||
|
|
c558f93a0e | ||
|
|
5458ee016d | ||
|
|
5886260e0b | ||
|
|
42c7a483a4 | ||
|
|
7f5858a66f | ||
|
|
4965429069 | ||
|
|
58ec8af8cd | ||
|
|
1af8584aca | ||
|
|
564098a631 | ||
|
|
6fceb31c44 | ||
|
|
2a1a27f8c7 | ||
|
|
0e5c8496ce | ||
|
|
7ff95b8f8a | ||
|
|
295c91c245 | ||
|
|
7322697707 | ||
|
|
ed4e3a51fd | ||
|
|
bdc34305be | ||
|
|
354504b534 | ||
|
|
f311105b54 | ||
|
|
2984b0000b | ||
|
|
98aadf8dcc | ||
|
|
1d3ddd279b | ||
|
|
349ee141bb | ||
|
|
9070159db7 | ||
|
|
0a4b81b0cc | ||
|
|
2de50f893f | ||
|
|
0157dd6b41 | ||
|
|
208c300460 | ||
|
|
46791a3bf2 | ||
|
|
16b54e9615 | ||
|
|
78b689f41c | ||
|
|
7b04d5d434 | ||
|
|
c22345e3e0 | ||
|
|
a90c179998 | ||
|
|
8eebeab692 | ||
|
|
66e0eeedfb | ||
|
|
3874f7abe9 | ||
|
|
98f74b0c7a | ||
|
|
dbffa5520c | ||
|
|
e336cbad62 | ||
|
|
2fab946b7a | ||
|
|
239df4694c | ||
|
|
1c9a324f3a | ||
|
|
dbdb7ec324 | ||
|
|
d43b3be6f0 | ||
|
|
34b46baaea | ||
|
|
dc8dda3aac | ||
|
|
1b2717e256 | ||
|
|
9d723aaaa6 | ||
|
|
78bfcd71af | ||
|
|
5175a1e193 | ||
|
|
2438447ad3 | ||
|
|
01385575fd | ||
|
|
3f0c4b0577 | ||
|
|
36438e85d1 | ||
|
|
545009078a | ||
|
|
5910ec68e3 | ||
|
|
3e7166bd2c | ||
|
|
dbfd1a751c | ||
|
|
dda26f7f48 | ||
|
|
54b30d1138 | ||
|
|
d8dc66781f | ||
|
|
4d37ada54d | ||
|
|
a8456a45ab | ||
|
|
08667b4d35 | ||
|
|
03f20814c9 | ||
|
|
2672ae4463 | ||
|
|
de4c1b3b66 | ||
|
|
ec82773ff7 | ||
|
|
cdb8d86fbf | ||
|
|
20dc48f221 | ||
|
|
0729e209c5 | ||
|
|
c5eaf0f7af | ||
|
|
dcae3ccaaa | ||
|
|
fcc36759f7 | ||
|
|
689c24c776 | ||
|
|
055121d698 | ||
|
|
402dd538bf | ||
|
|
8bdf773a2b | ||
|
|
460f79d5cf | ||
|
|
998f85b260 | ||
|
|
a0356d8d4d | ||
|
|
d68655f5c2 | ||
|
|
bba7076eca | ||
|
|
aae03a914d | ||
|
|
cdc9b47b78 | ||
|
|
41d40f53cf | ||
|
|
17a3bdb5eb | ||
|
|
dadc5295fa | ||
|
|
679c75006a | ||
|
|
cd9612e664 | ||
|
|
d01b3036d6 | ||
|
|
376bb328df | ||
|
|
6a08231591 | ||
|
|
411c4ef3ae | ||
|
|
6f32e09db5 | ||
|
|
86f6498ddd | ||
|
|
5619cbb0da | ||
|
|
9475e6151f | ||
|
|
af560802b3 | ||
|
|
3375220aee | ||
|
|
ce7af6a308 | ||
|
|
d446e00964 | ||
|
|
90fa65c96e | ||
|
|
3717962757 | ||
|
|
8bd2003a38 | ||
|
|
07909ab228 | ||
|
|
503a02ac42 | ||
|
|
8a0ba3a18a | ||
|
|
7b7767942f | ||
|
|
8c28c7c253 | ||
|
|
0d8a6e8136 | ||
|
|
6a4a09c8cf | ||
|
|
06adb3e045 | ||
|
|
c12f330432 | ||
|
|
a45611171a | ||
|
|
f58de15d45 | ||
|
|
aa6c9be133 | ||
|
|
2841f67166 | ||
|
|
b1bb07542a | ||
|
|
eb0544e083 | ||
|
|
d490891acc | ||
|
|
4da92509cb | ||
|
|
f85223c064 | ||
|
|
0397fccdb3 | ||
|
|
c93758b554 | ||
|
|
be67e75ef9 | ||
|
|
d8d4b230b0 | ||
|
|
0996c2d9b2 | ||
|
|
0bcc5a3695 | ||
|
|
e70743bf40 | ||
|
|
9ecb3d6a5a | ||
|
|
9f0fbb8531 | ||
|
|
52a1b30503 | ||
|
|
1dec481a7e | ||
|
|
ad48f43524 | ||
|
|
7efa04d561 | ||
|
|
bf7f771760 | ||
|
|
3acf6db835 | ||
|
|
aafcffd1ad | ||
|
|
82f68f1e93 | ||
|
|
3a02ae8b28 | ||
|
|
3022313fac | ||
|
|
22d5c27ca7 | ||
|
|
8665923337 | ||
|
|
efd101d0a0 | ||
|
|
36f17a156f | ||
|
|
2e84d2864c | ||
|
|
909f78b33c | ||
|
|
cb6c790d6c | ||
|
|
b790608f52 | ||
|
|
13e3bdc90b | ||
|
|
2a1a03ef9d | ||
|
|
fb25331661 | ||
|
|
51d2b0d6a5 | ||
|
|
de1a3e3765 | ||
|
|
68d28eb4ac | ||
|
|
ad150f4718 | ||
|
|
91e3249b23 | ||
|
|
24a7131b0b | ||
|
|
a0e318b43f | ||
|
|
e41e700f2d | ||
|
|
6c64e75412 | ||
|
|
0c5c0ce67e | ||
|
|
6ac091096b | ||
|
|
b05010bdc4 | ||
|
|
d033704f12 | ||
|
|
367119a5a2 | ||
|
|
d15c971125 | ||
|
|
4d532199b4 | ||
|
|
cd15906c29 | ||
|
|
4ee5c73bca | ||
|
|
cb1d9c38df | ||
|
|
bce3411cef | ||
|
|
a721a94902 | ||
|
|
3173290abb | ||
|
|
491b40ed80 | ||
|
|
ab1362264a | ||
|
|
09993a8ac8 | ||
|
|
8619367ac0 | ||
|
|
148c853000 | ||
|
|
14efbe6584 | ||
|
|
9f054bb97b | ||
|
|
6f07445185 | ||
|
|
80b5c6cd35 | ||
|
|
7ab225662c | ||
|
|
85dc9c738b | ||
|
|
a09bf963f4 | ||
|
|
242fd56aec | ||
|
|
a42fdee480 |
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
@@ -16,6 +16,8 @@ on:
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/misskey-world/**
|
||||
- packages/frontend-misskey-world-engine/**
|
||||
- packages/shared/eslint.config.js
|
||||
- .github/workflows/lint.yml
|
||||
pull_request:
|
||||
@@ -30,6 +32,8 @@ on:
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/misskey-world/**
|
||||
- packages/frontend-misskey-world-engine/**
|
||||
- packages/shared/eslint.config.js
|
||||
- .github/workflows/lint.yml
|
||||
jobs:
|
||||
@@ -65,6 +69,8 @@ jobs:
|
||||
- misskey-js
|
||||
- misskey-bubble-game
|
||||
- misskey-reversi
|
||||
- misskey-world
|
||||
- frontend-misskey-world-engine
|
||||
env:
|
||||
eslint-cache-version: v1
|
||||
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -81,3 +81,6 @@ vite.config.local-dev.ts.timestamp-*
|
||||
|
||||
# VSCode addon
|
||||
.favorites.json
|
||||
|
||||
# Affinity
|
||||
*.af~lock~
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -30,6 +30,8 @@ COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
|
||||
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
|
||||
COPY --link ["packages/misskey-world/package.json", "./packages/misskey-world/"]
|
||||
COPY --link ["packages/frontend-misskey-world-engine/package.json", "./packages/frontend-misskey-world-engine/"]
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -61,6 +63,8 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
||||
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||
COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"]
|
||||
COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bubble-game/"]
|
||||
COPY --link ["packages/misskey-world/package.json", "./packages/misskey-world/"]
|
||||
COPY --link ["packages/frontend-misskey-world-engine/package.json", "./packages/frontend-misskey-world-engine/"]
|
||||
|
||||
ARG NODE_ENV=production
|
||||
|
||||
@@ -99,12 +103,18 @@ COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/nod
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-reversi/node_modules ./packages/misskey-reversi/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-bubble-game/node_modules ./packages/misskey-bubble-game/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-world/node_modules ./packages/misskey-world/node_modules
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/frontend-misskey-world-engine/node_modules ./packages/frontend-misskey-world-engine/node_modules
|
||||
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/built ./built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/built ./packages/misskey-js/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-world/built ./packages/misskey-world/built
|
||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/frontend-misskey-world-engine/built ./packages/frontend-misskey-world-engine/built
|
||||
|
||||
COPY --chown=misskey:misskey . ./
|
||||
|
||||
ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so
|
||||
|
||||
@@ -1415,6 +1415,7 @@ viewRenotedChannel: "リノート先のチャンネルを見る"
|
||||
previewingTheme: "テーマのプレビュー中"
|
||||
previewingThemeRestore: "元に戻す"
|
||||
accessToken: "アクセストークン"
|
||||
choose: "選択"
|
||||
|
||||
_imageEditing:
|
||||
_vars:
|
||||
@@ -3563,3 +3564,457 @@ _qr:
|
||||
scanFile: "端末の画像をスキャン"
|
||||
raw: "テキスト"
|
||||
mfm: "MFM"
|
||||
|
||||
worldAvatar: "Worldアバター"
|
||||
worldAvatar_description: "MisskeyWorld / MisskeyRoomsで使用可能な3Dアバターを作成できます。"
|
||||
|
||||
_miWorld:
|
||||
separateRenderingThread: "描画を別スレッドに分離"
|
||||
separateRenderingThread_description: "有効にするとパフォーマンスが向上します。不安定になる場合は無効すると改善する可能性があります。"
|
||||
graphicsQuality: "グラフィックの品質"
|
||||
graphicsSettings: "グラフィック設定"
|
||||
frameRateLimitation: "フレームレート制限"
|
||||
higherValuePerformanceNote: "高くすると体験が向上しますが、消費電力が増加するなどパフォーマンスに影響を与えます。"
|
||||
resolution: "解像度"
|
||||
fov: "視野角"
|
||||
failedToInitialize: "初期化に失敗しました"
|
||||
crushed_description: "描画が継続できなくなりました。デバイスのリソース不足の可能性が考えられます。"
|
||||
antialiasing: "アンチエイリアス"
|
||||
avatar: "アバター"
|
||||
advancedCustomize: "高度なアレンジ"
|
||||
attachAccessory: "アクセサリーをつける"
|
||||
takeScreenShot: "スクリーンショット"
|
||||
onlineMenu: "オンラインメニュー"
|
||||
connectToOnline: "オンラインに接続"
|
||||
disconnectToOnline: "オンラインから切断"
|
||||
character: "キャラクター"
|
||||
sit: "座る"
|
||||
lyingDown: "寝そべる"
|
||||
standUp: "立ち上がる"
|
||||
|
||||
_avatars:
|
||||
_default:
|
||||
body: "ボディ"
|
||||
eyes: "目"
|
||||
mouth: "口"
|
||||
|
||||
_avatarAccessories:
|
||||
mug: "マグカップ"
|
||||
_mug:
|
||||
bodyMat: "コップの素材"
|
||||
liquidMat: "液体の素材"
|
||||
mikan: "みかん"
|
||||
|
||||
_miRoom:
|
||||
snapToGrid: "グリッドにスナップ"
|
||||
gridScale: "グリッドサイズ"
|
||||
thereAreUnsavedChanges: "未保存の変更があります"
|
||||
revertAllChangesConfirmation: "全ての変更を取り消し、部屋を最後に保存した状態まで戻しますか?"
|
||||
yourDeviceNotSupported_title: "MisskeyRoomを起動できません"
|
||||
yourDeviceNotSupported_description: "お使いのデバイスがMisskeyRoomをサポートしていないか、デバイスのリソース不足などにより一時的に利用できなくなっています。\nMisskeyRoomを動作させるには、WebGPUをサポートするデバイス・ブラウザが必要です。"
|
||||
imageFit: "画像のはめ込み"
|
||||
imageFit_cover: "覆う"
|
||||
imageFit_contain: "収める"
|
||||
imageFit_stretch: "引き伸ばす"
|
||||
material_metallic: "光沢"
|
||||
material_roughness: "粗さ"
|
||||
light_brightness: "明るさ"
|
||||
advancedCustomize: "高度なアレンジ"
|
||||
enterEditMode: "エディットモードを始める"
|
||||
exitEditMode: "エディットモードを終了"
|
||||
installFurniture: "家具を設置"
|
||||
roomCustomize: "部屋のカスタマイズ"
|
||||
changeRoomName: "ルーム名を編集"
|
||||
roomInfo: "ルーム情報"
|
||||
duplicate: "複製"
|
||||
grab: "掴む"
|
||||
furnitureCustomize: "家具のアレンジ"
|
||||
uninstallFurniture: "しまう"
|
||||
furnituresCount: "家具の数"
|
||||
attachedFilesCount: "添付ファイル数"
|
||||
_furniturePlacement:
|
||||
top: "上面設置"
|
||||
bottom: "下面設置"
|
||||
side: "側面設置"
|
||||
|
||||
_furnitures:
|
||||
haniwa: "はにわ"
|
||||
_haniwa:
|
||||
bodyMat: "本体の素材"
|
||||
insideColor: "中身の色"
|
||||
woodRingFloorLamp: "リングシェードフロアランプ"
|
||||
_woodRingFloorLamp:
|
||||
shadeMat: "シェードの素材"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
a4Case: "A4ケース"
|
||||
_a4Case:
|
||||
mat: "素材"
|
||||
aircon: "エアコン"
|
||||
allInOnePc: "一体型PC"
|
||||
_allInOnePc:
|
||||
bezelMat: "ベゼルの素材"
|
||||
bodyMat: "本体の素材"
|
||||
image: "画面の画像"
|
||||
image_desktop: "デスクトップ"
|
||||
screenBrightness: "画面の明るさ"
|
||||
aquarium: "水槽"
|
||||
aromaReedDiffuser: "アロマリードディフューザー"
|
||||
_aromaReedDiffuser:
|
||||
bottleMat: "ボトルの素材"
|
||||
oilMat: "オイルの素材"
|
||||
banknote: "紙幣"
|
||||
beamLamp: "ビームランプ"
|
||||
bed: "ベッド"
|
||||
_bed:
|
||||
frameMat: "フレームの素材"
|
||||
blind: "ブラインド"
|
||||
_blind:
|
||||
angle: "羽根の回転角度"
|
||||
blades: "羽根の枚数"
|
||||
open: "開閉状態"
|
||||
book: "本"
|
||||
_book:
|
||||
height: "高さ"
|
||||
thickness: "厚み"
|
||||
variation: "バリエーション"
|
||||
width: "幅"
|
||||
books: "本の束"
|
||||
_books:
|
||||
variation: "バリエーション"
|
||||
boxWallShelf: "ボックス型ウォールシェルフ"
|
||||
_boxWallShelf:
|
||||
bodyMat: "本体の素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
withBack: "背板"
|
||||
cactusS: "サボテン S"
|
||||
_cactusS:
|
||||
potMat: "鉢の素材"
|
||||
cardboardBox: "段ボール箱"
|
||||
_cardboardBox:
|
||||
variation: "種類"
|
||||
variation_aizon: "Aizon"
|
||||
variation_default: "デフォルト"
|
||||
variation_mikan: "みかん"
|
||||
ceilingFanLight: "シーリングファンライト"
|
||||
_ceilingFanLight:
|
||||
shadeMat: "シェードの素材"
|
||||
chair: "椅子"
|
||||
_chair:
|
||||
primaryMat: "メインの素材"
|
||||
secondaryMat: "サブの素材"
|
||||
frameMat: "フレームの素材"
|
||||
clippedPicture: "留められた写真"
|
||||
_clippedPicture:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
coffeeCup: "コーヒーカップ"
|
||||
colorBox: "カラーボックス"
|
||||
_colorBox:
|
||||
mat: "素材"
|
||||
cuboid: "直方体"
|
||||
_cuboid:
|
||||
mat: "素材"
|
||||
x: "X"
|
||||
y: "Y"
|
||||
z: "Z"
|
||||
cupNoodle: "インスタントラーメン"
|
||||
curtain: "カーテン"
|
||||
custardPudding: "プリン"
|
||||
descriptionPlate: "説明が書かれたプレート"
|
||||
desk: "デスク"
|
||||
_desk:
|
||||
boardMat: "天板の素材"
|
||||
depth: "奥行き"
|
||||
frameMat: "フレームの素材"
|
||||
width: "幅"
|
||||
desktopPc: "デスクトップPC"
|
||||
_desktopPc:
|
||||
bodyMat: "本体の素材"
|
||||
coverMat: "カバーの素材"
|
||||
inner1Mat: "内部素材1"
|
||||
inner2Mat: "内部素材2"
|
||||
inner3Mat: "内部素材3"
|
||||
ledColor: "LEDの色"
|
||||
djMixer: "DJミキサー"
|
||||
djPlayer: "DJプレーヤー"
|
||||
_djPlayer:
|
||||
image: "画像"
|
||||
"image:waveform": "波形"
|
||||
screenBrightness: "画面の明るさ"
|
||||
ductRailSpotLights: "スポットライト付きダクトレール"
|
||||
_ductRailSpotLights:
|
||||
angleH: "水平角度"
|
||||
angleV: "垂直角度"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
ductTape: "ガムテープ"
|
||||
electronicDisplayBoard: "電光掲示板"
|
||||
_electronicDisplayBoard:
|
||||
frameMat: "フレームの素材"
|
||||
ledBrightness: "LEDの明るさ"
|
||||
ledColor: "LEDの色"
|
||||
text: "テキスト"
|
||||
emptyBento: "空の弁当容器"
|
||||
energyDrink: "エナジードリンク"
|
||||
envelope: "封筒"
|
||||
facialTissue: "ティッシュ"
|
||||
glassCylinderPotPlant: "ガラスシリンダーの鉢植えと植物"
|
||||
handheldGameConsole: "携帯ゲーム機"
|
||||
_handheldGameConsole:
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
screenBrightness: "画面の明るさ"
|
||||
hangingDuctRail: "吊り下げダクトレール"
|
||||
_hangingDuctRail:
|
||||
bodyMat: "本体の素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
hangingTShirt: "吊り下げTシャツ"
|
||||
icosahedron: "正二十面体のオブジェ"
|
||||
_icosahedron:
|
||||
mat: "素材"
|
||||
ironFrameTable: "アイアンフレームテーブル"
|
||||
_ironFrameTable:
|
||||
boardMat: "天板の素材"
|
||||
depth: "奥行き"
|
||||
frameMat: "フレームの素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
issyoubin: "一升瓶"
|
||||
_issyoubin:
|
||||
variation: "種類"
|
||||
keyboard: "キーボード"
|
||||
_keyboard:
|
||||
bodyMat: "本体の素材"
|
||||
keyMat: "キーの素材"
|
||||
laptopPc: "ノートPC"
|
||||
_laptopPc:
|
||||
bezelMat: "ベゼルの素材"
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
openAngle: "開き具合"
|
||||
screenBrightness: "画面の明るさ"
|
||||
largeMousepad: "大きいマウスパッド"
|
||||
_largeMousepad:
|
||||
image: "画像"
|
||||
lavaLamp: "ラバランプ"
|
||||
_lavaLamp:
|
||||
bodyMat: "本体の素材"
|
||||
glassMat: "ガラスの素材"
|
||||
lavaColor: "オイルの色"
|
||||
lightColor: "ランプの色"
|
||||
letterCase: "レターケース"
|
||||
lowPartitionBar: "低いパーティションバー"
|
||||
_lowPartitionBar:
|
||||
bodyMat: "本体の素材"
|
||||
width: "幅"
|
||||
miObjet: "Miオブジェ"
|
||||
miPlate: "Miプレート"
|
||||
miPlateDisplayed: "飾られたMiプレート"
|
||||
milk: "牛乳"
|
||||
mixer: "ミキサー"
|
||||
monitor: "モニター"
|
||||
_monitor:
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
screenBrightness: "画面の明るさ"
|
||||
monitorSpeaker: "モニタースピーカー"
|
||||
_monitorSpeaker:
|
||||
mat: "素材"
|
||||
monstera: "モンステラ"
|
||||
_monstera:
|
||||
potMat: "鉢の素材"
|
||||
mug: "マグカップ"
|
||||
_mug:
|
||||
bodyMat: "コップの素材"
|
||||
liquidMat: "液体の素材"
|
||||
newtonsCradle: "ニュートンクレードル"
|
||||
_newtonsCradle:
|
||||
frameMat: "フレームの素材"
|
||||
openedCardboardBox: "開いた段ボール箱"
|
||||
pachira: "パキラ"
|
||||
_pachira:
|
||||
potMat: "鉢の素材"
|
||||
petBottle: "ペットボトル"
|
||||
_petBottle:
|
||||
empty: "空"
|
||||
variation: "種類"
|
||||
variation_greenTea: "緑茶"
|
||||
variation_mineralWater: "ミネラルウォーター"
|
||||
withCap: "キャップ"
|
||||
withLabel: "ラベル"
|
||||
piano: "ピアノ"
|
||||
_piano:
|
||||
bodyMat: "本体の素材"
|
||||
pictureFrame: "シンプルな額縁"
|
||||
_pictureFrame:
|
||||
depth: "厚さ"
|
||||
frameMat: "フレームの素材"
|
||||
frameThickness: "フレームの幅"
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
matHThickness: "マットの横幅"
|
||||
matVThickness: "マットの縦幅"
|
||||
width: "幅"
|
||||
withCover: "カバーあり"
|
||||
pizza: "ピザ"
|
||||
plant: "植物"
|
||||
plant2: "植物 2"
|
||||
poster: "ポスター"
|
||||
_poster:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
powerStrip: "電源タップ"
|
||||
radiometer: "ラジオメーター"
|
||||
randomBooks: "雑多な本"
|
||||
_randomBooks:
|
||||
count: "数"
|
||||
seed: "シード"
|
||||
stackVertically: "平積み"
|
||||
variation: "バリエーション"
|
||||
variation_mix: "いろいろ"
|
||||
variation_mixPlain: "いろいろ(無地)"
|
||||
recordPlayer: "レコードプレーヤー"
|
||||
rolledUpPoster: "丸めたポスター"
|
||||
roundRug: "円形のラグ"
|
||||
router: "ルーター"
|
||||
siphon: "サイフォン"
|
||||
snakeplant: "サンセベリア"
|
||||
_snakeplant:
|
||||
potMat: "鉢の素材"
|
||||
sofa: "ソファ"
|
||||
_sofa:
|
||||
bodyMat: "本体の素材"
|
||||
speaker: "スピーカー"
|
||||
_speaker:
|
||||
innerMat: "内側の素材"
|
||||
outerMat: "外側の素材"
|
||||
speakerStand: "スピーカースタンド"
|
||||
_speakerStand:
|
||||
bodyMat: "本体の素材"
|
||||
height: "高さ"
|
||||
spotLight: "スポットライト"
|
||||
_spotLight:
|
||||
angleH: "横方向の角度"
|
||||
angleV: "縦方向の角度"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
sprayer: "霧吹き"
|
||||
stanchionPole: "スタンションポール"
|
||||
_stanchionPole:
|
||||
bodyMat: "本体の素材"
|
||||
ropeMat: "ロープの素材"
|
||||
steelRack: "スチールラック"
|
||||
_steelRack:
|
||||
height: "高さ"
|
||||
numberOfShelfs: "シェルフの数"
|
||||
poleMat: "ポールの素材"
|
||||
shelfPositionOf: "シェルフの位置"
|
||||
shelfMat: "シェルフの素材"
|
||||
widthAndDepthVariation: "W x D"
|
||||
stormGlass: "ストームグラス"
|
||||
tableSalt: "食卓塩"
|
||||
tabletopCalendar: "卓上カレンダー"
|
||||
tabletopDigitalClock: "卓上デジタル時計"
|
||||
_tabletopDigitalClock:
|
||||
bodyMat: "本体の素材"
|
||||
lcdColor: "LCDの色"
|
||||
tabletopFlag: "卓上旗"
|
||||
_tabletopFlag:
|
||||
image: "画像"
|
||||
tabletopGlassPictureFrame: "卓上ガラス製フォトフレーム"
|
||||
_tabletopGlassPictureFrame:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
tabletopIronFrameStand: "卓上アイアンフレームスタンド"
|
||||
_tabletopIronFrameStand:
|
||||
boardMat: "板の素材"
|
||||
depth: "奥行き"
|
||||
frameMat: "フレームの素材"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
tabletopLcdButtonsController: "LCDボタン付き卓上コントローラー"
|
||||
_tabletopLcdButtonsController:
|
||||
bodyMat: "本体の素材"
|
||||
image: "画像"
|
||||
screenBrightness: "画面の明るさ"
|
||||
tabletopPictureFrame: "卓上フォトフレーム"
|
||||
_tabletopPictureFrame:
|
||||
depth: "厚さ"
|
||||
frameMat: "フレームの素材"
|
||||
frameThickness: "フレームの幅"
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
matHThickness: "マットの横幅"
|
||||
matVThickness: "マットの縦幅"
|
||||
width: "幅"
|
||||
tapestry: "タペストリー"
|
||||
_tapestry:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
tetrapod: "波消ブロック"
|
||||
tv: "テレビ"
|
||||
_tv:
|
||||
bodyMat: "本体の素材"
|
||||
screenBrightness: "画面の明るさ"
|
||||
twistedCubeObjet: "ねじれた立方体のオブジェ"
|
||||
usedTissue: "使用済みティッシュ"
|
||||
wallCanvas: "壁掛けキャンバス"
|
||||
_wallCanvas:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
wallClock: "壁掛け時計"
|
||||
_wallClock:
|
||||
frameMat: "フレームの素材"
|
||||
faceMat: "文字盤の素材"
|
||||
handsMat: "針の素材"
|
||||
wallGlassPictureFrame: "ガラスの壁掛けフォトフレーム"
|
||||
_wallGlassPictureFrame:
|
||||
height: "高さ"
|
||||
image: "画像"
|
||||
width: "幅"
|
||||
wallMirror: "壁掛けミラー"
|
||||
_wallMirror:
|
||||
frameMat: "フレームの素材"
|
||||
frameThickness: "フレームの厚み"
|
||||
height: "高さ"
|
||||
width: "幅"
|
||||
wallMountSpotLight: "ウォールマウントスポットライト"
|
||||
_wallMountSpotLight:
|
||||
angleH: "横方向の角度"
|
||||
angleV: "縦方向の角度"
|
||||
bodyMat: "本体の素材"
|
||||
light: "照明"
|
||||
wallShelf: "ウォールシェルフ"
|
||||
_wallShelf:
|
||||
boardMat: "板の素材"
|
||||
boardStyle: "板のスタイル"
|
||||
"boardStyle:color": "単色"
|
||||
"boardStyle:wood": "木目"
|
||||
style: "スタイル"
|
||||
wireBasket: "ワイヤーバスケット"
|
||||
_wireBasket:
|
||||
bodyMat: "本体の素材"
|
||||
wireNet: "ワイヤーネット"
|
||||
_wireNet:
|
||||
bodyMat: "本体の素材"
|
||||
woodRingsPendantLight: "リングペンダントライト"
|
||||
_woodRingsPendantLight:
|
||||
bodyMat: "本体の素材"
|
||||
length: "長さ"
|
||||
light: "照明"
|
||||
shadeMat: "シェードの素材"
|
||||
woodSoundAbsorbingPanel: "木製吸音パネル"
|
||||
ironFrameShelf: "アイアンフレームシェルフ"
|
||||
_ironFrameShelf:
|
||||
boardMat: "板の素材"
|
||||
frameMat: "フレームの素材"
|
||||
height: "段数"
|
||||
width: "幅"
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
"packages/i18n",
|
||||
"packages/misskey-reversi",
|
||||
"packages/misskey-bubble-game",
|
||||
"packages/misskey-world",
|
||||
"packages/icons-subsetter",
|
||||
"packages/frontend-misskey-world-engine",
|
||||
"packages/frontend-shared",
|
||||
"packages/frontend-builder",
|
||||
"packages/sw",
|
||||
|
||||
28
packages/backend/migration/1778744540138-world-room.js
Normal file
28
packages/backend/migration/1778744540138-world-room.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class WorldRoom1778744540138 {
|
||||
name = 'WorldRoom1778744540138'
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "world_room" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(1024) NOT NULL, "userId" character varying(32) NOT NULL, "likedCount" integer NOT NULL DEFAULT '0', "visibility" character varying(128) NOT NULL DEFAULT 'public', "def" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_40cfacaf35b0b54bb2281c89767" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_88289375952050da4a7752a366" ON "world_room" ("updatedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f803a5efb4125c5fd8a414285e" ON "world_room" ("userId") `);
|
||||
await queryRunner.query(`ALTER TABLE "world_room" ADD CONSTRAINT "FK_f803a5efb4125c5fd8a414285ed" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "world_room" DROP CONSTRAINT "FK_f803a5efb4125c5fd8a414285ed"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_f803a5efb4125c5fd8a414285e"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_88289375952050da4a7752a366"`);
|
||||
await queryRunner.query(`DROP TABLE "world_room"`);
|
||||
}
|
||||
}
|
||||
30
packages/backend/migration/1779921322355-world-avatar.js
Normal file
30
packages/backend/migration/1779921322355-world-avatar.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class WorldAvatar1779921322355 {
|
||||
name = 'WorldAvatar1779921322355'
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "world_avatar" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, "def" jsonb NOT NULL DEFAULT '{}', "active" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_e7a27262285cc2c27114871f866" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_0f1d0bdfaca455cc2f13defabe" ON "world_avatar" ("updatedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_4eba43c8e2540a92e99dd7f5a9" ON "world_avatar" ("userId") `);
|
||||
await queryRunner.query(`ALTER TABLE "world_room" ADD "accessCount" integer NOT NULL DEFAULT '0'`);
|
||||
await queryRunner.query(`ALTER TABLE "world_avatar" ADD CONSTRAINT "FK_4eba43c8e2540a92e99dd7f5a9a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "world_avatar" DROP CONSTRAINT "FK_4eba43c8e2540a92e99dd7f5a9a"`);
|
||||
await queryRunner.query(`ALTER TABLE "world_room" DROP COLUMN "accessCount"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_4eba43c8e2540a92e99dd7f5a9"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_0f1d0bdfaca455cc2f13defabe"`);
|
||||
await queryRunner.query(`DROP TABLE "world_avatar"`);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^22.15.0 || ^24.10.0"
|
||||
"node": "^22.15.0 || ^24.10.0 || ^26.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "pnpm compile-config && node ./built/entry.js",
|
||||
|
||||
@@ -154,6 +154,11 @@ import { ApQuestionService } from './activitypub/models/ApQuestionService.js';
|
||||
import { QueueModule } from './QueueModule.js';
|
||||
import { QueueService } from './QueueService.js';
|
||||
import { LoggerService } from './LoggerService.js';
|
||||
import { WorldRoomService } from './WorldRoomService.js';
|
||||
import { WorldRoomEntityService } from './entities/WorldRoomEntityService.js';
|
||||
import { WorldRoomMultiplayService } from './WorldRoomMultiplayService.js';
|
||||
import { WorldAvatarService } from './WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from './entities/WorldAvatarEntityService.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
@@ -229,6 +234,9 @@ const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatServic
|
||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
||||
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
|
||||
const $PageService: Provider = { provide: 'PageService', useExisting: PageService };
|
||||
const $WorldRoomService: Provider = { provide: 'WorldRoomService', useExisting: WorldRoomService };
|
||||
const $WorldRoomMultiplayService: Provider = { provide: 'WorldRoomMultiplayService', useExisting: WorldRoomMultiplayService };
|
||||
const $WorldAvatarService: Provider = { provide: 'WorldAvatarService', useExisting: WorldAvatarService };
|
||||
|
||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||
@@ -284,6 +292,8 @@ const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting
|
||||
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
|
||||
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
|
||||
const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService };
|
||||
const $WorldRoomEntityService: Provider = { provide: 'WorldRoomEntityService', useExisting: WorldRoomEntityService };
|
||||
const $WorldAvatarEntityService: Provider = { provide: 'WorldAvatarEntityService', useExisting: WorldAvatarEntityService };
|
||||
|
||||
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
|
||||
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
|
||||
@@ -382,6 +392,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
PageService,
|
||||
WorldRoomService,
|
||||
WorldRoomMultiplayService,
|
||||
WorldAvatarService,
|
||||
|
||||
ChartLoggerService,
|
||||
FederationChart,
|
||||
@@ -437,6 +450,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
SystemWebhookEntityService,
|
||||
WorldRoomEntityService,
|
||||
WorldAvatarEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
@@ -532,6 +547,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$PageService,
|
||||
$WorldRoomService,
|
||||
$WorldRoomMultiplayService,
|
||||
$WorldAvatarService,
|
||||
|
||||
$ChartLoggerService,
|
||||
$FederationChart,
|
||||
@@ -587,6 +605,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
$SystemWebhookEntityService,
|
||||
$WorldRoomEntityService,
|
||||
$WorldAvatarEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
@@ -682,6 +702,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
PageService,
|
||||
WorldRoomService,
|
||||
WorldRoomMultiplayService,
|
||||
WorldAvatarService,
|
||||
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
@@ -736,6 +759,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
SystemWebhookEntityService,
|
||||
WorldRoomEntityService,
|
||||
WorldAvatarEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
@@ -830,6 +855,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$PageService,
|
||||
$WorldRoomService,
|
||||
$WorldRoomMultiplayService,
|
||||
$WorldAvatarService,
|
||||
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
@@ -884,6 +912,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
$SystemWebhookEntityService,
|
||||
$WorldRoomEntityService,
|
||||
$WorldAvatarEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
|
||||
@@ -173,6 +173,16 @@ export interface ChatEventTypes {
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorldRoomEventTypes {
|
||||
enter: {
|
||||
user: Packed<'UserLite'>;
|
||||
avatar: Packed<'WorldAvatarLite'>['def'] | null;
|
||||
};
|
||||
left: {
|
||||
userId: MiUser['id'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReversiEventTypes {
|
||||
matched: {
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
@@ -315,6 +325,10 @@ export type GlobalEvents = {
|
||||
name: `chatRoomStream:${MiChatRoom['id']}`;
|
||||
payload: EventTypesToEventPayload<ChatEventTypes>;
|
||||
};
|
||||
worldRoom: {
|
||||
name: `worldRoomStream:${string}`;
|
||||
payload: EventTypesToEventPayload<WorldRoomEventTypes>;
|
||||
};
|
||||
reversi: {
|
||||
name: `reversiStream:${MiUser['id']}`;
|
||||
payload: EventTypesToEventPayload<ReversiEventTypes>;
|
||||
@@ -435,4 +449,9 @@ export class GlobalEventService {
|
||||
public publishReversiGameStream<K extends keyof ReversiGameEventTypes>(gameId: MiReversiGame['id'], type: K, value?: ReversiGameEventTypes[K]): void {
|
||||
this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishWorldRoomStream<K extends keyof WorldRoomEventTypes>(roomId: string, type: K, value?: WorldRoomEventTypes[K]): void {
|
||||
this.publish(`worldRoomStream:${roomId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
}
|
||||
|
||||
148
packages/backend/src/core/WorldAvatarService.ts
Normal file
148
packages/backend/src/core/WorldAvatarService.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
MiDriveFile,
|
||||
MiWorldAvatar,
|
||||
} from '@/models/_.js';
|
||||
import type { DriveFilesRepository, WorldAvatarsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
@Injectable()
|
||||
export class WorldAvatarService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.worldAvatarsRepository)
|
||||
private worldAvatarsRepository: WorldAvatarsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
public defaultAvatar = {
|
||||
type: 'default',
|
||||
body: {
|
||||
color: [0.8, 0.8, 0.8],
|
||||
roughness: 1,
|
||||
metallic: 0,
|
||||
},
|
||||
eyes: {
|
||||
type: 'a',
|
||||
color: [0, 0, 0],
|
||||
},
|
||||
mouth: {
|
||||
type: 'a',
|
||||
color: [0, 0, 0],
|
||||
},
|
||||
accessories: [],
|
||||
} satisfies MiWorldAvatar['def'];
|
||||
|
||||
@bindThis
|
||||
public async validateDef(
|
||||
me: MiUser,
|
||||
def: MiWorldAvatar['def'],
|
||||
): Promise<boolean> {
|
||||
// TODO
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findMyAvatarById(userId: MiUser['id'], avatarId: MiWorldAvatar['id']) {
|
||||
return this.worldAvatarsRepository.findOneBy({ id: avatarId, userId: userId });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findAvatarById(avatarId: MiWorldAvatar['id']) {
|
||||
return this.worldAvatarsRepository.findOne({ where: { id: avatarId }, relations: { user: true } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getMyAvatarsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiWorldAvatar['id'] | null, untilId?: MiWorldAvatar['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.worldAvatarsRepository.createQueryBuilder('avatar'), sinceId, untilId)
|
||||
.andWhere('avatar.userId = :userId', { userId });
|
||||
|
||||
const avatars = await query.take(limit).getMany();
|
||||
|
||||
return avatars;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(
|
||||
me: MiUser,
|
||||
body: Partial<MiWorldAvatar>,
|
||||
): Promise<MiWorldAvatar> {
|
||||
const currentAvatarsCount = await this.worldAvatarsRepository.countBy({ userId: me.id });
|
||||
|
||||
// TODO: limit by role policy
|
||||
|
||||
const avatar = await this.worldAvatarsRepository.insertOne(new MiWorldAvatar({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
name: body.name,
|
||||
def: body.def,
|
||||
userId: me.id,
|
||||
active: currentAvatarsCount === 0,
|
||||
}));
|
||||
|
||||
return avatar;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(
|
||||
avatar: MiWorldAvatar,
|
||||
body: Partial<MiWorldAvatar>,
|
||||
): Promise<void> {
|
||||
body.updatedAt = new Date();
|
||||
const updated = await this.worldAvatarsRepository.createQueryBuilder().update()
|
||||
.set(body)
|
||||
.where('id = :id', { id: avatar.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => {
|
||||
return response.raw[0];
|
||||
});
|
||||
|
||||
if (body.active) {
|
||||
await this.worldAvatarsRepository.createQueryBuilder().update()
|
||||
.set({ active: false })
|
||||
.where('userId = :userId', { userId: avatar.userId })
|
||||
.andWhere('id != :id', { id: avatar.id })
|
||||
.execute();
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(avatar: MiWorldAvatar, deleter?: MiUser): Promise<void> {
|
||||
await this.worldAvatarsRepository.delete(avatar.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActiveAvatarOfUser(userId: MiUser['id']) {
|
||||
return this.worldAvatarsRepository.findOneBy({ userId, active: true });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActiveAvatarOfUsers(userIds: MiUser['id'][]): Promise<MiWorldAvatar[]> {
|
||||
if (userIds.length === 0) return [];
|
||||
return this.worldAvatarsRepository.findBy({ userId: In(userIds), active: true });
|
||||
}
|
||||
}
|
||||
153
packages/backend/src/core/WorldRoomMultiplayService.ts
Normal file
153
packages/backend/src/core/WorldRoomMultiplayService.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, Not } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
MiWorldRoom,
|
||||
} from '@/models/_.js';
|
||||
import type { MiWorldAvatar, WorldRoomsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
|
||||
type PlayerState = {
|
||||
position: [number, number, number],
|
||||
rotation: [number, number, number],
|
||||
sit?: string; // id
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class WorldRoomMultiplayService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.worldRoomsRepository)
|
||||
private worldRoomsRepository: WorldRoomsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private userEntityService: UserEntityService,
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async enter(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
console.log('enter', { userId, roomId });
|
||||
|
||||
// TODO: atomicにやる
|
||||
const currentPlayers = await this.redisClient.hlen(`worldRoom:${roomId}:players`);
|
||||
if (currentPlayers < 10) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hset(`worldRoom:${roomId}:players`, userId, 1);
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:players`, 30, 'FIELDS', 1, userId);
|
||||
await redisPipeline.exec();
|
||||
} else {
|
||||
throw new Error('Room is full.');
|
||||
}
|
||||
|
||||
// TODO: 既に入っていたらスキップ
|
||||
const avatar = await this.worldAvatarService.getActiveAvatarOfUser(userId);
|
||||
this.globalEventService.publishWorldRoomStream(roomId, 'enter', {
|
||||
user: await this.userEntityService.pack(userId),
|
||||
avatar: avatar?.def,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async heartbeat(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
const exists = await this.redisClient.hexists(`worldRoom:${roomId}:players`, userId);
|
||||
if (exists) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:players`, 30, 'FIELDS', 1, userId);
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:playerStates`, 30, 'FIELDS', 1, userId);
|
||||
await redisPipeline.exec();
|
||||
} else {
|
||||
throw new Error('Not in room.');
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async left(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
console.log('left', { userId, roomId });
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hdel(`worldRoom:${roomId}:players`, userId);
|
||||
redisPipeline.hdel(`worldRoom:${roomId}:playerStates`, userId);
|
||||
await redisPipeline.exec();
|
||||
|
||||
this.globalEventService.publishWorldRoomStream(roomId, 'left', {
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updatePlayerState(userId: MiUser['id'], roomId: MiWorldRoom['id'], state: PlayerState) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hset(`worldRoom:${roomId}:playerStates`, userId, JSON.stringify(state));
|
||||
redisPipeline.hexpire(`worldRoom:${roomId}:playerStates`, 30, 'FIELDS', 1, userId);
|
||||
await redisPipeline.exec();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getPlayerStates(roomId: MiWorldRoom['id']): Promise<Record<string, PlayerState>> {
|
||||
const entries = await this.redisClient.hgetall(`worldRoom:${roomId}:playerStates`);
|
||||
return Object.fromEntries(Object.entries(entries).map(([userId, state]) => [userId, JSON.parse(state) as PlayerState]));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getPlayerStatesAndHeatbeat(userId: MiUser['id'], roomId: MiWorldRoom['id']): Promise<Record<string, PlayerState>> {
|
||||
// TODO: atomicにやる
|
||||
this.heartbeat(userId, roomId);
|
||||
return this.getPlayerStates(roomId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packPlayerProfile(user: Packed<'UserLite'>, avatar: Packed<'WorldAvatarLite'>['def'] | null) {
|
||||
return {
|
||||
user: {
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
avatarUrl: user.avatarUrl,
|
||||
},
|
||||
avatar: avatar ?? this.worldAvatarService.defaultAvatar,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getPlayerProfiles(roomId: MiWorldRoom['id'], userId?: MiUser['id']): Promise<Record<string, any>> {
|
||||
let playerIds = await this.redisClient.hkeys(`worldRoom:${roomId}:players`);
|
||||
playerIds = playerIds.filter(id => id !== userId);
|
||||
|
||||
const packedUsers = await this.userEntityService.packMany(playerIds);
|
||||
const avatars = await this.worldAvatarService.getActiveAvatarOfUsers(playerIds);
|
||||
|
||||
const profiles: Record<string, any> = {};
|
||||
for (const playerId of playerIds) {
|
||||
const packedUser = packedUsers.find(u => u.id === playerId);
|
||||
if (packedUser == null) continue;
|
||||
profiles[playerId] = this.packPlayerProfile(packedUser, avatars.find(a => a.userId === playerId)?.def ?? null);
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
175
packages/backend/src/core/WorldRoomService.ts
Normal file
175
packages/backend/src/core/WorldRoomService.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import {
|
||||
MiDriveFile,
|
||||
MiWorldRoom,
|
||||
} from '@/models/_.js';
|
||||
import type { DriveFilesRepository, WorldRoomsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
const driveFileReferencingOptions = {
|
||||
clippedPicture: ['image'],
|
||||
tapestry: ['image'],
|
||||
poster: ['image'],
|
||||
pictureFrame: ['image'],
|
||||
tabletopPictureFrame: ['image'],
|
||||
tabletopGlassPictureFrame: ['image'],
|
||||
wallCanvas: ['image'],
|
||||
wallGlassPictureFrame: ['image'],
|
||||
tabletopFlag: ['image'],
|
||||
tabletopLcdButtonsController: ['image'],
|
||||
djPlayer: ['image'],
|
||||
monitor: ['image'],
|
||||
allInOnePc: ['image'],
|
||||
laptopPc: ['image'],
|
||||
handheldGameConsole: ['image'],
|
||||
largeMousepad: ['image'],
|
||||
} as Record<string, string[]>;
|
||||
|
||||
@Injectable()
|
||||
export class WorldRoomService {
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.worldRoomsRepository)
|
||||
private worldRoomsRepository: WorldRoomsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async validateDef(
|
||||
me: MiUser,
|
||||
def: MiWorldRoom['def'],
|
||||
): Promise<boolean> {
|
||||
// TODO: スキーマ検証(関係ないプロパティを入れたり不正な値を入れたりできないように)
|
||||
// そのためにはJSON SchemaでRoomState/各objectのoptionsを定義する必要がある
|
||||
|
||||
const objectsLimit = 100; // TODO: ref role policy
|
||||
if (def.installedFurnitures.length > objectsLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachedFilesLimit = 30; // TODO: ref role policy
|
||||
|
||||
const attachedFileIds = this.collectReferencedDriveFileIds(def);
|
||||
if (attachedFileIds.size > attachedFilesLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachedFiles = attachedFileIds.size === 0 ? [] : await this.driveFilesRepository.findBy({ id: In([...attachedFileIds]), userId: me.id });
|
||||
for (const file of attachedFiles) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return false;
|
||||
}
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
return false;
|
||||
}
|
||||
if (Math.max(file.properties.width ?? 0, file.properties.height ?? 0) > 2048) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findMyRoomById(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
return this.worldRoomsRepository.findOneBy({ id: roomId, userId: userId });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async findRoomById(roomId: MiWorldRoom['id']) {
|
||||
return this.worldRoomsRepository.findOne({ where: { id: roomId }, relations: { user: true } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getRoomsOfUserWithPagination(userId: MiUser['id'], self: boolean, limit: number, sinceId?: MiWorldRoom['id'] | null, untilId?: MiWorldRoom['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.worldRoomsRepository.createQueryBuilder('room'), sinceId, untilId)
|
||||
.andWhere('room.userId = :userId', { userId });
|
||||
|
||||
if (!self) {
|
||||
query.andWhere('room.visibility = :visibility', { visibility: 'public' });
|
||||
}
|
||||
|
||||
const rooms = await query.take(limit).getMany();
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async create(
|
||||
me: MiUser,
|
||||
body: Partial<MiWorldRoom>,
|
||||
): Promise<MiWorldRoom> {
|
||||
const room = await this.worldRoomsRepository.insertOne(new MiWorldRoom({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
name: body.name,
|
||||
description: body.description,
|
||||
def: body.def,
|
||||
userId: me.id,
|
||||
visibility: body.visibility,
|
||||
}));
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(
|
||||
room: MiWorldRoom,
|
||||
body: Partial<MiWorldRoom>,
|
||||
): Promise<void> {
|
||||
body.updatedAt = new Date();
|
||||
return this.worldRoomsRepository.createQueryBuilder().update()
|
||||
.set(body)
|
||||
.where('id = :id', { id: room.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => {
|
||||
return response.raw[0];
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(room: MiWorldRoom, deleter?: MiUser): Promise<void> {
|
||||
await this.worldRoomsRepository.delete(room.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public collectReferencedDriveFileIds(roomState: MiWorldRoom['def']): Set<MiDriveFile['id']> {
|
||||
const fileIds = new Set<MiDriveFile['id']>();
|
||||
const installedFurnitures = roomState.installedFurnitures ?? roomState.installedObjects; // 後方互換性のため
|
||||
for (const o of installedFurnitures) {
|
||||
const def = driveFileReferencingOptions[o.type];
|
||||
if (def == null) continue;
|
||||
for (const key of def) {
|
||||
const optionValue = o.options[key];
|
||||
if (optionValue != null && optionValue.driveFileId != null && optionValue.driveFileId !== '') {
|
||||
fileIds.add(optionValue.driveFileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fileIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, MiWorldAvatar, WorldAvatarsRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class WorldAvatarEntityService {
|
||||
constructor(
|
||||
@Inject(DI.worldAvatarsRepository)
|
||||
private worldAvatarsRepository: WorldAvatarsRepository,
|
||||
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiWorldAvatar['id'] | MiWorldAvatar,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldAvatarLite'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const avatar = typeof src === 'object' ? src : await this.worldAvatarsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: avatar.id,
|
||||
def: avatar.def,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailed(
|
||||
src: MiWorldAvatar['id'] | MiWorldAvatar,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldAvatarDetailed'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const avatar = typeof src === 'object' ? src : await this.worldAvatarsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: avatar.id,
|
||||
createdAt: this.idService.parse(avatar.id).date.toISOString(),
|
||||
updatedAt: avatar.updatedAt.toISOString(),
|
||||
name: avatar.name,
|
||||
def: avatar.def,
|
||||
active: avatar.active,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLiteMany(
|
||||
avatars: MiWorldAvatar[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _users = avatars.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(avatars.map(avatar => this.packLite(avatar, me, { packedUser: _userMap.get(avatar.userId) })));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailedMany(
|
||||
avatars: MiWorldAvatar[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _users = avatars.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(avatars.map(avatar => this.packDetailed(avatar, me, { packedUser: _userMap.get(avatar.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
97
packages/backend/src/core/entities/WorldRoomEntityService.ts
Normal file
97
packages/backend/src/core/entities/WorldRoomEntityService.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, MiWorldRoom, WorldRoomsRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class WorldRoomEntityService {
|
||||
constructor(
|
||||
@Inject(DI.worldRoomsRepository)
|
||||
private worldRoomsRepository: WorldRoomsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private worldRoomService: WorldRoomService,
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiWorldRoom['id'] | MiWorldRoom,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldRoomLite'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const room = typeof src === 'object' ? src : await this.worldRoomsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: room.id,
|
||||
createdAt: this.idService.parse(room.id).date.toISOString(),
|
||||
updatedAt: room.updatedAt.toISOString(),
|
||||
userId: room.userId,
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(room.user ?? room.userId, me),
|
||||
name: room.name,
|
||||
description: room.description,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailed(
|
||||
src: MiWorldRoom['id'] | MiWorldRoom,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
},
|
||||
): Promise<Packed<'WorldRoomDetailed'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const room = typeof src === 'object' ? src : await this.worldRoomsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const attachedFileIds = this.worldRoomService.collectReferencedDriveFileIds(room.def);
|
||||
const attachedFiles = attachedFileIds.size === 0 ? [] : await this.driveFilesRepository.findBy({ id: In([...attachedFileIds]), userId: room.userId });
|
||||
|
||||
return await awaitAll({
|
||||
id: room.id,
|
||||
createdAt: this.idService.parse(room.id).date.toISOString(),
|
||||
updatedAt: room.updatedAt.toISOString(),
|
||||
userId: room.userId,
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(room.user ?? room.userId, me),
|
||||
name: room.name,
|
||||
description: room.description,
|
||||
def: room.def,
|
||||
attachedFiles: this.driveFileEntityService.packMany(attachedFiles),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLiteMany(
|
||||
rooms: MiWorldRoom[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
const _users = rooms.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(rooms.map(room => this.packLite(room, me, { packedUser: _userMap.get(room.userId) })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,5 +91,7 @@ export const DI = {
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
noteDraftsRepository: Symbol('noteDraftsRepository'),
|
||||
worldRoomsRepository: Symbol('worldRoomsRepository'),
|
||||
worldAvatarsRepository: Symbol('worldAvatarsRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
||||
@@ -75,6 +75,8 @@ import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-i
|
||||
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
|
||||
import { packedAchievementNameSchema, packedAchievementSchema } from '@/models/json-schema/achievement.js';
|
||||
import { packedNoteDraftSchema } from '@/models/json-schema/note-draft.js';
|
||||
import { packedWorldRoomDetailedSchema, packedWorldRoomLiteSchema } from '@/models/json-schema/world-room.js';
|
||||
import { packedWorldAvatarDetailedSchema, packedWorldAvatarLiteSchema } from '@/models/json-schema/world-avatar.js';
|
||||
|
||||
export const refs = {
|
||||
UserLite: packedUserLiteSchema,
|
||||
@@ -147,6 +149,10 @@ export const refs = {
|
||||
ChatRoom: packedChatRoomSchema,
|
||||
ChatRoomInvitation: packedChatRoomInvitationSchema,
|
||||
ChatRoomMembership: packedChatRoomMembershipSchema,
|
||||
WorldRoomLite: packedWorldRoomLiteSchema,
|
||||
WorldRoomDetailed: packedWorldRoomDetailedSchema,
|
||||
WorldAvatarLite: packedWorldAvatarLiteSchema,
|
||||
WorldAvatarDetailed: packedWorldAvatarDetailedSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
|
||||
@@ -84,6 +84,8 @@ import {
|
||||
MiChatRoomMembership,
|
||||
MiChatRoomInvitation,
|
||||
MiChatApproval,
|
||||
MiWorldRoom,
|
||||
MiWorldAvatar,
|
||||
} from './_.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DataSource } from 'typeorm';
|
||||
@@ -544,6 +546,18 @@ const $reversiGamesRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $worldRoomsRepository: Provider = {
|
||||
provide: DI.worldRoomsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiWorldRoom).extend(miRepository as MiRepository<MiWorldRoom>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $worldAvatarsRepository: Provider = {
|
||||
provide: DI.worldAvatarsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiWorldAvatar).extend(miRepository as MiRepository<MiWorldAvatar>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [
|
||||
@@ -623,6 +637,8 @@ const $reversiGamesRepository: Provider = {
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$worldRoomsRepository,
|
||||
$worldAvatarsRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
@@ -701,6 +717,8 @@ const $reversiGamesRepository: Provider = {
|
||||
$chatApprovalsRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$worldRoomsRepository,
|
||||
$worldAvatarsRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {
|
||||
|
||||
54
packages/backend/src/models/WorldAvatar.ts
Normal file
54
packages/backend/src/models/WorldAvatar.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('world_avatar')
|
||||
export class MiWorldAvatar {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(() => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public active: boolean;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
})
|
||||
public def: Record<string, any>;
|
||||
|
||||
constructor(data: Partial<MiWorldAvatar>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
packages/backend/src/models/WorldRoom.ts
Normal file
72
packages/backend/src/models/WorldRoom.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
export const worldRoomVisibility = ['public', 'private'] as const;
|
||||
export type WorldRoomVisibility = typeof worldRoomVisibility[number];
|
||||
|
||||
@Entity('world_room')
|
||||
export class MiWorldRoom {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public description: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(() => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
})
|
||||
public likedCount: number;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
})
|
||||
public accessCount: number;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, default: 'public',
|
||||
})
|
||||
public visibility: WorldRoomVisibility;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
})
|
||||
public def: Record<string, any>;
|
||||
|
||||
constructor(data: Partial<MiWorldRoom>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiChannel } from '@/models/Channel.js';
|
||||
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
|
||||
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
|
||||
import { MiChannelMuting } from "@/models/ChannelMuting.js";
|
||||
import { MiChannelMuting } from '@/models/ChannelMuting.js';
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiChatMessage } from '@/models/ChatMessage.js';
|
||||
import { MiChatRoom } from '@/models/ChatRoom.js';
|
||||
@@ -84,6 +84,8 @@ import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import { MiWorldRoom } from '@/models/WorldRoom.js';
|
||||
import { MiWorldAvatar } from '@/models/WorldAvatar.js';
|
||||
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
|
||||
|
||||
export interface MiRepository<T extends ObjectLiteral> {
|
||||
@@ -173,6 +175,8 @@ export {
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiWorldRoom,
|
||||
MiWorldAvatar,
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
|
||||
@@ -253,3 +257,5 @@ export type ChatRoomInvitationsRepository = Repository<MiChatRoomInvitation> & M
|
||||
export type ChatApprovalsRepository = Repository<MiChatApproval> & MiRepository<MiChatApproval>;
|
||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
|
||||
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
|
||||
export type WorldRoomsRepository = Repository<MiWorldRoom> & MiRepository<MiWorldRoom>;
|
||||
export type WorldAvatarsRepository = Repository<MiWorldAvatar> & MiRepository<MiWorldAvatar>;
|
||||
|
||||
52
packages/backend/src/models/json-schema/world-avatar.ts
Normal file
52
packages/backend/src/models/json-schema/world-avatar.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedWorldAvatarLiteSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
def: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedWorldAvatarDetailedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
def: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
active: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
95
packages/backend/src/models/json-schema/world-room.ts
Normal file
95
packages/backend/src/models/json-schema/world-room.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedWorldRoomLiteSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedWorldRoomDetailedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
def: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
attachedFiles: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -87,6 +87,8 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiSystemAccount } from '@/models/SystemAccount.js';
|
||||
import { MiWorldRoom } from '@/models/WorldRoom.js';
|
||||
import { MiWorldAvatar } from '@/models/WorldAvatar.js';
|
||||
|
||||
pg.types.setTypeParser(20, Number);
|
||||
|
||||
@@ -254,6 +256,8 @@ export const entities = [
|
||||
MiChatApproval,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiWorldRoom,
|
||||
MiWorldAvatar,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EndpointsModule } from '@/server/api/EndpointsModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import MainStreamConnection from '@/server/api/stream/Connection.js';
|
||||
import { ApiCallService } from './api/ApiCallService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { HealthServerService } from './HealthServerService.js';
|
||||
@@ -30,7 +31,6 @@ import { UrlPreviewService } from './web/UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
|
||||
import MainStreamConnection from '@/server/api/stream/Connection.js';
|
||||
import { MainChannel } from './api/stream/channels/main.js';
|
||||
import { AdminChannel } from './api/stream/channels/admin.js';
|
||||
import { AntennaChannel } from './api/stream/channels/antenna.js';
|
||||
@@ -49,6 +49,7 @@ import { ChatUserChannel } from './api/stream/channels/chat-user.js';
|
||||
import { ChatRoomChannel } from './api/stream/channels/chat-room.js';
|
||||
import { ReversiChannel } from './api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannel } from './api/stream/channels/reversi-game.js';
|
||||
import { WorldRoomChannel } from './api/stream/channels/world-room.js';
|
||||
import { NoteStreamingHidingService } from './api/stream/NoteStreamingHidingService.js';
|
||||
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
|
||||
|
||||
@@ -99,6 +100,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
||||
QueueStatsChannel,
|
||||
ServerStatsChannel,
|
||||
UserListChannel,
|
||||
WorldRoomChannel,
|
||||
NoteStreamingHidingService,
|
||||
OpenApiServerService,
|
||||
OAuth2ProviderService,
|
||||
|
||||
@@ -446,4 +446,14 @@ export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitati
|
||||
export * as 'chat/rooms/invitations/outbox' from './endpoints/chat/rooms/invitations/outbox.js';
|
||||
export * as 'chat/history' from './endpoints/chat/history.js';
|
||||
export * as 'chat/read-all' from './endpoints/chat/read-all.js';
|
||||
export * as 'world/rooms/create' from './endpoints/world/rooms/create.js';
|
||||
export * as 'world/rooms/update' from './endpoints/world/rooms/update.js';
|
||||
export * as 'world/rooms/delete' from './endpoints/world/rooms/delete.js';
|
||||
export * as 'world/rooms/list-by-user' from './endpoints/world/rooms/list-by-user.js';
|
||||
export * as 'world/rooms/show' from './endpoints/world/rooms/show.js';
|
||||
export * as 'world/avatars/create' from './endpoints/world/avatars/create.js';
|
||||
export * as 'world/avatars/update' from './endpoints/world/avatars/update.js';
|
||||
export * as 'world/avatars/delete' from './endpoints/world/avatars/delete.js';
|
||||
export * as 'world/avatars/list' from './endpoints/world/avatars/list.js';
|
||||
export * as 'world/avatars/show' from './endpoints/world/avatars/show.js';
|
||||
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:worldAvatar',
|
||||
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
max: 10,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldAvatarDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
},
|
||||
required: ['name', 'def'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// TODO: validate avatar
|
||||
|
||||
const avatar = await this.worldAvatarService.create(me, {
|
||||
name: ps.name,
|
||||
def: ps.def,
|
||||
});
|
||||
return await this.worldAvatarEntityService.packDetailed(avatar);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldAvatar',
|
||||
|
||||
errors: {
|
||||
noSuchAvatar: {
|
||||
message: 'No such avatar.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'd4e3753d-97bf-4a19-ab8e-21080fbc0f4c',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['avatarId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const avatar = await this.worldAvatarService.findMyAvatarById(me.id, ps.avatarId);
|
||||
if (avatar == null) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
await this.worldAvatarService.delete(avatar, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldAvatar',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldAvatarDetailed',
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
|
||||
|
||||
const avatars = await this.worldAvatarService.getMyAvatarsWithPagination(me.id, ps.limit, sinceId, untilId);
|
||||
return this.worldAvatarEntityService.packDetailedMany(avatars, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldAvatar',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldAvatarDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAvatar: {
|
||||
message: 'No such avatar.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: '857ae02f-8759-4d20-9adb-6e95fffe4fd8',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['avatarId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
private worldAvatarEntityService: WorldAvatarEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const avatar = await this.worldAvatarService.findAvatarById(ps.avatarId);
|
||||
if (avatar == null) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
if (avatar.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
return this.worldAvatarEntityService.packDetailed(avatar, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldAvatarService } from '@/core/WorldAvatarService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldAvatar'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldAvatar',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAvatar: {
|
||||
message: 'No such avatar.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'fcdb0f92-bda6-47f9-bd05-343e0e020933',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
active: { type: 'boolean' },
|
||||
},
|
||||
required: ['avatarId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldAvatarService: WorldAvatarService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const avatar = await this.worldAvatarService.findMyAvatarById(me.id, ps.avatarId);
|
||||
if (avatar == null) {
|
||||
throw new ApiError(meta.errors.noSuchAvatar);
|
||||
}
|
||||
|
||||
// TODO: validate avatar
|
||||
|
||||
await this.worldAvatarService.update(avatar, {
|
||||
name: ps.name,
|
||||
def: ps.def,
|
||||
active: ps.active,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { WorldRoomEntityService } from '@/core/entities/WorldRoomEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
kind: 'write:worldRoom',
|
||||
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
max: 10,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldRoomDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
description: { type: 'string', maxLength: 1024 },
|
||||
visibility: { type: 'string', enum: ['public', 'private'] },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
},
|
||||
required: ['name', 'visibility', 'def'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomService: WorldRoomService,
|
||||
private worldRoomEntityService: WorldRoomEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// TODO: validate room
|
||||
|
||||
const room = await this.worldRoomService.create(me, {
|
||||
name: ps.name,
|
||||
description: ps.description ?? '',
|
||||
visibility: ps.visibility,
|
||||
def: ps.def,
|
||||
});
|
||||
return await this.worldRoomEntityService.packDetailed(room);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldRoom',
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'd4e3753d-97bf-4a19-ab8e-21080fbc0f4c',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomService: WorldRoomService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.worldRoomService.findMyRoomById(me.id, ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
await this.worldRoomService.delete(room, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { WorldRoomEntityService } from '@/core/entities/WorldRoomEntityService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldRoom',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldRoomLite',
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomEntityService: WorldRoomEntityService,
|
||||
private worldRoomService: WorldRoomService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
|
||||
|
||||
const rooms = await this.worldRoomService.getRoomsOfUserWithPagination(ps.userId, ps.userId === me.id, ps.limit, sinceId, untilId);
|
||||
return this.worldRoomEntityService.packLiteMany(rooms, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { WorldRoomEntityService } from '@/core/entities/WorldRoomEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:worldRoom',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'WorldRoomDetailed',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: '857ae02f-8759-4d20-9adb-6e95fffe4fd8',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomService: WorldRoomService,
|
||||
private worldRoomEntityService: WorldRoomEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.worldRoomService.findRoomById(ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
if (room.userId !== me.id && room.visibility === 'private') {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
return this.worldRoomEntityService.packDetailed(room, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['worldRoom'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:worldRoom',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchRoom: {
|
||||
message: 'No such room.',
|
||||
code: 'NO_SUCH_ROOM',
|
||||
id: 'fcdb0f92-bda6-47f9-bd05-343e0e020933',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
roomId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', maxLength: 256 },
|
||||
description: { type: 'string', maxLength: 1024 },
|
||||
visibility: { type: 'string', enum: ['public', 'private'] },
|
||||
def: { type: 'object', additionalProperties: true },
|
||||
},
|
||||
required: ['roomId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private worldRoomService: WorldRoomService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const room = await this.worldRoomService.findMyRoomById(me.id, ps.roomId);
|
||||
if (room == null) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
// TODO: validate room
|
||||
|
||||
await this.worldRoomService.update(room, {
|
||||
name: ps.name,
|
||||
description: ps.description,
|
||||
visibility: ps.visibility,
|
||||
def: ps.def,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import { ChatUserChannel } from '@/server/api/stream/channels/chat-user.js';
|
||||
import { ChatRoomChannel } from '@/server/api/stream/channels/chat-room.js';
|
||||
import { ReversiChannel } from '@/server/api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannel } from '@/server/api/stream/channels/reversi-game.js';
|
||||
import { WorldRoomChannel } from '@/server/api/stream/channels/world-room.js';
|
||||
import type { ChannelRequest } from './channel.js';
|
||||
import type { ChannelConstructor } from './channel.js';
|
||||
import type Channel from './channel.js';
|
||||
@@ -338,6 +339,7 @@ export default class Connection {
|
||||
case 'chatRoom': return ChatRoomChannel;
|
||||
case 'reversi': return ReversiChannel;
|
||||
case 'reversiGame': return ReversiGameChannel;
|
||||
case 'worldRoom': return WorldRoomChannel;
|
||||
|
||||
default:
|
||||
throw new Error(`no such channel: ${name}`);
|
||||
|
||||
115
packages/backend/src/server/api/stream/channels/world-room.ts
Normal file
115
packages/backend/src/server/api/stream/channels/world-room.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { JsonObject } from '@/misc/json-value.js';
|
||||
import { WorldRoomService } from '@/core/WorldRoomService.js';
|
||||
import { WorldRoomMultiplayService } from '@/core/WorldRoomMultiplayService.js';
|
||||
import Channel, { type ChannelRequest } from '../channel.js';
|
||||
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
export class WorldRoomChannel extends Channel {
|
||||
public readonly chName = 'worldRoom';
|
||||
public static shouldShare = false;
|
||||
public static requireCredential = true as const;
|
||||
public static kind = 'read:worldRoom';
|
||||
private roomId: string;
|
||||
private intervalId: NodeJS.Timeout;
|
||||
private isEntered = false;
|
||||
|
||||
constructor(
|
||||
@Inject(REQUEST)
|
||||
request: ChannelRequest,
|
||||
|
||||
private worldRoomService: WorldRoomService,
|
||||
private worldRoomMultiplayService: WorldRoomMultiplayService,
|
||||
) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: JsonObject): Promise<boolean> {
|
||||
if (typeof params.roomId !== 'string') return false;
|
||||
if (!this.user) return false;
|
||||
|
||||
this.roomId = params.roomId;
|
||||
|
||||
const room = await this.worldRoomService.findRoomById(this.roomId);
|
||||
if (room == null) return false;
|
||||
|
||||
try {
|
||||
await this.enter();
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.subscriber.on(`worldRoomStream:${this.roomId}`, this.onEvent);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async enter() {
|
||||
if (this.isEntered) return;
|
||||
|
||||
await this.worldRoomMultiplayService.enter(this.user!.id, this.roomId);
|
||||
|
||||
this.isEntered = true;
|
||||
|
||||
this.send('entered', {
|
||||
playerProfiles: await this.worldRoomMultiplayService.getPlayerProfiles(this.roomId, this.user!.id),
|
||||
});
|
||||
|
||||
this.intervalId = setInterval(async () => {
|
||||
const states = await this.worldRoomMultiplayService.getPlayerStatesAndHeatbeat(this.user!.id, this.roomId);
|
||||
delete states[this.user!.id];
|
||||
this.send('sync', states);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onEvent(data: GlobalEvents['worldRoom']['payload']) {
|
||||
switch (data.type) {
|
||||
case 'enter': {
|
||||
if (data.body.user.id === this.user!.id) return; // 自分の入室は無視
|
||||
this.send('playerEntered', {
|
||||
id: data.body.user.id,
|
||||
profile: this.worldRoomMultiplayService.packPlayerProfile(data.body.user, data.body.avatar),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'left': {
|
||||
if (data.body.userId === this.user!.id) return; // 自分の退室は無視
|
||||
this.send('playerLeft', {
|
||||
id: data.body.userId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
switch (type) {
|
||||
case 'update':
|
||||
if (this.roomId && this.isEntered) {
|
||||
this.worldRoomMultiplayService.updatePlayerState(this.user!.id, this.roomId, body);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose() {
|
||||
this.subscriber.off(`worldRoomStream:${this.roomId}`, this.onEvent);
|
||||
|
||||
clearInterval(this.intervalId);
|
||||
this.worldRoomMultiplayService.left(this.user!.id, this.roomId);
|
||||
}
|
||||
}
|
||||
5
packages/frontend-misskey-world-engine/README.md
Normal file
5
packages/frontend-misskey-world-engine/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# frontend用Misskey Worldエンジン
|
||||
|
||||
エンジンはWeb Worker内で動作し、ほぼすべてのMisskey Webの機能は使えないため、意図しないそれらへの参照/依存が原理的に発生しないように別パッケージとする
|
||||
|
||||
ただしヘッドレス動作することは(今のところ)意図していない
|
||||
28
packages/frontend-misskey-world-engine/eslint.config.js
Normal file
28
packages/frontend-misskey-world-engine/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import sharedConfig from '../shared/eslint.config.js';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default [
|
||||
...sharedConfig,
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules',
|
||||
'built',
|
||||
'coverage',
|
||||
'jest.config.ts',
|
||||
'test',
|
||||
'test-d',
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
project: ['./tsconfig.json'],
|
||||
sourceType: 'module',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
34
packages/frontend-misskey-world-engine/package.json
Normal file
34
packages/frontend-misskey-world-engine/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "frontend-misskey-world-engine",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
|
||||
"typecheck": "tsgo --noEmit",
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.2",
|
||||
"@typescript-eslint/parser": "8.59.2",
|
||||
"esbuild": "0.28.0",
|
||||
"execa": "9.6.1",
|
||||
"nodemon": "3.1.14",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6"
|
||||
},
|
||||
"files": [
|
||||
"built"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babylonjs/core": "9.11.0",
|
||||
"@babylonjs/inspector": "9.11.0",
|
||||
"@babylonjs/loaders": "9.11.0",
|
||||
"@babylonjs/materials": "9.11.0",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"eventemitter3": "5.0.4",
|
||||
"seedrandom": "3.0.5",
|
||||
"tinycolor2": "1.6.0",
|
||||
"hls.js": "1.6.16"
|
||||
}
|
||||
}
|
||||
126
packages/frontend-misskey-world-engine/src/EngineBase.ts
Normal file
126
packages/frontend-misskey-world-engine/src/EngineBase.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
const IN_WEB_WORKER = typeof window === 'undefined';
|
||||
|
||||
export type EngineBaseEvents = {
|
||||
'loadingProgress': (ctx: { progress: number }) => void;
|
||||
'contextlost': (ctx: { reason: string; message: string; }) => void;
|
||||
};
|
||||
|
||||
export abstract class EngineBase<EVs extends EngineBaseEvents> extends EventEmitter<{
|
||||
'ev': (ctx: { type: keyof EVs; ctx: Parameters<EVs[keyof EVs]>[0] }) => void;
|
||||
}> {
|
||||
declare _eventTypes?: EVs;
|
||||
|
||||
protected engine: BABYLON.WebGPUEngine;
|
||||
public scene: BABYLON.Scene;
|
||||
protected fps: number | null = null;
|
||||
protected disposed = false;
|
||||
|
||||
public inputs: EventEmitter<{
|
||||
'click': (event: { x: number; y: number; }) => void;
|
||||
'keydown': (event: { code: string; shiftKey: boolean; }) => void;
|
||||
'keyup': (event: { code: string; shiftKey: boolean; }) => void;
|
||||
'wheel': (event: { deltaY: number; }) => void;
|
||||
'zoom': (event: { delta: number; }) => void;
|
||||
'pointer': (event: { x: number; y: number; }) => void;
|
||||
}> = new EventEmitter();
|
||||
|
||||
constructor(options: {
|
||||
engine: BABYLON.WebGPUEngine;
|
||||
fps: number | null;
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.fps = options.fps;
|
||||
|
||||
this.engine = options.engine;
|
||||
// doNotHandleContextLostがtrueだとそもそも呼ばれない
|
||||
//babylonEngine.onContextLostObservable.add(() => {
|
||||
// os.alert({
|
||||
// type: 'error',
|
||||
// title: i18n.ts.somethingHappened,
|
||||
// text: i18n.ts._miWorld.crushed_description,
|
||||
// });
|
||||
//});
|
||||
this.engine._device.lost.then((info) => { // TODO: babylonEngineの内部プロパティに依存しない方法をforumで聞く
|
||||
this.ev('contextlost', { reason: info.reason, message: info.message }); // transferableじゃないデータが含まれている可能性も考慮してinfoそのままは送らない
|
||||
});
|
||||
|
||||
this.scene = new BABYLON.Scene(this.engine);
|
||||
}
|
||||
|
||||
private currentRafId: number | null = null;
|
||||
|
||||
protected startRenderLoop() {
|
||||
if (this.fps == null) {
|
||||
this.engine.runRenderLoop(() => {
|
||||
this.scene.render();
|
||||
});
|
||||
} else {
|
||||
let then = 0;
|
||||
const interval = 1000 / this.fps;
|
||||
|
||||
const renderLoop = (timeStamp: number) => {
|
||||
if (this.disposed) return;
|
||||
|
||||
// workerで実行される可能性がある
|
||||
this.currentRafId = requestAnimationFrame(renderLoop);
|
||||
|
||||
const delta = timeStamp - then;
|
||||
if (delta <= interval) return;
|
||||
then = timeStamp - (delta % interval);
|
||||
|
||||
this.engine.beginFrame();
|
||||
this.scene.render();
|
||||
this.engine.endFrame();
|
||||
};
|
||||
|
||||
// workerで実行される可能性がある
|
||||
this.currentRafId = requestAnimationFrame(renderLoop);
|
||||
}
|
||||
}
|
||||
|
||||
public pauseRender() { // TODO: srと同じく参照カウント方式にした方が便利そう
|
||||
this.engine.stopRenderLoop();
|
||||
if (this.currentRafId != null) {
|
||||
// workerで実行される可能性がある
|
||||
cancelAnimationFrame(this.currentRafId);
|
||||
this.currentRafId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public resumeRender() {
|
||||
this.startRenderLoop();
|
||||
}
|
||||
|
||||
public abstract init(): Promise<void>;
|
||||
|
||||
protected ev<K extends keyof EVs>(type: K, ctx: Parameters<EVs[K]>[0]) {
|
||||
this.emit('ev', { type, ctx });
|
||||
}
|
||||
|
||||
public async takeScreenshot() {
|
||||
return await BABYLON.Tools.CreateScreenshotAsync(this.engine, this.scene.activeCamera!, { precision: 1 });
|
||||
}
|
||||
|
||||
public abstract resize(): void;
|
||||
|
||||
public destroy() {
|
||||
this.engine.stopRenderLoop();
|
||||
if (this.currentRafId != null) {
|
||||
// workerで実行される可能性がある
|
||||
cancelAnimationFrame(this.currentRafId);
|
||||
this.currentRafId = null;
|
||||
}
|
||||
this.engine.dispose();
|
||||
this.scene.dispose();
|
||||
this.disposed = true;
|
||||
}
|
||||
}
|
||||
264
packages/frontend-misskey-world-engine/src/PlayerContainer.ts
Normal file
264
packages/frontend-misskey-world-engine/src/PlayerContainer.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { AccessoryContainer } from './avatars/AccessoryContainer.js';
|
||||
import { getAccessoryDef } from './avatars/accessory-defs.js';
|
||||
import { Timer } from './utility.js';
|
||||
import type { WorldAvatar } from 'misskey-world/src/types.js';
|
||||
|
||||
export type PlayerProfile = {
|
||||
user: {
|
||||
name: string;
|
||||
username: string;
|
||||
avatarUrl: string;
|
||||
} | null;
|
||||
avatar: WorldAvatar;
|
||||
};
|
||||
|
||||
export type PlayerState = {
|
||||
position: [number, number, number],
|
||||
rotation: [number, number, number],
|
||||
sit?: string; // id
|
||||
};
|
||||
|
||||
const DEFAULT_FACE_PARTS_EYES = {
|
||||
'_none_': null,
|
||||
'a': '/client-assets/world/avatars/eyes-a.png',
|
||||
'b': '/client-assets/world/avatars/eyes-b.png',
|
||||
'c': '/client-assets/world/avatars/eyes-c.png',
|
||||
'd': '/client-assets/world/avatars/eyes-d.png',
|
||||
'e': '/client-assets/world/avatars/eyes-e.png',
|
||||
'f': '/client-assets/world/avatars/eyes-f.png',
|
||||
'g': '/client-assets/world/avatars/eyes-g.png',
|
||||
};
|
||||
|
||||
const DEFAULT_FACE_PARTS_MOUTH = {
|
||||
'_none_': null,
|
||||
'a': '/client-assets/world/avatars/mouth-a.png',
|
||||
'b': '/client-assets/world/avatars/mouth-b.png',
|
||||
'c': '/client-assets/world/avatars/mouth-c.png',
|
||||
'd': '/client-assets/world/avatars/mouth-d.png',
|
||||
'e': '/client-assets/world/avatars/mouth-e.png',
|
||||
'f': '/client-assets/world/avatars/mouth-f.png',
|
||||
'g': '/client-assets/world/avatars/mouth-g.png',
|
||||
'h': '/client-assets/world/avatars/mouth-h.png',
|
||||
'i': '/client-assets/world/avatars/mouth-i.png',
|
||||
};
|
||||
|
||||
export class PlayerContainer {
|
||||
public id: string;
|
||||
private profile: PlayerProfile;
|
||||
public root: BABYLON.TransformNode;
|
||||
private subRootContainerForAnim: BABYLON.TransformNode;
|
||||
private subRoot: BABYLON.TransformNode;
|
||||
private modelRoot: BABYLON.TransformNode | null = null;
|
||||
private sr: BABYLON.SnapshotRenderingHelper;
|
||||
private scene: BABYLON.Scene;
|
||||
public registerMeshes: (meshes: BABYLON.Mesh[]) => void = () => {};
|
||||
private animationObserver: BABYLON.Observer<BABYLON.Scene> | null = null;
|
||||
private accessoryContainers: AccessoryContainer[] = [];
|
||||
private timer: Timer = new Timer();
|
||||
|
||||
constructor(params: { id: string; profile: PlayerProfile; state: PlayerState | null; sr: BABYLON.SnapshotRenderingHelper; scene: BABYLON.Scene; }) {
|
||||
this.id = params.id;
|
||||
this.profile = params.profile;
|
||||
this.sr = params.sr;
|
||||
this.scene = params.scene;
|
||||
|
||||
this.root = new BABYLON.TransformNode(`player:${this.id}`, params.scene);
|
||||
this.root.rotationQuaternion = null;
|
||||
this.subRootContainerForAnim = new BABYLON.TransformNode(`player:${this.id}:subRootContainerForAnim`, params.scene);
|
||||
this.subRootContainerForAnim.parent = this.root;
|
||||
this.subRoot = new BABYLON.TransformNode(`player:${this.id}:subRoot`, params.scene);
|
||||
this.subRoot.parent = this.subRootContainerForAnim;
|
||||
if (params.state) this.applyState(params.state, true);
|
||||
|
||||
console.log('PlayerContainer created', this.id);
|
||||
}
|
||||
|
||||
public async loadAvatar() {
|
||||
const filePath = '/client-assets/world/avatars/default.glb';
|
||||
const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene);
|
||||
|
||||
// babylonによって自動で追加される右手系変換用ノード
|
||||
const modelRootMesh = loaderResult.meshes[0] as BABYLON.Mesh;
|
||||
|
||||
// meshじゃなくtransform nodeにしてパフォーマンス向上
|
||||
this.modelRoot = new BABYLON.TransformNode('__root__', this.scene);
|
||||
this.modelRoot.parent = this.subRoot;
|
||||
this.modelRoot.scaling.x = -1;
|
||||
this.modelRoot.scaling = this.modelRoot.scaling.scale(WORLD_SCALE);// cmをmに
|
||||
|
||||
for (const m of modelRootMesh.getChildren()) {
|
||||
if (m.parent === modelRootMesh) {
|
||||
m.parent = this.modelRoot;
|
||||
}
|
||||
}
|
||||
|
||||
modelRootMesh.dispose();
|
||||
|
||||
//const avatarTex = new BABYLON.Texture(this.profile.avatarUrl, this.scene, false, false);
|
||||
|
||||
const eyesBlinkTexture = new BABYLON.Texture('/client-assets/world/avatars/eyes-blink.png', this.scene, false, false);
|
||||
eyesBlinkTexture.hasAlpha = true;
|
||||
|
||||
let eyesTex: BABYLON.Texture | null = null;
|
||||
if (this.profile.avatar.eyes.type in DEFAULT_FACE_PARTS_EYES) {
|
||||
const eyesTexPath = DEFAULT_FACE_PARTS_EYES[this.profile.avatar.eyes.type];
|
||||
if (eyesTexPath) {
|
||||
eyesTex = new BABYLON.Texture(eyesTexPath, this.scene, false, false);
|
||||
eyesTex.hasAlpha = true;
|
||||
}
|
||||
}
|
||||
|
||||
let mouthTex: BABYLON.Texture | null = null;
|
||||
if (this.profile.avatar.mouth.type in DEFAULT_FACE_PARTS_MOUTH) {
|
||||
const mouthTexPath = DEFAULT_FACE_PARTS_MOUTH[this.profile.avatar.mouth.type];
|
||||
if (mouthTexPath) {
|
||||
mouthTex = new BABYLON.Texture(mouthTexPath, this.scene, false, false);
|
||||
mouthTex.hasAlpha = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const mesh of this.modelRoot.getChildMeshes()) {
|
||||
//if (mesh.name.includes('__AVATAR__')) {
|
||||
// const mat = new BABYLON.PBRMaterial('', this.scene);
|
||||
// mat.albedoColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
// mat.albedoTexture = avatarTex;
|
||||
// mat.emissiveColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
// mat.emissiveTexture = avatarTex;
|
||||
// mat.roughness = 0;
|
||||
// mat.metallic = 0;
|
||||
// mat.backFaceCulling = false;
|
||||
// mesh.material = mat;
|
||||
//}
|
||||
if (mesh.name.includes('__BODY__')) {
|
||||
mesh.material.albedoColor = new BABYLON.Color3(this.profile.avatar.body.color[0], this.profile.avatar.body.color[1], this.profile.avatar.body.color[2]);
|
||||
}
|
||||
if (mesh.name.includes('__EYES__')) {
|
||||
const mat = new BABYLON.PBRMaterial('', this.scene);
|
||||
mat.albedoColor = new BABYLON.Color3(this.profile.avatar.eyes.color[0], this.profile.avatar.eyes.color[1], this.profile.avatar.eyes.color[2]);
|
||||
mat.albedoTexture = eyesTex;
|
||||
mat.roughness = 1;
|
||||
mat.metallic = 0;
|
||||
mesh.material = mat;
|
||||
|
||||
// TODO: SRを無効にせずに表現する方法を考える
|
||||
const blink = () => {
|
||||
if (mesh.isDisposed()) return;
|
||||
|
||||
this.sr.disableSnapshotRendering();
|
||||
mat.albedoTexture = eyesBlinkTexture;
|
||||
this.sr.enableSnapshotRendering();
|
||||
|
||||
this.timer.setTimeout(() => {
|
||||
this.sr.disableSnapshotRendering();
|
||||
mat.albedoTexture = eyesTex;
|
||||
this.sr.enableSnapshotRendering();
|
||||
|
||||
this.timer.setTimeout(() => {
|
||||
blink();
|
||||
}, Math.random() * 10000);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
this.timer.setTimeout(() => {
|
||||
blink();
|
||||
}, Math.random() * 10000);
|
||||
}
|
||||
if (mesh.name.includes('__MOUTH__')) {
|
||||
if (mouthTex != null) {
|
||||
const mat = new BABYLON.PBRMaterial('', this.scene);
|
||||
mat.albedoColor = new BABYLON.Color3(this.profile.avatar.mouth.color[0], this.profile.avatar.mouth.color[1], this.profile.avatar.mouth.color[2]);
|
||||
mat.albedoTexture = mouthTex;
|
||||
mat.roughness = 1;
|
||||
mat.metallic = 0;
|
||||
mesh.material = mat;
|
||||
} else {
|
||||
mesh.isVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.registerMeshes(this.modelRoot.getChildMeshes());
|
||||
|
||||
this.accessoryContainers = await Promise.all(this.profile.avatar.accessories.map(ac => this.loadAccessory({
|
||||
type: ac.type,
|
||||
id: ac.id,
|
||||
position: new BABYLON.Vector3(0, cm(19), 0),
|
||||
rotation: new BABYLON.Vector3(0, 0, 0),
|
||||
options: ac.options,
|
||||
})));
|
||||
|
||||
const anim = new BABYLON.Animation('', 'position.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: cm(0) },
|
||||
{ frame: 30, value: cm(-2) },
|
||||
{ frame: 60, value: cm(0) },
|
||||
{ frame: 90, value: cm(2) },
|
||||
{ frame: 120, value: cm(0) },
|
||||
]);
|
||||
this.subRootContainerForAnim.animations = [anim];
|
||||
this.animationObserver = this.scene.onAfterAnimationsObservable.add(() => {
|
||||
this.sr.updateMesh(this.subRootContainerForAnim.getChildMeshes(), false);
|
||||
});
|
||||
this.scene.beginAnimation(this.subRootContainerForAnim, 0, 120, true);
|
||||
}
|
||||
|
||||
private async loadAccessory(args: {
|
||||
type: string;
|
||||
id: string;
|
||||
position: BABYLON.Vector3;
|
||||
rotation: BABYLON.Vector3;
|
||||
options: Record<string, unknown>;
|
||||
}) {
|
||||
const def = getAccessoryDef(args.type);
|
||||
|
||||
const container = new AccessoryContainer({
|
||||
id: args.id,
|
||||
type: args.type,
|
||||
position: args.position.clone(),
|
||||
rotation: args.rotation.clone(),
|
||||
options: args.options,
|
||||
sr: this.sr,
|
||||
getIsSrReady: () => true,
|
||||
lightContainer: this.lightContainer,
|
||||
graphicsQuality: this.graphicsQuality,
|
||||
scene: this.scene,
|
||||
});
|
||||
container.registerMeshes = (meshes) => {
|
||||
this.registerMeshes(meshes);
|
||||
};
|
||||
|
||||
await container.load();
|
||||
|
||||
container.root.parent = this.subRoot;
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
public applyState(state: PlayerState, forInit = false) {
|
||||
this.root.position.set(...state.position);
|
||||
this.subRoot.rotation.set(...state.rotation);
|
||||
if (!forInit) {
|
||||
const meshes = this.root.getChildMeshes();
|
||||
if (meshes.length > 0) this.sr.updateMesh(meshes);
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.timer.dispose();
|
||||
if (this.animationObserver != null) {
|
||||
this.scene.onAfterAnimationsObservable.remove(this.animationObserver);
|
||||
}
|
||||
for (const ac of this.accessoryContainers) {
|
||||
ac.destroy();
|
||||
}
|
||||
this.accessoryContainers = [];
|
||||
this.root.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { ArcRotateCameraManualInput, getMeshesBoundingBox, GRAPHICS_QUALITY } from './utility.js';
|
||||
import { PlayerContainer, type PlayerProfile } from './PlayerContainer.js';
|
||||
import { EngineBase } from './EngineBase.js';
|
||||
import { deepClone } from './clone.js';
|
||||
import type { WorldAvatar } from 'misskey-world/src/types.js';
|
||||
|
||||
export class AvatarPreviewEngine extends EngineBase<{ // PlayerPreviewEngineに改名した方がいいかもしれない
|
||||
'loadingProgress': (ctx: { progress: number }) => void;
|
||||
'contextlost': (ctx: { reason: string; message: string; }) => void;
|
||||
}> {
|
||||
private sr: BABYLON.SnapshotRenderingHelper;
|
||||
private shadowGenerator: BABYLON.ShadowGenerator;
|
||||
private camera: BABYLON.ArcRotateCamera;
|
||||
private avatarOptions: WorldAvatar | null = null;
|
||||
private playerContainer: PlayerContainer | null = null;
|
||||
private envMapIndoor: BABYLON.CubeTexture;
|
||||
private roomLight: BABYLON.SpotLight;
|
||||
private pipeline: BABYLON.DefaultRenderingPipeline;
|
||||
private graphicsQuality: number;
|
||||
private profile: PlayerProfile;
|
||||
|
||||
constructor(profile: PlayerProfile, options: {
|
||||
engine: BABYLON.WebGPUEngine;
|
||||
graphicsQuality: number;
|
||||
fps: number | null;
|
||||
}) {
|
||||
super({
|
||||
engine: options.engine,
|
||||
fps: options.fps,
|
||||
});
|
||||
|
||||
registerBuiltInLoaders();
|
||||
|
||||
this.graphicsQuality = options.graphicsQuality;
|
||||
this.profile = deepClone(profile);
|
||||
|
||||
this.scene.autoClear = false;
|
||||
this.scene.skipPointerMovePicking = true;
|
||||
this.scene.skipFrustumClipping = true; // snapshot renderingでは全てのメッシュがアクティブになっている必要があるため
|
||||
this.scene.clearColor = new BABYLON.Color4(0.01, 0.01, 0.01, 1);
|
||||
|
||||
this.sr = new BABYLON.SnapshotRenderingHelper(this.scene);
|
||||
|
||||
this.camera = new BABYLON.ArcRotateCamera('camera', Math.PI / 2, Math.PI / 2.5, cm(300), new BABYLON.Vector3(0, cm(90), 0), this.scene);
|
||||
this.camera.minZ = cm(1);
|
||||
this.camera.maxZ = cm(100000);
|
||||
this.camera.fov = 0.5;
|
||||
this.camera.lowerRadiusLimit = cm(50);
|
||||
this.camera.upperRadiusLimit = cm(1000);
|
||||
this.camera.inputs.clear();
|
||||
this.camera.inputs.add(new ArcRotateCameraManualInput(this.scene, {
|
||||
rotationSensitivity: 0.0005,
|
||||
}));
|
||||
|
||||
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.scene);
|
||||
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(500), cm(500), cm(500));
|
||||
this.envMapIndoor.level = 0.6;
|
||||
|
||||
this.roomLight = new BABYLON.SpotLight('roomLight', new BABYLON.Vector3(cm(50), cm(249), cm(50)), new BABYLON.Vector3(0, -1, 0), 16, 8, this.scene);
|
||||
this.roomLight.diffuse = new BABYLON.Color3(1.0, 0.9, 0.8);
|
||||
this.roomLight.shadowMinZ = cm(10);
|
||||
this.roomLight.shadowMaxZ = cm(500);
|
||||
this.roomLight.radius = cm(30);
|
||||
this.roomLight.intensity = 15 * WORLD_SCALE * WORLD_SCALE;
|
||||
|
||||
this.shadowGenerator = new BABYLON.ShadowGenerator(2048, this.roomLight);
|
||||
this.shadowGenerator.forceBackFacesOnly = true;
|
||||
this.shadowGenerator.bias = 0.0001;
|
||||
this.shadowGenerator.usePercentageCloserFiltering = true;
|
||||
this.shadowGenerator.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
|
||||
this.shadowGenerator.getShadowMap().refreshRate = 60;
|
||||
|
||||
const gl = new BABYLON.GlowLayer('glow', this.scene, {
|
||||
blurKernelSize: 64,
|
||||
});
|
||||
gl.intensity = 0.5;
|
||||
this.scene.setRenderingAutoClearDepthStencil(gl.renderingGroupId, false);
|
||||
this.sr.updateMeshesForEffectLayer(gl);
|
||||
|
||||
this.pipeline = new BABYLON.DefaultRenderingPipeline('default', true, this.scene);
|
||||
this.pipeline.samples = 4;
|
||||
if (this.graphicsQuality >= GRAPHICS_QUALITY.HIGH) {
|
||||
this.pipeline.bloomEnabled = true;
|
||||
this.pipeline.bloomThreshold = 0.95;
|
||||
this.pipeline.bloomWeight = 0.1;
|
||||
this.pipeline.bloomKernel = 256;
|
||||
this.pipeline.bloomScale = 2;
|
||||
}
|
||||
this.pipeline.sharpenEnabled = true;
|
||||
this.pipeline.sharpen.edgeAmount = 0.5;
|
||||
}
|
||||
|
||||
public async init() {
|
||||
this.startRenderLoop();
|
||||
|
||||
await this.scene.whenReadyAsync();
|
||||
this.sr.enableSnapshotRendering();
|
||||
|
||||
this.inputs.on('wheel', (ev) => {
|
||||
this.camera.fov += ev.deltaY * 0.0005;
|
||||
this.camera.fov = Math.max(0.25, Math.min(0.5, this.camera.fov));
|
||||
});
|
||||
|
||||
this.inputs.on('zoom', (ev) => {
|
||||
this.camera.fov += -ev.delta * 0.0015;
|
||||
this.camera.fov = Math.max(0.25, Math.min(0.5, this.camera.fov));
|
||||
});
|
||||
|
||||
this.inputs.on('pointer', (ev) => {
|
||||
(this.camera.inputs.attached.manual as ArcRotateCameraManualInput).setRotationVector({ x: ev.x, y: ev.y });
|
||||
});
|
||||
|
||||
await this.load();
|
||||
}
|
||||
|
||||
private async load() {
|
||||
this.sr.disableSnapshotRendering();
|
||||
|
||||
this.playerContainer = new PlayerContainer({
|
||||
id: '',
|
||||
profile: this.profile,
|
||||
state: {
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0],
|
||||
},
|
||||
sr: this.sr,
|
||||
scene: this.scene,
|
||||
});
|
||||
this.playerContainer.registerMeshes = (meshes) => {
|
||||
for (const mesh of meshes) {
|
||||
mesh.receiveShadows = true;
|
||||
this.shadowGenerator.addShadowCaster(mesh);
|
||||
|
||||
if (mesh.material) {
|
||||
if (mesh.material instanceof BABYLON.MultiMaterial) {
|
||||
for (const subMat of mesh.material.subMaterials) {
|
||||
(subMat as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor;
|
||||
(subMat as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
|
||||
(subMat as BABYLON.PBRMaterial).anisotropy.isEnabled = false; // なんかきれいにレンダリングされないため
|
||||
}
|
||||
} else {
|
||||
(mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor;
|
||||
(mesh.material as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
|
||||
(mesh.material as BABYLON.PBRMaterial).anisotropy.isEnabled = false; // なんかきれいにレンダリングされないため
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.scene.meshes.includes(mesh)) this.scene.addMesh(mesh);
|
||||
}
|
||||
};
|
||||
|
||||
await this.playerContainer.loadAvatar();
|
||||
|
||||
const boundingInfo = getMeshesBoundingBox(this.playerContainer.root.getChildMeshes().filter(m => m.isEnabled() && m.isVisible), true);
|
||||
|
||||
this.camera.setTarget(new BABYLON.Vector3(0, boundingInfo.centerWorld.y, 0));
|
||||
|
||||
// zoom to fit
|
||||
const size = boundingInfo.extendSize;
|
||||
const distance = Math.max(size.x, size.y, size.z) * 2;
|
||||
this.camera.radius = distance * 5;
|
||||
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public clearPlayer() {
|
||||
this.sr.disableSnapshotRendering();
|
||||
if (this.playerContainer != null) {
|
||||
this.playerContainer.destroy();
|
||||
this.playerContainer = null;
|
||||
}
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public async updateAvatar(value: WorldAvatar) {
|
||||
this.profile.avatar = value;
|
||||
this.clearPlayer();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
public resize() {
|
||||
// 一旦snapshot renderingを無効にしておかないとエラーが出る(babylonのバグ?)
|
||||
// ~~...が、一旦無効にしたらしたで複数のマテリアルがそれぞれ入れ替わる(?)という謎の現象が発生するためコメントアウトしとく(エラー出てもレンダリングが止まったりするわけでもないし)~~
|
||||
// ↑追記: engine.resizeした後に一瞬待つことで回避できることが判明
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.engine.resize(true);
|
||||
// workerで実行される可能性がある
|
||||
|
||||
setTimeout(() => {
|
||||
this.sr.enableSnapshotRendering();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
this.playerContainer?.destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { AvatarPreviewEngine } from './avatarPreviewEngine.js';
|
||||
import { registerBabylonRuntime } from './babylonRuntime.js';
|
||||
import type { PlayerProfile } from './PlayerContainer.js';
|
||||
|
||||
registerBabylonRuntime();
|
||||
|
||||
export async function createAvatarPreviewEngine(params: {
|
||||
canvas: HTMLCanvasElement; options: { graphicsQuality: number; resolution: number; fps: number | null }; profile: PlayerProfile;
|
||||
}) {
|
||||
const babylonEngine = new BABYLON.WebGPUEngine(params.canvas, { doNotHandleContextLost: true, powerPreference: 'low-power', antialias: true });
|
||||
babylonEngine.compatibilityMode = false;
|
||||
babylonEngine.enableOfflineSupport = false;
|
||||
await babylonEngine.initAsync();
|
||||
if (params.options.resolution === 2) babylonEngine.setHardwareScalingLevel(0.5);
|
||||
if (params.options.resolution === 0.5) babylonEngine.setHardwareScalingLevel(2);
|
||||
|
||||
const engine = new AvatarPreviewEngine(params.profile, {
|
||||
engine: babylonEngine,
|
||||
...params.options,
|
||||
});
|
||||
|
||||
return engine;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { AvatarPreviewEngine } from './avatarPreviewEngine.js';
|
||||
import { registerBabylonRuntime } from './babylonRuntime.js';
|
||||
import type { PlayerProfile } from './PlayerContainer.js';
|
||||
|
||||
registerBabylonRuntime();
|
||||
|
||||
let engine: AvatarPreviewEngine | null = null;
|
||||
let canvas: OffscreenCanvas | null = null;
|
||||
|
||||
// TODO: 他のWorkerと実装を共通化
|
||||
onmessage = async (event) => {
|
||||
//console.log('Worker received message:', event.data);
|
||||
|
||||
switch (event.data?.type) {
|
||||
case 'init': {
|
||||
const profile = event.data.profile as PlayerProfile;
|
||||
canvas = event.data.canvas as OffscreenCanvas;
|
||||
const babylonEngine = new BABYLON.WebGPUEngine(canvas, { doNotHandleContextLost: true, powerPreference: 'low-power', antialias: true });
|
||||
babylonEngine.compatibilityMode = false;
|
||||
babylonEngine.enableOfflineSupport = false;
|
||||
await babylonEngine.initAsync();
|
||||
if (event.data.options.resolution === 2) babylonEngine.setHardwareScalingLevel(0.5);
|
||||
if (event.data.options.resolution === 0.5) babylonEngine.setHardwareScalingLevel(2);
|
||||
|
||||
engine = new AvatarPreviewEngine(profile, {
|
||||
engine: babylonEngine,
|
||||
...event.data.options,
|
||||
});
|
||||
|
||||
engine.on('ev', ({ type, ctx }) => {
|
||||
self.postMessage({ type: 'ev', ev: { type, ctx } });
|
||||
});
|
||||
|
||||
await engine.init();
|
||||
|
||||
self.postMessage({ type: 'inited' });
|
||||
break;
|
||||
}
|
||||
case 'resize': {
|
||||
canvas.width = event.data.width;
|
||||
canvas.height = event.data.height;
|
||||
if (engine != null) engine.resize();
|
||||
break;
|
||||
}
|
||||
case 'input:keydown': {
|
||||
if (engine == null) break;
|
||||
engine.inputs.emit('keydown', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'input:keyup': {
|
||||
if (engine == null) break;
|
||||
engine.inputs.emit('keyup', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'input:click': {
|
||||
if (engine == null) break;
|
||||
engine.inputs.emit('click', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'input:wheel': {
|
||||
if (engine == null) break;
|
||||
engine.inputs.emit('wheel', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'input:zoom': {
|
||||
if (engine == null) break;
|
||||
engine.inputs.emit('zoom', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'input:pointer': {
|
||||
if (engine == null) break;
|
||||
engine.inputs.emit('pointer', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'call': {
|
||||
if (engine == null) {
|
||||
console.error('Failed to call: Engine is not initialized yet!!!');
|
||||
break;
|
||||
}
|
||||
const res = engine[event.data.fn](...(event.data.args ?? []));
|
||||
if (event.data.needReturnValue) {
|
||||
if (res instanceof Promise) {
|
||||
res.then((r) => {
|
||||
self.postMessage({ type: 'return', id: event.data.id, value: r });
|
||||
});
|
||||
} else {
|
||||
self.postMessage({ type: 'return', id: event.data.id, value: res });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'set': {
|
||||
if (engine == null) {
|
||||
console.error('Failed to set: Engine is not initialized yet!!!');
|
||||
break;
|
||||
}
|
||||
engine[event.data.key] = event.data.value;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn('Unrecognized message type:', event.data?.type);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { camelToKebab, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { ModelExplorer, scaleMorph, Timer } from '../utility.js';
|
||||
import { convertRawOptions, type ConvertedOptions, type RawOptions } from '../mono.js';
|
||||
import { getAccessoryDef } from './accessory-defs.js';
|
||||
import type { AvatarAccessoryInstance } from './accessory.js';
|
||||
|
||||
export class AccessoryContainer {
|
||||
public id: string;
|
||||
public type: string;
|
||||
private options: ConvertedOptions;
|
||||
public root: BABYLON.TransformNode;
|
||||
private subRoot: BABYLON.TransformNode | null = null;
|
||||
public instance: AvatarAccessoryInstance | null = null;
|
||||
public model: ModelExplorer | null = null;
|
||||
private scene: BABYLON.Scene;
|
||||
public registerMeshes: (meshes: BABYLON.Mesh[]) => void = () => {};
|
||||
private sr: BABYLON.SnapshotRenderingHelper;
|
||||
private getIsSrReady: () => boolean;
|
||||
private lightContainer: BABYLON.ClusteredLightContainer;
|
||||
private graphicsQuality: number;
|
||||
private timer: Timer = new Timer();
|
||||
|
||||
constructor(args: {
|
||||
id: string;
|
||||
type: string;
|
||||
options: RawOptions;
|
||||
position: BABYLON.Vector3;
|
||||
rotation: BABYLON.Vector3;
|
||||
sr: BABYLON.SnapshotRenderingHelper;
|
||||
getIsSrReady: () => boolean;
|
||||
lightContainer: BABYLON.ClusteredLightContainer;
|
||||
scene: BABYLON.Scene;
|
||||
graphicsQuality: number;
|
||||
}) {
|
||||
this.id = args.id;
|
||||
this.type = args.type;
|
||||
const def = getAccessoryDef(this.type);
|
||||
this.options = convertRawOptions(def.options.schema, args.options, { files: [] });
|
||||
this.sr = args.sr;
|
||||
this.getIsSrReady = args.getIsSrReady;
|
||||
this.lightContainer = args.lightContainer;
|
||||
this.scene = args.scene;
|
||||
this.graphicsQuality = args.graphicsQuality;
|
||||
this.root = new BABYLON.TransformNode(`accessory_${args.id}_${args.type}`, this.scene);
|
||||
this.root.position = args.position;
|
||||
this.root.rotation = args.rotation;
|
||||
}
|
||||
|
||||
public async load() {
|
||||
const def = getAccessoryDef(this.type);
|
||||
|
||||
const filePath = def.path != null ? `/client-assets/world/objects/${def.path(this.options)}.glb` : `/client-assets/world/objects/${camelToKebab(this.type)}/${camelToKebab(this.type)}.glb`;
|
||||
const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene);
|
||||
|
||||
// babylonによって自動で追加される右手系変換用ノード
|
||||
const subRootMesh = loaderResult.meshes[0] as BABYLON.Mesh;
|
||||
|
||||
// meshじゃなくtransform nodeにしてパフォーマンス向上
|
||||
this.subRoot = new BABYLON.TransformNode('__root__', this.scene);
|
||||
this.subRoot.parent = this.root;
|
||||
this.subRoot.scaling.x = -1;
|
||||
this.subRoot.scaling = this.subRoot.scaling.scale(WORLD_SCALE);// cmをmに
|
||||
|
||||
for (const m of subRootMesh.getChildren()) {
|
||||
if (m.parent === subRootMesh) {
|
||||
m.parent = this.subRoot;
|
||||
}
|
||||
}
|
||||
|
||||
subRootMesh.dispose();
|
||||
|
||||
this.registerMeshes(this.subRoot.getChildMeshes());
|
||||
|
||||
this.model = new ModelExplorer(this.subRoot);
|
||||
|
||||
this.instance = await def.createInstance({
|
||||
scene: this.scene,
|
||||
sr: {
|
||||
updateMesh: (mesh) => {
|
||||
if (!this.getIsSrReady()) return;
|
||||
this.sr.updateMesh(mesh);
|
||||
},
|
||||
reset: () => {
|
||||
if (!this.getIsSrReady()) return;
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.sr.enableSnapshotRendering();
|
||||
},
|
||||
fixParticleSystem: (ps) => this.sr.fixParticleSystem(ps),
|
||||
},
|
||||
lc: this.lightContainer,
|
||||
root: this.root,
|
||||
options: this.options,
|
||||
model: this.model!,
|
||||
timer: this.timer,
|
||||
graphicsQuality: this.graphicsQuality,
|
||||
reloadModel: () => {
|
||||
this.reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async reload() {
|
||||
this.timer.dispose();
|
||||
this.instance?.dispose?.();
|
||||
this.instance = null;
|
||||
this.model = null;
|
||||
this.subRoot?.dispose();
|
||||
this.root.removeChild(this.subRoot);
|
||||
this.scene.removeTransformNode(this.subRoot);
|
||||
|
||||
this.timer = new Timer();
|
||||
|
||||
await this.load();
|
||||
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public optionsUpdated(options: Record<string, unknown>, key: string, value: any) {
|
||||
if (this.instance == null) return;
|
||||
this.options[key] = options[key]; // 参照を切れさせないようにプロパティ個別にmutate
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.instance.onOptionsUpdated?.([key, this.options[key]]);
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.timer.dispose();
|
||||
this.instance?.dispose?.();
|
||||
this.subRoot.dispose();
|
||||
this.root.dispose();
|
||||
this.scene.removeTransformNode(this.root);
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { mikan_schema } from 'misskey-world/src/avatars/accessories/mikan.schema.js';
|
||||
import { defineAccessory } from '../accessory.js';
|
||||
|
||||
export const mikan = defineAccessory(mikan_schema, {
|
||||
createInstance: ({ scene, root, sr }) => {
|
||||
return {
|
||||
dispose: () => {
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm } from 'misskey-world/src/utility.js';
|
||||
import { mug_schema } from 'misskey-world/src/avatars/accessories/mug.schema.js';
|
||||
import { defineAccessory } from '../accessory.js';
|
||||
|
||||
export const mug = defineAccessory(mug_schema, {
|
||||
createInstance: ({ options, scene, root, sr, model }) => {
|
||||
const emitter = new BABYLON.TransformNode('emitter', scene);
|
||||
emitter.parent = root;
|
||||
emitter.position = new BABYLON.Vector3(0, cm(5), 0);
|
||||
const ps = new BABYLON.ParticleSystem('steamParticleSystem', 8, scene);
|
||||
ps.particleTexture = new BABYLON.Texture('/client-assets/world/objects/mug/steam.png');
|
||||
ps.emitter = emitter;
|
||||
ps.minEmitBox = new BABYLON.Vector3(cm(-1), 0, cm(-1));
|
||||
ps.maxEmitBox = new BABYLON.Vector3(cm(1), 0, cm(1));
|
||||
ps.minEmitPower = cm(10);
|
||||
ps.maxEmitPower = cm(12);
|
||||
ps.minLifeTime = 2;
|
||||
ps.maxLifeTime = 3;
|
||||
ps.addSizeGradient(0, cm(10), cm(12));
|
||||
ps.addSizeGradient(1, cm(18), cm(20));
|
||||
ps.direction1 = new BABYLON.Vector3(-0.3, 1, 0.3);
|
||||
ps.direction2 = new BABYLON.Vector3(0.3, 1, -0.3);
|
||||
ps.emitRate = 0.5;
|
||||
ps.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;
|
||||
ps.color1 = new BABYLON.Color4(1, 1, 1, 0.3);
|
||||
ps.color2 = new BABYLON.Color4(1, 1, 1, 0.2);
|
||||
ps.colorDead = new BABYLON.Color4(1, 1, 1, 0);
|
||||
ps.preWarmCycles = Math.random() * 1000;
|
||||
ps.start();
|
||||
sr.fixParticleSystem(ps);
|
||||
|
||||
const bodyMaterial = model.findMaterial('__X_MUG__');
|
||||
|
||||
const applyBodyMat = () => {
|
||||
bodyMaterial.albedoColor = new BABYLON.Color3(options.bodyMat.color[0], options.bodyMat.color[1], options.bodyMat.color[2]);
|
||||
bodyMaterial.roughness = options.bodyMat.roughness;
|
||||
bodyMaterial.metallic = options.bodyMat.metallic;
|
||||
};
|
||||
|
||||
applyBodyMat();
|
||||
|
||||
const liquidMaterial = model.findMaterial('__X_LIQUID__');
|
||||
|
||||
const applyLiquidMat = () => {
|
||||
liquidMaterial.albedoColor = new BABYLON.Color3(options.liquidMat.color[0], options.liquidMat.color[1], options.liquidMat.color[2]);
|
||||
liquidMaterial.roughness = options.liquidMat.roughness;
|
||||
liquidMaterial.metallic = options.liquidMat.metallic;
|
||||
};
|
||||
|
||||
applyLiquidMat();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'bodyMat': applyBodyMat(); break;
|
||||
case 'liquidMat': applyLiquidMat(); break;
|
||||
}
|
||||
},
|
||||
dispose: () => {
|
||||
ps.stop();
|
||||
emitter.dispose();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { mug } from './accessories/mug.js';
|
||||
import { mikan } from './accessories/mikan.js';
|
||||
import type { AvatarAccessoryDef } from './accessory.js';
|
||||
|
||||
export const AVATAR_ACCESSORY_DEFS = [
|
||||
mug,
|
||||
mikan,
|
||||
] as AvatarAccessoryDef[];
|
||||
|
||||
export function getAccessoryDef(type: string): AvatarAccessoryDef {
|
||||
const def = AVATAR_ACCESSORY_DEFS.find(x => x.id === type) as AvatarAccessoryDef | undefined;
|
||||
if (def == null) {
|
||||
throw new Error(`Unrecognized accessory type: ${type}`);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { ModelExplorer, type Timer } from '../utility.js';
|
||||
import type { AccessorySchemaDef } from 'misskey-world/src/avatars/accessory.js';
|
||||
import type { OptionsSchema } from 'misskey-world/src/mono.js';
|
||||
import type { ConvertedOptions, GetConvertedOptionsSchemaValues } from '../mono.js';
|
||||
|
||||
export type AvatarAccessoryInstance<Options = any> = {
|
||||
onOptionsUpdated?: <K extends keyof Options, V extends Options[K]>(kv: [K, V]) => void;
|
||||
dispose: () => void;
|
||||
};
|
||||
|
||||
export type SnapshotRenderingHelperWrapper = {
|
||||
updateMesh: (meshes: BABYLON.Mesh[]) => void;
|
||||
reset: () => void;
|
||||
fixParticleSystem: (ps: BABYLON.ParticleSystem) => void;
|
||||
};
|
||||
|
||||
export type AvatarAccessoryDef<Schema extends AccessorySchemaDef = AccessorySchemaDef> = Schema & {
|
||||
path?: (options: string extends keyof Schema['options']['schema'] ? ConvertedOptions : Readonly<GetConvertedOptionsSchemaValues<Schema['options']['schema']>>) => string;
|
||||
createInstance: (args: {
|
||||
scene: BABYLON.Scene;
|
||||
// TODO: snapshot renderingの関心を隠蔽した方が綺麗かもしれない
|
||||
// 例えばmaterialUpdatedというメソッドを用意して内部的にresetを呼ぶなど
|
||||
sr: SnapshotRenderingHelperWrapper;
|
||||
lc: BABYLON.ClusteredLightContainer | null;
|
||||
root: BABYLON.TransformNode;
|
||||
options: string extends keyof Schema['options']['schema'] ? ConvertedOptions : Readonly<GetConvertedOptionsSchemaValues<Schema['options']['schema']>>;
|
||||
model: ModelExplorer;
|
||||
timer: Timer;
|
||||
graphicsQuality: number;
|
||||
reloadModel: () => void;
|
||||
}) => AvatarAccessoryInstance<string extends keyof Schema['options']['schema'] ? ConvertedOptions : GetConvertedOptionsSchemaValues<Schema['options']['schema']>> | Promise<AvatarAccessoryInstance<Schema['options']['schema'] extends undefined ? ConvertedOptions : GetConvertedOptionsSchemaValues<Schema['options']['schema']>>>; // TODO: createInstanceをasyncにするのではなく、別にreadyみたいなものを返させる
|
||||
};
|
||||
|
||||
export function defineAccessorySchema<const OpSc extends OptionsSchema>(def: AccessorySchemaDef<OpSc>): AccessorySchemaDef<OpSc> {
|
||||
return def;
|
||||
}
|
||||
|
||||
export function defineAccessory<const Schema extends AccessorySchemaDef<any>>(schema: Schema, def: Pick<AvatarAccessoryDef<Schema>, 'path' | 'createInstance'>): AvatarAccessoryDef<Schema> {
|
||||
return { ...schema, ...def };
|
||||
}
|
||||
35
packages/frontend-misskey-world-engine/src/babylonRuntime.ts
Normal file
35
packages/frontend-misskey-world-engine/src/babylonRuntime.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
//import '@babylonjs/core';
|
||||
|
||||
export function registerBabylonRuntime(): void {
|
||||
BABYLON.RegisterStandardEngineExtensions();
|
||||
BABYLON.RegisterStandardWebGPUEngineExtensions();
|
||||
BABYLON.RegisterAbstractEngineAlpha();
|
||||
BABYLON.RegisterAbstractEngineTexture();
|
||||
BABYLON.RegisterAbstractEngineCubeTexture();
|
||||
BABYLON.RegisterAbstractEngineQuery();
|
||||
BABYLON.RegisterAbstractEngineTextureSelector();
|
||||
BABYLON.RegisterAbstractEngineTimeQuery();
|
||||
BABYLON.RegisterAbstractEngineViews();
|
||||
BABYLON.RegisterEnginesWebGPUExtensionsEngineRawTexture();
|
||||
BABYLON.RegisterEnginesWebGPUExtensionsEngineReadTexture();
|
||||
BABYLON.RegisterEnginesWebGPUExtensionsEngineCubeTexture();
|
||||
BABYLON.RegisterEnginesWebGPUExtensionsEngineRenderTargetCube();
|
||||
BABYLON.RegisterEnginesWebGPUExtensionsEngineQuery();
|
||||
BABYLON.RegisterEnginesWebGPUExtensionsEngineDynamicTexture();
|
||||
BABYLON.RegisterBufferAlign();
|
||||
BABYLON.RegisterCubeTexture();
|
||||
BABYLON.RegisterStandardMaterial();
|
||||
BABYLON.RegisterRay();
|
||||
BABYLON.RegisterAnimation();
|
||||
BABYLON.RegisterAnimatable();
|
||||
BABYLON.RegisterCollisionCoordinator();
|
||||
BABYLON.RegisterPostProcessRenderPipelineManagerSceneComponent(
|
||||
BABYLON.PostProcessRenderPipelineManager,
|
||||
);
|
||||
}
|
||||
25
packages/frontend-misskey-world-engine/src/clone.ts
Normal file
25
packages/frontend-misskey-world-engine/src/clone.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// structredCloneが遅いため
|
||||
// SEE: http://var.blog.jp/archives/86038606.html
|
||||
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
|
||||
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
|
||||
|
||||
export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[];
|
||||
|
||||
export function deepClone<T extends Cloneable>(x: T): T {
|
||||
if (typeof x === 'object') {
|
||||
if (x === null) return x;
|
||||
if (Array.isArray(x)) return x.map(deepClone) as T;
|
||||
const obj = {} as Record<string | number | symbol, Cloneable>;
|
||||
for (const [k, v] of Object.entries(x)) {
|
||||
obj[k] = v === undefined ? undefined : deepClone(v);
|
||||
}
|
||||
return obj as T;
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}
|
||||
597
packages/frontend-misskey-world-engine/src/engine.ts
Normal file
597
packages/frontend-misskey-world-engine/src/engine.ts
Normal file
@@ -0,0 +1,597 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import Hls from 'hls.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { RecyvlingTextGrid, Timer, createPlaneUvMapper, randomRange } from './utility.js';
|
||||
import { TIME_MAP } from './utility.js';
|
||||
import { EngineBase } from './EngineBase.js';
|
||||
|
||||
const SNAPSHOT_RENDERING = false; // 実験的
|
||||
const USE_GLOW = true; // ドローコールが増えて重い
|
||||
const IN_WEB_WORKER = typeof window === 'undefined';
|
||||
|
||||
export class WorldEngine extends EngineBase<{
|
||||
'playSfxUrl': (ctx: {
|
||||
url: string;
|
||||
options: {
|
||||
volume: number;
|
||||
playbackRate: number;
|
||||
};
|
||||
}) => void;
|
||||
'loadingProgress': (ctx: { progress: number }) => void;
|
||||
}> {
|
||||
private shadowGeneratorForSunLight: BABYLON.ShadowGenerator;
|
||||
public camera: BABYLON.UniversalCamera;
|
||||
private time: 0 | 1 | 2 = 0; // 0: 昼, 1: 夕, 2: 夜
|
||||
private envMap: BABYLON.CubeTexture;
|
||||
public lightContainer: BABYLON.ClusteredLightContainer;
|
||||
public sr: BABYLON.SnapshotRenderingHelper;
|
||||
private gl: BABYLON.GlowLayer | null = null;
|
||||
private textMaterial: BABYLON.StandardMaterial;
|
||||
private translucentTextMaterial: BABYLON.StandardMaterial;
|
||||
private reflectionProbe: BABYLON.ReflectionProbe;
|
||||
public timer: Timer = new Timer();
|
||||
public isSitting = false;
|
||||
|
||||
constructor(options: {
|
||||
canvas: HTMLCanvasElement;
|
||||
engine: BABYLON.WebGPUEngine;
|
||||
}) {
|
||||
super({
|
||||
engine: options.engine,
|
||||
fps: null,
|
||||
});
|
||||
|
||||
registerBuiltInLoaders();
|
||||
|
||||
this.scene.autoClear = false;
|
||||
//this.scene.autoClearDepthAndStencil = false;
|
||||
this.scene.skipPointerMovePicking = true;
|
||||
this.scene.skipFrustumClipping = true; // snapshot renderingでは全てのメッシュがアクティブになっている必要があるため
|
||||
this.scene.gravity = new BABYLON.Vector3(0, -0.1, 0).scale(WORLD_SCALE);
|
||||
|
||||
this.sr = new BABYLON.SnapshotRenderingHelper(this.scene);
|
||||
|
||||
const skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(50000) }, this.scene);
|
||||
const skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.scene);
|
||||
skyboxMat.backFaceCulling = false;
|
||||
skyboxMat.disableLighting = true;
|
||||
skybox.material = skyboxMat;
|
||||
skybox.infiniteDistance = true;
|
||||
|
||||
this.time = TIME_MAP[new Date().getHours() as keyof typeof TIME_MAP];
|
||||
//this.time = TIME_MAP[12 as keyof typeof TIME_MAP];
|
||||
|
||||
if (this.time === 0) {
|
||||
skyboxMat.emissiveColor = new BABYLON.Color3(1, 1, 1);
|
||||
} else if (this.time === 1) {
|
||||
skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.68, 0.66);
|
||||
} else {
|
||||
skyboxMat.emissiveColor = new BABYLON.Color3(0.48, 0.5, 0.6);
|
||||
}
|
||||
this.scene.ambientColor = new BABYLON.Color3(0.9, 0.9, 0.9);
|
||||
|
||||
this.envMap = BABYLON.CubeTexture.CreateFromPrefilteredData(this.time === 2 ? '/client-assets/room/outdoor-night.env' : '/client-assets/room/outdoor-day.env', this.scene);
|
||||
//this.envMap.level = 1;
|
||||
this.envMap.level = 0;
|
||||
|
||||
this.scene.collisionsEnabled = true;
|
||||
|
||||
this.camera = new BABYLON.UniversalCamera('camera', new BABYLON.Vector3(cm(0), cm(250), cm(3000)), this.scene);
|
||||
this.camera.attachControl(this.canvas);
|
||||
this.camera.minZ = cm(1);
|
||||
this.camera.maxZ = cm(100000);
|
||||
this.camera.fov = 1;
|
||||
this.camera.ellipsoid = new BABYLON.Vector3(cm(15), cm(65), cm(15));
|
||||
this.camera.checkCollisions = true;
|
||||
this.camera.applyGravity = true;
|
||||
this.camera.needMoveForGravity = true;
|
||||
this.camera.keysUp.push(87); // W
|
||||
this.camera.keysDown.push(83); // S
|
||||
this.camera.keysLeft.push(65); // A
|
||||
this.camera.keysRight.push(68); // D
|
||||
const normalSpeed = 0.03 * WORLD_SCALE;
|
||||
this.camera.speed = normalSpeed;
|
||||
this.scene.onKeyboardObservable.add((kbInfo) => {
|
||||
switch (kbInfo.type) {
|
||||
case BABYLON.KeyboardEventTypes.KEYDOWN:
|
||||
if (kbInfo.event.key === 'Shift') {
|
||||
this.camera.speed = normalSpeed * 4;
|
||||
}
|
||||
break;
|
||||
case BABYLON.KeyboardEventTypes.KEYUP:
|
||||
if (kbInfo.event.key === 'Shift') {
|
||||
this.camera.speed = normalSpeed;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
//this.scene.activeCamera = this.camera;
|
||||
|
||||
//this.reflectionProbe = new BABYLON.ReflectionProbe('rp', 512, this.scene, true, true, false);
|
||||
//this.reflectionProbe.refreshRate = 60;
|
||||
|
||||
//const mainLight = new BABYLON.PointLight('mainLight', new BABYLON.Vector3(0, cm(300), 0), this.scene);
|
||||
//mainLight.intensity = 10000000;
|
||||
//mainLight.radius = cm(1000);
|
||||
//mainLight.diffuse = new BABYLON.Color3(1, 1, 1);
|
||||
|
||||
const ambientLight1 = new BABYLON.HemisphericLight('ambientLight1', new BABYLON.Vector3(0, 1, 0), this.scene);
|
||||
ambientLight1.diffuse = new BABYLON.Color3(1.0, 0.9, 0.8);
|
||||
ambientLight1.intensity = 1;
|
||||
const ambientLight2 = new BABYLON.HemisphericLight('ambientLight2', new BABYLON.Vector3(0, -1, 0), this.scene);
|
||||
ambientLight2.diffuse = new BABYLON.Color3(0.8, 0.9, 1.0);
|
||||
ambientLight2.intensity = 1;
|
||||
//ambientLight.intensity = 0;
|
||||
|
||||
const sunLight = new BABYLON.DirectionalLight('sunLight', new BABYLON.Vector3(0, -1, 0), this.scene);
|
||||
sunLight.position = new BABYLON.Vector3(cm(0), cm(10000), cm(0));
|
||||
sunLight.diffuse = this.time === 0 ? new BABYLON.Color3(1.0, 1.0, 1.0) : this.time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0);
|
||||
sunLight.intensity = this.time === 0 ? 2 : this.time === 1 ? 0.5 : 0.25;
|
||||
sunLight.shadowMinZ = cm(1000);
|
||||
sunLight.shadowMaxZ = cm(2000);
|
||||
|
||||
this.shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(4096, sunLight);
|
||||
this.shadowGeneratorForSunLight.forceBackFacesOnly = true;
|
||||
this.shadowGeneratorForSunLight.bias = 0.0001;
|
||||
this.shadowGeneratorForSunLight.usePercentageCloserFiltering = true;
|
||||
this.shadowGeneratorForSunLight.usePoissonSampling = true;
|
||||
//this.shadowGeneratorForSunLight.getShadowMap().refreshRate = 60;
|
||||
|
||||
this.lightContainer = new BABYLON.ClusteredLightContainer('clustered', [], this.scene);
|
||||
|
||||
if (USE_GLOW) {
|
||||
this.gl = new BABYLON.GlowLayer('glow', this.scene, {
|
||||
//mainTextureFixedSize: 512,
|
||||
blurKernelSize: 64,
|
||||
});
|
||||
this.gl.intensity = 0.5;
|
||||
this.gl.addExcludedMesh(skybox);
|
||||
this.scene.setRenderingAutoClearDepthStencil(this.gl.renderingGroupId, false);
|
||||
|
||||
if (SNAPSHOT_RENDERING) {
|
||||
this.sr.updateMeshesForEffectLayer(this.gl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async init() {
|
||||
await this.loadEnvModel();
|
||||
|
||||
if (SNAPSHOT_RENDERING) {
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
this.inputs.on('keydown', (ev) => {
|
||||
});
|
||||
|
||||
this.inputs.on('wheel', (ev) => {
|
||||
this.camera.fov += ev.deltaY * 0.001;
|
||||
this.camera.fov = Math.max(0.25, Math.min(1, this.camera.fov));
|
||||
});
|
||||
|
||||
this.inputs.on('click', (ev) => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private async loadEnvModel() {
|
||||
const envObj = await BABYLON.ImportMeshAsync('/client-assets/world/lobby/default.glb', this.scene);
|
||||
envObj.meshes[0].scaling = envObj.meshes[0].scaling.scale(WORLD_SCALE);
|
||||
envObj.meshes[0].bakeCurrentTransformIntoVertices();
|
||||
for (const mesh of envObj.meshes) {
|
||||
if (mesh.name === '__root__') continue;
|
||||
if (mesh.name.includes('__COLLISION__')) {
|
||||
mesh.checkCollisions = true;
|
||||
mesh.isVisible = false;
|
||||
}
|
||||
if (this.reflectionProbe != null) {
|
||||
if (mesh.material) (mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.reflectionProbe.cubeTexture;
|
||||
if (mesh.material) (mesh.material as BABYLON.PBRMaterial).realTimeFiltering = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.textMaterial = new BABYLON.StandardMaterial('textMaterial', this.scene);
|
||||
this.textMaterial.diffuseTexture = new BABYLON.Texture('/client-assets/world/chars.png', this.scene, false, false);
|
||||
this.textMaterial.diffuseTexture.hasAlpha = true;
|
||||
this.textMaterial.disableLighting = true;
|
||||
this.textMaterial.transparencyMode = BABYLON.Material.MATERIAL_ALPHABLEND;
|
||||
this.textMaterial.useAlphaFromDiffuseTexture = true;
|
||||
this.textMaterial.freeze();
|
||||
|
||||
this.translucentTextMaterial = this.textMaterial.clone('translucentTextMaterial');
|
||||
this.translucentTextMaterial.alpha = 0.25;
|
||||
|
||||
{
|
||||
const objet = envObj.meshes.find(m => m.name.includes('__OBJET__'));
|
||||
objet.rotation = objet.rotationQuaternion.toEulerAngles();
|
||||
objet.rotationQuaternion = null;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 5000, value: -(Math.PI * 2) },
|
||||
]);
|
||||
objet.animations = [anim];
|
||||
this.scene.beginAnimation(objet, 0, 5000, true);
|
||||
}
|
||||
|
||||
{
|
||||
const ring = envObj.meshes.find(m => m.name.includes('__LED_RING__'));
|
||||
ring.rotation = ring.rotationQuaternion.toEulerAngles();
|
||||
ring.rotationQuaternion = null;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 5000, value: -(Math.PI * 2) },
|
||||
]);
|
||||
ring.animations = [anim];
|
||||
this.scene.beginAnimation(ring, 0, 5000, true);
|
||||
}
|
||||
|
||||
{
|
||||
const messageRingRoot = new BABYLON.TransformNode('', this.scene);
|
||||
const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_OUTER_1__'));
|
||||
messageRing.parent = messageRingRoot;
|
||||
messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles();
|
||||
messageRing.rotationQuaternion = null;
|
||||
const text = new RecyvlingTextGrid(messageRing, 256, {
|
||||
meshFlipped: true,
|
||||
material: this.textMaterial,
|
||||
});
|
||||
|
||||
text.write('Wellcome to Misskey World!');
|
||||
|
||||
//messageRingRoot.rotation.x = Math.PI / 4;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 10000, value: -(Math.PI * 2) },
|
||||
]);
|
||||
messageRing.animations = [anim];
|
||||
this.scene.beginAnimation(messageRing, 0, 10000, true);
|
||||
|
||||
const texts = [
|
||||
'Wellcome to Misskey World!',
|
||||
'Enjoy your stay!',
|
||||
'Feel free to look around!',
|
||||
'This is a virtual space for Misskey users!',
|
||||
//'You can chat, play games, and more!',
|
||||
//'Check out the bulletin board for announcements',
|
||||
'Have a nice day with Misskey!',
|
||||
'MAINTENANCE will begin at 9:00 A.M.',
|
||||
];
|
||||
|
||||
let currentTextIndex = 1;
|
||||
|
||||
this.timer.setInterval(() => {
|
||||
const textToShow = texts[currentTextIndex];
|
||||
currentTextIndex = (currentTextIndex + 1) % texts.length;
|
||||
text.writeWithAnimation(textToShow);
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
{
|
||||
const messageRingRoot = new BABYLON.TransformNode('', this.scene);
|
||||
const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_OUTER_2__'));
|
||||
messageRing.parent = messageRingRoot;
|
||||
messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles();
|
||||
messageRing.rotationQuaternion = null;
|
||||
const text = new RecyvlingTextGrid(messageRing, 256, {
|
||||
meshFlipped: true,
|
||||
material: this.translucentTextMaterial,
|
||||
repeatSeparator: ' ',
|
||||
});
|
||||
|
||||
messageRingRoot.rotation.x = Math.PI / 2;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 10000, value: -(Math.PI * 2) },
|
||||
]);
|
||||
messageRing.animations = [anim];
|
||||
this.scene.beginAnimation(messageRing, 0, 10000, true);
|
||||
|
||||
this.timer.setInterval(() => {
|
||||
text.write(Date.now().toString());
|
||||
}, 10);
|
||||
}
|
||||
|
||||
{
|
||||
const messageRingRoot = new BABYLON.TransformNode('', this.scene);
|
||||
const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_INNER_1__'));
|
||||
messageRing.parent = messageRingRoot;
|
||||
messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles();
|
||||
messageRing.rotationQuaternion = null;
|
||||
const text = new RecyvlingTextGrid(messageRing, 64, {
|
||||
material: this.textMaterial,
|
||||
repeatSeparator: ' ',
|
||||
});
|
||||
|
||||
//messageRingRoot.rotation.x = Math.PI / 4;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 10000, value: (Math.PI * 2) },
|
||||
]);
|
||||
messageRing.animations = [anim];
|
||||
this.scene.beginAnimation(messageRing, 0, 10000, true);
|
||||
|
||||
this.timer.setInterval(() => {
|
||||
const now = new Date();
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0');
|
||||
text.write(`${hours}:${minutes}:${seconds}`);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
{
|
||||
const messageRingRoot = new BABYLON.TransformNode('', this.scene);
|
||||
const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_INNER_2__'));
|
||||
messageRing.parent = messageRingRoot;
|
||||
messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles();
|
||||
messageRing.rotationQuaternion = null;
|
||||
const text = new RecyvlingTextGrid(messageRing, 64, {
|
||||
material: this.textMaterial,
|
||||
repeatSeparator: ' ',
|
||||
});
|
||||
|
||||
//messageRingRoot.rotation.x = Math.PI / 4;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 10000, value: -(Math.PI * 2) },
|
||||
]);
|
||||
messageRing.animations = [anim];
|
||||
this.scene.beginAnimation(messageRing, 0, 10000, true);
|
||||
|
||||
this.timer.setInterval(() => {
|
||||
const now = new Date();
|
||||
const years = now.getFullYear().toString();
|
||||
const months = (now.getMonth() + 1).toString().padStart(2, '0');
|
||||
const days = now.getDate().toString().padStart(2, '0');
|
||||
text.write(`${years}/${months}/${days}`);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const sphereRoot = new BABYLON.TransformNode('', this.scene);
|
||||
sphereRoot.position = new BABYLON.Vector3(cm(0), cm(1000 + (100 * i)), cm(0));
|
||||
const rotation = Math.random() * Math.PI * 2;
|
||||
const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(randomRange(50, 300)), segments: 16 }, this.scene);
|
||||
sphere.parent = sphereRoot;
|
||||
sphere.position = new BABYLON.Vector3(cm(0), cm(0), cm(randomRange(2000, 7000)));
|
||||
|
||||
const mat = new BABYLON.PBRMaterial('', this.scene);
|
||||
const color = tinycolor({ h: Math.random() * 360, s: 1, l: 0.5 }).toRgb();
|
||||
mat.emissiveColor = new BABYLON.Color3(color.r / 255, color.g / 255, color.b / 255);
|
||||
mat.disableLighting = true;
|
||||
this.gl?.addExcludedMesh(sphere);
|
||||
sphere.material = mat;
|
||||
|
||||
const speed = randomRange(5000, 30000);
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: rotation },
|
||||
{ frame: speed, value: Math.random() < 0.5 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) },
|
||||
]);
|
||||
sphereRoot.animations = [anim];
|
||||
this.scene.beginAnimation(sphereRoot, 0, speed, true);
|
||||
|
||||
if (this.reflectionProbe != null) this.reflectionProbe.renderList.push(sphere);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 64; i++) {
|
||||
const sphereRoot = new BABYLON.TransformNode('', this.scene);
|
||||
sphereRoot.position = new BABYLON.Vector3(cm(0), cm(randomRange(-5000, 5000)), cm(0));
|
||||
const rotation = Math.random() * Math.PI * 2;
|
||||
const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(randomRange(500, 3000)), segments: 16 }, this.scene);
|
||||
sphere.parent = sphereRoot;
|
||||
sphere.position = new BABYLON.Vector3(cm(0), cm(0), cm(randomRange(10000, 15000)));
|
||||
|
||||
const mat = new BABYLON.PBRMaterial('', this.scene);
|
||||
const color = tinycolor({ h: Math.random() * 360, s: randomRange(0, 1), l: randomRange(0.75, 1) }).toRgb();
|
||||
mat.emissiveColor = new BABYLON.Color3(color.r / 255, color.g / 255, color.b / 255);
|
||||
mat.disableLighting = true;
|
||||
this.gl?.addExcludedMesh(sphere);
|
||||
sphere.material = mat;
|
||||
|
||||
const speed = randomRange(10000, 100000);
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: rotation },
|
||||
{ frame: speed, value: Math.random() < 0.5 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) },
|
||||
]);
|
||||
sphereRoot.animations = [anim];
|
||||
this.scene.beginAnimation(sphereRoot, 0, speed, true);
|
||||
|
||||
if (this.reflectionProbe != null) this.reflectionProbe.renderList.push(sphere);
|
||||
}
|
||||
|
||||
//const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(10) }, this.scene);
|
||||
|
||||
const adsCountCol = 4;
|
||||
const adsCountRow = 2;
|
||||
for (let j = 0; j < adsCountRow; j++) {
|
||||
for (let i = 0; i < adsCountCol; i++) {
|
||||
const adRoot = new BABYLON.TransformNode(`ad_${j}_${i}_root`, this.scene);
|
||||
adRoot.position = new BABYLON.Vector3(cm(0), cm(500 + (1000 * j)), cm(0));
|
||||
const rotation = (i / adsCountCol) * Math.PI * 2;
|
||||
const adMesh = BABYLON.MeshBuilder.CreatePlane(`ad_${j}_${i}`, { width: cm(1000), height: cm(700) }, this.scene);
|
||||
adMesh.parent = adRoot;
|
||||
adMesh.position = new BABYLON.Vector3(cm(0), cm(0), cm(7500));
|
||||
|
||||
const tex = new BABYLON.Texture('/client-assets/world/lobby/dummy-ads/angry_ai.png', this.scene);
|
||||
const adMat = new BABYLON.StandardMaterial(`ad_${j}_${i}_mat`, this.scene);
|
||||
adMat.emissiveTexture = tex;
|
||||
adMat.disableLighting = true;
|
||||
adMesh.material = adMat;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: rotation },
|
||||
{ frame: 15000, value: j % 2 === 0 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) },
|
||||
]);
|
||||
adRoot.animations = [anim];
|
||||
this.scene.beginAnimation(adRoot, 0, 15000, true);
|
||||
}
|
||||
}
|
||||
|
||||
const worldRingH = envObj.meshes.find(m => m.name.includes('__WORLD_RING_H__'));
|
||||
const worldRingM = envObj.meshes.find(m => m.name.includes('__WORLD_RING_M__'));
|
||||
|
||||
worldRingH.material.reflectionTexture = null;
|
||||
worldRingM.material.reflectionTexture = null;
|
||||
|
||||
if (this.reflectionProbe != null) this.reflectionProbe.renderList.push(worldRingH);
|
||||
if (this.reflectionProbe != null) this.reflectionProbe.renderList.push(worldRingM);
|
||||
|
||||
worldRingH.rotation = worldRingH.rotationQuaternion.toEulerAngles();
|
||||
worldRingM.rotation = worldRingM.rotationQuaternion.toEulerAngles();
|
||||
worldRingH.rotationQuaternion = null;
|
||||
worldRingM.rotationQuaternion = null;
|
||||
|
||||
const _1h = 1000 * 60 * 60;
|
||||
const _12h = _1h * 12;
|
||||
const _7days = _1h * 24 * 7;
|
||||
const _30days = _1h * 24 * 30;
|
||||
|
||||
this.timer.setInterval(() => {
|
||||
const time = Date.now();
|
||||
worldRingH.rotation.x = ((time % _12h) / _12h) * Math.PI * 2;
|
||||
worldRingM.rotation.y = -(((time % _1h) / _1h) * Math.PI);
|
||||
}, 100);
|
||||
|
||||
const screenMeshes = envObj.meshes.filter(m => m.name.includes('__SCREEN__'));
|
||||
const screenMaterial = screenMeshes[0].material as BABYLON.PBRMaterial;
|
||||
|
||||
const videoEl = document.createElement('video');
|
||||
videoEl.crossOrigin = 'anonymous';
|
||||
|
||||
const hls = new Hls();
|
||||
hls.loadSource('https://tvs.misskey.io/official/hq-beta/ts:abr.m3u8');
|
||||
hls.attachMedia(videoEl);
|
||||
|
||||
this.timer.setTimeout(() => {
|
||||
const tex = new BABYLON.VideoTexture('', videoEl, this.scene, true, true);
|
||||
tex.level = 0.5;
|
||||
tex.video.loop = true;
|
||||
tex.video.volume = 0.25;
|
||||
tex.video.muted = true;
|
||||
|
||||
screenMaterial.albedoColor = new BABYLON.Color3(0, 0, 0);
|
||||
screenMaterial.emissiveTexture = tex;
|
||||
screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
|
||||
|
||||
tex.onLoadObservable.addOnce(() => {
|
||||
tex.video.play();
|
||||
for (const mesh of screenMeshes) {
|
||||
if (mesh instanceof BABYLON.InstancedMesh) continue;
|
||||
//normalizeUvToSquare(mesh);
|
||||
const updateUv = createPlaneUvMapper(mesh);
|
||||
if (tex == null) return;
|
||||
const srcAspect = tex.getSize().width / tex.getSize().height;
|
||||
const targetAspect = 16 / 9;
|
||||
updateUv(srcAspect, targetAspect, 'cover');
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
const emitter = new BABYLON.TransformNode('emitter', this.scene);
|
||||
emitter.position = new BABYLON.Vector3(0, cm(-1000), 0);
|
||||
const ps = new BABYLON.ParticleSystem('', 128, this.scene);
|
||||
ps.particleTexture = new BABYLON.Texture('/client-assets/world/objects/lava-lamp/bubble.png');
|
||||
ps.emitter = emitter;
|
||||
ps.isLocal = true;
|
||||
ps.minEmitBox = new BABYLON.Vector3(cm(-1000), 0, cm(-1000));
|
||||
ps.maxEmitBox = new BABYLON.Vector3(cm(1000), 0, cm(1000));
|
||||
ps.minEmitPower = cm(100);
|
||||
ps.maxEmitPower = cm(500);
|
||||
ps.minLifeTime = 30;
|
||||
ps.maxLifeTime = 30;
|
||||
ps.minSize = cm(30);
|
||||
ps.maxSize = cm(300);
|
||||
ps.direction1 = new BABYLON.Vector3(0, 1, 0);
|
||||
ps.direction2 = new BABYLON.Vector3(0, 1, 0);
|
||||
ps.emitRate = 1.5;
|
||||
ps.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;
|
||||
ps.color1 = new BABYLON.Color4(1, 1, 1, 0.3);
|
||||
ps.color2 = new BABYLON.Color4(1, 1, 1, 0.2);
|
||||
ps.colorDead = new BABYLON.Color4(1, 1, 1, 0);
|
||||
ps.preWarmCycles = Math.random() * 1000;
|
||||
ps.start();
|
||||
}
|
||||
|
||||
public sitChair(furnitureId: string) {
|
||||
this.isSitting = true;
|
||||
this.fixedCamera.parent = this.objectMeshs.get(furnitureId);
|
||||
this.fixedCamera.position = new BABYLON.Vector3(0, cm(120), 0);
|
||||
this.fixedCamera.rotation = new BABYLON.Vector3(0, 0, 0);
|
||||
this.scene.activeCamera = this.fixedCamera;
|
||||
this.selectFuniture(null);
|
||||
}
|
||||
|
||||
public standUp() {
|
||||
this.isSitting = false;
|
||||
this.scene.activeCamera = this.camera;
|
||||
this.fixedCamera.parent = null;
|
||||
}
|
||||
|
||||
private playSfxUrl(url: string, options: { volume: number; playbackRate: number }) {
|
||||
this.emit('playSfxUrl', { url, options });
|
||||
}
|
||||
|
||||
public resize() {
|
||||
this.engine.resize(true);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
this.timer.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class MessageRing {
|
||||
constructor(mesh: BABYLON.Mesh, scene: BABYLON.Scene, options: { material: BABYLON.StandardMaterial; repeatSeparator: string; }) {
|
||||
const messageRingRoot = new BABYLON.TransformNode('', this.scene);
|
||||
const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_INNER_1__'));
|
||||
messageRing.parent = messageRingRoot;
|
||||
messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles();
|
||||
messageRing.rotationQuaternion = null;
|
||||
const text = new RecyvlingTextGrid(messageRing, 64, {
|
||||
material: this.textMaterial,
|
||||
repeatSeparator: ' ',
|
||||
});
|
||||
|
||||
//messageRingRoot.rotation.x = Math.PI / 4;
|
||||
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 10000, value: (Math.PI * 2) },
|
||||
]);
|
||||
messageRing.animations = [anim];
|
||||
this.scene.beginAnimation(messageRing, 0, 10000, true);
|
||||
|
||||
this.timer.setInterval(() => {
|
||||
const now = new Date();
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0');
|
||||
text.write(`${hours}:${minutes}:${seconds}`);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
25
packages/frontend-misskey-world-engine/src/id.ts
Normal file
25
packages/frontend-misskey-world-engine/src/id.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// ランダムな文字列が生成できればなんでも良い(時系列でソートできるなら尚良)が、とりあえずaidの実装を拝借
|
||||
|
||||
const TIME2000 = 946684800000;
|
||||
let counter = Math.floor(Math.random() * 10000);
|
||||
|
||||
function getTime(time: number): string {
|
||||
time = time - TIME2000;
|
||||
if (time < 0) time = 0;
|
||||
|
||||
return time.toString(36).padStart(8, '0');
|
||||
}
|
||||
|
||||
function getNoise(): string {
|
||||
return counter.toString(36).padStart(2, '0').slice(-2);
|
||||
}
|
||||
|
||||
export function genId(): string {
|
||||
counter++;
|
||||
return getTime(Date.now()) + getNoise();
|
||||
}
|
||||
52
packages/frontend-misskey-world-engine/src/mono.ts
Normal file
52
packages/frontend-misskey-world-engine/src/mono.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { OptionsSchema, NumberOptionSchema, BooleanOptionSchema, StringOptionSchema, ColorOptionSchema, MaterialOptionSchema, LightOptionSchema, EnumOptionSchema, RangeOptionSchema, ImageOptionSchema, SeedOptionSchema } from 'misskey-world/src/mono.js';
|
||||
|
||||
export type RawOptions = Record<string, unknown> & {
|
||||
readonly __brand: unique symbol;
|
||||
};
|
||||
|
||||
export type ConvertedOptions = Record<string, unknown> & {
|
||||
readonly __brand: unique symbol;
|
||||
};
|
||||
|
||||
type RawImageValue<Presets extends string = string> = { type: Presets | null | '_custom_'; driveFileId?: string | null; fit?: 'cover' | 'contain' | 'stretch'; };
|
||||
|
||||
type ConvertedImageValue<Presets extends string = string> = { type: Presets | null | '_custom_'; custom?: { url: string; } | null; fit?: 'cover' | 'contain' | 'stretch'; };
|
||||
export type GetConvertedOptionsSchemaValues<T extends OptionsSchema> = {
|
||||
[K in keyof T]:
|
||||
T[K] extends NumberOptionSchema ? number :
|
||||
T[K] extends BooleanOptionSchema ? boolean :
|
||||
T[K] extends StringOptionSchema ? string :
|
||||
T[K] extends ColorOptionSchema ? [number, number, number] :
|
||||
T[K] extends MaterialOptionSchema ? { color: [number, number, number]; metallic: number; roughness: number; } :
|
||||
T[K] extends LightOptionSchema ? { color: [number, number, number]; brightness: number; } :
|
||||
T[K] extends EnumOptionSchema ? T[K]['enum'][number]['value'] :
|
||||
T[K] extends RangeOptionSchema ? number :
|
||||
T[K] extends ImageOptionSchema ? ConvertedImageValue<T[K]['presets'][number]['value']> :
|
||||
T[K] extends SeedOptionSchema ? number :
|
||||
never;
|
||||
};
|
||||
|
||||
export function convertRawOptions<OpSc extends OptionsSchema>(schema: OpSc, raw: RawOptions, attachments: { files: { id: string; url: string; }[] }): ConvertedOptions {
|
||||
const converted = {} as ConvertedOptions;
|
||||
for (const record of Object.entries(schema)) {
|
||||
const k = record[0];
|
||||
const v = raw[k];
|
||||
if (record[1].type === 'image') {
|
||||
const _v = v as unknown as RawImageValue;
|
||||
const file = _v.type === '_custom_' ? attachments.files.find(f => f.id === _v.driveFileId) : null;
|
||||
if (file != null && file.url.startsWith('http://syu-win.local:3000/')) { // debug
|
||||
file.url = file.url.replace('http://syu-win.local:3000/', 'https://local-mi.syuilo.dev/');
|
||||
}
|
||||
|
||||
converted[k] = { type: _v.type, custom: file != null ? { url: file.url } : null, fit: _v.fit } satisfies ConvertedImageValue;
|
||||
} else {
|
||||
converted[k] = v;
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { camelToKebab, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { scaleMorph, Timer } from '../utility.js';
|
||||
import { convertRawOptions, type ConvertedOptions, type RawOptions } from '../mono.js';
|
||||
import { getFurnitureDef } from './furniture-defs.js';
|
||||
import { ModelManager, SYSTEM_MESH_NAMES } from './utility.js';
|
||||
import type { RoomFunitureInstance } from './furniture.js';
|
||||
import type { RoomAttachments } from 'misskey-world/src/room/type.js';
|
||||
|
||||
function mergeMeshes(meshes: BABYLON.Mesh[], root: BABYLON.Mesh, hasTexture: boolean) {
|
||||
const excludeMeshes = root.getChildMeshes().filter(m => SYSTEM_MESH_NAMES.some(s => m.name.includes(s)));
|
||||
|
||||
const childMeshes = root.getChildMeshes().filter(m => !excludeMeshes.some(x => x === m) && m.isVisible && !m.isDisposed());
|
||||
|
||||
const toMerge = [] as BABYLON.Mesh[];
|
||||
for (const mesh of childMeshes) {
|
||||
if (mesh instanceof BABYLON.InstancedMesh) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mesh.hasInstances) continue;
|
||||
|
||||
if (mesh instanceof BABYLON.Mesh) {
|
||||
toMerge.push(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
if (toMerge.length <= 1) { // マージ対象が一つしかない状態でマージするのは単純に無駄なのと、babylonのバグが知らないけどなぜか法線が反転する
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const mesh of toMerge) {
|
||||
if (hasTexture) {
|
||||
if (mesh.getVerticesData(BABYLON.VertexBuffer.UVKind) == null) {
|
||||
const vertexCount = mesh.getTotalVertices();
|
||||
const uvs = new Array(vertexCount * 2).fill(0);
|
||||
mesh.setVerticesData(BABYLON.VertexBuffer.UVKind, uvs, false, 2);
|
||||
}
|
||||
if (mesh.getVerticesData(BABYLON.VertexBuffer.UV2Kind) == null) {
|
||||
const vertexCount = mesh.getTotalVertices();
|
||||
const uvs = new Array(vertexCount * 2).fill(0);
|
||||
mesh.setVerticesData(BABYLON.VertexBuffer.UV2Kind, uvs, false, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const merged = BABYLON.Mesh.MergeMeshes(toMerge, true, false, undefined, false, true);
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
export class FurnitureContainer {
|
||||
public id: string;
|
||||
public type: string;
|
||||
private options: ConvertedOptions;
|
||||
public root: BABYLON.TransformNode;
|
||||
private subRoot: BABYLON.TransformNode | null = null;
|
||||
public instance: RoomFunitureInstance | null = null;
|
||||
public model: ModelManager | null = null;
|
||||
private scene: BABYLON.Scene;
|
||||
public registerMeshes: (meshes: BABYLON.Mesh[]) => void = () => {};
|
||||
private sr: BABYLON.SnapshotRenderingHelper;
|
||||
private getIsSrReady: () => boolean;
|
||||
private lightContainer: BABYLON.ClusteredLightContainer;
|
||||
private graphicsQuality: number;
|
||||
private timer: Timer = new Timer();
|
||||
private sitChair: () => void = () => {};
|
||||
|
||||
constructor(args: {
|
||||
id: string;
|
||||
type: string;
|
||||
options: RawOptions;
|
||||
roomAttachments: RoomAttachments;
|
||||
position: BABYLON.Vector3;
|
||||
rotation: BABYLON.Vector3;
|
||||
sr: BABYLON.SnapshotRenderingHelper;
|
||||
getIsSrReady: () => boolean;
|
||||
lightContainer: BABYLON.ClusteredLightContainer;
|
||||
scene: BABYLON.Scene;
|
||||
graphicsQuality: number;
|
||||
sitChair?: () => void;
|
||||
}) {
|
||||
this.id = args.id;
|
||||
this.type = args.type;
|
||||
const def = getFurnitureDef(this.type);
|
||||
this.options = convertRawOptions(def.options.schema, args.options, args.roomAttachments);
|
||||
this.sr = args.sr;
|
||||
this.getIsSrReady = args.getIsSrReady;
|
||||
this.lightContainer = args.lightContainer;
|
||||
this.scene = args.scene;
|
||||
this.graphicsQuality = args.graphicsQuality;
|
||||
this.root = new BABYLON.TransformNode(`furniture_${args.id}_${args.type}`, this.scene);
|
||||
this.root.position = args.position;
|
||||
this.root.rotation = args.rotation;
|
||||
if (args.sitChair != null) this.sitChair = args.sitChair;
|
||||
}
|
||||
|
||||
public async load() {
|
||||
const def = getFurnitureDef(this.type);
|
||||
|
||||
const filePath = def.path != null ? `/client-assets/world/objects/${def.path(this.options)}.glb` : `/client-assets/world/objects/${camelToKebab(this.type)}/${camelToKebab(this.type)}.glb`;
|
||||
const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene);
|
||||
|
||||
// babylonによって自動で追加される右手系変換用ノード
|
||||
const subRootMesh = loaderResult.meshes[0] as BABYLON.Mesh;
|
||||
|
||||
// 不要なUVを掃除
|
||||
if (!def.hasTexture) {
|
||||
for (const m of loaderResult.meshes) {
|
||||
if (m.geometry != null) {
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UVKind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV2Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV3Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV4Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV5Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV6Kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (def.canPreMeshesMerging) {
|
||||
const merged = mergeMeshes(loaderResult.meshes, subRootMesh, def.hasTexture);
|
||||
if (merged != null) {
|
||||
merged.setParent(subRootMesh);
|
||||
merged.name = 'preMerged';
|
||||
|
||||
merged.material.freeze();
|
||||
if (merged.material instanceof BABYLON.MultiMaterial) {
|
||||
for (const subMat of merged.material.subMaterials) {
|
||||
subMat.freeze();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 再帰的にする
|
||||
for (const m of loaderResult.transformNodes) {
|
||||
if (m.getChildren().length === 0) {
|
||||
m.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// meshじゃなくtransform nodeにしてパフォーマンス向上
|
||||
this.subRoot = new BABYLON.TransformNode('__root__', this.scene);
|
||||
this.subRoot.parent = this.root;
|
||||
this.subRoot.scaling.x = -1;
|
||||
this.subRoot.scaling = this.subRoot.scaling.scale(WORLD_SCALE);// cmをmに
|
||||
|
||||
for (const m of subRootMesh.getChildren()) {
|
||||
if (m.parent === subRootMesh) {
|
||||
m.parent = this.subRoot;
|
||||
}
|
||||
}
|
||||
|
||||
subRootMesh.dispose();
|
||||
|
||||
this.registerMeshes(this.subRoot.getChildMeshes());
|
||||
|
||||
this.model = new ModelManager(this.subRoot, loaderResult.meshes.filter(m => !m.isDisposed() && m.name !== '__root__'), def.hasTexture, (meshes) => {
|
||||
this.registerMeshes(meshes);
|
||||
});
|
||||
|
||||
this.instance = await def.createInstance({
|
||||
scene: this.scene,
|
||||
sr: {
|
||||
updateMesh: (mesh) => {
|
||||
if (!this.getIsSrReady()) return;
|
||||
this.sr.updateMesh(mesh);
|
||||
},
|
||||
reset: () => {
|
||||
if (!this.getIsSrReady()) return;
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.sr.enableSnapshotRendering();
|
||||
},
|
||||
fixParticleSystem: (ps) => this.sr.fixParticleSystem(ps),
|
||||
},
|
||||
lc: this.lightContainer,
|
||||
root: this.root,
|
||||
options: this.options,
|
||||
model: this.model!,
|
||||
id: this.id,
|
||||
timer: this.timer,
|
||||
graphicsQuality: this.graphicsQuality,
|
||||
reloadModel: () => {
|
||||
this.reload();
|
||||
},
|
||||
sitChair: () => {
|
||||
this.sitChair();
|
||||
},
|
||||
stickyMarkerMeshUpdated: (mesh) => {
|
||||
// TODO
|
||||
//// stickyな子の位置を更新
|
||||
//if (mesh.name.includes('__TOP__')) {
|
||||
// mesh.unfreezeWorldMatrix();
|
||||
// mesh.computeWorldMatrix(true);
|
||||
// const updateChildStickyObjectPosition = (furnitureId: string) => {
|
||||
// const stickyFunitureIds = Array.from(this.roomState.installedFurnitures.filter(o => o.sticky === furnitureId)).map(o => o.id);
|
||||
// for (const soid of stickyFunitureIds) {
|
||||
// const soMesh = this.objectEntities.get(soid)!.rootMesh;
|
||||
// soMesh.unfreezeWorldMatrix();
|
||||
// for (const m of soMesh.getChildMeshes()) {
|
||||
// m.unfreezeWorldMatrix();
|
||||
// }
|
||||
// console.log(mesh.getAbsolutePosition().y);
|
||||
// soMesh.position.y = mesh.getAbsolutePosition().y;
|
||||
// updateChildStickyObjectPosition(soid);
|
||||
// }
|
||||
// };
|
||||
// updateChildStickyObjectPosition(args.id);
|
||||
//}
|
||||
},
|
||||
});
|
||||
|
||||
this.instance.onInited?.();
|
||||
}
|
||||
|
||||
public interact(iid: string | null = null) {
|
||||
if (this.instance == null) return;
|
||||
if (iid == null) {
|
||||
if (this.instance.primaryInteraction != null) {
|
||||
this.instance.interactions[this.instance.primaryInteraction].fn();
|
||||
}
|
||||
} else {
|
||||
this.instance.interactions[iid].fn();
|
||||
}
|
||||
}
|
||||
|
||||
public async reload() {
|
||||
this.timer.dispose();
|
||||
this.instance?.dispose?.();
|
||||
this.instance = null;
|
||||
this.model = null;
|
||||
this.subRoot?.dispose();
|
||||
this.root.removeChild(this.subRoot);
|
||||
this.scene.removeTransformNode(this.subRoot);
|
||||
|
||||
this.timer = new Timer();
|
||||
|
||||
await this.load();
|
||||
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public optionsUpdated(options: RawOptions, key: string, value: any, roomAttachments: RoomAttachments) {
|
||||
if (this.instance == null) return;
|
||||
const def = getFurnitureDef(this.type);
|
||||
const convertedOptions = convertRawOptions(def.options.schema, options, roomAttachments);
|
||||
this.options[key] = convertedOptions[key]; // 参照を切れさせないようにプロパティ個別にmutate
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.instance.onOptionsUpdated?.([key, this.options[key]]);
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.timer.dispose();
|
||||
this.instance?.dispose?.();
|
||||
this.subRoot.dispose();
|
||||
this.root.dispose();
|
||||
this.scene.removeTransformNode(this.root);
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
}
|
||||
1599
packages/frontend-misskey-world-engine/src/room/engine.ts
Normal file
1599
packages/frontend-misskey-world-engine/src/room/engine.ts
Normal file
File diff suppressed because it is too large
Load Diff
79
packages/frontend-misskey-world-engine/src/room/env.ts
Normal file
79
packages/frontend-misskey-world-engine/src/room/env.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { findMaterial, GRAPHICS_QUALITY } from '../utility.js';
|
||||
import { SYSTEM_HEYA_MESH_NAMES } from './utility.js';
|
||||
import type { RoomEngine } from './engine.js';
|
||||
import type { SimpleEnvOptions, JapaneseEnvOptions, MuseumEnvOptions, CustomMadoriEnvOptions } from 'misskey-world/src/room/env.js';
|
||||
|
||||
export abstract class EnvManager<T = any> {
|
||||
protected engine: RoomEngine;
|
||||
public abstract envMapIndoor: BABYLON.CubeTexture | null;
|
||||
public abstract maxCameraZ: number;
|
||||
protected shadowGenerators: BABYLON.ShadowGenerator[] = [];
|
||||
|
||||
constructor(engine: RoomEngine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
abstract load(options: T, scene: BABYLON.Scene, engine: RoomEngine): Promise<void>;
|
||||
abstract applyOptions(options: T): void;
|
||||
abstract setTime(time: number): void;
|
||||
abstract updateRoomLightColor(color: BABYLON.Color3): void;
|
||||
abstract turnOnRoomLight(): void;
|
||||
abstract turnOffRoomLight(): void;
|
||||
|
||||
public addShadowCaster(mesh: BABYLON.AbstractMesh) {
|
||||
for (const shadowGen of this.shadowGenerators) {
|
||||
shadowGen.addShadowCaster(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
public removeShadowCaster(mesh: BABYLON.AbstractMesh) {
|
||||
for (const shadowGen of this.shadowGenerators) {
|
||||
shadowGen.removeShadowCaster(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
protected registerMeshes(meshes: BABYLON.AbstractMesh[]) {
|
||||
for (const mesh of meshes) {
|
||||
if (!this.engine.scene.meshes.includes(mesh)) this.engine.scene.addMesh(mesh);
|
||||
|
||||
if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) {
|
||||
mesh.isPickable = false;
|
||||
mesh.receiveShadows = false;
|
||||
mesh.isVisible = false;
|
||||
mesh.checkCollisions = false;
|
||||
if (mesh.name.includes('__COLLISION__')) {
|
||||
mesh.checkCollisions = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
mesh.isPickable = false;
|
||||
mesh.checkCollisions = false;
|
||||
if (mesh.material != null) {
|
||||
(mesh.material as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
|
||||
|
||||
if (mesh.material instanceof BABYLON.MultiMaterial) {
|
||||
for (const subMat of mesh.material.subMaterials) {
|
||||
subMat.reflectionTexture = this.envMapIndoor;
|
||||
}
|
||||
} else if (mesh.material instanceof BABYLON.PBRMaterial) {
|
||||
mesh.material.reflectionTexture = this.envMapIndoor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
for (const shadowGen of this.shadowGenerators) {
|
||||
shadowGen.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { findMaterial, GRAPHICS_QUALITY } from '../../utility.js';
|
||||
import { SYSTEM_HEYA_MESH_NAMES } from '../utility.js';
|
||||
import { EnvManager } from '../env.js';
|
||||
import type { RoomEngine } from '../engine.js';
|
||||
import type { CustomMadoriEnvOptions } from 'misskey-world/src/room/env.js';
|
||||
|
||||
export class CustomMadoriEnvManager extends EnvManager<CustomMadoriEnvOptions> {
|
||||
private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null;
|
||||
private meshes: BABYLON.Mesh[] = [];
|
||||
private rootNode: BABYLON.TransformNode;
|
||||
private unitRootNodes: (BABYLON.TransformNode | null)[] = [];
|
||||
private floorRootNode: BABYLON.TransformNode | null = null;
|
||||
private wallRootNode: BABYLON.TransformNode | null = null;
|
||||
private floorMaterials: Record<string, BABYLON.PBRMaterial> = {};
|
||||
private wallMaterials: Record<string, BABYLON.PBRMaterial> = {};
|
||||
private wallBeamMaterials: Record<string, BABYLON.PBRMaterial> = {};
|
||||
private pillarMaterials: Record<string, BABYLON.PBRMaterial> = {};
|
||||
private ceilingMaterials: Record<string, BABYLON.PBRMaterial> = {};
|
||||
private beamMesh: BABYLON.Mesh | null = null;
|
||||
private baseboardMesh: BABYLON.Mesh | null = null;
|
||||
private wallARootNode: BABYLON.TransformNode | null = null;
|
||||
private wallBRootNode: BABYLON.TransformNode | null = null;
|
||||
private skybox: BABYLON.Mesh | null = null;
|
||||
private skyboxMat: BABYLON.StandardMaterial | null = null;
|
||||
private roomLight: BABYLON.DirectionalLight | null = null;
|
||||
public envMapIndoor: BABYLON.CubeTexture | null = null;
|
||||
public maxCameraZ = cm(3000);
|
||||
|
||||
constructor(engine: RoomEngine) {
|
||||
super(engine);
|
||||
|
||||
this.rootNode = new BABYLON.TransformNode('customMadoriRoot', this.engine.scene);
|
||||
//this.rootNode.scaling = new BABYLON.Vector3(WORLD_SCALE, WORLD_SCALE, WORLD_SCALE);
|
||||
}
|
||||
|
||||
public async load(options: CustomMadoriEnvOptions) {
|
||||
this.skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(3000) }, this.engine.scene);
|
||||
this.skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.engine.scene);
|
||||
this.skyboxMat.backFaceCulling = false;
|
||||
this.skyboxMat.disableLighting = true;
|
||||
this.skybox.material = this.skyboxMat;
|
||||
this.skybox.infiniteDistance = true;
|
||||
|
||||
this.roomLight = new BABYLON.DirectionalLight('env:RoomLight', new BABYLON.Vector3(0, -1, 0), this.engine.scene);
|
||||
this.roomLight.position = new BABYLON.Vector3(0, cm(300), 0);
|
||||
this.roomLight.diffuse = new BABYLON.Color3(...this.engine.roomState.roomLightColor);
|
||||
this.roomLight.shadowMinZ = cm(10);
|
||||
this.roomLight.shadowMaxZ = cm(500);
|
||||
this.roomLight.radius = cm(30);
|
||||
|
||||
if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) {
|
||||
const shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.roomLight);
|
||||
shadowGeneratorForRoomLight.forceBackFacesOnly = true;
|
||||
shadowGeneratorForRoomLight.bias = 0.0005;
|
||||
shadowGeneratorForRoomLight.usePercentageCloserFiltering = true;
|
||||
shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
|
||||
if (this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM) {
|
||||
shadowGeneratorForRoomLight.getShadowMap().refreshRate = 60; // 効いてなさそう babylonのバグ?
|
||||
}
|
||||
//shadowGeneratorForRoomLight.useContactHardeningShadow = true;
|
||||
//shadowGeneratorForRoomLight.contactHardeningLightSizeUVRatio = 0.01;
|
||||
this.shadowGenerators.push(shadowGeneratorForRoomLight);
|
||||
}
|
||||
|
||||
for (const materialDef of options.flooringMaterials) {
|
||||
const mat = new BABYLON.PBRMaterial(`flooring_${materialDef.id}`, this.engine.scene);
|
||||
mat.albedoColor = new BABYLON.Color3(...materialDef.color);
|
||||
mat.metallic = 0;
|
||||
mat.roughness = 1;
|
||||
|
||||
const texPath = materialDef.texture === 'wood' ? '/client-assets/room/textures/flooring-wood.png'
|
||||
: materialDef.texture === 'concrete' ? '/client-assets/room/textures/concrete3.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.engine.scene, false, false);
|
||||
mat.albedoTexture = tex;
|
||||
}
|
||||
|
||||
//mat.freeze();
|
||||
this.floorMaterials[materialDef.id] = mat;
|
||||
}
|
||||
|
||||
for (const materialDef of options.wallMaterials) {
|
||||
const mat = new BABYLON.PBRMaterial(`wall_${materialDef.id}`, this.engine.scene);
|
||||
mat.albedoColor = new BABYLON.Color3(...materialDef.color);
|
||||
mat.metallic = 0;
|
||||
mat.roughness = 1;
|
||||
|
||||
const texPath = materialDef.texture === 'wood' ? '/client-assets/room/textures/wall-wood2.png'
|
||||
: materialDef.texture === 'concrete' ? '/client-assets/room/textures/concrete1.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.engine.scene, false, false);
|
||||
mat.albedoTexture = tex;
|
||||
}
|
||||
|
||||
//mat.freeze();
|
||||
this.wallMaterials[materialDef.id] = mat;
|
||||
}
|
||||
|
||||
for (const materialDef of options.ceilingMaterials) {
|
||||
const mat = new BABYLON.PBRMaterial(`ceiling_${materialDef.id}`, this.engine.scene);
|
||||
mat.albedoColor = new BABYLON.Color3(...materialDef.color);
|
||||
mat.metallic = 0;
|
||||
mat.roughness = 1;
|
||||
|
||||
const texPath = materialDef.texture === 'wood' ? '/client-assets/room/textures/ceiling-wood.png'
|
||||
: materialDef.texture === 'concrete' ? '/client-assets/room/textures/concrete3.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.engine.scene, false, false);
|
||||
mat.albedoTexture = tex;
|
||||
}
|
||||
|
||||
//mat.freeze();
|
||||
this.ceilingMaterials[materialDef.id] = mat;
|
||||
}
|
||||
|
||||
this.loaderResult = await BABYLON.LoadAssetContainerAsync('/client-assets/room/envs/custom-madori/units.glb', this.engine.scene);
|
||||
|
||||
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.engine.scene);
|
||||
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(2000), cm(500), cm(2000));
|
||||
|
||||
this.meshes = this.loaderResult.meshes.filter(m => m instanceof BABYLON.Mesh);
|
||||
this.meshes[0].rotationQuaternion = null;
|
||||
this.meshes[0].rotation = new BABYLON.Vector3(0, 0, 0);
|
||||
|
||||
for (const m of this.meshes[0].getChildren()) {
|
||||
if (m.parent === this.meshes[0]) {
|
||||
m.parent = this.rootNode;
|
||||
}
|
||||
}
|
||||
|
||||
// instanced mesh を通常の mesh に変換 (そうしないとマテリアルが共有される)
|
||||
for (const mesh of this.loaderResult.meshes) {
|
||||
if (mesh instanceof BABYLON.InstancedMesh) {
|
||||
const realizedMesh = mesh.sourceMesh.clone(mesh.name, null, true);
|
||||
|
||||
realizedMesh.position = mesh.position.clone();
|
||||
if (mesh.rotationQuaternion) {
|
||||
realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone();
|
||||
} else {
|
||||
realizedMesh.rotation = mesh.rotation.clone();
|
||||
}
|
||||
realizedMesh.scaling = mesh.scaling.clone();
|
||||
realizedMesh.parent = mesh.parent;
|
||||
|
||||
mesh.dispose();
|
||||
this.engine.scene.removeMesh(mesh);
|
||||
this.meshes.push(realizedMesh);
|
||||
}
|
||||
}
|
||||
|
||||
this.floorRootNode = this.loaderResult.transformNodes.find(t => t.name.includes('__FLOOR__'))!;
|
||||
this.wallRootNode = this.loaderResult.transformNodes.find(t => t.name.includes('__WALL__'))!;
|
||||
this.beamMesh = this.loaderResult.meshes.find(m => m.name.includes('__BEAM__')) as BABYLON.Mesh;
|
||||
this.baseboardMesh = this.loaderResult.meshes.find(m => m.name.includes('__BASEBOARD__')) as BABYLON.Mesh;
|
||||
this.wallARootNode = this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_A__'))!;
|
||||
this.wallBRootNode = this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_B__'))!;
|
||||
|
||||
const baseboardMaterial = findMaterial(this.rootNode, '__BASEBOARD__');
|
||||
//baseboardMaterial.metadata.disableEnvMap = true;
|
||||
|
||||
for (const mesh of this.meshes) {
|
||||
if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) continue;
|
||||
mesh.receiveShadows = true;
|
||||
}
|
||||
|
||||
await this.applyOptions(options);
|
||||
}
|
||||
|
||||
private createUnit(options: CustomMadoriEnvOptions, x: number, z: number) {
|
||||
function indexToPos(index: number): [number, number] {
|
||||
const z = Math.floor(index / options.dimension[0]);
|
||||
const x = index % options.dimension[0];
|
||||
return [x, z];
|
||||
}
|
||||
|
||||
function posToIndex(x: number, z: number): number {
|
||||
if (x < 0 || z < 0 || x >= options.dimension[0] || z >= options.dimension[1]) return -1;
|
||||
return x + (options.dimension[0] * z);
|
||||
}
|
||||
|
||||
const unitDef = options.units[posToIndex(x, z)];
|
||||
if (unitDef == null) return;
|
||||
|
||||
const unitNDef = options.units[posToIndex(x, z + 1)];
|
||||
const unitSDef = options.units[posToIndex(x, z - 1)];
|
||||
const unitWDef = options.units[posToIndex(x + 1, z)];
|
||||
const unitEDef = options.units[posToIndex(x - 1, z)];
|
||||
|
||||
const shiftedX = x - (options.dimension[0] / 2) + 0.5;
|
||||
|
||||
const unitRoot = new BABYLON.TransformNode(`unit_${x}_${z}`, this.engine.scene);
|
||||
unitRoot.parent = this.rootNode;
|
||||
unitRoot.position = new BABYLON.Vector3(cm(100) * shiftedX, 0, cm(100) * z);
|
||||
|
||||
const defaultFlooringMaterial = this.floorMaterials[options.flooringMaterials[0].id];
|
||||
const unitFloorRootNode = this.floorRootNode.clone(`unit_${x}_${z}_floor`, unitRoot)!;
|
||||
unitFloorRootNode.scaling = new BABYLON.Vector3(-WORLD_SCALE, WORLD_SCALE, WORLD_SCALE);
|
||||
const flooringMesh = unitFloorRootNode.getChildMeshes().find(m => m.name.includes('__FLOOR__'));
|
||||
flooringMesh.material = unitDef.flooring?.material != null && this.floorMaterials[unitDef.flooring.material] != null ? this.floorMaterials[unitDef.flooring.material] : defaultFlooringMaterial;
|
||||
const defaultCeilingMaterial = this.ceilingMaterials[options.ceilingMaterials[0].id];
|
||||
const ceilingMesh = unitFloorRootNode.getChildMeshes().find(m => m.name.includes('__CEILING__'));
|
||||
ceilingMesh.material = unitDef.ceiling?.material != null && this.ceilingMaterials[unitDef.ceiling.material] != null ? this.ceilingMaterials[unitDef.ceiling.material] : defaultCeilingMaterial;
|
||||
const defaultWallMaterial = this.wallMaterials[options.wallMaterials[0].id];
|
||||
|
||||
const createWall = (dir: 'n' | 's' | 'w' | 'e') => {
|
||||
const wallDef = unitDef.walls?.[dir] ?? {};
|
||||
const wallRootNode = this.wallRootNode.clone(`unit_${x}_${z}_wall_${dir}`, unitRoot)!;
|
||||
wallRootNode.scaling = new BABYLON.Vector3(-WORLD_SCALE, WORLD_SCALE, WORLD_SCALE);
|
||||
|
||||
switch (dir) {
|
||||
case 'n':
|
||||
wallRootNode.rotation = new BABYLON.Vector3(0, Math.PI, 0);
|
||||
wallRootNode.position = new BABYLON.Vector3(0, 0, cm(50));
|
||||
break;
|
||||
case 's':
|
||||
wallRootNode.position = new BABYLON.Vector3(0, 0, cm(-50));
|
||||
break;
|
||||
case 'w':
|
||||
wallRootNode.rotation = new BABYLON.Vector3(0, -Math.PI / 2, 0);
|
||||
wallRootNode.position = new BABYLON.Vector3(cm(50), 0, 0);
|
||||
break;
|
||||
case 'e':
|
||||
wallRootNode.rotation = new BABYLON.Vector3(0, Math.PI / 2, 0);
|
||||
wallRootNode.position = new BABYLON.Vector3(cm(-50), 0, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
const beamMesh = wallRootNode.getChildMeshes().find(m => m.name.includes('__BEAM__'));
|
||||
beamMesh.isVisible = wallDef.withBeam === true;
|
||||
|
||||
const baseboardMesh = wallRootNode.getChildMeshes().find(m => m.name.includes('__BASEBOARD__'));
|
||||
baseboardMesh.isVisible = wallDef.withBaseboard === true;
|
||||
|
||||
switch (wallDef.type) {
|
||||
case 'window': {
|
||||
const wallNode = this.wallBRootNode.clone('', wallRootNode)!;
|
||||
const wallMesh = wallNode.getChildMeshes().find(m => m.name.includes('__WALL__'))!;
|
||||
wallMesh.material = wallDef.material != null && this.wallMaterials[wallDef.material] != null ? this.wallMaterials[wallDef.material] : defaultWallMaterial;
|
||||
break;
|
||||
}
|
||||
case 'door': {
|
||||
//wallMeshOriginal = this.wallAMesh;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const wallNode = this.wallARootNode.clone('', wallRootNode)!;
|
||||
const wallMesh = wallNode.getChildMeshes().find(m => m.name.includes('__WALL__'))!;
|
||||
wallMesh.material = wallDef.material != null && this.wallMaterials[wallDef.material] != null ? this.wallMaterials[wallDef.material] : defaultWallMaterial;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (unitNDef == null) createWall('n');
|
||||
if (unitSDef == null) createWall('s');
|
||||
if (unitWDef == null) createWall('w');
|
||||
if (unitEDef == null) createWall('e');
|
||||
|
||||
for (const mesh of unitRoot.getChildMeshes()) {
|
||||
this.meshes.push(mesh);
|
||||
}
|
||||
|
||||
this.registerMeshes(unitRoot.getChildMeshes());
|
||||
|
||||
return unitRoot;
|
||||
}
|
||||
|
||||
public setTime(time: number) {
|
||||
if (this.skyboxMat == null) return;
|
||||
|
||||
if (time === 0) {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.9, 1.0);
|
||||
} else if (time === 1) {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.8, 0.5, 0.3);
|
||||
} else {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.05, 0.05, 0.2);
|
||||
}
|
||||
|
||||
if (this.sunLight != null) {
|
||||
this.sunLight.diffuse = time === 0 ? new BABYLON.Color3(1.0, 0.9, 0.8) : time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0);
|
||||
this.sunLight.intensity = time === 0 ? 3 : time === 1 ? 1 : 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
public updateRoomLightColor(color: BABYLON.Color3): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.diffuse = color;
|
||||
}
|
||||
|
||||
public turnOnRoomLight(): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.intensity = 0.0005 * WORLD_SCALE * WORLD_SCALE;
|
||||
if (this.envMapIndoor != null) this.envMapIndoor.level = 0.2;
|
||||
for (const m of this.engine.scene.materials) {
|
||||
if (m.metadata?.disableEnvMap) {
|
||||
m.ambientColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public turnOffRoomLight(): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.intensity = 0;
|
||||
if (this.envMapIndoor != null) this.envMapIndoor.level = 0.025;
|
||||
for (const m of this.engine.scene.materials) {
|
||||
if (m.metadata?.disableEnvMap) {
|
||||
m.ambientColor = new BABYLON.Color3(0.025, 0.025, 0.025);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public applyOptions(options: CustomMadoriEnvOptions) {
|
||||
// TODO: 返り値をpromiseにしてちゃんとテクスチャが読み終わってからresolveする
|
||||
|
||||
for (const n of this.unitRootNodes) {
|
||||
if (n != null) n.dispose();
|
||||
}
|
||||
this.unitRootNodes = [];
|
||||
|
||||
for (let z = 0; z < options.dimension[1]; z++) {
|
||||
for (let x = 0; x < options.dimension[0]; x++) {
|
||||
const node = this.createUnit(options, x, z);
|
||||
this.unitRootNodes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
for (const m of this.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
this.skybox?.dispose();
|
||||
this.skyboxMat?.dispose();
|
||||
this.envMapIndoor?.dispose();
|
||||
this.roomLight?.dispose();
|
||||
this.sunLight?.dispose();
|
||||
if (this.loaderResult != null) {
|
||||
for (const m of this.loaderResult.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
for (const t of this.loaderResult.transformNodes) {
|
||||
t.dispose(false, true);
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
180
packages/frontend-misskey-world-engine/src/room/envs/japanese.ts
Normal file
180
packages/frontend-misskey-world-engine/src/room/envs/japanese.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { findMaterial, GRAPHICS_QUALITY } from '../../utility.js';
|
||||
import { SYSTEM_HEYA_MESH_NAMES } from '../utility.js';
|
||||
import { EnvManager } from '../env.js';
|
||||
import type { RoomEngine } from '../engine.js';
|
||||
import type { SimpleEnvOptions, JapaneseEnvOptions } from 'misskey-world/src/room/env.js';
|
||||
|
||||
export class JapaneseEnvManager extends EnvManager<JapaneseEnvOptions> {
|
||||
private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null;
|
||||
private meshes: BABYLON.Mesh[] = [];
|
||||
private skybox: BABYLON.Mesh | null = null;
|
||||
private skyboxMat: BABYLON.StandardMaterial | null = null;
|
||||
private roomLight: BABYLON.SpotLight | null = null;
|
||||
private sunLight: BABYLON.DirectionalLight | null = null;
|
||||
public envMapIndoor: BABYLON.CubeTexture | null = null;
|
||||
public maxCameraZ = cm(1000);
|
||||
|
||||
constructor(engine: RoomEngine) {
|
||||
super(engine);
|
||||
}
|
||||
|
||||
public async load(options: JapaneseEnvOptions) {
|
||||
this.skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(1000) }, this.engine.scene);
|
||||
this.skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.engine.scene);
|
||||
this.skyboxMat.backFaceCulling = false;
|
||||
this.skyboxMat.disableLighting = true;
|
||||
this.skybox.material = this.skyboxMat;
|
||||
this.skybox.infiniteDistance = true;
|
||||
|
||||
this.roomLight = new BABYLON.SpotLight('env:RoomLight', new BABYLON.Vector3(0, cm(249), 0), new BABYLON.Vector3(0, -1, 0), 16, 8, this.engine.scene);
|
||||
this.roomLight.diffuse = new BABYLON.Color3(...this.engine.roomState.roomLightColor);
|
||||
this.roomLight.shadowMinZ = cm(10);
|
||||
this.roomLight.shadowMaxZ = cm(300);
|
||||
this.roomLight.radius = cm(30);
|
||||
|
||||
if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) {
|
||||
const shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.roomLight);
|
||||
shadowGeneratorForRoomLight.forceBackFacesOnly = true;
|
||||
shadowGeneratorForRoomLight.bias = 0.0005;
|
||||
shadowGeneratorForRoomLight.usePercentageCloserFiltering = true;
|
||||
shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
|
||||
if (this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM) {
|
||||
shadowGeneratorForRoomLight.getShadowMap().refreshRate = 60;
|
||||
}
|
||||
//shadowGeneratorForRoomLight.useContactHardeningShadow = true;
|
||||
//shadowGeneratorForRoomLight.contactHardeningLightSizeUVRatio = 0.01;
|
||||
this.shadowGenerators.push(shadowGeneratorForRoomLight);
|
||||
}
|
||||
|
||||
if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) {
|
||||
this.sunLight = new BABYLON.DirectionalLight('env:SunLight', new BABYLON.Vector3(0.2, -1, -1), this.engine.scene);
|
||||
this.sunLight.position = new BABYLON.Vector3(cm(-20), cm(1000), cm(1000));
|
||||
this.sunLight.shadowMinZ = cm(1000);
|
||||
this.sunLight.shadowMaxZ = cm(2000);
|
||||
|
||||
const shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.sunLight);
|
||||
shadowGeneratorForSunLight.forceBackFacesOnly = true;
|
||||
shadowGeneratorForSunLight.bias = 0.00001;
|
||||
shadowGeneratorForSunLight.usePercentageCloserFiltering = true;
|
||||
shadowGeneratorForSunLight.usePoissonSampling = true;
|
||||
if (this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM) {
|
||||
shadowGeneratorForSunLight.getShadowMap().refreshRate = 60;
|
||||
}
|
||||
this.shadowGenerators.push(shadowGeneratorForSunLight);
|
||||
}
|
||||
|
||||
this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/envs/japanese/japanese.glb', this.engine.scene);
|
||||
|
||||
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.engine.scene);
|
||||
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(500), cm(500), cm(500));
|
||||
|
||||
this.meshes = this.loaderResult.meshes.filter(m => m instanceof BABYLON.Mesh);
|
||||
this.meshes[0].scaling = this.meshes[0].scaling.scale(WORLD_SCALE);
|
||||
this.meshes[0].rotationQuaternion = null;
|
||||
this.meshes[0].rotation = new BABYLON.Vector3(0, 0, 0);
|
||||
|
||||
// instanced mesh を通常の mesh に変換 (そうしないとマテリアルが共有される)
|
||||
for (const mesh of this.loaderResult.meshes) {
|
||||
if (mesh instanceof BABYLON.InstancedMesh) {
|
||||
const realizedMesh = mesh.sourceMesh.clone(mesh.name, null, true);
|
||||
|
||||
realizedMesh.position = mesh.position.clone();
|
||||
if (mesh.rotationQuaternion) {
|
||||
realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone();
|
||||
} else {
|
||||
realizedMesh.rotation = mesh.rotation.clone();
|
||||
}
|
||||
realizedMesh.scaling = mesh.scaling.clone();
|
||||
realizedMesh.parent = mesh.parent;
|
||||
|
||||
mesh.dispose();
|
||||
this.engine.scene.removeMesh(mesh);
|
||||
this.meshes.push(realizedMesh);
|
||||
}
|
||||
}
|
||||
|
||||
for (const mesh of this.meshes) {
|
||||
if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) continue;
|
||||
mesh.receiveShadows = true;
|
||||
|
||||
this.addShadowCaster(mesh);
|
||||
}
|
||||
|
||||
await this.applyOptions(options);
|
||||
}
|
||||
|
||||
public setTime(time: number) {
|
||||
if (this.skyboxMat == null) return;
|
||||
|
||||
if (time === 0) {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.9, 1.0);
|
||||
} else if (time === 1) {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.8, 0.5, 0.3);
|
||||
} else {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.05, 0.05, 0.2);
|
||||
}
|
||||
|
||||
if (this.sunLight != null) {
|
||||
this.sunLight.diffuse = time === 0 ? new BABYLON.Color3(1.0, 0.9, 0.8) : time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0);
|
||||
this.sunLight.intensity = time === 0 ? 3 : time === 1 ? 1 : 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
public updateRoomLightColor(color: BABYLON.Color3): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.diffuse = color;
|
||||
}
|
||||
|
||||
public turnOnRoomLight(): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.intensity = 18 * WORLD_SCALE * WORLD_SCALE;
|
||||
if (this.envMapIndoor != null) this.envMapIndoor.level = 0.6;
|
||||
for (const m of this.engine.scene.materials) {
|
||||
if (m.metadata?.disableEnvMap) {
|
||||
m.ambientColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public turnOffRoomLight(): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.intensity = 0;
|
||||
if (this.envMapIndoor != null) this.envMapIndoor.level = 0.025;
|
||||
for (const m of this.engine.scene.materials) {
|
||||
if (m.metadata?.disableEnvMap) {
|
||||
m.ambientColor = new BABYLON.Color3(0.025, 0.025, 0.025);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public applyOptions(options: SimpleEnvOptions) {
|
||||
this.registerMeshes(this.meshes);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
for (const m of this.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
this.skybox?.dispose();
|
||||
this.skyboxMat?.dispose();
|
||||
this.envMapIndoor?.dispose();
|
||||
this.roomLight?.dispose();
|
||||
this.sunLight?.dispose();
|
||||
if (this.loaderResult != null) {
|
||||
for (const m of this.loaderResult.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
for (const t of this.loaderResult.transformNodes) {
|
||||
t.dispose(false, true);
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
159
packages/frontend-misskey-world-engine/src/room/envs/museum.ts
Normal file
159
packages/frontend-misskey-world-engine/src/room/envs/museum.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { findMaterial, GRAPHICS_QUALITY } from '../../utility.js';
|
||||
import { SYSTEM_HEYA_MESH_NAMES } from '../utility.js';
|
||||
import { EnvManager } from '../env.js';
|
||||
import type { RoomEngine } from '../engine.js';
|
||||
import type { MuseumEnvOptions } from 'misskey-world/src/room/env.js';
|
||||
|
||||
export class MuseumEnvManager extends EnvManager<MuseumEnvOptions> {
|
||||
private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null;
|
||||
private meshes: BABYLON.Mesh[] = [];
|
||||
private roomLight: BABYLON.DirectionalLight | null = null;
|
||||
private subRoomLights: BABYLON.SpotLight[] = [];
|
||||
public envMapIndoor: BABYLON.CubeTexture | null = null;
|
||||
public maxCameraZ = cm(3000);
|
||||
|
||||
constructor(engine: RoomEngine) {
|
||||
super(engine);
|
||||
}
|
||||
|
||||
public async load(options: MuseumEnvOptions) {
|
||||
this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/envs/museum/museum.glb', this.engine.scene);
|
||||
|
||||
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.engine.scene);
|
||||
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(2000), cm(500), cm(2000));
|
||||
|
||||
this.meshes = this.loaderResult.meshes.filter(m => m instanceof BABYLON.Mesh);
|
||||
this.meshes[0].scaling = this.meshes[0].scaling.scale(WORLD_SCALE);
|
||||
this.meshes[0].rotationQuaternion = null;
|
||||
this.meshes[0].rotation = new BABYLON.Vector3(0, 0, 0);
|
||||
|
||||
// instanced mesh を通常の mesh に変換 (そうしないとマテリアルが共有される)
|
||||
for (const mesh of this.loaderResult.meshes) {
|
||||
if (mesh instanceof BABYLON.InstancedMesh) {
|
||||
const realizedMesh = mesh.sourceMesh.clone(mesh.name, null, true);
|
||||
|
||||
realizedMesh.position = mesh.position.clone();
|
||||
if (mesh.rotationQuaternion) {
|
||||
realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone();
|
||||
} else {
|
||||
realizedMesh.rotation = mesh.rotation.clone();
|
||||
}
|
||||
realizedMesh.scaling = mesh.scaling.clone();
|
||||
realizedMesh.parent = mesh.parent;
|
||||
|
||||
mesh.dispose();
|
||||
this.engine.scene.removeMesh(mesh);
|
||||
this.meshes.push(realizedMesh);
|
||||
}
|
||||
}
|
||||
|
||||
this.roomLight = new BABYLON.DirectionalLight('env:RoomLight', new BABYLON.Vector3(0, -1, 0), this.engine.scene);
|
||||
this.roomLight.position = new BABYLON.Vector3(0, cm(300), 0);
|
||||
this.roomLight.diffuse = new BABYLON.Color3(...this.engine.roomState.roomLightColor);
|
||||
this.roomLight.shadowMinZ = cm(10);
|
||||
this.roomLight.shadowMaxZ = cm(500);
|
||||
this.roomLight.radius = cm(30);
|
||||
|
||||
if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) {
|
||||
const shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.roomLight);
|
||||
shadowGeneratorForRoomLight.forceBackFacesOnly = true;
|
||||
shadowGeneratorForRoomLight.bias = 0.00001;
|
||||
shadowGeneratorForRoomLight.normalBias = 0.005;
|
||||
shadowGeneratorForRoomLight.usePercentageCloserFiltering = true;
|
||||
shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
|
||||
if (this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM) {
|
||||
shadowGeneratorForRoomLight.getShadowMap().refreshRate = 60;
|
||||
}
|
||||
//this.shadowGeneratorForRoomLight.useContactHardeningShadow = true;
|
||||
this.shadowGenerators.push(shadowGeneratorForRoomLight);
|
||||
}
|
||||
|
||||
for (const node of this.meshes.filter(mesh => mesh.name.includes('__LIGHT__'))) {
|
||||
const light = new BABYLON.SpotLight('env:SubRoomLight', node.position, new BABYLON.Vector3(0, -1, 0), 16, 8, this.engine.scene, true);
|
||||
light.diffuse = new BABYLON.Color3(...this.engine.roomState.roomLightColor);
|
||||
light.range = cm(500);
|
||||
light.radius = cm(15);
|
||||
light.parent = this.meshes[0];
|
||||
this.engine.lightContainer.addLight(light);
|
||||
this.subRoomLights.push(light);
|
||||
}
|
||||
|
||||
for (const mesh of this.meshes) {
|
||||
if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) continue;
|
||||
mesh.receiveShadows = true;
|
||||
//this.addShadowCaster(mesh);
|
||||
}
|
||||
|
||||
await this.applyOptions(options);
|
||||
}
|
||||
|
||||
public setTime(time: number) {
|
||||
}
|
||||
|
||||
public updateRoomLightColor(color: BABYLON.Color3): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.diffuse = color;
|
||||
for (const subLight of this.subRoomLights) {
|
||||
subLight.diffuse = color;
|
||||
}
|
||||
}
|
||||
|
||||
public turnOnRoomLight(): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.intensity = 0.00005 * WORLD_SCALE * WORLD_SCALE;
|
||||
for (const subLight of this.subRoomLights) {
|
||||
subLight.intensity = 20 * WORLD_SCALE * WORLD_SCALE;
|
||||
}
|
||||
if (this.envMapIndoor != null) this.envMapIndoor.level = 0.2;
|
||||
for (const m of this.engine.scene.materials) {
|
||||
if (m.metadata?.disableEnvMap) {
|
||||
m.ambientColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public turnOffRoomLight(): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.intensity = 0;
|
||||
for (const subLight of this.subRoomLights) {
|
||||
subLight.intensity = 0;
|
||||
}
|
||||
if (this.envMapIndoor != null) this.envMapIndoor.level = 0.025;
|
||||
for (const m of this.engine.scene.materials) {
|
||||
if (m.metadata?.disableEnvMap) {
|
||||
m.ambientColor = new BABYLON.Color3(0.025, 0.025, 0.025);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public applyOptions(options: MuseumEnvOptions) {
|
||||
this.registerMeshes(this.meshes);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.envMapIndoor?.dispose();
|
||||
this.roomLight?.dispose();
|
||||
for (const subLight of this.subRoomLights) {
|
||||
subLight.dispose();
|
||||
}
|
||||
if (this.loaderResult != null) {
|
||||
for (const m of this.loaderResult.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
for (const t of this.loaderResult.transformNodes) {
|
||||
t.dispose(false, true);
|
||||
}
|
||||
}
|
||||
for (const m of this.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
393
packages/frontend-misskey-world-engine/src/room/envs/simple.ts
Normal file
393
packages/frontend-misskey-world-engine/src/room/envs/simple.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { findMaterial, GRAPHICS_QUALITY } from '../../utility.js';
|
||||
import { SYSTEM_HEYA_MESH_NAMES } from '../utility.js';
|
||||
import { EnvManager } from '../env.js';
|
||||
import type { RoomEngine } from '../engine.js';
|
||||
import type { SimpleEnvOptions } from 'misskey-world/src/room/env.js';
|
||||
|
||||
// TODO: マテリアルは必要になるまで作成しないようにする
|
||||
|
||||
export class SimpleEnvManager extends EnvManager<SimpleEnvOptions> {
|
||||
private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null;
|
||||
private meshes: BABYLON.Mesh[] = [];
|
||||
private wallRoots: Record<'n' | 's' | 'w' | 'e', BABYLON.TransformNode> = null as any;
|
||||
private wallMaterials: Record<'n' | 's' | 'w' | 'e', BABYLON.PBRMaterial> | null = null;
|
||||
private wallBeamMaterials: Record<'n' | 's' | 'w' | 'e', BABYLON.PBRMaterial> | null = null;
|
||||
private pillarRoots: Record<'nw' | 'ne' | 'sw' | 'se', BABYLON.TransformNode> | null = null;
|
||||
private pillarMaterials: Record<'nw' | 'ne' | 'sw' | 'se', BABYLON.PBRMaterial> | null = null;
|
||||
private ceilingMaterial: BABYLON.PBRMaterial | null = null;
|
||||
private floorMaterial: BABYLON.PBRMaterial | null = null;
|
||||
private skybox: BABYLON.Mesh | null = null;
|
||||
private skyboxMat: BABYLON.StandardMaterial | null = null;
|
||||
private roomLight: BABYLON.SpotLight | null = null;
|
||||
private sunLight: BABYLON.DirectionalLight | null = null;
|
||||
public envMapIndoor: BABYLON.CubeTexture | null = null;
|
||||
public maxCameraZ = cm(1000);
|
||||
|
||||
constructor(engine: RoomEngine) {
|
||||
super(engine);
|
||||
}
|
||||
|
||||
public async load(options: SimpleEnvOptions) {
|
||||
this.skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(1000) }, this.engine.scene);
|
||||
this.skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.engine.scene);
|
||||
this.skyboxMat.backFaceCulling = false;
|
||||
this.skyboxMat.disableLighting = true;
|
||||
this.skybox.material = this.skyboxMat;
|
||||
this.skybox.infiniteDistance = true;
|
||||
|
||||
this.roomLight = new BABYLON.SpotLight('env:RoomLight', new BABYLON.Vector3(0, cm(249), 0), new BABYLON.Vector3(0, -1, 0), 16, 8, this.engine.scene);
|
||||
this.roomLight.diffuse = new BABYLON.Color3(...this.engine.roomState.roomLightColor);
|
||||
this.roomLight.shadowMinZ = cm(10);
|
||||
this.roomLight.shadowMaxZ = cm(300);
|
||||
this.roomLight.radius = cm(30);
|
||||
|
||||
if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) {
|
||||
const shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.roomLight);
|
||||
shadowGeneratorForRoomLight.forceBackFacesOnly = true;
|
||||
shadowGeneratorForRoomLight.bias = 0.0005;
|
||||
shadowGeneratorForRoomLight.usePercentageCloserFiltering = true;
|
||||
shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
|
||||
if (this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM) {
|
||||
shadowGeneratorForRoomLight.getShadowMap().refreshRate = 60; // 効いてなさそう babylonのバグ?
|
||||
}
|
||||
//shadowGeneratorForRoomLight.useContactHardeningShadow = true;
|
||||
//shadowGeneratorForRoomLight.contactHardeningLightSizeUVRatio = 0.01;
|
||||
this.shadowGenerators.push(shadowGeneratorForRoomLight);
|
||||
}
|
||||
|
||||
if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) {
|
||||
this.sunLight = new BABYLON.DirectionalLight('env:SunLight', new BABYLON.Vector3(0.2, -1, -1), this.engine.scene);
|
||||
this.sunLight.position = new BABYLON.Vector3(cm(-20), cm(1000), cm(1000));
|
||||
this.sunLight.shadowMinZ = cm(1000);
|
||||
this.sunLight.shadowMaxZ = cm(2000);
|
||||
|
||||
const shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.sunLight);
|
||||
shadowGeneratorForSunLight.forceBackFacesOnly = true;
|
||||
shadowGeneratorForSunLight.bias = 0.00001;
|
||||
shadowGeneratorForSunLight.usePercentageCloserFiltering = true;
|
||||
shadowGeneratorForSunLight.usePoissonSampling = true;
|
||||
if (this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM) {
|
||||
shadowGeneratorForSunLight.getShadowMap().refreshRate = 60; // 効いてなさそう babylonのバグ?
|
||||
}
|
||||
this.shadowGenerators.push(shadowGeneratorForSunLight);
|
||||
}
|
||||
|
||||
this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/envs/default/300.glb', this.engine.scene);
|
||||
|
||||
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.engine.scene);
|
||||
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(500), cm(500), cm(500));
|
||||
|
||||
this.meshes = this.loaderResult.meshes.filter(m => m instanceof BABYLON.Mesh);
|
||||
this.meshes[0].scaling = this.meshes[0].scaling.scale(WORLD_SCALE);
|
||||
this.meshes[0].rotationQuaternion = null;
|
||||
this.meshes[0].rotation = new BABYLON.Vector3(0, 0, 0);
|
||||
|
||||
// instanced mesh を通常の mesh に変換 (そうしないとマテリアルが共有される)
|
||||
for (const mesh of this.loaderResult.meshes) {
|
||||
if (mesh instanceof BABYLON.InstancedMesh) {
|
||||
const realizedMesh = mesh.sourceMesh.clone(mesh.name, null, true);
|
||||
|
||||
realizedMesh.position = mesh.position.clone();
|
||||
if (mesh.rotationQuaternion) {
|
||||
realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone();
|
||||
} else {
|
||||
realizedMesh.rotation = mesh.rotation.clone();
|
||||
}
|
||||
realizedMesh.scaling = mesh.scaling.clone();
|
||||
realizedMesh.parent = mesh.parent;
|
||||
|
||||
mesh.dispose();
|
||||
this.engine.scene.removeMesh(mesh);
|
||||
this.meshes.push(realizedMesh);
|
||||
}
|
||||
}
|
||||
|
||||
this.wallRoots = {
|
||||
n: this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_N__'))!,
|
||||
s: this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_S__'))!,
|
||||
w: this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_W__'))!,
|
||||
e: this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_E__'))!,
|
||||
};
|
||||
|
||||
this.pillarRoots = {
|
||||
nw: this.loaderResult.transformNodes.find(t => t.name.includes('__PILLAR_NW__'))!,
|
||||
ne: this.loaderResult.transformNodes.find(t => t.name.includes('__PILLAR_NE__'))!,
|
||||
sw: this.loaderResult.transformNodes.find(t => t.name.includes('__PILLAR_SW__'))!,
|
||||
se: this.loaderResult.transformNodes.find(t => t.name.includes('__PILLAR_SE__'))!,
|
||||
};
|
||||
|
||||
const wallMaterial = findMaterial(this.meshes[0], '__WALL__');
|
||||
//wallMaterial.metadata.disableEnvMap = true;
|
||||
this.wallMaterials = {
|
||||
n: wallMaterial.clone('wallNMaterial'),
|
||||
s: wallMaterial.clone('wallSMaterial'),
|
||||
w: wallMaterial.clone('wallWMaterial'),
|
||||
e: wallMaterial.clone('wallEMaterial'),
|
||||
};
|
||||
|
||||
const beamMaterial = findMaterial(this.meshes[0], '__BEAM__');
|
||||
//beamMaterial.metadata.disableEnvMap = true;
|
||||
this.wallBeamMaterials = {
|
||||
n: beamMaterial.clone('wallNBeamMaterial'),
|
||||
s: beamMaterial.clone('wallSBeamMaterial'),
|
||||
w: beamMaterial.clone('wallWBeamMaterial'),
|
||||
e: beamMaterial.clone('wallEBeamMaterial'),
|
||||
};
|
||||
|
||||
const pillarMaterial = findMaterial(this.meshes[0], '__PILLAR__');
|
||||
//pillarMaterial.metadata.disableEnvMap = true;
|
||||
this.pillarMaterials = {
|
||||
nw: pillarMaterial.clone('pillarNWMaterial'),
|
||||
ne: pillarMaterial.clone('pillarNEMaterial'),
|
||||
sw: pillarMaterial.clone('pillarSWMaterial'),
|
||||
se: pillarMaterial.clone('pillarSEMaterial'),
|
||||
};
|
||||
|
||||
for (const [k, v] of Object.entries(this.wallRoots)) {
|
||||
for (const m of v.getChildMeshes().filter(m => m.material === wallMaterial)) {
|
||||
m.material = this.wallMaterials[k];
|
||||
}
|
||||
for (const m of v.getChildMeshes().filter(m => m.material === beamMaterial)) {
|
||||
m.material = this.wallBeamMaterials[k];
|
||||
}
|
||||
}
|
||||
for (const [k, v] of Object.entries(this.pillarRoots)) {
|
||||
for (const m of v.getChildMeshes().filter(m => m.material === pillarMaterial)) {
|
||||
m.material = this.pillarMaterials[k];
|
||||
}
|
||||
}
|
||||
|
||||
this.ceilingMaterial = findMaterial(this.meshes[0], '__CEILING__');
|
||||
//this.ceilingMaterial.metadata.disableEnvMap = true;
|
||||
this.floorMaterial = findMaterial(this.meshes[0], '__FLOOR__');
|
||||
//this.floorMaterial.metadata.disableEnvMap = true;
|
||||
|
||||
const baseboardMaterial = findMaterial(this.meshes[0], '__BASEBOARD__');
|
||||
//baseboardMaterial.metadata.disableEnvMap = true;
|
||||
|
||||
for (const mesh of this.meshes) {
|
||||
if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) continue;
|
||||
mesh.receiveShadows = true;
|
||||
|
||||
if (mesh.material !== this.floorMaterial) { // 床は他の何にも影を落とさないことが確定している
|
||||
this.addShadowCaster(mesh);
|
||||
}
|
||||
}
|
||||
|
||||
await this.applyOptions(options);
|
||||
}
|
||||
|
||||
public setTime(time: number) {
|
||||
if (this.skyboxMat == null) return;
|
||||
|
||||
if (time === 0) {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.9, 1.0);
|
||||
} else if (time === 1) {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.8, 0.5, 0.3);
|
||||
} else {
|
||||
this.skyboxMat.emissiveColor = new BABYLON.Color3(0.05, 0.05, 0.2);
|
||||
}
|
||||
|
||||
if (this.sunLight != null) {
|
||||
this.sunLight.diffuse = time === 0 ? new BABYLON.Color3(1.0, 0.9, 0.8) : time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0);
|
||||
this.sunLight.intensity = time === 0 ? 3 : time === 1 ? 1 : 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
public updateRoomLightColor(color: BABYLON.Color3): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.diffuse = color;
|
||||
}
|
||||
|
||||
public turnOnRoomLight(): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.intensity = 18 * WORLD_SCALE * WORLD_SCALE;
|
||||
if (this.envMapIndoor != null) this.envMapIndoor.level = 0.6;
|
||||
for (const m of this.engine.scene.materials) {
|
||||
if (m.metadata?.disableEnvMap) {
|
||||
m.ambientColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public turnOffRoomLight(): void {
|
||||
if (this.roomLight == null) return;
|
||||
this.roomLight.intensity = 0;
|
||||
if (this.envMapIndoor != null) this.envMapIndoor.level = 0.025;
|
||||
for (const m of this.engine.scene.materials) {
|
||||
if (m.metadata?.disableEnvMap) {
|
||||
m.ambientColor = new BABYLON.Color3(0.025, 0.025, 0.025);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public applyOptions(options: SimpleEnvOptions) {
|
||||
// TODO: 返り値をpromiseにしてちゃんとテクスチャが読み終わってからresolveする
|
||||
|
||||
for (const type of ['n', 's', 'w', 'e'] as const) {
|
||||
const wallRoot = this.wallRoots[type];
|
||||
const wallOptions = options.walls[type];
|
||||
|
||||
for (const mesh of wallRoot.getChildMeshes()) {
|
||||
if (mesh.name.includes('__BEAM__')) {
|
||||
mesh.setEnabled(wallOptions.withBeam);
|
||||
} else if (mesh.name.includes('__BASEBOARD__')) {
|
||||
mesh.setEnabled(wallOptions.withBaseboard);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const targetMaterial = this.wallMaterials[type];
|
||||
|
||||
targetMaterial.unfreeze();
|
||||
targetMaterial.albedoColor = new BABYLON.Color3(...wallOptions.color);
|
||||
|
||||
const texPath = wallOptions.material === 'wood' ? '/client-assets/room/textures/wall-wood2.png'
|
||||
: wallOptions.material === 'concrete' ? '/client-assets/room/textures/concrete1.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.meshes[0].getScene(), false, false);
|
||||
targetMaterial.albedoTexture = tex;
|
||||
} else {
|
||||
targetMaterial.albedoTexture = null;
|
||||
}
|
||||
|
||||
targetMaterial.freeze();
|
||||
}
|
||||
|
||||
{
|
||||
const targetMaterial = this.wallBeamMaterials[type];
|
||||
|
||||
targetMaterial.unfreeze();
|
||||
targetMaterial.albedoColor = new BABYLON.Color3(...wallOptions.beamColor);
|
||||
|
||||
const texPath = wallOptions.beamMaterial === 'wood' ? '/client-assets/room/textures/wall-wood2.png'
|
||||
: wallOptions.beamMaterial === 'concrete' ? '/client-assets/room/textures/concrete1.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.meshes[0].getScene(), false, false);
|
||||
targetMaterial.albedoTexture = tex;
|
||||
} else {
|
||||
targetMaterial.albedoTexture = null;
|
||||
}
|
||||
|
||||
targetMaterial.freeze();
|
||||
}
|
||||
}
|
||||
|
||||
for (const type of ['nw', 'ne', 'sw', 'se'] as const) {
|
||||
const pillarRoot = this.pillarRoots[type];
|
||||
const pillarOptions = options.pillars[type];
|
||||
|
||||
let isEnabled = pillarOptions.show;
|
||||
if (!isEnabled) {
|
||||
// 梁同士が直交することは許さない(z-fightingが発生する)ので柱を強制追加
|
||||
if (type === 'nw') {
|
||||
isEnabled = options.walls.n.withBeam && options.walls.w.withBeam;
|
||||
} else if (type === 'ne') {
|
||||
isEnabled = options.walls.n.withBeam && options.walls.e.withBeam;
|
||||
} else if (type === 'sw') {
|
||||
isEnabled = options.walls.s.withBeam && options.walls.w.withBeam;
|
||||
} else if (type === 'se') {
|
||||
isEnabled = options.walls.s.withBeam && options.walls.e.withBeam;
|
||||
}
|
||||
}
|
||||
pillarRoot.setEnabled(isEnabled);
|
||||
|
||||
const targetMaterial = this.pillarMaterials[type];
|
||||
|
||||
targetMaterial.unfreeze();
|
||||
targetMaterial.albedoColor = new BABYLON.Color3(...pillarOptions.color);
|
||||
|
||||
const texPath = pillarOptions.material === 'wood' ? '/client-assets/room/textures/wall-wood2.png'
|
||||
: pillarOptions.material === 'concrete' ? '/client-assets/room/textures/concrete1.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.meshes[0].getScene(), false, false);
|
||||
targetMaterial.albedoTexture = tex;
|
||||
} else {
|
||||
targetMaterial.albedoTexture = null;
|
||||
}
|
||||
|
||||
targetMaterial.freeze();
|
||||
}
|
||||
|
||||
{
|
||||
this.ceilingMaterial.unfreeze();
|
||||
this.ceilingMaterial.albedoColor = new BABYLON.Color3(...options.ceiling.color);
|
||||
|
||||
const texPath = options.ceiling.material === 'wood' ? '/client-assets/room/textures/ceiling-wood.png'
|
||||
: options.ceiling.material === 'concrete' ? '/client-assets/room/textures/concrete3.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.meshes[0].getScene(), false, false);
|
||||
this.ceilingMaterial.albedoTexture = tex;
|
||||
} else {
|
||||
this.ceilingMaterial.albedoTexture = null;
|
||||
}
|
||||
|
||||
this.ceilingMaterial.freeze();
|
||||
}
|
||||
|
||||
{
|
||||
this.floorMaterial.unfreeze();
|
||||
this.floorMaterial.albedoColor = new BABYLON.Color3(...options.flooring.color);
|
||||
|
||||
const texPath = options.flooring.material === 'wood' ? '/client-assets/room/textures/flooring-wood.png'
|
||||
: options.flooring.material === 'concrete' ? '/client-assets/room/textures/concrete3.png'
|
||||
: null;
|
||||
|
||||
if (texPath != null) {
|
||||
const tex = new BABYLON.Texture(texPath, this.meshes[0].getScene(), false, false);
|
||||
this.floorMaterial.albedoTexture = tex;
|
||||
} else {
|
||||
this.floorMaterial.albedoTexture = null;
|
||||
}
|
||||
|
||||
this.floorMaterial.freeze();
|
||||
}
|
||||
|
||||
this.registerMeshes(this.meshes);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
for (const m of this.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
for (const m of Object.values(this.wallMaterials ?? {})) {
|
||||
m.dispose();
|
||||
}
|
||||
for (const m of Object.values(this.wallBeamMaterials ?? {})) {
|
||||
m.dispose();
|
||||
}
|
||||
for (const m of Object.values(this.pillarMaterials ?? {})) {
|
||||
m.dispose();
|
||||
}
|
||||
this.skybox?.dispose();
|
||||
this.skyboxMat?.dispose();
|
||||
this.envMapIndoor?.dispose();
|
||||
this.roomLight?.dispose();
|
||||
this.sunLight?.dispose();
|
||||
if (this.loaderResult != null) {
|
||||
for (const m of this.loaderResult.meshes) {
|
||||
m.dispose(false, true);
|
||||
}
|
||||
for (const t of this.loaderResult.transformNodes) {
|
||||
t.dispose(false, true);
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { a4Case } from './furnitures/a4Case.js';
|
||||
import { aircon } from './furnitures/aircon.js';
|
||||
import { allInOnePc } from './furnitures/allInOnePc.js';
|
||||
import { aquarium } from './furnitures/aquarium.js';
|
||||
import { aromaReedDiffuser } from './furnitures/aromaReedDiffuser.js';
|
||||
import { banknote } from './furnitures/banknote.js';
|
||||
import { beamLamp } from './furnitures/beamLamp.js';
|
||||
import { bed } from './furnitures/bed.js';
|
||||
import { blind } from './furnitures/blind.js';
|
||||
import { book } from './furnitures/book.js';
|
||||
import { books } from './furnitures/books.js';
|
||||
import { boxWallShelf } from './furnitures/boxWallShelf.js';
|
||||
import { cactusS } from './furnitures/cactusS.js';
|
||||
import { cardboardBox } from './furnitures/cardboardBox.js';
|
||||
import { ceilingFanLight } from './furnitures/ceilingFanLight.js';
|
||||
import { chair } from './furnitures/chair.js';
|
||||
import { clippedPicture } from './furnitures/clippedPicture.js';
|
||||
import { coffeeCup } from './furnitures/coffeeCup.js';
|
||||
import { colorBox } from './furnitures/colorBox.js';
|
||||
import { cuboid } from './furnitures/cuboid.js';
|
||||
import { cupNoodle } from './furnitures/cupNoodle.js';
|
||||
import { curtain } from './furnitures/curtain.js';
|
||||
import { custardPudding } from './furnitures/custardPudding.js';
|
||||
import { debugHipoly } from './furnitures/debugHipoly.js';
|
||||
import { debugMetal } from './furnitures/debugMetal.js';
|
||||
import { descriptionPlate } from './furnitures/descriptionPlate.js';
|
||||
import { desk } from './furnitures/desk.js';
|
||||
import { desktopPc } from './furnitures/desktopPc.js';
|
||||
import { djMixer } from './furnitures/djMixer.js';
|
||||
import { djPlayer } from './furnitures/djPlayer.js';
|
||||
import { ductRailSpotLights } from './furnitures/ductRailSpotLights.js';
|
||||
import { ductTape } from './furnitures/ductTape.js';
|
||||
import { electronicDisplayBoard } from './furnitures/electronicDisplayBoard.js';
|
||||
import { emptyBento } from './furnitures/emptyBento.js';
|
||||
import { energyDrink } from './furnitures/energyDrink.js';
|
||||
import { envelope } from './furnitures/envelope.js';
|
||||
import { facialTissue } from './furnitures/facialTissue.js';
|
||||
import { glassCylinderPotPlant } from './furnitures/glassCylinderPotPlant.js';
|
||||
import { handheldGameConsole } from './furnitures/handheldGameConsole.js';
|
||||
import { hangingDuctRail } from './furnitures/hangingDuctRail.js';
|
||||
import { hangingTShirt } from './furnitures/hangingTShirt.js';
|
||||
import { icosahedron } from './furnitures/icosahedron.js';
|
||||
import { ironFrameShelf } from './furnitures/ironFrameShelf.js';
|
||||
import { ironFrameTable } from './furnitures/ironFrameTable.js';
|
||||
import { issyoubin } from './furnitures/issyoubin.js';
|
||||
import { keyboard } from './furnitures/keyboard.js';
|
||||
import { laptopPc } from './furnitures/laptopPc.js';
|
||||
import { largeMousepad } from './furnitures/largeMousepad.js';
|
||||
import { lavaLamp } from './furnitures/lavaLamp.js';
|
||||
import { letterCase } from './furnitures/letterCase.js';
|
||||
import { lowPartitionBar } from './furnitures/lowPartitionBar.js';
|
||||
import { miObjet } from './furnitures/miObjet.js';
|
||||
import { milk } from './furnitures/milk.js';
|
||||
import { miPlate } from './furnitures/miPlate.js';
|
||||
import { miPlateDisplayed } from './furnitures/miPlateDisplayed.js';
|
||||
import { mixer } from './furnitures/mixer.js';
|
||||
import { monitor } from './furnitures/monitor.js';
|
||||
import { monitorSpeaker } from './furnitures/monitorSpeaker.js';
|
||||
import { monstera } from './furnitures/monstera.js';
|
||||
import { mug } from './furnitures/mug.js';
|
||||
import { newtonsCradle } from './furnitures/newtonsCradle.js';
|
||||
import { openedCardboardBox } from './furnitures/openedCardboardBox.js';
|
||||
import { pachira } from './furnitures/pachira.js';
|
||||
import { petBottle } from './furnitures/petBottle.js';
|
||||
import { piano } from './furnitures/piano.js';
|
||||
import { pictureFrame } from './furnitures/pictureFrame.js';
|
||||
import { pizza } from './furnitures/pizza.js';
|
||||
import { plant } from './furnitures/plant.js';
|
||||
import { plant2 } from './furnitures/plant2.js';
|
||||
import { poster } from './furnitures/poster.js';
|
||||
import { powerStrip } from './furnitures/powerStrip.js';
|
||||
import { radiometer } from './furnitures/radiometer.js';
|
||||
import { randomBooks } from './furnitures/randomBooks.js';
|
||||
import { recordPlayer } from './furnitures/recordPlayer.js';
|
||||
import { rolledUpPoster } from './furnitures/rolledUpPoster.js';
|
||||
import { roundRug } from './furnitures/roundRug.js';
|
||||
import { router } from './furnitures/router.js';
|
||||
import { siphon } from './furnitures/siphon.js';
|
||||
import { snakeplant } from './furnitures/snakeplant.js';
|
||||
import { sofa } from './furnitures/sofa.js';
|
||||
import { speaker } from './furnitures/speaker.js';
|
||||
import { speakerStand } from './furnitures/speakerStand.js';
|
||||
import { spotLight } from './furnitures/spotLight.js';
|
||||
import { sprayer } from './furnitures/sprayer.js';
|
||||
import { stanchionPole } from './furnitures/stanchionPole.js';
|
||||
import { steelRack } from './furnitures/steelRack.js';
|
||||
import { stormGlass } from './furnitures/stormGlass.js';
|
||||
import { tableSalt } from './furnitures/tableSalt.js';
|
||||
import { tabletopCalendar } from './furnitures/tabletopCalendar.js';
|
||||
import { tabletopDigitalClock } from './furnitures/tabletopDigitalClock.js';
|
||||
import { tabletopFlag } from './furnitures/tabletopFlag.js';
|
||||
import { tabletopGlassPictureFrame } from './furnitures/tabletopGlassPictureFrame.js';
|
||||
import { tabletopIronFrameStand } from './furnitures/tabletopIronFrameStand.js';
|
||||
import { tabletopLcdButtonsController } from './furnitures/tabletopLcdButtonsController.js';
|
||||
import { tabletopPictureFrame } from './furnitures/tabletopPictureFrame.js';
|
||||
import { tapestry } from './furnitures/tapestry.js';
|
||||
import { tetrapod } from './furnitures/tetrapod.js';
|
||||
import { tv } from './furnitures/tv.js';
|
||||
import { twistedCubeObjet } from './furnitures/twistedCubeObjet.js';
|
||||
import { usedTissue } from './furnitures/usedTissue.js';
|
||||
import { wallCanvas } from './furnitures/wallCanvas.js';
|
||||
import { wallClock } from './furnitures/wallClock.js';
|
||||
import { wallGlassPictureFrame } from './furnitures/wallGlassPictureFrame.js';
|
||||
import { wallMirror } from './furnitures/wallMirror.js';
|
||||
import { wallMountSpotLight } from './furnitures/wallMountSpotLight.js';
|
||||
import { wallShelf } from './furnitures/wallShelf.js';
|
||||
import { wireBasket } from './furnitures/wireBasket.js';
|
||||
import { wireNet } from './furnitures/wireNet.js';
|
||||
import { woodRingFloorLamp } from './furnitures/woodRingFloorLamp.js';
|
||||
import { woodRingsPendantLight } from './furnitures/woodRingsPendantLight.js';
|
||||
import { woodSoundAbsorbingPanel } from './furnitures/woodSoundAbsorbingPanel.js';
|
||||
import { haniwa } from './furnitures/haniwa.js';
|
||||
|
||||
export const FUNITURE_DEFS = [
|
||||
a4Case,
|
||||
aircon,
|
||||
allInOnePc,
|
||||
aquarium,
|
||||
aromaReedDiffuser,
|
||||
banknote,
|
||||
beamLamp,
|
||||
bed,
|
||||
blind,
|
||||
books,
|
||||
boxWallShelf,
|
||||
cactusS,
|
||||
cardboardBox,
|
||||
ceilingFanLight,
|
||||
chair,
|
||||
coffeeCup,
|
||||
colorBox,
|
||||
cuboid,
|
||||
cupNoodle,
|
||||
custardPudding,
|
||||
desk,
|
||||
desktopPc,
|
||||
djMixer,
|
||||
djPlayer,
|
||||
ductRailSpotLights,
|
||||
ductTape,
|
||||
electronicDisplayBoard,
|
||||
emptyBento,
|
||||
energyDrink,
|
||||
envelope,
|
||||
facialTissue,
|
||||
glassCylinderPotPlant,
|
||||
hangingTShirt,
|
||||
icosahedron,
|
||||
ironFrameShelf,
|
||||
ironFrameTable,
|
||||
issyoubin,
|
||||
keyboard,
|
||||
laptopPc,
|
||||
largeMousepad,
|
||||
lavaLamp,
|
||||
letterCase,
|
||||
miObjet,
|
||||
milk,
|
||||
miPlate,
|
||||
miPlateDisplayed,
|
||||
mixer,
|
||||
monitor,
|
||||
monitorSpeaker,
|
||||
monstera,
|
||||
mug,
|
||||
newtonsCradle,
|
||||
openedCardboardBox,
|
||||
pachira,
|
||||
petBottle,
|
||||
piano,
|
||||
pictureFrame,
|
||||
pizza,
|
||||
plant,
|
||||
plant2,
|
||||
poster,
|
||||
powerStrip,
|
||||
radiometer,
|
||||
randomBooks,
|
||||
recordPlayer,
|
||||
rolledUpPoster,
|
||||
roundRug,
|
||||
router,
|
||||
siphon,
|
||||
snakeplant,
|
||||
sofa,
|
||||
speaker,
|
||||
speakerStand,
|
||||
sprayer,
|
||||
steelRack,
|
||||
stormGlass,
|
||||
tableSalt,
|
||||
tabletopCalendar,
|
||||
tabletopDigitalClock,
|
||||
tabletopFlag,
|
||||
tabletopGlassPictureFrame,
|
||||
tabletopIronFrameStand,
|
||||
tabletopPictureFrame,
|
||||
tabletopLcdButtonsController,
|
||||
tapestry,
|
||||
tetrapod,
|
||||
tv,
|
||||
twistedCubeObjet,
|
||||
usedTissue,
|
||||
wallCanvas,
|
||||
wallClock,
|
||||
wallGlassPictureFrame,
|
||||
wallMirror,
|
||||
wallMountSpotLight,
|
||||
wallShelf,
|
||||
woodRingFloorLamp,
|
||||
woodRingsPendantLight,
|
||||
woodSoundAbsorbingPanel,
|
||||
hangingDuctRail,
|
||||
spotLight,
|
||||
lowPartitionBar,
|
||||
descriptionPlate,
|
||||
stanchionPole,
|
||||
handheldGameConsole,
|
||||
debugMetal,
|
||||
curtain,
|
||||
wireNet,
|
||||
clippedPicture,
|
||||
wireBasket,
|
||||
haniwa,
|
||||
] as FurnitureDef[];
|
||||
|
||||
export function getFurnitureDef(type: string): FurnitureDef {
|
||||
const def = FUNITURE_DEFS.find(x => x.id === type) as FurnitureDef | undefined;
|
||||
if (def == null) {
|
||||
throw new Error(`Unrecognized funiture type: ${type}`);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
113
packages/frontend-misskey-world-engine/src/room/furniture.ts
Normal file
113
packages/frontend-misskey-world-engine/src/room/furniture.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { createPlaneUvMapper } from '../utility.js';
|
||||
import type { Timer } from '../utility.js';
|
||||
import type { ModelManager } from './utility.js';
|
||||
import type { FurnitureSchemaDef } from 'misskey-world/src/room/object.js';
|
||||
import type { OptionsSchema } from 'misskey-world/src/mono.js';
|
||||
import type { ConvertedOptions, GetConvertedOptionsSchemaValues } from '../mono.js';
|
||||
|
||||
export type RoomFunitureInstance<Options = any> = {
|
||||
onInited?: () => void;
|
||||
onOptionsUpdated?: <K extends keyof Options, V extends Options[K]>(kv: [K, V]) => void;
|
||||
interactions: Record<string, {
|
||||
fn: () => void;
|
||||
}>;
|
||||
primaryInteraction?: string | null;
|
||||
resetTemporaryState?: () => void;
|
||||
dispose: () => void;
|
||||
};
|
||||
|
||||
export type SnapshotRenderingHelperWrapper = {
|
||||
updateMesh: (meshes: BABYLON.Mesh[]) => void;
|
||||
reset: () => void;
|
||||
fixParticleSystem: (ps: BABYLON.ParticleSystem) => void;
|
||||
};
|
||||
|
||||
export type FurnitureDef<Schema extends FurnitureSchemaDef = FurnitureSchemaDef> = Schema & {
|
||||
path?: (options: string extends keyof Schema['options']['schema'] ? ConvertedOptions : Readonly<GetConvertedOptionsSchemaValues<Schema['options']['schema']>>) => string;
|
||||
createInstance: (args: {
|
||||
scene: BABYLON.Scene;
|
||||
// TODO: snapshot renderingの関心を隠蔽した方が綺麗かもしれない
|
||||
// 例えばmaterialUpdatedというメソッドを用意して内部的にresetを呼ぶなど
|
||||
sr: SnapshotRenderingHelperWrapper;
|
||||
lc: BABYLON.ClusteredLightContainer | null;
|
||||
root: BABYLON.TransformNode;
|
||||
options: string extends keyof Schema['options']['schema'] ? ConvertedOptions : Readonly<GetConvertedOptionsSchemaValues<Schema['options']['schema']>>;
|
||||
model: ModelManager;
|
||||
id: string;
|
||||
timer: Timer;
|
||||
graphicsQuality: number;
|
||||
stickyMarkerMeshUpdated?: (mesh: BABYLON.Mesh) => void;
|
||||
sitChair?: () => void;
|
||||
reloadModel: () => void;
|
||||
}) => RoomFunitureInstance<string extends keyof Schema['options']['schema'] ? ConvertedOptions : GetConvertedOptionsSchemaValues<Schema['options']['schema']>> | Promise<RoomFunitureInstance<Schema['options']['schema'] extends undefined ? ConvertedOptions : GetConvertedOptionsSchemaValues<Schema['options']['schema']>>>; // TODO: createInstanceをasyncにするのではなく、別にreadyみたいなものを返させる
|
||||
};
|
||||
|
||||
export function defineFurnitureSchema<const OpSc extends OptionsSchema>(def: FurnitureSchemaDef<OpSc>): FurnitureSchemaDef<OpSc> {
|
||||
return def;
|
||||
}
|
||||
|
||||
export function defineFuniture<const Schema extends FurnitureSchemaDef<any>>(schema: Schema, def: Pick<FurnitureDef<Schema>, 'path' | 'createInstance'>): FurnitureDef<Schema> {
|
||||
return { ...schema, ...def };
|
||||
}
|
||||
|
||||
export const createTextureManager = (targetMesh: BABYLON.Mesh, calcTargetAspect: () => number, scene: BABYLON.Scene) => {
|
||||
let currentUrl: string | null = null;
|
||||
let currentTexture: BABYLON.Texture | null = null;
|
||||
|
||||
const updateUv = createPlaneUvMapper(targetMesh);
|
||||
|
||||
const applyFit = (method?: 'cover' | 'contain' | 'stretch') => {
|
||||
if (currentTexture == null) return;
|
||||
|
||||
const srcAspect = currentTexture.getSize().width / currentTexture.getSize().height;
|
||||
|
||||
updateUv(srcAspect, calcTargetAspect(), method ?? 'cover');
|
||||
};
|
||||
|
||||
const change = (url: string | null, fit?: 'cover' | 'contain' | 'stretch') => new Promise<BABYLON.Texture | null>((resolve) => {
|
||||
if (currentUrl === url) {
|
||||
applyFit(fit);
|
||||
resolve(currentTexture);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTexture != null) {
|
||||
currentTexture.dispose();
|
||||
}
|
||||
|
||||
currentUrl = url;
|
||||
if (url == null) {
|
||||
currentTexture = null;
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
currentTexture = new BABYLON.Texture(url, scene, false, false, undefined, () => {
|
||||
currentTexture!.level = 1;
|
||||
currentTexture!.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE;
|
||||
currentTexture!.wrapV = BABYLON.Texture.MIRROR_ADDRESSMODE;
|
||||
applyFit(fit);
|
||||
resolve(currentTexture);
|
||||
}, (message, exception) => {
|
||||
console.warn('Failed to load texture:', message, exception);
|
||||
currentTexture!.dispose();
|
||||
currentTexture = null;
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
change,
|
||||
applyFit,
|
||||
dispose: () => {
|
||||
if (currentTexture != null) {
|
||||
currentTexture.dispose();
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { a4Case_schema } from 'misskey-world/src/room/furnitures/a4Case.schema.js';
|
||||
|
||||
export const a4Case = defineFuniture(a4Case_schema, {
|
||||
createInstance: ({ options, model }) => {
|
||||
const bodyMesh = model.findMesh('__X_BODY__');
|
||||
const bodyMaterial = bodyMesh.material as BABYLON.PBRMaterial;
|
||||
|
||||
const applyMat = () => {
|
||||
bodyMaterial.albedoColor = new BABYLON.Color3(options.mat.color[0], options.mat.color[1], options.mat.color[2]);
|
||||
bodyMaterial.roughness = options.mat.roughness;
|
||||
bodyMaterial.metallic = options.mat.metallic;
|
||||
};
|
||||
|
||||
applyMat();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, _v]) => {
|
||||
applyMat();
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { aircon_schema } from 'misskey-world/src/room/furnitures/aircon.schema.js';
|
||||
|
||||
export const aircon = defineFuniture(aircon_schema, {
|
||||
createInstance: () => {
|
||||
return {
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { allInOnePc_schema } from 'misskey-world/src/room/furnitures/allInOnePc.schema.js';
|
||||
import { createTextureManager, defineFuniture } from '../furniture.js';
|
||||
import { getLightRangeFactorByGraphicsQuality } from '../utility.js';
|
||||
|
||||
export const allInOnePc = defineFuniture(allInOnePc_schema, {
|
||||
createInstance: async ({ lc, scene, options, model, graphicsQuality }) => {
|
||||
const matrix = model.root.getWorldMatrix(true);
|
||||
const scale = new BABYLON.Vector3();
|
||||
matrix.decompose(scale);
|
||||
|
||||
// TODO: graphicsQualityがLOWならそもそも追加しない
|
||||
const light = new BABYLON.SpotLight('', new BABYLON.Vector3(cm(0), cm(30) / Math.abs(scale.y), 0), new BABYLON.Vector3(0, 0, 1), Math.PI / 1, 2, scene, lc != null);
|
||||
light.parent = model.root;
|
||||
light.diffuse = new BABYLON.Color3(1.0, 1.0, 1.0);
|
||||
light.range = cm(100) * getLightRangeFactorByGraphicsQuality(graphicsQuality);
|
||||
light.radius = cm(20);
|
||||
if (lc != null) lc.addLight(light);
|
||||
|
||||
const screenMesh = model.findMesh('__X_SCREEN__');
|
||||
|
||||
const bodyMaterial = model.findMaterial('__X_BODY__');
|
||||
const bezelMaterial = model.findMaterial('__X_BEZEL__');
|
||||
const screenMaterial = model.findMaterial('__X_SCREEN__');
|
||||
|
||||
screenMaterial.ambientColor = new BABYLON.Color3(0, 0, 0);
|
||||
screenMaterial.albedoColor = new BABYLON.Color3(0, 0, 0);
|
||||
screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
|
||||
screenMaterial.roughness = 0;
|
||||
screenMaterial.metallic = 0;
|
||||
|
||||
const textureManager = createTextureManager(screenMesh, () => 50 / 27.5, scene);
|
||||
|
||||
const applyScreenBrightness = () => {
|
||||
const b = options.screenBrightness;
|
||||
screenMaterial.emissiveIntensity = b * 2;
|
||||
light.intensity = (5 * b) * WORLD_SCALE * WORLD_SCALE;
|
||||
};
|
||||
|
||||
applyScreenBrightness();
|
||||
|
||||
const applyImage = () => {
|
||||
screenMaterial.unfreeze();
|
||||
let url: string | null = null;
|
||||
if (options.image.type === '_custom_') {
|
||||
url = options.image.custom?.url ?? null;
|
||||
} else if (options.image.type === 'desktop') {
|
||||
url = '/assets/objects/all-in-one-pc/desktop.png';
|
||||
}
|
||||
return textureManager.change(url, options.image.fit).then((tex) => {
|
||||
screenMaterial.emissiveTexture = tex;
|
||||
});
|
||||
};
|
||||
|
||||
await applyImage();
|
||||
|
||||
const applyBodyMat = () => {
|
||||
bodyMaterial.albedoColor = new BABYLON.Color3(options.bodyMat.color[0], options.bodyMat.color[1], options.bodyMat.color[2]);
|
||||
bodyMaterial.roughness = options.bodyMat.roughness;
|
||||
bodyMaterial.metallic = options.bodyMat.metallic;
|
||||
};
|
||||
|
||||
const applyBezelMat = () => {
|
||||
bezelMaterial.albedoColor = new BABYLON.Color3(options.bezelMat.color[0], options.bezelMat.color[1], options.bezelMat.color[2]);
|
||||
bezelMaterial.roughness = options.bezelMat.roughness;
|
||||
bezelMaterial.metallic = options.bezelMat.metallic;
|
||||
};
|
||||
|
||||
applyBodyMat();
|
||||
applyBezelMat();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'bodyMat': applyBodyMat(); break;
|
||||
case 'bezelMat': applyBezelMat(); break;
|
||||
case 'screenBrightness': applyScreenBrightness(); break;
|
||||
case 'image': applyImage(); break;
|
||||
}
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {
|
||||
light.dispose();
|
||||
if (lc != null) lc.removeLight(light);
|
||||
scene.removeLight(light); // lc使用時はsceneには追加してないはずだが、これがないとクラッシュする babylonのバグ?
|
||||
|
||||
textureManager.dispose();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { cm } from 'misskey-world/src/utility.js';
|
||||
import { aquarium_schema } from 'misskey-world/src/room/furnitures/aquarium.schema.js';
|
||||
|
||||
export const aquarium = defineFuniture(aquarium_schema, {
|
||||
createInstance: ({ scene, root }) => {
|
||||
return {
|
||||
onInited: () => {
|
||||
const noiseTexture = new BABYLON.NoiseProceduralTexture('perlin', 256, scene);
|
||||
noiseTexture.animationSpeedFactor = 70;
|
||||
noiseTexture.persistence = 10;
|
||||
noiseTexture.brightness = 0.5;
|
||||
noiseTexture.octaves = 5;
|
||||
|
||||
const emitter = new BABYLON.TransformNode('emitter', scene);
|
||||
emitter.parent = root;
|
||||
emitter.position = new BABYLON.Vector3(cm(17), cm(7), cm(-9));
|
||||
const ps = new BABYLON.ParticleSystem('', 128, scene);
|
||||
ps.particleTexture = new BABYLON.Texture('/client-assets/world/objects/lava-lamp/bubble.png');
|
||||
ps.emitter = emitter;
|
||||
ps.isLocal = true;
|
||||
ps.minEmitBox = new BABYLON.Vector3(cm(-2), 0, cm(-2));
|
||||
ps.maxEmitBox = new BABYLON.Vector3(cm(2), 0, cm(2));
|
||||
ps.minEmitPower = cm(40);
|
||||
ps.maxEmitPower = cm(60);
|
||||
ps.minLifeTime = 0.5;
|
||||
ps.maxLifeTime = 0.5;
|
||||
ps.minSize = cm(0.1);
|
||||
ps.maxSize = cm(1);
|
||||
ps.direction1 = new BABYLON.Vector3(0, 1, 0);
|
||||
ps.direction2 = new BABYLON.Vector3(0, 1, 0);
|
||||
ps.noiseTexture = noiseTexture;
|
||||
ps.noiseStrength = new BABYLON.Vector3(500, 0, 500);
|
||||
ps.emitRate = 32;
|
||||
ps.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD;
|
||||
//ps.color1 = new BABYLON.Color4(1, 1, 1, 0.3);
|
||||
//ps.color2 = new BABYLON.Color4(1, 1, 1, 0.2);
|
||||
//ps.colorDead = new BABYLON.Color4(1, 1, 1, 0);
|
||||
ps.preWarmCycles = Math.random() * 1000;
|
||||
ps.start();
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {
|
||||
// TODO
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { aromaReedDiffuser_schema } from 'misskey-world/src/room/furnitures/aromaReedDiffuser.schema.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
|
||||
export const aromaReedDiffuser = defineFuniture(aromaReedDiffuser_schema, {
|
||||
createInstance: ({ options, model }) => {
|
||||
const bottleMaterial = model.findMaterial('__X_BOTTLE__');
|
||||
const oilMaterial = model.findMaterial('__X_OIL__');
|
||||
|
||||
const applyBottleMat = () => {
|
||||
bottleMaterial.albedoColor = new BABYLON.Color3(options.bottleMat.color[0], options.bottleMat.color[1], options.bottleMat.color[2]);
|
||||
bottleMaterial.roughness = options.bottleMat.roughness;
|
||||
bottleMaterial.metallic = options.bottleMat.metallic;
|
||||
};
|
||||
|
||||
const applyOilMat = () => {
|
||||
oilMaterial.albedoColor = new BABYLON.Color3(options.oilMat.color[0], options.oilMat.color[1], options.oilMat.color[2]);
|
||||
oilMaterial.roughness = options.oilMat.roughness;
|
||||
oilMaterial.metallic = options.oilMat.metallic;
|
||||
};
|
||||
|
||||
applyBottleMat();
|
||||
applyOilMat();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
applyBottleMat();
|
||||
applyOilMat();
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { banknote_schema } from 'misskey-world/src/room/furnitures/banknote.schema.js';
|
||||
|
||||
export const banknote = defineFuniture(banknote_schema, {
|
||||
createInstance: () => {
|
||||
return {
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { getLightRangeFactorByGraphicsQuality } from '../utility.js';
|
||||
import { beamLamp_schema } from 'misskey-world/src/room/furnitures/beamLamp.schema.js';
|
||||
|
||||
export const beamLamp = defineFuniture(beamLamp_schema, {
|
||||
createInstance: ({ lc, root, scene, graphicsQuality }) => {
|
||||
const light = new BABYLON.PointLight('beamLampLight', new BABYLON.Vector3(0, cm(10), 0), scene, lc != null);
|
||||
light.parent = root;
|
||||
light.diffuse = new BABYLON.Color3(1.0, 0.5, 0.2);
|
||||
light.intensity = 0.03 * WORLD_SCALE * WORLD_SCALE;
|
||||
light.range = cm(100) * getLightRangeFactorByGraphicsQuality(graphicsQuality);
|
||||
if (lc != null) lc.addLight(light);
|
||||
|
||||
return {
|
||||
onInited: () => {
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {
|
||||
light.dispose();
|
||||
if (lc != null) lc.removeLight(light);
|
||||
scene.removeLight(light); // lc使用時はsceneには追加してないはずだが、これがないとクラッシュする babylonのバグ?
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { bed_schema } from 'misskey-world/src/room/furnitures/bed.schema.js';
|
||||
|
||||
export const bed = defineFuniture(bed_schema, {
|
||||
createInstance: ({ options, model }) => {
|
||||
const bodyMesh = model.findMesh('__X_BODY__');
|
||||
const bodyMaterial = bodyMesh.material as BABYLON.PBRMaterial;
|
||||
|
||||
const applyFrameMat = () => {
|
||||
bodyMaterial.albedoColor = new BABYLON.Color3(options.frameMat.color[0], options.frameMat.color[1], options.frameMat.color[2]);
|
||||
bodyMaterial.roughness = options.frameMat.roughness;
|
||||
bodyMaterial.metallic = options.frameMat.metallic;
|
||||
};
|
||||
|
||||
applyFrameMat();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'frameMat': applyFrameMat(); break;
|
||||
}
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { cm, remap } from 'misskey-world/src/utility.js';
|
||||
import { createOverridedStates } from '../utility.js';
|
||||
import { blind_schema } from 'misskey-world/src/room/furnitures/blind.schema.js';
|
||||
|
||||
export const blind = defineFuniture(blind_schema, {
|
||||
createInstance: ({ options, model }) => {
|
||||
const temp = createOverridedStates({
|
||||
angle: () => options.angle,
|
||||
open: () => options.open,
|
||||
});
|
||||
|
||||
const blade = model.findMesh('__X_BLADE__');
|
||||
blade.rotation = new BABYLON.Vector3(options.angle, 0, 0);
|
||||
|
||||
let blades = [] as BABYLON.InstancedMesh[];
|
||||
|
||||
const applyOpeningState = () => {
|
||||
for (const b of blades) {
|
||||
b.dispose();
|
||||
}
|
||||
blades = [];
|
||||
|
||||
const matrix = blade.parent.getWorldMatrix(true);
|
||||
const scale = new BABYLON.Vector3();
|
||||
matrix.decompose(scale);
|
||||
|
||||
for (let i = 0; i < options.blades; i++) {
|
||||
const b = blade.clone('blade_' + i); // createInstanceを使いたいが、削除するときになぜかエラーになる
|
||||
if (i / options.blades < temp.open) {
|
||||
b.position.y -= (i * cm(4)) / Math.abs(scale.y);
|
||||
} else {
|
||||
b.position.y -= (((options.blades - 1) * temp.open * cm(4)) + (i * cm(0.3))) / Math.abs(scale.y);
|
||||
}
|
||||
blades.push(b);
|
||||
}
|
||||
|
||||
const length = Math.abs(blades.at(-1).position.y * Math.abs(scale.y));
|
||||
|
||||
for (const mesh of model.root.getChildMeshes()) {
|
||||
if (mesh.morphTargetManager != null && mesh.morphTargetManager.getTargetByName('Length') != null) {
|
||||
mesh.morphTargetManager.getTargetByName('Length').influence = remap(length, cm(10), cm(200), 0, 1);
|
||||
}
|
||||
}
|
||||
model.updated();
|
||||
|
||||
model.updated();
|
||||
};
|
||||
|
||||
const applyAngle = () => {
|
||||
for (const b of [blade, ...blades]) {
|
||||
b.rotation.x = temp.angle;
|
||||
b.rotation.x += Math.random() * 0.3 - 0.15;
|
||||
}
|
||||
};
|
||||
|
||||
applyOpeningState();
|
||||
applyAngle();
|
||||
|
||||
return {
|
||||
onInited: () => {
|
||||
|
||||
},
|
||||
interactions: {
|
||||
adjustBladeRotation: {
|
||||
label: 'Adjust blade rotation',
|
||||
fn: () => {
|
||||
temp.angle += Math.PI / 8;
|
||||
if (temp.angle >= Math.PI / 2) temp.angle = -Math.PI / 2;
|
||||
applyAngle();
|
||||
},
|
||||
},
|
||||
openClose: {
|
||||
label: 'Open/close',
|
||||
fn: () => {
|
||||
temp.open -= 0.25;
|
||||
if (temp.open < 0) temp.open = 1;
|
||||
applyOpeningState();
|
||||
},
|
||||
},
|
||||
},
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
temp.$reset();
|
||||
switch (k) {
|
||||
case 'angle': applyAngle(); break;
|
||||
case 'open': applyOpeningState(); break;
|
||||
case 'blades': applyOpeningState(); break;
|
||||
}
|
||||
},
|
||||
resetTemporaryState: () => {
|
||||
temp.$reset();
|
||||
applyAngle();
|
||||
applyOpeningState();
|
||||
},
|
||||
primaryInteraction: 'openClose',
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { book_schema } from 'misskey-world/src/room/furnitures/book.schema.js';
|
||||
|
||||
export const book = defineFuniture(book_schema, {
|
||||
createInstance: ({ options, model }) => {
|
||||
const bodyMesh = model.findMesh('__X_BODY__');
|
||||
|
||||
const applySize = () => {
|
||||
bodyMesh.morphTargetManager!.getTargetByName('Width')!.influence = options.width;
|
||||
bodyMesh.morphTargetManager!.getTargetByName('Height')!.influence = options.height;
|
||||
bodyMesh.morphTargetManager!.getTargetByName('Thickness')!.influence = options.thickness;
|
||||
model.updated();
|
||||
};
|
||||
|
||||
applySize();
|
||||
|
||||
return {
|
||||
onInited: () => {
|
||||
},
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'width':
|
||||
case 'height':
|
||||
case 'thickness':
|
||||
applySize();
|
||||
break;
|
||||
}
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { cm } from 'misskey-world/src/utility.js';
|
||||
import { books_schema } from 'misskey-world/src/room/furnitures/books.schema.js';
|
||||
|
||||
export const books = defineFuniture(books_schema, {
|
||||
createInstance: ({ scene, options, model }) => {
|
||||
const coverMaterial = model.findMaterial('__X_COVER__');
|
||||
|
||||
const applyVariation = () => {
|
||||
const coverTexture =
|
||||
options.variation === 'A' ? new BABYLON.Texture('/client-assets/world/objects/books/textures/a.png', scene, false, false) :
|
||||
options.variation === 'B' ? new BABYLON.Texture('/client-assets/world/objects/books/textures/b.png', scene, false, false) :
|
||||
options.variation === 'C' ? new BABYLON.Texture('/client-assets/world/objects/books/textures/c.png', scene, false, false) :
|
||||
options.variation === 'D' ? new BABYLON.Texture('/client-assets/world/objects/books/textures/d.png', scene, false, false) :
|
||||
new BABYLON.Texture('/client-assets/world/objects/books/textures/e.png', scene, false, false);
|
||||
coverMaterial.albedoTexture = coverTexture;
|
||||
};
|
||||
|
||||
applyVariation();
|
||||
|
||||
const bookMeshes = [
|
||||
model.findMeshes('__X_BOOK_1__'),
|
||||
model.findMeshes('__X_BOOK_2__'),
|
||||
model.findMeshes('__X_BOOK_3__'),
|
||||
model.findMeshes('__X_BOOK_4__'),
|
||||
model.findMeshes('__X_BOOK_5__'),
|
||||
model.findMeshes('__X_BOOK_6__'),
|
||||
model.findMeshes('__X_BOOK_7__'),
|
||||
model.findMeshes('__X_BOOK_8__'),
|
||||
model.findMeshes('__X_BOOK_9__'),
|
||||
model.findMeshes('__X_BOOK_10__'),
|
||||
];
|
||||
|
||||
for (const meshes of bookMeshes) {
|
||||
const z = Math.random() * 0.005;
|
||||
const y = Math.random() * 0.0025;
|
||||
for (const mesh of meshes) {
|
||||
mesh.position.z -= z;
|
||||
mesh.position.y += y;
|
||||
}
|
||||
}
|
||||
|
||||
model.updated();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
applyVariation();
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { boxWallShelf_schema } from 'misskey-world/src/room/furnitures/boxWallShelf.schema.js';
|
||||
|
||||
export const boxWallShelf = defineFuniture(boxWallShelf_schema, {
|
||||
createInstance: async ({ scene, options, model }) => {
|
||||
const backMesh = model.findMesh('__X_BACK__');
|
||||
const bodyMaterial = model.findMaterial('__X_BODY__');
|
||||
|
||||
const applySize = () => {
|
||||
for (const mesh of model.root.getChildMeshes()) {
|
||||
if (mesh.morphTargetManager != null && mesh.morphTargetManager.getTargetByName('W') != null) {
|
||||
mesh.morphTargetManager.getTargetByName('W').influence = options.width;
|
||||
}
|
||||
if (mesh.morphTargetManager != null && mesh.morphTargetManager.getTargetByName('H') != null) {
|
||||
mesh.morphTargetManager.getTargetByName('H').influence = options.height;
|
||||
}
|
||||
}
|
||||
model.updated();
|
||||
};
|
||||
|
||||
applySize();
|
||||
|
||||
const applyBodyMat = () => {
|
||||
bodyMaterial.albedoColor = new BABYLON.Color3(options.bodyMat.color[0], options.bodyMat.color[1], options.bodyMat.color[2]);
|
||||
bodyMaterial.roughness = options.bodyMat.roughness;
|
||||
bodyMaterial.metallic = options.bodyMat.metallic;
|
||||
};
|
||||
|
||||
applyBodyMat();
|
||||
|
||||
const applyWithBack = () => {
|
||||
backMesh.isVisible = options.withBack;
|
||||
model.updated();
|
||||
};
|
||||
|
||||
applyWithBack();
|
||||
|
||||
return {
|
||||
onInited: () => {
|
||||
|
||||
},
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'width':
|
||||
case 'height':
|
||||
applySize();
|
||||
break;
|
||||
case 'bodyMat':
|
||||
applyBodyMat();
|
||||
break;
|
||||
case 'withBack':
|
||||
applyWithBack();
|
||||
break;
|
||||
}
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { cactusS_schema } from 'misskey-world/src/room/furnitures/cactusS.schema.js';
|
||||
|
||||
export const cactusS = defineFuniture(cactusS_schema, {
|
||||
createInstance: ({ options, model }) => {
|
||||
const potMaterial = model.findMaterial('__X_POT__');
|
||||
|
||||
const applyPotMat = () => {
|
||||
potMaterial.albedoColor = new BABYLON.Color3(options.potMat.color[0], options.potMat.color[1], options.potMat.color[2]);
|
||||
potMaterial.roughness = options.potMat.roughness;
|
||||
potMaterial.metallic = options.potMat.metallic;
|
||||
};
|
||||
|
||||
applyPotMat();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
applyPotMat();
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { cardboardBox_schema } from 'misskey-world/src/room/furnitures/cardboardBox.schema.js';
|
||||
|
||||
export const cardboardBox = defineFuniture(cardboardBox_schema, {
|
||||
createInstance: ({ scene, options, model }) => {
|
||||
const material = model.findMaterial('__X_BODY__');
|
||||
|
||||
let tex: BABYLON.Texture | null = null;
|
||||
|
||||
const applyVariation = () => {
|
||||
if (options.variation === 'mikan') {
|
||||
tex = new BABYLON.Texture('/client-assets/world/objects/cardboard-box/textures/mikan.png', scene, false, false);
|
||||
} else if (options.variation === 'aizon') {
|
||||
tex = new BABYLON.Texture('/client-assets/world/objects/cardboard-box/textures/aizon.png', scene, false, false);
|
||||
}
|
||||
|
||||
if (tex != null) {
|
||||
material.albedoTexture = tex;
|
||||
material.albedoColor = new BABYLON.Color3(1, 1, 1);
|
||||
} else {
|
||||
material.albedoTexture = null;
|
||||
material.albedoColor = new BABYLON.Color3(0.6, 0.485, 0.31);
|
||||
}
|
||||
};
|
||||
|
||||
applyVariation();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'variation':
|
||||
applyVariation();
|
||||
break;
|
||||
}
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {
|
||||
if (tex) {
|
||||
tex.dispose();
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { ceilingFanLight_schema } from 'misskey-world/src/room/furnitures/ceilingFanLight.schema.js';
|
||||
|
||||
export const ceilingFanLight = defineFuniture(ceilingFanLight_schema, {
|
||||
createInstance: ({ options, sr, scene, model }) => {
|
||||
const shadeMaterial = model.findMaterial('__X_SHADE__');
|
||||
|
||||
const applyShadeMat = () => {
|
||||
shadeMaterial.albedoColor = new BABYLON.Color3(options.shadeMat.color[0], options.shadeMat.color[1], options.shadeMat.color[2]);
|
||||
shadeMaterial.roughness = options.shadeMat.roughness;
|
||||
shadeMaterial.metallic = options.shadeMat.metallic;
|
||||
};
|
||||
|
||||
applyShadeMat();
|
||||
|
||||
const rotor = model.findMesh('Rotor');
|
||||
model.bakeExcludeMeshes = [rotor, ...rotor.getChildMeshes()];
|
||||
|
||||
let animationObserver: BABYLON.Observer<BABYLON.Scene>;
|
||||
|
||||
return {
|
||||
onInited: () => {
|
||||
rotor.rotation = rotor.rotationQuaternion != null ? rotor.rotationQuaternion.toEulerAngles() : rotor.rotation;
|
||||
const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
|
||||
anim.setKeys([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 100, value: Math.PI * 2 },
|
||||
]);
|
||||
rotor.animations = [anim];
|
||||
animationObserver = scene.onAfterAnimationsObservable.add(() => {
|
||||
sr.updateMesh([rotor, ...rotor.getChildMeshes()], false);
|
||||
});
|
||||
scene.beginAnimation(rotor, 0, 100, true);
|
||||
},
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'shadeMat': applyShadeMat(); break;
|
||||
}
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {
|
||||
if (animationObserver != null) {
|
||||
scene.onAfterAnimationsObservable.remove(animationObserver);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { chair_schema } from 'misskey-world/src/room/furnitures/chair.schema.js';
|
||||
|
||||
export const chair = defineFuniture(chair_schema, {
|
||||
createInstance: ({ model, options, sitChair }) => {
|
||||
const primaryMaterial = model.findMaterial('__X_PRIMARY__');
|
||||
const secondaryMaterial = model.findMaterial('__X_SECONDARY__');
|
||||
const frameMaterial = model.findMaterial('__X_FRAME__');
|
||||
|
||||
const applyPrimaryMat = () => {
|
||||
primaryMaterial.albedoColor = new BABYLON.Color3(options.primaryMat.color[0], options.primaryMat.color[1], options.primaryMat.color[2]);
|
||||
primaryMaterial.roughness = options.primaryMat.roughness;
|
||||
primaryMaterial.metallic = options.primaryMat.metallic;
|
||||
};
|
||||
|
||||
const applySecondaryMat = () => {
|
||||
secondaryMaterial.albedoColor = new BABYLON.Color3(options.secondaryMat.color[0], options.secondaryMat.color[1], options.secondaryMat.color[2]);
|
||||
secondaryMaterial.roughness = options.secondaryMat.roughness;
|
||||
secondaryMaterial.metallic = options.secondaryMat.metallic;
|
||||
};
|
||||
|
||||
const applyFrameMat = () => {
|
||||
frameMaterial.albedoColor = new BABYLON.Color3(options.frameMat.color[0], options.frameMat.color[1], options.frameMat.color[2]);
|
||||
frameMaterial.roughness = options.frameMat.roughness;
|
||||
frameMaterial.metallic = options.frameMat.metallic;
|
||||
};
|
||||
|
||||
applyPrimaryMat();
|
||||
applySecondaryMat();
|
||||
applyFrameMat();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'primaryMat': applyPrimaryMat(); break;
|
||||
case 'secondaryMat': applySecondaryMat(); break;
|
||||
case 'frameMat': applyFrameMat(); break;
|
||||
}
|
||||
},
|
||||
interactions: {
|
||||
sit: {
|
||||
label: 'Sit',
|
||||
fn: () => {
|
||||
sitChair?.();
|
||||
},
|
||||
},
|
||||
},
|
||||
primaryInteraction: 'sit',
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { createTextureManager, defineFuniture } from '../furniture.js';
|
||||
import { remap } from 'misskey-world/src/utility.js';
|
||||
import { clippedPicture_schema } from 'misskey-world/src/room/furnitures/clippedPicture.schema.js';
|
||||
|
||||
export const clippedPicture = defineFuniture(clippedPicture_schema, {
|
||||
createInstance: async ({ scene, options, model }) => {
|
||||
const pictureMesh = model.findMesh('__X_PICTURE__');
|
||||
pictureMesh.rotationQuaternion = null;
|
||||
|
||||
const pictureMaterial = model.findMaterial('__X_PICTURE__');
|
||||
pictureMaterial.albedoColor = new BABYLON.Color3(1, 1, 1);
|
||||
|
||||
const textureManager = createTextureManager(pictureMesh, () => {
|
||||
const targetWidth = remap(options.width, 0, 1, 2, 100); // 最小値(値を0にした場合)でのサイズは2cmで、最大値(値を1にした場合)でのサイズは100cmなので。比率の計算だから単位はなんでもいいけど、とにかく0が0にならない点を考慮させる必要がある
|
||||
const targetHeight = remap(options.height, 0, 1, 2, 100); // 最小値(値を0にした場合)でのサイズは2cmで、最大値(値を1にした場合)でのサイズは100cmなので。比率の計算だから単位はなんでもいいけど、とにかく0が0にならない点を考慮させる必要がある
|
||||
return targetWidth / targetHeight;
|
||||
}, scene);
|
||||
|
||||
const applySize = () => {
|
||||
for (const mesh of model.root.getChildMeshes()) {
|
||||
if (mesh.morphTargetManager != null && mesh.morphTargetManager.getTargetByName('Width') != null) {
|
||||
mesh.morphTargetManager.getTargetByName('Width').influence = options.width;
|
||||
}
|
||||
if (mesh.morphTargetManager != null && mesh.morphTargetManager.getTargetByName('Height') != null) {
|
||||
mesh.morphTargetManager.getTargetByName('Height').influence = options.height;
|
||||
}
|
||||
}
|
||||
model.updated();
|
||||
textureManager.applyFit();
|
||||
};
|
||||
|
||||
applySize();
|
||||
|
||||
const applyImage = () => {
|
||||
pictureMaterial.unfreeze();
|
||||
let url: string | null = null;
|
||||
if (options.image.type === '_custom_') {
|
||||
url = options.image.custom?.url ?? null;
|
||||
}
|
||||
return textureManager.change(url, options.image.fit).then((tex) => {
|
||||
pictureMaterial.albedoTexture = tex;
|
||||
});
|
||||
};
|
||||
|
||||
await applyImage();
|
||||
|
||||
return {
|
||||
onInited: () => {
|
||||
|
||||
},
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'width': applySize(); break;
|
||||
case 'height': applySize(); break;
|
||||
case 'image': applyImage(); break;
|
||||
}
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {
|
||||
textureManager.dispose();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { coffeeCup_schema } from 'misskey-world/src/room/furnitures/coffeeCup.schema.js';
|
||||
|
||||
export const coffeeCup = defineFuniture(coffeeCup_schema, {
|
||||
createInstance: () => {
|
||||
return {
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { colorBox_schema } from 'misskey-world/src/room/furnitures/colorBox.schema.js';
|
||||
|
||||
export const colorBox = defineFuniture(colorBox_schema, {
|
||||
createInstance: ({ options, model }) => {
|
||||
const bodyMaterial = model.findMaterial('__X_BODY__');
|
||||
|
||||
const applyMat = () => {
|
||||
bodyMaterial.albedoColor = new BABYLON.Color3(options.mat.color[0], options.mat.color[1], options.mat.color[2]);
|
||||
bodyMaterial.roughness = options.mat.roughness;
|
||||
bodyMaterial.metallic = options.mat.metallic;
|
||||
};
|
||||
|
||||
applyMat();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
applyMat();
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { cuboid_schema } from 'misskey-world/src/room/furnitures/cuboid.schema.js';
|
||||
|
||||
export const cuboid = defineFuniture(cuboid_schema, {
|
||||
createInstance: async ({ scene, options, model }) => {
|
||||
const mesh = model.findMesh('__X_BODY__');
|
||||
const mat = model.findMaterial('__X_BODY__');
|
||||
|
||||
const applySize = () => {
|
||||
mesh.morphTargetManager!.getTargetByName('X')!.influence = options.x;
|
||||
mesh.morphTargetManager!.getTargetByName('Y')!.influence = options.y;
|
||||
mesh.morphTargetManager!.getTargetByName('Z')!.influence = options.z;
|
||||
model.updated();
|
||||
};
|
||||
|
||||
applySize();
|
||||
|
||||
const applyMat = () => {
|
||||
mat.albedoColor = new BABYLON.Color3(options.mat.color[0], options.mat.color[1], options.mat.color[2]);
|
||||
mat.roughness = options.mat.roughness;
|
||||
mat.metallic = options.mat.metallic;
|
||||
};
|
||||
|
||||
applyMat();
|
||||
|
||||
return {
|
||||
onInited: () => {
|
||||
|
||||
},
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'mat':
|
||||
applyMat();
|
||||
break;
|
||||
case 'x':
|
||||
case 'y':
|
||||
case 'z':
|
||||
applySize();
|
||||
break;
|
||||
}
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { cm } from 'misskey-world/src/utility.js';
|
||||
import { yuge } from '../utility.js';
|
||||
import { cupNoodle_schema } from 'misskey-world/src/room/furnitures/cupNoodle.schema.js';
|
||||
|
||||
export const cupNoodle = defineFuniture(cupNoodle_schema, {
|
||||
createInstance: ({ scene, root, sr }) => {
|
||||
let yugeDispose: (() => void) | null = null;
|
||||
|
||||
return {
|
||||
onInited: () => {
|
||||
yugeDispose = yuge(scene, root, new BABYLON.Vector3(0, cm(10), 0), sr);
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {
|
||||
yugeDispose?.();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { curtain_schema } from 'misskey-world/src/room/furnitures/curtain.schema.js';
|
||||
|
||||
export const curtain = defineFuniture(curtain_schema, {
|
||||
createInstance: () => {
|
||||
return {
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { custardPudding_schema } from 'misskey-world/src/room/furnitures/custardPudding.schema.js';
|
||||
|
||||
export const custardPudding = defineFuniture(custardPudding_schema, {
|
||||
createInstance: () => {
|
||||
return {
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { debugHipoly_schema } from 'misskey-world/src/room/furnitures/debugHipoly.schema.js';
|
||||
|
||||
export const debugHipoly = defineFuniture(debugHipoly_schema, {
|
||||
createInstance: () => {
|
||||
return {
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { debugMetal_schema } from 'misskey-world/src/room/furnitures/debugMetal.schema.js';
|
||||
|
||||
export const debugMetal = defineFuniture(debugMetal_schema, {
|
||||
createInstance: () => {
|
||||
return {
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { descriptionPlate_schema } from 'misskey-world/src/room/furnitures/descriptionPlate.schema.js';
|
||||
|
||||
export const descriptionPlate = defineFuniture(descriptionPlate_schema, {
|
||||
createInstance: () => {
|
||||
return {
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { desk_schema } from 'misskey-world/src/room/furnitures/desk.schema.js';
|
||||
|
||||
export const desk = defineFuniture(desk_schema, {
|
||||
createInstance: ({ options, model }) => {
|
||||
const frameMaterial = model.findMaterial('__X_FRAME__');
|
||||
const boardMaterial = model.findMaterial('__X_BOARD__');
|
||||
|
||||
const applyFrameMat = () => {
|
||||
frameMaterial.albedoColor = new BABYLON.Color3(options.frameMat.color[0], options.frameMat.color[1], options.frameMat.color[2]);
|
||||
frameMaterial.roughness = options.frameMat.roughness;
|
||||
frameMaterial.metallic = options.frameMat.metallic;
|
||||
};
|
||||
|
||||
applyFrameMat();
|
||||
|
||||
const applyBoardMat = () => {
|
||||
boardMaterial.albedoColor = new BABYLON.Color3(options.boardMat.color[0], options.boardMat.color[1], options.boardMat.color[2]);
|
||||
boardMaterial.roughness = options.boardMat.roughness;
|
||||
boardMaterial.metallic = options.boardMat.metallic;
|
||||
};
|
||||
|
||||
applyBoardMat();
|
||||
|
||||
const applySize = () => {
|
||||
for (const mesh of model.root.getChildMeshes()) {
|
||||
if (mesh.morphTargetManager != null && mesh.morphTargetManager.getTargetByName('W') != null) {
|
||||
mesh.morphTargetManager.getTargetByName('W').influence = options.width;
|
||||
}
|
||||
if (mesh.morphTargetManager != null && mesh.morphTargetManager.getTargetByName('D') != null) {
|
||||
mesh.morphTargetManager.getTargetByName('D').influence = options.depth;
|
||||
}
|
||||
}
|
||||
model.updated();
|
||||
};
|
||||
|
||||
applySize();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'frameMat': applyFrameMat(); break;
|
||||
case 'boardMat': applyBoardMat(); break;
|
||||
case 'width': applySize(); break;
|
||||
case 'depth': applySize(); break;
|
||||
}
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { getLightRangeFactorByGraphicsQuality } from '../utility.js';
|
||||
import { desktopPc_schema } from 'misskey-world/src/room/furnitures/desktopPc.schema.js';
|
||||
|
||||
export const desktopPc = defineFuniture(desktopPc_schema, {
|
||||
createInstance: ({ options, model, root, scene, lc, graphicsQuality }) => {
|
||||
// TODO: graphicsQualityがLOWならそもそも追加しない
|
||||
const light1 = new BABYLON.SpotLight('', new BABYLON.Vector3(0, cm(10), cm(22)), new BABYLON.Vector3(0, 0, 1), Math.PI / 1, 2, scene, lc != null);
|
||||
light1.parent = root;
|
||||
light1.intensity = 0.05 * WORLD_SCALE * WORLD_SCALE;
|
||||
light1.range = cm(30) * getLightRangeFactorByGraphicsQuality(graphicsQuality);
|
||||
if (lc != null) lc.addLight(light1);
|
||||
|
||||
const light2 = new BABYLON.SpotLight('', new BABYLON.Vector3(cm(-5), cm(33), cm(-9)), new BABYLON.Vector3(1, 0, 0), Math.PI / 1, 2, scene, lc != null);
|
||||
light2.parent = root;
|
||||
light2.intensity = 0.05 * WORLD_SCALE * WORLD_SCALE;
|
||||
light2.range = cm(30) * getLightRangeFactorByGraphicsQuality(graphicsQuality);
|
||||
if (lc != null) lc.addLight(light2);
|
||||
|
||||
const bodyMaterial = model.findMaterial('__X_BODY__');
|
||||
const coverMaterial = model.findMaterial('__X_COVER__');
|
||||
const inner1Material = model.findMaterial('__X_INNER__');
|
||||
const inner2Material = model.findMaterial('__X_INNER2__');
|
||||
const inner3Material = model.findMaterial('__X_TUBE__');
|
||||
const ledMaterial = model.findMaterial('__X_LED__');
|
||||
|
||||
ledMaterial.emissiveIntensity = 1;
|
||||
|
||||
const applyBodyMat = () => {
|
||||
bodyMaterial.albedoColor = new BABYLON.Color3(options.bodyMat.color[0], options.bodyMat.color[1], options.bodyMat.color[2]);
|
||||
bodyMaterial.roughness = options.bodyMat.roughness;
|
||||
bodyMaterial.metallic = options.bodyMat.metallic;
|
||||
};
|
||||
|
||||
applyBodyMat();
|
||||
|
||||
const applyCoverMat = () => {
|
||||
coverMaterial.albedoColor = new BABYLON.Color3(options.coverMat.color[0], options.coverMat.color[1], options.coverMat.color[2]);
|
||||
coverMaterial.roughness = options.coverMat.roughness;
|
||||
coverMaterial.metallic = options.coverMat.metallic;
|
||||
};
|
||||
|
||||
applyCoverMat();
|
||||
|
||||
const applyInner1Mat = () => {
|
||||
inner1Material.albedoColor = new BABYLON.Color3(options.inner1Mat.color[0], options.inner1Mat.color[1], options.inner1Mat.color[2]);
|
||||
inner1Material.roughness = options.inner1Mat.roughness;
|
||||
inner1Material.metallic = options.inner1Mat.metallic;
|
||||
};
|
||||
|
||||
applyInner1Mat();
|
||||
|
||||
const applyInner2Mat = () => {
|
||||
inner2Material.albedoColor = new BABYLON.Color3(options.inner2Mat.color[0], options.inner2Mat.color[1], options.inner2Mat.color[2]);
|
||||
inner2Material.roughness = options.inner2Mat.roughness;
|
||||
inner2Material.metallic = options.inner2Mat.metallic;
|
||||
};
|
||||
|
||||
applyInner2Mat();
|
||||
|
||||
const applyInner3Mat = () => {
|
||||
inner3Material.albedoColor = new BABYLON.Color3(options.inner3Mat.color[0], options.inner3Mat.color[1], options.inner3Mat.color[2]);
|
||||
inner3Material.roughness = options.inner3Mat.roughness;
|
||||
inner3Material.metallic = options.inner3Mat.metallic;
|
||||
};
|
||||
|
||||
applyInner3Mat();
|
||||
|
||||
const applyLedColor = () => {
|
||||
const [r, g, b] = options.ledColor;
|
||||
ledMaterial.emissiveColor = new BABYLON.Color3(r, g, b);
|
||||
light1.diffuse = new BABYLON.Color3(r, g, b);
|
||||
light2.diffuse = new BABYLON.Color3(r, g, b);
|
||||
};
|
||||
|
||||
applyLedColor();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
applyBodyMat();
|
||||
applyCoverMat();
|
||||
applyInner1Mat();
|
||||
applyInner2Mat();
|
||||
applyInner3Mat();
|
||||
applyLedColor();
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {
|
||||
light1.dispose();
|
||||
light2.dispose();
|
||||
if (lc != null) {
|
||||
lc.removeLight(light1);
|
||||
lc.removeLight(light2);
|
||||
}
|
||||
scene.removeLight(light1); // lc使用時はsceneには追加してないはずだが、これがないとクラッシュする babylonのバグ?
|
||||
scene.removeLight(light2); // lc使用時はsceneには追加してないはずだが、これがないとクラッシュする babylonのバグ?
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { djMixer_schema } from 'misskey-world/src/room/furnitures/djMixer.schema.js';
|
||||
|
||||
export const djMixer = defineFuniture(djMixer_schema, {
|
||||
createInstance: () => {
|
||||
return {
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { djPlayer_schema } from 'misskey-world/src/room/furnitures/djPlayer.schema.js';
|
||||
import { createTextureManager, defineFuniture } from '../furniture.js';
|
||||
import { normalizeUvToSquare } from '../../utility.js';
|
||||
|
||||
export const djPlayer = defineFuniture(djPlayer_schema, {
|
||||
createInstance: async ({ model, options, scene }) => {
|
||||
const screenMesh = model.findMesh('__X_SCREEN__');
|
||||
const screenMaterial = model.findMaterial('__X_SCREEN__');
|
||||
screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
|
||||
screenMaterial.roughness = 0;
|
||||
screenMaterial.metallic = 0;
|
||||
|
||||
normalizeUvToSquare(screenMesh);
|
||||
|
||||
const textureManager = createTextureManager(screenMesh, () => 15.6 / 8.33, scene);
|
||||
|
||||
const applyScreenBrightness = () => {
|
||||
const b = options.screenBrightness;
|
||||
screenMaterial.emissiveIntensity = b * 2;
|
||||
};
|
||||
|
||||
applyScreenBrightness();
|
||||
|
||||
const applyImage = () => {
|
||||
screenMaterial.unfreeze();
|
||||
let url: string | null = null;
|
||||
if (options.image.type === '_custom_') {
|
||||
url = options.image.custom?.url ?? null;
|
||||
} else if (options.image.type === 'waveform') {
|
||||
url = '/client-assets/world/objects/dj-player/textures/display-waveform.png';
|
||||
}
|
||||
return textureManager.change(url, options.image.fit).then((tex) => {
|
||||
screenMaterial.emissiveTexture = tex;
|
||||
});
|
||||
};
|
||||
|
||||
await applyImage();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'screenBrightness': applyScreenBrightness(); break;
|
||||
case 'image': applyImage(); break;
|
||||
}
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {
|
||||
textureManager.dispose();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { getLightRangeFactorByGraphicsQuality } from '../utility.js';
|
||||
import { cm, remap, WORLD_SCALE } from 'misskey-world/src/utility.js';
|
||||
import { ductRailSpotLights_schema } from 'misskey-world/src/room/furnitures/ductRailSpotLights.schema.js';
|
||||
|
||||
export const ductRailSpotLights = defineFuniture(ductRailSpotLights_schema, {
|
||||
createInstance: ({ lc, scene, options, model, graphicsQuality }) => {
|
||||
const bodyMaterial = model.findMaterial('__X_BODY__');
|
||||
|
||||
const applyBodyMat = () => {
|
||||
bodyMaterial.albedoColor = new BABYLON.Color3(options.bodyMat.color[0], options.bodyMat.color[1], options.bodyMat.color[2]);
|
||||
bodyMaterial.roughness = options.bodyMat.roughness;
|
||||
bodyMaterial.metallic = options.bodyMat.metallic;
|
||||
};
|
||||
|
||||
applyBodyMat();
|
||||
|
||||
const lamps = model.findMeshes('__X_LAMP__');
|
||||
const lights: BABYLON.SpotLight[] = [];
|
||||
for (const lamp of lamps) {
|
||||
const light = new BABYLON.SpotLight('', new BABYLON.Vector3(cm(0), cm(0), 0), new BABYLON.Vector3(0, -1, 0), Math.PI / 1, 2, scene, lc != null);
|
||||
light.parent = lamp;
|
||||
light.radius = cm(8);
|
||||
if (lc != null) lc.addLight(light);
|
||||
lights.push(light);
|
||||
}
|
||||
|
||||
const applyLight = () => {
|
||||
const [r, g, b] = options.light.color;
|
||||
for (const light of lights) {
|
||||
light.diffuse = new BABYLON.Color3(r, g, b);
|
||||
light.intensity = 5 * options.light.brightness * WORLD_SCALE * WORLD_SCALE;
|
||||
light.range = remap(options.light.brightness, 0, 1, cm(200), cm(400)) * getLightRangeFactorByGraphicsQuality(graphicsQuality);
|
||||
}
|
||||
for (const lamp of lamps) {
|
||||
const emissive = lamp.material as BABYLON.PBRMaterial;
|
||||
emissive.emissiveColor = new BABYLON.Color3(r, g, b);
|
||||
emissive.emissiveIntensity = options.light.brightness * 100;
|
||||
}
|
||||
};
|
||||
|
||||
applyLight();
|
||||
|
||||
const shades = model.findMeshes('__X_SHADE__');
|
||||
|
||||
const applyAngle = () => {
|
||||
for (const shade of shades) {
|
||||
shade.rotationQuaternion = null;
|
||||
shade.rotation = new BABYLON.Vector3(0, 0, 0);
|
||||
shade.addRotation(remap(options.angleV, 0, 1, Math.PI / 2, -Math.PI / 2), 0, 0);
|
||||
shade.addRotation(0, 0, remap(options.angleH, 0, 1, -Math.PI / 2, Math.PI / 2));
|
||||
}
|
||||
model.updated();
|
||||
};
|
||||
|
||||
applyAngle();
|
||||
|
||||
return {
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'bodyMat': applyBodyMat(); break;
|
||||
case 'light': applyLight(); break;
|
||||
case 'angleV':
|
||||
case 'angleH':
|
||||
applyAngle();
|
||||
break;
|
||||
}
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {
|
||||
for (const light of lights) {
|
||||
light.dispose();
|
||||
if (lc != null) lc.removeLight(light);
|
||||
scene.removeLight(light); // lc使用時はsceneには追加してないはずだが、これがないとクラッシュする babylonのバグ?
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { ductTape_schema } from 'misskey-world/src/room/furnitures/ductTape.schema.js';
|
||||
|
||||
export const ductTape = defineFuniture(ductTape_schema, {
|
||||
createInstance: () => {
|
||||
return {
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core/pure.js';
|
||||
import { electronicDisplayBoard_schema } from 'misskey-world/src/room/furnitures/electronicDisplayBoard.schema.js';
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { RecyvlingTextGrid } from '../../utility.js';
|
||||
|
||||
export const electronicDisplayBoard = defineFuniture(electronicDisplayBoard_schema, {
|
||||
createInstance: async ({ scene, options, model, timer }) => {
|
||||
const frameMaterial = model.findMaterial('__X_BODY__');
|
||||
|
||||
const textMaterial = new BABYLON.PBRMaterial('textMaterial', scene);
|
||||
textMaterial.albedoColor = new BABYLON.Color3(0, 0, 0);
|
||||
textMaterial.roughness = 1;
|
||||
|
||||
const texLoading = Promise.withResolvers<void>();
|
||||
|
||||
const tex = new BABYLON.Texture('/client-assets/room/textures/dot-matrix-chars.png', scene, false, false, undefined, () => {
|
||||
tex.level = 1;
|
||||
textMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
|
||||
textMaterial.emissiveTexture = tex;
|
||||
textMaterial.albedoTexture = tex;
|
||||
textMaterial.disableLighting = true;
|
||||
textMaterial.emissiveTexture.hasAlpha = true;
|
||||
textMaterial.transparencyMode = BABYLON.Material.MATERIAL_ALPHABLEND;
|
||||
textMaterial.useAlphaFromAlbedoTexture = true;
|
||||
textMaterial.freeze();
|
||||
texLoading.resolve();
|
||||
}, (message, exception) => {
|
||||
console.warn('Failed to load texture:', message, exception);
|
||||
textMaterial.emissiveColor = new BABYLON.Color3(0, 1, 0);
|
||||
textMaterial.emissiveTexture = null;
|
||||
texLoading.resolve();
|
||||
});
|
||||
|
||||
await texLoading.promise;
|
||||
|
||||
const maxChars = 6;
|
||||
|
||||
const displayMesh = model.findMesh('__X_DISPLAY__');
|
||||
displayMesh.material = textMaterial;
|
||||
const textManager = new RecyvlingTextGrid(displayMesh, maxChars, {
|
||||
meshFlipped: true,
|
||||
material: textMaterial,
|
||||
charUScale: 1.15,
|
||||
});
|
||||
|
||||
model.bakeExcludeMeshes = [displayMesh];
|
||||
|
||||
const applyFrameMat = () => {
|
||||
frameMaterial.albedoColor = new BABYLON.Color3(options.frameMat.color[0], options.frameMat.color[1], options.frameMat.color[2]);
|
||||
frameMaterial.roughness = options.frameMat.roughness;
|
||||
frameMaterial.metallic = options.frameMat.metallic;
|
||||
};
|
||||
|
||||
applyFrameMat();
|
||||
|
||||
const applyLedColor = () => {
|
||||
const [r, g, b] = options.ledColor;
|
||||
textMaterial.emissiveColor = new BABYLON.Color3(r, g, b);
|
||||
};
|
||||
|
||||
applyLedColor();
|
||||
|
||||
const applyLedBrightness = () => {
|
||||
textMaterial.emissiveIntensity = options.ledBrightness * 2;
|
||||
};
|
||||
|
||||
applyLedBrightness();
|
||||
|
||||
let text = '';
|
||||
|
||||
const applyText = () => {
|
||||
text = options.text + ' ';
|
||||
};
|
||||
|
||||
applyText();
|
||||
|
||||
let textIndex = 0;
|
||||
timer.setInterval(() => {
|
||||
let displayText = '';
|
||||
for (let i = 0; i < maxChars; i++) {
|
||||
displayText += text[(textIndex + i) % text.length];
|
||||
}
|
||||
textManager.write(displayText);
|
||||
|
||||
textIndex = (textIndex + 1) % text.length;
|
||||
}, 500);
|
||||
|
||||
return {
|
||||
onInited: () => {
|
||||
|
||||
},
|
||||
onOptionsUpdated: ([k, v]) => {
|
||||
switch (k) {
|
||||
case 'text': applyText(); break;
|
||||
case 'frameMat': applyFrameMat(); break;
|
||||
case 'ledColor': applyLedColor(); break;
|
||||
case 'ledBrightness': applyLedBrightness(); break;
|
||||
}
|
||||
},
|
||||
interactions: {},
|
||||
dispose: () => {
|
||||
textManager.dispose();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { emptyBento_schema } from 'misskey-world/src/room/furnitures/emptyBento.schema.js';
|
||||
|
||||
export const emptyBento = defineFuniture(emptyBento_schema, {
|
||||
createInstance: () => {
|
||||
return {
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineFuniture } from '../furniture.js';
|
||||
import { energyDrink_schema } from 'misskey-world/src/room/furnitures/energyDrink.schema.js';
|
||||
|
||||
export const energyDrink = defineFuniture(energyDrink_schema, {
|
||||
createInstance: () => {
|
||||
return {
|
||||
interactions: {},
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user