diff --git a/public/buildings/gwacheon.baked.bin b/public/buildings/gwacheon.baked.bin index c8a6d46..0511620 100644 Binary files a/public/buildings/gwacheon.baked.bin and b/public/buildings/gwacheon.baked.bin differ diff --git a/public/buildings/gwacheon.baked.bin.gz b/public/buildings/gwacheon.baked.bin.gz index 7df4be1..14bac5f 100644 Binary files a/public/buildings/gwacheon.baked.bin.gz and b/public/buildings/gwacheon.baked.bin.gz differ diff --git a/public/buildings/gwacheon.baked.json b/public/buildings/gwacheon.baked.json index 19c807d..24d93e4 100644 --- a/public/buildings/gwacheon.baked.json +++ b/public/buildings/gwacheon.baked.json @@ -1,8 +1,8 @@ { "version": 1, - "sourceHash": "b1af9482e14c57eb27998ef06b39c129d02770cf3eba5037b9983147c64f7dba", - "bufferHash": "d8cf0a36a46572060189e2ca9e9a61e448fb3d0b441abc346b56b7c4603e0d65", - "byteLength": 11036560, + "sourceHash": "bf56c2042d6f3ffe22b1258cf7ec9c266d4ca20ddab581acb126fe26d1b71246", + "bufferHash": "618e2ec1d1ea94c044fbeb49a6025d9fcf76120f052a14a82ee9e833335844f1", + "byteLength": 10245280, "terrain": { "origin": { "latitude": 37.445, @@ -23,79 +23,1519 @@ "meshes": [ { "source": "tagged", - "vertices": 113280, + "vertices": 11058, "position": 0, - "normal": 1359360, - "lightHeight": 2718720, - "index": 3171840, - "indexCount": 205500, - "indexType": "uint32", + "normal": 132696, + "lightHeight": 265392, + "index": 309624, + "indexCount": 20208, + "indexType": "uint16", "box": [ - 968.7347412109375, - -581.7264404296875, - -1581.2288818359375, - 4514.0634765625, + 1990.0223388671875, + -566.657470703125, + 989.5302124023438, + 3008.971923828125, + -509.38629150390625, + 2000.2432861328125 + ], + "sphere": [ + 2499.4971313476562, + -538.0218811035156, + 1494.8867492675781, + 625.1403033456916 + ] + }, + { + "source": "tagged", + "vertices": 14436, + "position": 350040, + "normal": 523272, + "lightHeight": 696504, + "index": 754248, + "indexCount": 26856, + "indexType": "uint16", + "box": [ + 2029.9027099609375, + -565.3079223632812, + 1968.2987060546875, + 3000.396728515625, -475.2237243652344, + 3000.07568359375 + ], + "sphere": [ + 2515.1497192382812, + -520.2658233642578, + 2484.1871948242188, + 620.9645871008289 + ] + }, + { + "source": "tagged", + "vertices": 3666, + "position": 807960, + "normal": 851952, + "lightHeight": 895944, + "index": 910608, + "indexCount": 6648, + "indexType": "uint16", + "box": [ + 2985.31640625, + -573.4452514648438, + -6.019999980926514, + 4004.523681640625, + -517.7538452148438, + 951.748046875 + ], + "sphere": [ + 3494.9200439453125, + -545.5995483398438, + 472.86402344703674, + 592.6728106016157 + ] + }, + { + "source": "tagged", + "vertices": 4182, + "position": 923904, + "normal": 974088, + "lightHeight": 1024272, + "index": 1041000, + "indexCount": 7272, + "indexType": "uint16", + "box": [ + 1999.0623779296875, + -553.9342041015625, + 3049.955078125, + 3006.898681640625, + -514.7127685546875, 3871.245361328125 ], "sphere": [ - 2741.3991088867188, - -528.4750823974609, - 1145.0082397460938, - 3153.873357261772 + 2502.9805297851562, + -534.323486328125, + 3460.6002197265625, + 632.3245873443619 + ] + }, + { + "source": "tagged", + "vertices": 5358, + "position": 1055544, + "normal": 1119840, + "lightHeight": 1184136, + "index": 1205568, + "indexCount": 9528, + "indexType": "uint16", + "box": [ + 991.6519775390625, + -562.0457153320312, + 2999.94384765625, + 2010.0164794921875, + -529.3822021484375, + 3869.3974609375 + ], + "sphere": [ + 1500.834228515625, + -545.7139587402344, + 3434.670654296875, + 668.4681281784638 + ] + }, + { + "source": "tagged", + "vertices": 864, + "position": 1224624, + "normal": 1234992, + "lightHeight": 1245360, + "index": 1248816, + "indexCount": 1584, + "indexType": "uint16", + "box": [ + 3005.676025390625, + -525.2055053710938, + 2991.09619140625, + 3248.484130859375, + -505.73468017578125, + 3752.110107421875 + ], + "sphere": [ + 3127.080078125, + -515.4700927734375, + 3371.6031494140625, + 391.3302918456449 + ] + }, + { + "source": "tagged", + "vertices": 5844, + "position": 1251984, + "normal": 1322112, + "lightHeight": 1392240, + "index": 1415616, + "indexCount": 10428, + "indexType": "uint16", + "box": [ + 3989.47705078125, + -516.524658203125, + 2155.928955078125, + 4219.091796875, + -478.5941467285156, + 2659.421630859375 + ], + "sphere": [ + 4104.284423828125, + -497.5594024658203, + 2407.67529296875, + 268.5800770599544 + ] + }, + { + "source": "tagged", + "vertices": 3636, + "position": 1436472, + "normal": 1480104, + "lightHeight": 1523736, + "index": 1538280, + "indexCount": 6744, + "indexType": "uint16", + "box": [ + 1019.0587158203125, + -558.5562133789062, + 2024.829833984375, + 2025.080322265625, + -502.58935546875, + 3005.344482421875 + ], + "sphere": [ + 1522.0695190429688, + -530.5727844238281, + 2515.087158203125, + 621.4709437626252 + ] + }, + { + "source": "tagged", + "vertices": 4056, + "position": 1551768, + "normal": 1600440, + "lightHeight": 1649112, + "index": 1665336, + "indexCount": 7296, + "indexType": "uint16", + "box": [ + 2399.493408203125, + -573.3099975585938, + -7.995755672454834, + 3005.00732421875, + -477.4343566894531, + 996.9020385742188 + ], + "sphere": [ + 2702.2503662109375, + -525.3721771240234, + 494.45314145088196, + 585.1093563370331 + ] + }, + { + "source": "tagged", + "vertices": 9486, + "position": 1679928, + "normal": 1793760, + "lightHeight": 1907592, + "index": 1945536, + "indexCount": 17316, + "indexType": "uint16", + "box": [ + 3002.441650390625, + -554.7164916992188, + 1986.975341796875, + 4003.69189453125, + -495.44903564453125, + 3006.90478515625 + ], + "sphere": [ + 3503.0667724609375, + -525.082763671875, + 2496.9400634765625, + 655.179034554249 + ] + }, + { + "source": "tagged", + "vertices": 8670, + "position": 1980168, + "normal": 2084208, + "lightHeight": 2188248, + "index": 2222928, + "indexCount": 15792, + "indexType": "uint16", + "box": [ + 3129.1669921875, + -581.4207763671875, + -1581.2288818359375, + 4010.90625, + -541.5855712890625, + -991.3208618164062 + ], + "sphere": [ + 3570.03662109375, + -561.503173828125, + -1286.2748718261719, + 493.87148600904067 + ] + }, + { + "source": "tagged", + "vertices": 8964, + "position": 2254512, + "normal": 2362080, + "lightHeight": 2469648, + "index": 2505504, + "indexCount": 16212, + "indexType": "uint16", + "box": [ + 2999.349365234375, + -567.2628173828125, + 1013.7841186523438, + 3696.780029296875, + -510.9555358886719, + 1982.0465087890625 + ], + "sphere": [ + 3348.064697265625, + -539.1091766357422, + 1497.9153137207031, + 543.523779232221 + ] + }, + { + "source": "tagged", + "vertices": 13830, + "position": 2537928, + "normal": 2703888, + "lightHeight": 2869848, + "index": 2925168, + "indexCount": 25116, + "indexType": "uint16", + "box": [ + 2312.572021484375, + -575.4396362304688, + -981.1544189453125, + 3003.3173828125, + -527.7566528320312, + -71.8021240234375 + ], + "sphere": [ + 2657.9447021484375, + -551.59814453125, + -526.478271484375, + 528.2240627178587 + ] + }, + { + "source": "tagged", + "vertices": 2400, + "position": 2975400, + "normal": 3004200, + "lightHeight": 3033000, + "index": 3042600, + "indexCount": 4392, + "indexType": "uint16", + "box": [ + 4024.096923828125, + -570.1903076171875, + 40.56344223022461, + 4503.033203125, + -513.4575805664062, + 607.9717407226562 + ], + "sphere": [ + 4263.5650634765625, + -541.8239440917969, + 324.26759147644043, + 368.2654066210293 + ] + }, + { + "source": "tagged", + "vertices": 6210, + "position": 3051384, + "normal": 3125904, + "lightHeight": 3200424, + "index": 3225264, + "indexCount": 11172, + "indexType": "uint16", + "box": [ + 3988.986083984375, + -581.5474243164062, + -957.4118041992188, + 4514.0634765625, + -534.553466796875, + -4.812453746795654 + ], + "sphere": [ + 4251.5247802734375, + -558.0504455566406, + -481.1121289730072, + 533.7486239080677 + ] + }, + { + "source": "tagged", + "vertices": 306, + "position": 3247608, + "normal": 3251280, + "lightHeight": 3254952, + "index": 3256176, + "indexCount": 540, + "indexType": "uint16", + "box": [ + 1833.488037109375, + -552.224609375, + 1253.0126953125, + 2030.300048828125, + -507.626953125, + 1898.3787841796875 + ], + "sphere": [ + 1931.89404296875, + -529.92578125, + 1575.6957397460938, + 324.9363120686425 + ] + }, + { + "source": "tagged", + "vertices": 8532, + "position": 3257256, + "normal": 3359640, + "lightHeight": 3462024, + "index": 3496152, + "indexCount": 15156, + "indexType": "uint16", + "box": [ + 2991.234375, + -580.3607788085938, + -1006.1098022460938, + 4008.443603515625, + -556.07080078125, + 1.7571762800216675 + ], + "sphere": [ + 3499.8389892578125, + -568.2157897949219, + -502.17631298303604, + 625.2452199357979 + ] + }, + { + "source": "tagged", + "vertices": 366, + "position": 3526464, + "normal": 3530856, + "lightHeight": 3535248, + "index": 3536712, + "indexCount": 636, + "indexType": "uint16", + "box": [ + 4016.491943359375, + -581.7264404296875, + -1287.9664306640625, + 4131.6728515625, + -572.1187133789062, + -1170.6903076171875 + ], + "sphere": [ + 4074.0823974609375, + -576.9225769042969, + -1229.328369140625, + 64.81275112572958 + ] + }, + { + "source": "tagged", + "vertices": 114, + "position": 3537984, + "normal": 3539352, + "lightHeight": 3540720, + "index": 3541176, + "indexCount": 204, + "indexType": "uint16", + "box": [ + 968.7347412109375, + -550.8993530273438, + 3031.947509765625, + 991.8428955078125, + -541.79931640625, + 3235.147705078125 + ], + "sphere": [ + 980.288818359375, + -546.3493347167969, + 3133.547607421875, + 101.9842366146371 + ] + }, + { + "source": "tagged", + "vertices": 978, + "position": 3541584, + "normal": 3553320, + "lightHeight": 3565056, + "index": 3568968, + "indexCount": 1812, + "indexType": "uint16", + "box": [ + 4022.41357421875, + -553.6287231445312, + 1217.3280029296875, + 4509.3876953125, + -510.69561767578125, + 1691.7518310546875 + ], + "sphere": [ + 4265.900634765625, + -532.1621704101562, + 1454.5399169921875, + 331.85616627585574 + ] + }, + { + "source": "tagged", + "vertices": 96, + "position": 3572592, + "normal": 3573744, + "lightHeight": 3574896, + "index": 3575280, + "indexCount": 180, + "indexType": "uint16", + "box": [ + 971.7979125976562, + -546.6626586914062, + 2993.030517578125, + 987.65625, + -537.9126586914062, + 3005.477783203125 + ], + "sphere": [ + 979.7270812988281, + -542.2876586914062, + 2999.254150390625, + 10.972017300202676 + ] + }, + { + "source": "tagged", + "vertices": 228, + "position": 3575640, + "normal": 3578376, + "lightHeight": 3581112, + "index": 3582024, + "indexCount": 408, + "indexType": "uint16", + "box": [ + 2424.775390625, + -562.0496826171875, + -1118.5771484375, + 2895.797119140625, + -533.0678100585938, + -987.0118408203125 + ], + "sphere": [ + 2660.2862548828125, + -547.5587463378906, + -1052.7944946289062, + 240.7193703498679 ] }, { "source": "levels", - "vertices": 59898, - "position": 3993840, - "normal": 4712616, - "lightHeight": 5431392, - "index": 5670984, - "indexCount": 108384, + "vertices": 1062, + "position": 3582840, + "normal": 3595584, + "lightHeight": 3608328, + "index": 3612576, + "indexCount": 1860, "indexType": "uint16", "box": [ - 972.2301025390625, - -578.3482666015625, - -1654.0888671875, - 4493.0498046875, - -490.0949401855469, + 2042.5111083984375, + -552.81201171875, + 3037.042236328125, + 2943.78125, + -525.0560302734375, 3798.428466796875 ], "sphere": [ - 2732.6399536132812, - -534.2216033935547, - 1072.1697998046875, - 2836.1249323113802 + 2493.1461791992188, + -538.9340209960938, + 3417.7353515625, + 580.1437298183052 + ] + }, + { + "source": "levels", + "vertices": 10698, + "position": 3616296, + "normal": 3744672, + "lightHeight": 3873048, + "index": 3915840, + "indexCount": 19392, + "indexType": "uint16", + "box": [ + 2056.357421875, + -567.89501953125, + 986.0142822265625, + 3016.818359375, + -512.334716796875, + 1991.8946533203125 + ], + "sphere": [ + 2536.587890625, + -540.1148681640625, + 1488.9544677734375, + 691.6815848330912 + ] + }, + { + "source": "levels", + "vertices": 7134, + "position": 3954624, + "normal": 4040232, + "lightHeight": 4125840, + "index": 4154376, + "indexCount": 12588, + "indexType": "uint16", + "box": [ + 1985.1802978515625, + -565.3265380859375, + 1982.2718505859375, + 3010.2744140625, + -508.8655700683594, + 2986.77685546875 + ], + "sphere": [ + 2497.7273559570312, + -537.0960540771484, + 2484.5243530273438, + 644.1546365705751 + ] + }, + { + "source": "levels", + "vertices": 1800, + "position": 4179552, + "normal": 4201152, + "lightHeight": 4222752, + "index": 4229952, + "indexCount": 3168, + "indexType": "uint16", + "box": [ + 2185.224853515625, + -575.1696166992188, + -994.9154663085938, + 3003.57177734375, + -525.4735717773438, + -115.7607421875 + ], + "sphere": [ + 2594.3983154296875, + -550.3215942382812, + -555.3381042480469, + 529.6996827528538 + ] + }, + { + "source": "levels", + "vertices": 7314, + "position": 4236288, + "normal": 4324056, + "lightHeight": 4411824, + "index": 4441080, + "indexCount": 13332, + "indexType": "uint16", + "box": [ + 2468.454833984375, + -573.2288208007812, + 84.60311889648438, + 3009.99658203125, + -543.5411987304688, + 1003.3505859375 + ], + "sphere": [ + 2739.2257080078125, + -558.385009765625, + 543.9768524169922, + 501.52804963503837 + ] + }, + { + "source": "levels", + "vertices": 4986, + "position": 4467744, + "normal": 4527576, + "lightHeight": 4587408, + "index": 4607352, + "indexCount": 9444, + "indexType": "uint16", + "box": [ + 2973.493896484375, + -567.24267578125, + 999.1060791015625, + 3987.729736328125, + -515.7973022460938, + 1985.253173828125 + ], + "sphere": [ + 3480.61181640625, + -541.5199890136719, + 1492.1796264648438, + 661.439392238622 + ] + }, + { + "source": "levels", + "vertices": 288, + "position": 4626240, + "normal": 4629696, + "lightHeight": 4633152, + "index": 4634304, + "indexCount": 528, + "indexType": "uint16", + "box": [ + 972.2301025390625, + -549.6044311523438, + 3041.695068359375, + 999.6212768554688, + -545.0777587890625, + 3110.639892578125 + ], + "sphere": [ + 985.9256896972656, + -547.3410949707031, + 3076.16748046875, + 34.892038226521514 + ] + }, + { + "source": "levels", + "vertices": 3096, + "position": 4635360, + "normal": 4672512, + "lightHeight": 4709664, + "index": 4722048, + "indexCount": 5460, + "indexType": "uint16", + "box": [ + 2991.527099609375, + -554.8306884765625, + 2016.3568115234375, + 4005.658447265625, + -496.567626953125, + 2942.385498046875 + ], + "sphere": [ + 3498.5927734375, + -525.6991577148438, + 2479.3711547851562, + 628.9348352241616 + ] + }, + { + "source": "levels", + "vertices": 1038, + "position": 4732968, + "normal": 4745424, + "lightHeight": 4757880, + "index": 4762032, + "indexCount": 1824, + "indexType": "uint16", + "box": [ + 1333.411865234375, + -555.7409057617188, + 2998.37255859375, + 1962.7843017578125, + -537.3331298828125, + 3769.445068359375 + ], + "sphere": [ + 1648.0980834960938, + -546.5370178222656, + 3383.9088134765625, + 492.8235646616814 + ] + }, + { + "source": "levels", + "vertices": 102, + "position": 4765680, + "normal": 4766904, + "lightHeight": 4768128, + "index": 4768536, + "indexCount": 180, + "indexType": "uint16", + "box": [ + 3118.85595703125, + -520.4533081054688, + 3724.57861328125, + 3141.813720703125, + -516.7210083007812, + 3769.354736328125 + ], + "sphere": [ + 3130.3348388671875, + -518.587158203125, + 3746.9666748046875, + 23.585706045124503 + ] + }, + { + "source": "levels", + "vertices": 2178, + "position": 4768896, + "normal": 4795032, + "lightHeight": 4821168, + "index": 4829880, + "indexCount": 3996, + "indexType": "uint16", + "box": [ + 3081.539306640625, + -573.7467041015625, + -1654.0888671875, + 3941.7822265625, + -539.1572875976562, + -1043.9615478515625 + ], + "sphere": [ + 3511.6607666015625, + -556.4519958496094, + -1349.0252075195312, + 501.68532797303567 + ] + }, + { + "source": "levels", + "vertices": 984, + "position": 4837872, + "normal": 4849680, + "lightHeight": 4861488, + "index": 4865424, + "indexCount": 1776, + "indexType": "uint16", + "box": [ + 3994.151611328125, + -578.3482666015625, + -698.2391967773438, + 4384.71435546875, + -563.0484619140625, + -197.252197265625 + ], + "sphere": [ + 4189.4329833984375, + -570.6983642578125, + -447.7456970214844, + 315.78832730183325 + ] + }, + { + "source": "levels", + "vertices": 660, + "position": 4868976, + "normal": 4876896, + "lightHeight": 4884816, + "index": 4887456, + "indexCount": 1152, + "indexType": "uint16", + "box": [ + 3063.11328125, + -578.0612182617188, + -931.1696166992188, + 3998.248291015625, + -567.3152465820312, + -206.93212890625 + ], + "sphere": [ + 3530.6807861328125, + -572.688232421875, + -569.0508728027344, + 565.1684744973242 + ] + }, + { + "source": "levels", + "vertices": 702, + "position": 4889760, + "normal": 4898184, + "lightHeight": 4906608, + "index": 4909416, + "indexCount": 1320, + "indexType": "uint16", + "box": [ + 4001.928466796875, + -555.166015625, + 1132.5528564453125, + 4493.0498046875, + -531.276611328125, + 1949.3753662109375 + ], + "sphere": [ + 4247.4891357421875, + -543.2213134765625, + 1540.964111328125, + 471.0237173351459 + ] + }, + { + "source": "levels", + "vertices": 1512, + "position": 4912056, + "normal": 4930200, + "lightHeight": 4948344, + "index": 4954392, + "indexCount": 2724, + "indexType": "uint16", + "box": [ + 1122.5999755859375, + -552.87548828125, + 2072.6669921875, + 2009.154052734375, + -505.2105407714844, + 2955.1357421875 + ], + "sphere": [ + 1565.8770141601562, + -529.0430145263672, + 2513.9013671875, + 548.0566673398743 + ] + }, + { + "source": "levels", + "vertices": 8640, + "position": 4959840, + "normal": 5063520, + "lightHeight": 5167200, + "index": 5201760, + "indexCount": 15204, + "indexType": "uint16", + "box": [ + 3994.05224609375, + -516.4728393554688, + 2180.025146484375, + 4191.73291015625, + -490.0949401855469, + 2581.2392578125 + ], + "sphere": [ + 4092.892578125, + -503.2838897705078, + 2380.6322021484375, + 203.5336307175623 + ] + }, + { + "source": "levels", + "vertices": 6756, + "position": 5232168, + "normal": 5313240, + "lightHeight": 5394312, + "index": 5421336, + "indexCount": 12660, + "indexType": "uint16", + "box": [ + 2986.50732421875, + -567.441162109375, + 282.39892578125, + 3979.717041015625, + -545.8469848632812, + 1012.5849609375 + ], + "sphere": [ + 3483.1121826171875, + -556.6440734863281, + 647.491943359375, + 615.3540203294584 + ] + }, + { + "source": "levels", + "vertices": 948, + "position": 5446656, + "normal": 5458032, + "lightHeight": 5469408, + "index": 5473200, + "indexCount": 1776, + "indexType": "uint16", + "box": [ + 4035.662841796875, + -563.2916259765625, + 388.1066589355469, + 4476.93310546875, + -517.9544067382812, + 981.816650390625 + ], + "sphere": [ + 4256.2979736328125, + -540.6230163574219, + 684.9616546630859, + 354.96879209692474 ] }, { "source": "fallback", - "vertices": 119274, - "position": 5887752, - "normal": 7319040, - "lightHeight": 8750328, - "index": 9227424, - "indexCount": 190140, - "indexType": "uint32", + "vertices": 144, + "position": 5476752, + "normal": 5478480, + "lightHeight": 5480208, + "index": 5480784, + "indexCount": 228, + "indexType": "uint16", + "box": [ + 971.7410278320312, + -546.2933959960938, + 2152.082763671875, + 1001.4540405273438, + -494.5089416503906, + 2994.05517578125 + ], + "sphere": [ + 986.5975341796875, + -520.4011688232422, + 2573.0689697265625, + 422.0284581029812 + ] + }, + { + "source": "fallback", + "vertices": 594, + "position": 5481240, + "normal": 5488368, + "lightHeight": 5495496, + "index": 5497872, + "indexCount": 972, + "indexType": "uint16", "box": [ 970.5235595703125, - -583.4852294921875, - -1658.1075439453125, - 4522.517578125, - -376.1947937011719, + -551.1123046875, + 3001.9326171875, + 999.57763671875, + -537.7996826171875, + 3247.64306640625 + ], + "sphere": [ + 985.0505981445312, + -544.4559936523438, + 3124.787841796875, + 123.150587790976 + ] + }, + { + "source": "fallback", + "vertices": 9684, + "position": 5499816, + "normal": 5616024, + "lightHeight": 5732232, + "index": 5770968, + "indexCount": 15120, + "indexType": "uint16", + "box": [ + 999.0203857421875, + -562.388427734375, + 2998.27197265625, + 1997.5745849609375, + -527.9518432617188, 3883.84912109375 ], "sphere": [ - 2746.5205688476562, - -479.8400115966797, - 1112.8707885742188, - 3129.6090105644394 + 1498.2974853515625, + -545.1701354980469, + 3441.060546875, + 660.7409434441644 + ] + }, + { + "source": "fallback", + "vertices": 4854, + "position": 5801208, + "normal": 5859456, + "lightHeight": 5917704, + "index": 5937120, + "indexCount": 7704, + "indexType": "uint16", + "box": [ + 997.0316162109375, + -557.6749877929688, + 1986.20556640625, + 2022.642822265625, + -494.44903564453125, + 3001.40625 + ], + "sphere": [ + 1509.8372192382812, + -526.06201171875, + 2493.805908203125, + 689.4746199500595 + ] + }, + { + "source": "fallback", + "vertices": 96, + "position": 5952528, + "normal": 5953680, + "lightHeight": 5954832, + "index": 5955216, + "indexCount": 144, + "indexType": "uint16", + "box": [ + 1387.857421875, + -441.3124694824219, + 766.4912109375, + 1861.74462890625, + -376.1947937011719, + 780.518798828125 + ], + "sphere": [ + 1624.801025390625, + -408.7536315917969, + 773.5050048828125, + 239.25351376013103 + ] + }, + { + "source": "fallback", + "vertices": 660, + "position": 5955504, + "normal": 5963424, + "lightHeight": 5971344, + "index": 5973984, + "indexCount": 1044, + "indexType": "uint16", + "box": [ + 1545.2227783203125, + -552.7486572265625, + 1257.6688232421875, + 2003.585693359375, + -509.6701965332031, + 2036.440185546875 + ], + "sphere": [ + 1774.4042358398438, + -531.2094268798828, + 1647.0545043945312, + 408.22991345339676 + ] + }, + { + "source": "fallback", + "vertices": 7110, + "position": 5976072, + "normal": 6061392, + "lightHeight": 6146712, + "index": 6175152, + "indexCount": 11220, + "indexType": "uint16", + "box": [ + 1994.7398681640625, + -553.9523315429688, + 3005.247802734375, + 3002.902587890625, + -513.7872314453125, + 3878.896240234375 + ], + "sphere": [ + 2498.8212280273438, + -533.8697814941406, + 3442.072021484375, + 644.4342138875472 + ] + }, + { + "source": "fallback", + "vertices": 5118, + "position": 6197592, + "normal": 6259008, + "lightHeight": 6320424, + "index": 6340896, + "indexCount": 8340, + "indexType": "uint16", + "box": [ + 2003.0179443359375, + -568.970947265625, + 1006.4867553710938, + 2997.62646484375, + -522.599609375, + 1998.798583984375 + ], + "sphere": [ + 2500.3222045898438, + -545.7852783203125, + 1502.6426696777344, + 663.0244485953968 + ] + }, + { + "source": "fallback", + "vertices": 6510, + "position": 6357576, + "normal": 6435696, + "lightHeight": 6513816, + "index": 6539856, + "indexCount": 10404, + "indexType": "uint16", + "box": [ + 2000.1478271484375, + -563.0704956054688, + 2001.297607421875, + 2995.444091796875, + -528.5598754882812, + 2987.93408203125 + ], + "sphere": [ + 2497.7959594726562, + -545.815185546875, + 2494.6158447265625, + 597.1271605480532 + ] + }, + { + "source": "fallback", + "vertices": 11916, + "position": 6560664, + "normal": 6703656, + "lightHeight": 6846648, + "index": 6894312, + "indexCount": 19044, + "indexType": "uint16", + "box": [ + 2080.812744140625, + -575.61767578125, + -975.6700439453125, + 2999.469970703125, + -506.2872619628906, + -4.426574230194092 + ], + "sphere": [ + 2540.141357421875, + -540.9524688720703, + -490.0483090877533, + 623.917726882083 + ] + }, + { + "source": "fallback", + "vertices": 1158, + "position": 6932400, + "normal": 6946296, + "lightHeight": 6960192, + "index": 6964824, + "indexCount": 1836, + "indexType": "uint16", + "box": [ + 2075.66259765625, + -559.8402099609375, + -1649.3135986328125, + 2780.482177734375, + -454.652099609375, + -1042.7408447265625 + ], + "sphere": [ + 2428.0723876953125, + -507.24615478515625, + -1346.0272216796875, + 443.5776305647615 + ] + }, + { + "source": "fallback", + "vertices": 2844, + "position": 6968496, + "normal": 7002624, + "lightHeight": 7036752, + "index": 7048128, + "indexCount": 4500, + "indexType": "uint16", + "box": [ + 2408.38525390625, + -573.3406372070312, + -4.989656925201416, + 2995.81201171875, + -523.015380859375, + 952.4990844726562 + ], + "sphere": [ + 2702.0986328125, + -548.1780090332031, + 473.7547137737274, + 559.1476670122321 + ] + }, + { + "source": "fallback", + "vertices": 19716, + "position": 7057128, + "normal": 7293720, + "lightHeight": 7530312, + "index": 7609176, + "indexCount": 31248, + "indexType": "uint16", + "box": [ + 2996.66162109375, + -581.1370239257812, + -1010.0269165039062, + 4005.427978515625, + -552.8853759765625, + 21.755138397216797 + ], + "sphere": [ + 3501.0447998046875, + -567.0111999511719, + -494.1358890533447, + 668.9703827082369 + ] + }, + { + "source": "fallback", + "vertices": 1182, + "position": 7671672, + "normal": 7685856, + "lightHeight": 7700040, + "index": 7704768, + "indexCount": 1944, + "indexType": "uint16", + "box": [ + 3000.177734375, + -529.0629272460938, + 3033.14501953125, + 3233.46484375, + -508.10888671875, + 3826.180908203125 + ], + "sphere": [ + 3116.8212890625, + -518.5859069824219, + 3429.6629638671875, + 412.65688011032586 + ] + }, + { + "source": "fallback", + "vertices": 6642, + "position": 7708656, + "normal": 7788360, + "lightHeight": 7868064, + "index": 7894632, + "indexCount": 10476, + "indexType": "uint16", + "box": [ + 2996.001953125, + -567.8796997070312, + 981.072265625, + 3929.34619140625, + -512.9638671875, + 1993.0963134765625 + ], + "sphere": [ + 3462.674072265625, + -540.4217834472656, + 1487.0842895507812, + 650.6600856890644 + ] + }, + { + "source": "fallback", + "vertices": 7026, + "position": 7915584, + "normal": 7999896, + "lightHeight": 8084208, + "index": 8112312, + "indexCount": 11508, + "indexType": "uint16", + "box": [ + 3006.1328125, + -551.4927978515625, + 1995.775390625, + 3982.119384765625, + -472.918701171875, + 2997.353271484375 + ], + "sphere": [ + 3494.1260986328125, + -512.2057495117188, + 2496.5643310546875, + 679.6603627449039 + ] + }, + { + "source": "fallback", + "vertices": 16218, + "position": 8135328, + "normal": 8329944, + "lightHeight": 8524560, + "index": 8589432, + "indexCount": 25548, + "indexType": "uint16", + "box": [ + 3000.362060546875, + -574.9637451171875, + -24.446950912475586, + 4001.453125, + -548.2450561523438, + 1020.167724609375 + ], + "sphere": [ + 3500.9075927734375, + -561.6044006347656, + 497.8603868484497, + 648.4178343021399 + ] + }, + { + "source": "fallback", + "vertices": 3798, + "position": 8640528, + "normal": 8686104, + "lightHeight": 8731680, + "index": 8746872, + "indexCount": 6036, + "indexType": "uint16", + "box": [ + 3023.84716796875, + -582.288330078125, + -1658.1075439453125, + 4009.01513671875, + -529.7459106445312, + -1002.9141845703125 + ], + "sphere": [ + 3516.43115234375, + -556.0171203613281, + -1330.5108642578125, + 586.2090450220167 + ] + }, + { + "source": "fallback", + "vertices": 2538, + "position": 8758944, + "normal": 8789400, + "lightHeight": 8819856, + "index": 8830008, + "indexCount": 4140, + "indexType": "uint16", + "box": [ + 3977.04150390625, + -570.6123657226562, + 8.06588077545166, + 4516.4140625, + -519.4344482421875, + 991.1395263671875 + ], + "sphere": [ + 4246.727783203125, + -545.0234069824219, + 499.6027035713196, + 538.0995536311374 + ] + }, + { + "source": "fallback", + "vertices": 7686, + "position": 8838288, + "normal": 8930520, + "lightHeight": 9022752, + "index": 9053496, + "indexCount": 12636, + "indexType": "uint16", + "box": [ + 3954.953369140625, + -581.9120483398438, + -1026.131103515625, + 4518.47802734375, + -556.7305908203125, + 66.92224884033203 + ], + "sphere": [ + 4236.7156982421875, + -569.3213195800781, + -479.6044273376465, + 612.3414821416246 + ] + }, + { + "source": "fallback", + "vertices": 1050, + "position": 9078768, + "normal": 9091368, + "lightHeight": 9103968, + "index": 9108168, + "indexCount": 1704, + "indexType": "uint16", + "box": [ + 3988.15966796875, + -517.4783325195312, + 2155.44775390625, + 4521.677734375, + -457.1512451171875, + 2712.47412109375 + ], + "sphere": [ + 4254.918701171875, + -487.3147888183594, + 2433.9609375, + 339.6448559309977 + ] + }, + { + "source": "fallback", + "vertices": 1530, + "position": 9111576, + "normal": 9129936, + "lightHeight": 9148296, + "index": 9154416, + "indexCount": 2352, + "indexType": "uint16", + "box": [ + 4001.23681640625, + -583.4852294921875, + -1272.7957763671875, + 4522.517578125, + -568.8721313476562, + -977.8870849609375 + ], + "sphere": [ + 4261.877197265625, + -576.1786804199219, + -1125.3414306640625, + 287.4320899022702 + ] + }, + { + "source": "fallback", + "vertices": 1200, + "position": 9159120, + "normal": 9173520, + "lightHeight": 9187920, + "index": 9192720, + "indexCount": 1992, + "indexType": "uint16", + "box": [ + 4018.66015625, + -555.4862670898438, + 1038.4110107421875, + 4516.66162109375, + -515.09326171875, + 1858.5458984375 + ], + "sphere": [ + 4267.660888671875, + -535.2897644042969, + 1448.4784545898438, + 464.8585137241389 ] } ], "spill": { - "offset": 9987984, + "offset": 9196704, "size": 1024, "bounds": [ 955.9872155081416, diff --git a/src/constants/buildings.ts b/src/constants/buildings.ts index 559d3a4..fbc187b 100644 --- a/src/constants/buildings.ts +++ b/src/constants/buildings.ts @@ -3,6 +3,7 @@ export const BUILDINGS = { assetManifestUrl: '/buildings/gwacheon.baked.json', assetBufferUrl: '/buildings/gwacheon.baked.bin', assetVersion: 1, + tileSize: 1000, metersPerLevel: 3, // Visual placeholders only: not a claim about actual floors. fallbackHeight: { apartments: 15, residential: 9, house: 6, commercial: 12, greenhouse: 3, roof: 3, school: 12, default: 9 } as Record, diff --git a/src/constants/character.ts b/src/constants/character.ts index fb49464..e97ca49 100644 --- a/src/constants/character.ts +++ b/src/constants/character.ts @@ -1,6 +1,7 @@ export const CHARACTER = { url: '/characters/red-sheep.glb?v=rounded-v10', scale: 0.62, + visibilityRadius: 3, // Includes raised arms, umbrella and idle hops around the ground anchor. selfBadgeGap: 0.12, outline: 0.018, ink: '#352724', diff --git a/src/constants/renderQuality.ts b/src/constants/renderQuality.ts index edcf604..e5738f9 100644 --- a/src/constants/renderQuality.ts +++ b/src/constants/renderQuality.ts @@ -1,4 +1,5 @@ export const RENDER_QUALITY = { + mobileFps: 30, mobileCap: 1.5, mobilePixelBudget: 1_500_000, levels: [0.75, 1, 1.25, 1.5, 1.75, 2] as readonly number[], diff --git a/src/multiplayer/remotePlayers.ts b/src/multiplayer/remotePlayers.ts index abeb7f3..a8648ca 100644 --- a/src/multiplayer/remotePlayers.ts +++ b/src/multiplayer/remotePlayers.ts @@ -1,11 +1,14 @@ -import { type Object3D, type Raycaster, MathUtils, Vector3 } from 'three'; +import { type Camera, type Object3D, type Raycaster, Frustum, Matrix4, MathUtils, Sphere, Vector3 } from 'three'; import type { Sheep } from '../scene/sheep.ts'; import type { PlayerState } from './protocol.ts'; import { MULTIPLAYER } from '../constants/multiplayer.ts'; import { MOVEMENT } from '../constants/movement.ts'; +import { CHARACTER } from '../constants/character.ts'; export function createRemotePlayers(template: Sheep, scene: Object3D, heightAt: (x: number, z: number) => number) { const players = new Map(); + const frustum = new Frustum(), projection = new Matrix4(); + const bounds = new Sphere(new Vector3(), CHARACTER.visibilityRadius); return { get(id: string) { return players.get(id)?.sheep.group; }, pick(ray: Raycaster, maxDistance: number) { @@ -54,18 +57,31 @@ export function createRemotePlayers(template: Sheep, scene: Object3D, heightAt: entry.sheep.setEquipment(state.equipment.umbrella, state.equipment.raincoat, state.equipment.hood, state.equipment.flashlight); } }, - update(delta: number, serverTime: number) { + update(delta: number, serverTime: number, camera?: Camera) { + if (camera) { + camera.updateMatrixWorld(true); + frustum.setFromProjectionMatrix(projection.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse)); + } let changed = false; for (const entry of players.values()) { const { sheep, state, from } = entry; const duration = MULTIPLAYER.interpolationMs / 1000; const interpolating = entry.elapsed < duration; - entry.elapsed += Math.max(0, delta); - const fraction = Math.min(1, entry.elapsed / duration); - const x = MathUtils.lerp(from.x, state.x, fraction), z = MathUtils.lerp(from.z, state.z, fraction); - sheep.group.position.set(x, heightAt(x, z), z); - const angle = Math.atan2(Math.sin(state.yaw - entry.yaw), Math.cos(state.yaw - entry.yaw)); - sheep.group.rotation.y = entry.yaw + angle * fraction; + if (interpolating) { + entry.elapsed = Math.min(duration, entry.elapsed + Math.max(0, delta)); + const fraction = Math.min(1, entry.elapsed / duration); + const x = MathUtils.lerp(from.x, state.x, fraction), z = MathUtils.lerp(from.z, state.z, fraction); + if (x !== sheep.group.position.x || z !== sheep.group.position.z) { + sheep.group.position.set(x, heightAt(x, z), z); + } + const angle = Math.atan2(Math.sin(state.yaw - entry.yaw), Math.cos(state.yaw - entry.yaw)); + sheep.group.rotation.y = entry.yaw + angle * fraction; + } + bounds.center.copy(sheep.group.position); + const visible = !camera || frustum.intersectsSphere(bounds); + changed = changed || sheep.group.visible !== visible; + sheep.group.visible = visible; + if (!visible) continue; const active = sheep.pose(state.emote, Math.max(0, serverTime - state.emoteStartedAt) / 1000, state.moving && serverTime - state.updatedAt < MULTIPLAYER.staleMovementMs, serverTime / 1000); changed = changed || interpolating || active; diff --git a/src/scene/buildings.ts b/src/scene/buildings.ts index bb28499..863c14a 100644 --- a/src/scene/buildings.ts +++ b/src/scene/buildings.ts @@ -69,7 +69,7 @@ export async function loadBuildings(): Promise { export function createBuildings(data: BuildingData, terrain: Awaited>) { const group = new Group(); - const geometries: Record<'tagged' | 'levels' | 'fallback', ExtrudeGeometry[]> = { tagged: [], levels: [], fallback: [] }; + const geometries = { tagged: new Map(), levels: new Map(), fallback: new Map() }; const counts = { tagged: 0, levels: 0, fallback: 0 }; const lightSources: BuildingLightSource[] = []; const surface = (x: number, z: number) => terrainHeightAt(terrain.metadata, terrain.heights, x, z) - terrain.metadata.observerSurfaceElevation - (x * x + z * z) / (2 * TERRAIN.earthRadiusMeters); @@ -107,19 +107,23 @@ export function createBuildings(data: BuildingData, terrain: Awaited g.dispose()); - // Lighting uses world XZ and local height, not UVs. Retain normals and - // lighting heights when sharing vertices so facade/roof seams stay sharp. - merged.deleteAttribute('uv'); - const indexed = indexBuildingGeometry(merged); - merged.dispose(); - group.add(createBuildingMesh(source, indexed)); + for (const bucket of geometries[source].values()) { + const merged = mergeGeometries(bucket, false)!; + bucket.forEach(g => g.dispose()); + // Lighting uses world XZ and local height, not UVs. Retain normals and + // lighting heights when sharing vertices so facade/roof seams stay sharp. + merged.deleteAttribute('uv'); + const indexed = indexBuildingGeometry(merged); + merged.dispose(); + group.add(createBuildingMesh(source, indexed)); + } } const target = structurePosition(BUILDINGS.target.latitude, BUILDINGS.target.longitude, terrain.metadata.origin); target.y = surface(target.x, target.z) + 20; diff --git a/src/scene/frameClock.ts b/src/scene/frameClock.ts new file mode 100644 index 0000000..c7cef78 --- /dev/null +++ b/src/scene/frameClock.ts @@ -0,0 +1,17 @@ +/** Pace the whole update loop while retaining real elapsed time between accepted frames. */ +export function createFrameClock(fps: number) { + const interval = 1000 / fps; + let previous: number | undefined; + let next = 0; + return { + reset() { previous = undefined; next = 0; }, + tick(now: number): number | undefined { + if (previous !== undefined && now + .001 < next) return; + const delta = previous === undefined ? 0 : Math.max(0, now - previous) / 1000; + next = previous === undefined || !interval ? now + interval + : next + (Math.floor(Math.max(0, now + .001 - next) / interval) + 1) * interval; + previous = now; + return delta; + }, + }; +} diff --git a/src/scene/lobbySunrise.ts b/src/scene/lobbySunrise.ts index 8179867..c967eff 100644 --- a/src/scene/lobbySunrise.ts +++ b/src/scene/lobbySunrise.ts @@ -13,6 +13,9 @@ import { SUMMIT_TERRAIN } from '../constants/summitTerrain.ts'; import { SCENE } from '../constants/scene.ts'; import { TERRAIN } from '../constants/terrain.ts'; import { LOBBY_CHARACTER, LOBBY_SUNRISE } from '../constants/lobby.ts'; +import { RENDER_QUALITY } from '../constants/renderQuality.ts'; +import { createFrameClock } from './frameClock.ts'; +import { renderPixelRatio } from './adaptiveResolution.ts'; /** Same Gwanaksan DSM, summit correction, sky and solar disc as the observation scene. */ export function createLobbySunrise(canvas: HTMLCanvasElement) { @@ -60,26 +63,33 @@ export function createLobbySunrise(canvas: HTMLCanvasElement) { foreground.add(sheep.group); const head = sheep.group.getObjectByName('Head')!; const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches; + const mobile = matchMedia('(pointer: coarse)').matches; + const frameClock = createFrameClock(mobile ? RENDER_QUALITY.mobileFps : Infinity); let progress = reducedMotion ? 1 : 0; let frame = 0; let elapsed = 0; - let lastFrame = 0; + let previousProgress = -1; + let gazeAngle = 0; const render = () => { - const date = new Date(LOBBY_SUNRISE.from + (LOBBY_SUNRISE.to - LOBBY_SUNRISE.from) * progress); - const state = calculateSun(date, observer); - direction.set(...directionFromAngles(state.azimuth, state.altitude)); - sky.update(direction, state.altitude); - sun.update(direction, state.altitude); - nightSky.update(date, observer, state.altitude); - const moonAltitude = getMoonPosition(date, observer.latitude, observer.longitude).altitude; - const gazeAltitude = MathUtils.lerp(moonAltitude, state.altitude, MathUtils.smoothstep(state.altitude, -8, 0)); + if (progress !== previousProgress) { + previousProgress = progress; + const date = new Date(LOBBY_SUNRISE.from + (LOBBY_SUNRISE.to - LOBBY_SUNRISE.from) * progress); + const state = calculateSun(date, observer); + direction.set(...directionFromAngles(state.azimuth, state.altitude)); + sky.update(direction, state.altitude); + sun.update(direction, state.altitude); + nightSky.update(date, observer, state.altitude); + const moonAltitude = getMoonPosition(date, observer.latitude, observer.longitude).altitude; + const gazeAltitude = MathUtils.lerp(moonAltitude, state.altitude, MathUtils.smoothstep(state.altitude, -8, 0)); + gazeAngle = -MathUtils.degToRad(MathUtils.clamp(gazeAltitude, 8, 28)); + sheep.updateLighting(Math.max(LOBBY_CHARACTER.minimumLightingAltitude, state.altitude), 1); + ambient.intensity = MathUtils.lerp(0.12, 1.8, MathUtils.smoothstep(state.altitude, -8, 15)); + fog.color.copy(night).lerp(dawn, MathUtils.smoothstep(state.altitude, -8, 0)); + fog.color.lerp(day, MathUtils.smoothstep(state.altitude, 0, 15)); + renderer.toneMappingExposure = MathUtils.lerp(0.12, LOBBY_SUNRISE.maxExposure, MathUtils.smoothstep(state.altitude, -12, 5)); + } sheep.pose('idle', 0, false, elapsed / 1000); - head.rotateX(-MathUtils.degToRad(MathUtils.clamp(gazeAltitude, 8, 28))); - sheep.updateLighting(Math.max(LOBBY_CHARACTER.minimumLightingAltitude, state.altitude), 1); - ambient.intensity = MathUtils.lerp(0.12, 1.8, MathUtils.smoothstep(state.altitude, -8, 15)); - fog.color.copy(night).lerp(dawn, MathUtils.smoothstep(state.altitude, -8, 0)); - fog.color.lerp(day, MathUtils.smoothstep(state.altitude, 0, 15)); - renderer.toneMappingExposure = MathUtils.lerp(0.12, LOBBY_SUNRISE.maxExposure, MathUtils.smoothstep(state.altitude, -12, 5)); + head.rotateX(gazeAngle); renderer.render(scene, camera); const { clientWidth: width, clientHeight: height } = canvas; const size = Math.min(LOBBY_CHARACTER.maxSize, width * LOBBY_CHARACTER.widthFraction, height * LOBBY_CHARACTER.heightFraction); @@ -94,19 +104,28 @@ export function createLobbySunrise(canvas: HTMLCanvasElement) { const { clientWidth: width, clientHeight: height } = canvas; camera.aspect = width / Math.max(1, height); camera.updateProjectionMatrix(); - renderer.setPixelRatio(Math.min(devicePixelRatio, LOBBY_SUNRISE.maxPixelRatio)); + renderer.setPixelRatio(renderPixelRatio(devicePixelRatio, LOBBY_SUNRISE.maxPixelRatio, width, height, mobile)); renderer.setSize(width, height, false); render(); }; const tick = (now: number) => { - if (disposed) return; - if (!document.hidden && lastFrame) elapsed += now - lastFrame; - lastFrame = now; - progress = MathUtils.clamp((elapsed - LOBBY_SUNRISE.nightHoldMs) / LOBBY_SUNRISE.durationMs, 0, 1); - if (!document.hidden) render(); + if (disposed || document.hidden) return; + const delta = frameClock.tick(now); + if (delta !== undefined) { + elapsed += delta * 1000; + progress = MathUtils.clamp((elapsed - LOBBY_SUNRISE.nightHoldMs) / LOBBY_SUNRISE.durationMs, 0, 1); + render(); + } if (!reducedMotion) frame = requestAnimationFrame(tick); }; - const visible = () => { lastFrame = 0; if (!document.hidden) render(); }; + const visible = () => { + cancelAnimationFrame(frame); + frameClock.reset(); + if (!document.hidden) { + render(); + if (!reducedMotion) frame = requestAnimationFrame(tick); + } + }; cleanup = () => { cancelAnimationFrame(frame); window.removeEventListener('resize', resize); @@ -126,7 +145,7 @@ export function createLobbySunrise(canvas: HTMLCanvasElement) { window.addEventListener('resize', resize); document.addEventListener('visibilitychange', visible); resize(); - if (progress < 1) frame = requestAnimationFrame(tick); + if (!document.hidden && progress < 1) frame = requestAnimationFrame(tick); })(); return { ready, dispose() { disposed = true; cleanup?.(); cleanup = undefined; } }; } diff --git a/src/scene/sheep.ts b/src/scene/sheep.ts index 8daf5e5..957b6f6 100644 --- a/src/scene/sheep.ts +++ b/src/scene/sheep.ts @@ -121,6 +121,7 @@ export function createSheep(model: Group, gear?: { umbrella: Group; raincoat: Gr const rigJoints = Object.fromEntries(['Head', 'ArmLeft', 'ArmRight', 'LegLeft', 'LegRight'] .map(name => [name, group.getObjectByName(name)!])); const joints = rigJoints; + const jointEntries = Object.entries(joints); const rigCoatParts = coatParts.map(part => copy(part)!); const rigHood = copy(hood), rigFoldedHood = copy(foldedHood), rigUmbrella = copy(gear?.umbrella); const rigFlashlight = copy(gear?.flashlight); @@ -233,7 +234,7 @@ export function createSheep(model: Group, gear?: { umbrella: Group; raincoat: Gr || previousUmbrellaPose !== umbrellaPose || umbrellaPose !== Number(umbrellaOpen); previousMoving = moving; previousEmote = emote; - for (const [name, joint] of Object.entries(joints)) joint.quaternion.copy(rest[name]); + for (const [name, joint] of jointEntries) joint.quaternion.copy(rest[name]); joints.LegLeft.position.copy(legPositions[0]); joints.LegRight.position.copy(legPositions[1]); if (emote === 'wave') { diff --git a/src/scene/world.ts b/src/scene/world.ts index d5386d3..27bb038 100644 --- a/src/scene/world.ts +++ b/src/scene/world.ts @@ -31,6 +31,9 @@ import { COAST } from '../constants/coast.ts'; import { GISAMUN } from '../constants/gisamun.ts'; import { createHarbour, isGisamun } from './harbour.ts'; import { createRenderInvalidation } from './renderInvalidation.ts'; +import { createFrameClock } from './frameClock.ts'; +import { renderPixelRatio } from './adaptiveResolution.ts'; +import { RENDER_QUALITY } from '../constants/renderQuality.ts'; import { createTerrainMinimap } from '../ui/terrainMinimap.ts'; import { SUMMIT_TERRAIN } from '../constants/summitTerrain.ts'; import { createSummitWalking } from '../controls/summitWalking.ts'; @@ -80,6 +83,8 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited number) | null = null; let playerMoving = false; const profiling = import.meta.env.DEV && new URLSearchParams(location.search).has('profile'); + const mobile = matchMedia('(pointer: coarse)').matches; + const frameClock = createFrameClock(mobile && !profiling ? RENDER_QUALITY.mobileFps : Infinity); let profile: ReturnType | undefined; let disposed = false; const summitStarted = performance.now(); @@ -171,6 +176,17 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited { + if (previousCameraDistance === cameraDistance && followPosition.equals(camera.position) + && followRotation.equals(camera.quaternion)) return; + updateThirdPersonCamera(followCamera, camera, (x, z) => harbour?.heightAt(x, z) ?? terrain.heightAt(x, z), cameraDistance); + followPosition.copy(camera.position); + followRotation.copy(camera.quaternion); + previousCameraDistance = cameraDistance; + }; let selectedCompanion: string | undefined; let companionAnchor: ReturnType | undefined; const directionButton = document.querySelector('#direction-toggle')!; @@ -180,7 +196,7 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited { const bounds = canvas.getBoundingClientRect(); pointer.set((x - bounds.left) / bounds.width * 2 - 1, -(y - bounds.top) / bounds.height * 2 + 1); - if (sheep) updateThirdPersonCamera(followCamera, camera, (x, z) => harbour?.heightAt(x, z) ?? terrain.heightAt(x, z), cameraDistance); + if (sheep) updateFollowCamera(); const view = sheep ? followCamera : camera; view.updateMatrixWorld(true); destinationRay.setFromCamera(pointer, view); @@ -372,7 +388,8 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited { + const renderFrame = (now: number) => { + if (document.hidden) return; + const delta = frameClock.tick(now); + if (delta === undefined) return; const cpuStart = profile?.active() ? performance.now() : 0; - const delta = (now - previousTime) / 1000; - previousTime = now; const replayPosition = profile?.step(now); let moving = false; let turning = false; @@ -426,7 +443,6 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited harbour?.heightAt(x, z) ?? terrain.heightAt(x, z), cameraDistance); + updateFollowCamera(); } const activeCamera = overview ? overviewCamera : sheep ? followCamera : camera; + if (remotePlayers?.update(delta, sharedNow ? sharedNow() : Date.now(), activeCamera)) rendering.invalidate(); companionAnchor?.update(activeCamera, !overview); flashlight?.update(flashlightOn && !overview); const precipitating = precipitation.update(delta, activeCamera.position, atmosphere.rain, atmosphere.snowfall, @@ -475,7 +492,7 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited { + frameClock.reset(); + rendering.invalidate(); + renderer.setAnimationLoop(document.hidden ? null : renderFrame); + }; + document.addEventListener('visibilitychange', onVisibility); + onVisibility(); const world = { buildingCounts: buildings?.counts, captureFrame(view?: PerspectiveCamera) { const activeCamera = view ?? (overview ? overviewCamera : sheep ? followCamera : camera); - if (composer && !profile?.disablePost) { + remotePlayers?.update(0, sharedNow ? sharedNow() : Date.now(), activeCamera); + rendering.invalidate(); + if (composer && bloom?.enabled && !profile?.disablePost) { renderPass.camera = activeCamera; composer.render(0); } else renderer.render(scene, activeCamera); @@ -584,14 +610,14 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited harbour?.heightAt(x, z) ?? terrain.heightAt(x, z), cameraDistance); + updateFollowCamera(); } }, lookAtSun() { if (!solarState) return; restoreView(); camera.lookAt(camera.position.clone().add(new Vector3(...directionFromAngles(solarState.azimuth, 0)))); - if (sheep) updateThirdPersonCamera(followCamera, camera, (x, z) => harbour?.heightAt(x, z) ?? terrain.heightAt(x, z), cameraDistance); + if (sheep) updateFollowCamera(); }, setEyeHeight(meters: number) { eyeHeight = meters; @@ -622,6 +648,7 @@ export function createWorld(canvas: HTMLCanvasElement, terrainData: Awaited { if (disposed) return; profile = createRenderProfile(renderer, scene, camera, () => world.resetPosition(), target, () => - composer && !profile?.disablePost ? composer.passes.filter(pass => pass.enabled).map(pass => pass.constructor.name) : ['WebGLRenderer'], resize); + composer && bloom?.enabled && !profile?.disablePost ? composer.passes.filter(pass => pass.enabled).map(pass => pass.constructor.name) : ['WebGLRenderer'], resize); renderer.info.autoReset = false; resize(); }); diff --git a/src/ui/compass.ts b/src/ui/compass.ts index fe77ee3..1769aac 100644 --- a/src/ui/compass.ts +++ b/src/ui/compass.ts @@ -6,6 +6,7 @@ export function createCompass() { const scale = root.querySelector('.compass-scale')!; const bearing = root.querySelector('.compass-bearing')!; const direction = new Vector3(); + let previousHeading: number | undefined; const ticks = Array.from({ length: 360 / COMPASS.tickDegrees }, (_, index) => { const degrees = index * COMPASS.tickDegrees; const tick = document.createElement('span'); @@ -19,6 +20,8 @@ export function createCompass() { update(camera: Camera) { camera.getWorldDirection(direction); const heading = (Math.atan2(direction.x, -direction.z) * 180 / Math.PI + 360) % 360; + if (heading === previousHeading) return; + previousHeading = heading; for (const { tick, degrees } of ticks) { const offset = (degrees - heading + 540) % 360 - 180; tick.hidden = Math.abs(offset) > COMPASS.visibleDegrees / 2; diff --git a/src/ui/timeController.ts b/src/ui/timeController.ts index 947b9de..f7bc190 100644 --- a/src/ui/timeController.ts +++ b/src/ui/timeController.ts @@ -85,7 +85,7 @@ export class TimeController { }; private sync = () => { - if (this.mode === 'LIVE') this.render(this.sharedClock ? this.sharedClock() : new Date()); + if (!document.hidden && this.mode === 'LIVE') this.render(this.sharedClock ? this.sharedClock() : new Date()); }; private render(date: Date) { diff --git a/tests/buildings.test.ts b/tests/buildings.test.ts index 8165b6c..6e5566f 100644 --- a/tests/buildings.test.ts +++ b/tests/buildings.test.ts @@ -51,7 +51,8 @@ test('downloaded footprints are deduplicated, closed, and generate finite merged const city=createBuildings(data,terrain); assert.equal(Object.values(city.counts).reduce((a,b)=>a+b,0),data.features.length); assert.ok(city.counts.tagged>0 && city.counts.levels>0 && city.counts.fallback>0); - assert.ok(city.group.children.length<=3); + assert.ok(city.group.children.length > 3, 'separate city districts can be culled independently'); + assert.ok(city.group.children.length < 100, 'batch buildings instead of one draw call per building'); for(const mesh of city.group.children as Mesh[]) assert.ok(Array.from(mesh.geometry.attributes.position.array).every(Number.isFinite)); city.dispose(); }); diff --git a/tests/frame-clock.test.ts b/tests/frame-clock.test.ts new file mode 100644 index 0000000..e2aade1 --- /dev/null +++ b/tests/frame-clock.test.ts @@ -0,0 +1,24 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { createFrameClock } from '../src/scene/frameClock.ts'; + +test('30 fps pacing retains elapsed simulation time on 60 and 120 Hz displays', () => { + for (const hz of [60, 120]) { + const clock = createFrameClock(30); + let frames = 0, elapsed = 0; + for (let i = 0; i <= hz * 10; i++) { + const delta = clock.tick(i * 1000 / hz); + if (delta !== undefined) { frames++; elapsed += delta; } + } + assert.equal(frames, 301); + assert.ok(Math.abs(elapsed - 10) < 1e-8); + } +}); + +test('resume resets elapsed time instead of simulating the hidden interval', () => { + const clock = createFrameClock(30); + clock.tick(0); clock.tick(100); + clock.reset(); + assert.equal(clock.tick(60000), 0); + assert.equal(clock.tick(60001), undefined); +}); diff --git a/tests/multiplayer-client.test.ts b/tests/multiplayer-client.test.ts index f0550b2..fc11abc 100644 --- a/tests/multiplayer-client.test.ts +++ b/tests/multiplayer-client.test.ts @@ -1,6 +1,6 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { BoxGeometry, Group, Mesh, MeshBasicMaterial, Raycaster, Vector3 } from 'three'; +import { BoxGeometry, Group, Mesh, MeshBasicMaterial, PerspectiveCamera, Raycaster, Vector3 } from 'three'; import { createServerClock } from '../src/multiplayer/clock.ts'; import { createRemotePlayers } from '../src/multiplayer/remotePlayers.ts'; import type { Sheep } from '../src/scene/sheep.ts'; @@ -16,6 +16,27 @@ test('server clock uses round trip midpoint and monotonic elapsed time', () => { assert.equal(clock.now(), 1001020); }); +test('offscreen players skip posing but keep movement current and reappear when the camera turns', () => { + const scene = new Group(), camera = new PerspectiveCamera(60, 1, .1, 100); + let poses = 0; + const template = { group: new Group(), clone() { return { group: new Group(), setEquipment() {}, + pose() { poses++; return true; }, dispose() {} } as unknown as Sheep; } } as Sheep; + const remote = createRemotePlayers(template, scene, () => 0); + const player: PlayerState = { id: 'other', nickname: '친구', x: 0, z: 10, yaw: 0, moving: false, + emote: 'idle', equipment: { umbrella: false, raincoat: false, hood: false }, updatedAt: 1000, emoteStartedAt: 1000 }; + remote.synchronize([player], 'self'); + for (let i = 0; i < 300; i++) remote.update(1 / 30, 1000 + i * 1000 / 30, camera); + assert.equal(poses, 0); + assert.equal(remote.get('other')!.visible, false); + remote.synchronize([{ ...player, x: 2, updatedAt: 11000 }], 'self'); + remote.update(.2, 11200, camera); + assert.equal(remote.get('other')!.position.x, 2); + camera.lookAt(2, 0, 10); + assert.equal(remote.update(.03, 11230, camera), true); + assert.equal(poses, 1); + assert.equal(remote.get('other')!.visible, true); +}); + test('companion selection picks visible players and chooses the nearest reachable unoccupied side', () => { const scene = new Group(); const geometry = new BoxGeometry(1, 2, 1), material = new MeshBasicMaterial(); @@ -51,7 +72,8 @@ test('remote snapshot reconciles membership, equipment and interpolated motion w let disposed = 0; const template = { clone() { return { group: new Group(), setEquipment: (...v: boolean[]) => equipment.push(v), pose: () => false, dispose() { this.group.removeFromParent(); disposed++; } } as unknown as Sheep; } } as Sheep; - const remote = createRemotePlayers(template, scene, () => 3); + let heightSamples = 0; + const remote = createRemotePlayers(template, scene, () => { heightSamples++; return 3; }); const player: PlayerState = { id: 'other', nickname: '방문자', x: 0, z: 0, yaw: 0, moving: false, emote: 'idle', equipment: { umbrella: true, raincoat: true, hood: false, flashlight: true }, updatedAt: 1000, emoteStartedAt: 1000 }; remote.synchronize([player, { ...player, id: 'self' }], 'self'); @@ -63,6 +85,9 @@ test('remote snapshot reconciles membership, equipment and interpolated motion w remote.update(.2, 1300); assert.equal(scene.children[0].position.x, 1); assert.equal(scene.children[0].position.y, 3); + const samplesAfterMoving = heightSamples; + for (let i = 0; i < 300; i++) remote.update(1 / 30, 1300 + i * 1000 / 30); + assert.equal(heightSamples, samplesAfterMoving, 'stationary players reuse their terrain height'); remote.synchronize([], 'self'); assert.equal(scene.children.length, 0); assert.equal(disposed, 1);