Compare commits

...

203 Commits

Author SHA1 Message Date
8c66b0bd72 graphics v0.0.11-depth2 2026-03-27 10:03:40 -07:00
7c16b41717 graphics: do not discard depth 2026-03-27 09:17:38 -07:00
69f182bca1 graphics v0.0.11-depth 2026-03-27 09:17:38 -07:00
9c30fa04a5 graphics: allow depth texture to be copied 2026-03-27 09:17:38 -07:00
ad4de709da graphics: use well-defined depth texture format 2026-03-27 09:17:38 -07:00
288ce04060 graphics: fix fullscreen 2026-03-26 15:54:01 -07:00
4898a5f866 graphics: label depth texture 2026-03-26 11:23:47 -07:00
1d1f9cd00d graphics v0.0.11 depth_texture 2026-03-26 09:37:20 -07:00
310d9f837e graphics: need depth texture 2026-03-26 09:36:38 -07:00
e0c8c92eb1 graphics v0.0.10 depth_texture_view 2026-03-26 07:54:33 -07:00
f40b28b776 graphics: depth_texture_view 2026-03-26 07:53:52 -07:00
33353b36b7 physics v0.0.2 update bnum 2026-03-25 12:27:30 -07:00
68f8b3bc53 graphics v0.0.9 update bnum 2026-03-25 09:28:29 -07:00
d7d1a6f9b6 snf 0.4.0 update bnum 2026-03-25 09:27:36 -07:00
e4e8b70118 common v0.9.0 update bnum 2026-03-25 09:25:54 -07:00
76e50231c2 update deps 2026-03-25 09:25:54 -07:00
170be30284 linear_ops v0.2.0 update bnum 2026-03-25 09:22:17 -07:00
7193b96e7c fixed_wide v0.3.0 update bnum 2026-03-25 09:20:14 -07:00
b3bccb9f50 fixed_wide: fix compile 2026-03-25 09:20:14 -07:00
8f04516f78 derive debug for FEV 2026-03-25 08:07:46 -07:00
e1313e1e73 physics: add zero time tests 2026-03-25 08:07:46 -07:00
48316a677a Update bnum to 0.14.3 (#45)
Rather involved update due to it being the foundation of the physics.

Reviewed-on: #45
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2026-03-24 18:02:17 +00:00
9b6be8bc68 physics: use core 2026-03-19 21:37:11 -07:00
93988affd7 physics: fix lint 2026-03-19 11:08:10 -07:00
78a80212d2 setup: use display handle tech 2026-03-19 11:08:10 -07:00
125e718ec8 graphics v0.0.8 dynamic limits + wgpu 29.0.0 2026-03-19 08:10:14 -07:00
0be912cc61 update deps 2026-03-19 07:58:07 -07:00
cd91cca93f update to wgpu 29.0.0 2026-03-19 07:58:07 -07:00
78d89ed3d9 graphics: dynamic wgpu Limits 2026-03-19 07:35:04 -07:00
87853d3de8 Revert "graphics: use cool macro"
This reverts commit 4ebd251273.
2026-03-19 07:19:33 -07:00
e33a419b99 common v0.8.7 traverse_bvh 2026-03-13 10:17:19 -07:00
797b5d2100 common: bvh: arbitrary traversal 2026-03-13 10:17:19 -07:00
36447a76cf graphics v0.0.7 surface 2026-03-10 07:50:07 -07:00
44f988ea4a graphics: common surface code 2026-03-10 07:48:43 -07:00
7c14acb377 graphics v0.0.6 no surface in api 2026-03-09 11:17:46 -07:00
98421ed24a graphics: use existing encoder 2026-03-09 09:31:10 -07:00
4ebd251273 graphics: use cool macro 2026-03-09 08:49:46 -07:00
05534a4863 graphics: remove SurfaceConfiguration from interfaces 2026-03-09 08:45:29 -07:00
4dab573a3c fixed_wide: annihilate non-euclid division 2026-03-06 09:10:29 -08:00
841ae94255 rbx_loader v0.10.2 do not generate meshes with no polygons 2026-03-05 08:55:37 -08:00
c44f4863bb rbx_loader: some meshes have no polygons LOL 2026-03-05 08:54:10 -08:00
298a7fcbde rbx_loader: do not generate empty polygon groups 2026-03-05 08:24:45 -08:00
7a3ea40551 rbx_loader: simplify union 2026-03-05 08:05:59 -08:00
4615ee24f3 graphics: tweak syntax 2026-03-05 07:47:59 -08:00
251f7cc627 graphics v0.0.5 create texture_color + fix webgl 2026-03-04 09:28:25 -08:00
5519a9c4d9 rbx_loader v0.10.1 improve SurfaceAppearance + allow zero-size mesh 2026-03-04 09:27:18 -08:00
378619045f graphics: hardcode downlevel to webgl2 2026-03-04 09:23:45 -08:00
67d39d5e03 graphics: create texture_color, distinct from model_color 2026-03-04 09:23:45 -08:00
daeabab337 shader: more intermediate variables for understandability 2026-03-04 09:23:45 -08:00
c3b9496e15 rbx_loader: do not fail zero size meshes & unions 2026-03-04 09:23:45 -08:00
c606adaaeb rbx_loader: don't fail to load SurfaceAppearance ColorMapContent if NormalMap etc is missing 2026-03-04 09:23:45 -08:00
85589f0b00 rbx_loader: fix typos 2026-03-04 09:23:23 -08:00
9d26e8cf6d map-tool: disable all image formats except png and jpeg 2026-03-03 14:40:57 -08:00
80c4c1e50d map-tool v3.0.1 support SurfaceAppearance 2026-03-03 09:26:20 -08:00
0df71c6c57 rbx_loader v0.10.0 support SurfaceAppearance + rbx_mesh CSGMDL5 fix 2026-03-03 09:23:15 -08:00
1a04188b8a rbx_loader: update rbx_mesh 2026-03-03 09:20:37 -08:00
aae7a23753 map-tool: add command to extract union data 2026-03-03 09:19:38 -08:00
96234f6bb1 rbx_loader: load SurfaceAppearance for MeshParts 2026-03-03 07:12:46 -08:00
1cdd4676d7 map-tool: download SurfaceAppearance content 2026-03-03 07:12:46 -08:00
b7e1c8d723 map-tool: alphabetize content search 2026-03-03 07:12:46 -08:00
f781d104ab roblox_emulator v0.5.3 add execution timeout 2026-03-03 07:12:46 -08:00
a710e33072 roblox_emulator: add script timeout 2026-03-03 07:12:46 -08:00
971e95afd9 rbx_loader: remove stupid RobloxMeshBytes forcing ownership 2026-03-02 16:18:01 -08:00
e8a13d86ec map-tool v3.0.0 custom loaders 2026-03-02 10:51:54 -08:00
2e1e8f524c map-tool: move convert functions to cli 2026-03-02 10:51:54 -08:00
7151c53553 map-tool: edit get_unique_assets to use &WeakDom reference 2026-03-02 10:36:38 -08:00
e06e51628a map-tool: remove get_unique_assets_from_file 2026-03-02 10:36:36 -08:00
ea362d18a8 bsp_loader v0.5.0 custom loaders 2026-03-02 10:23:20 -08:00
13734bfe17 rbx_loader v0.9.0 custom loaders 2026-03-02 10:23:20 -08:00
74bd6d0e84 map-tool: roblox, source optional features 2026-03-02 10:16:39 -08:00
ab09e384d3 rbx_loader: move loader files to map-tool + remove hardcoded file reads 2026-03-02 10:03:15 -08:00
99d38a6eae bsp_loader: move loader files to map-tool + remove hardcoded file reads 2026-03-02 09:45:33 -08:00
e0d1ad48a8 map-tool: accept WeakDom into convert_to_snf 2026-03-02 08:57:11 -08:00
58ea19fbea map-tool: put cli code into modules 2026-03-02 07:57:49 -08:00
d5791ac7a1 map-tool: move converters to folders 2026-03-02 07:40:44 -08:00
db7d7b3b36 Make map tool usable as a library (#42)
Makes map tool work for external libs. It works if you have problems merge it and fix it later.

Reviewed-on: #42
Reviewed-by: Rhys Lloyd <quaternions@noreply@itzana.me>
Co-authored-by: itzaname <me@sliving.io>
Co-committed-by: itzaname <me@sliving.io>
2026-03-01 20:38:05 +00:00
06d0d70791 move required_limits to setup 2026-02-28 18:21:04 -08:00
4aab9679bf graphics v0.0.4 surface errors 2026-02-28 17:53:25 -08:00
d32dcc5a58 graphics: surface all errors in setup 2026-02-28 17:52:08 -08:00
fa43f764f6 graphics v0.0.3 update deps 2026-02-28 15:47:59 -08:00
4ce8516664 common v0.8.6 update deps 2026-02-28 15:47:29 -08:00
d6441c86bc update deps 2026-02-28 15:47:29 -08:00
3371f3359b common v0.8.5 fix set_scale for paused Timer 2026-02-27 09:09:23 -08:00
34d5288620 common: timer: fix set_scale for paused timer 2026-02-27 09:07:59 -08:00
82de08312d v0.8.4 rem_euclid & div_euclid 2026-02-27 08:42:06 -08:00
4b224b1827 common: integer: add rem_euclid & div_euclid to Time 2026-02-27 08:40:16 -08:00
42862ce573 common: annotate more with #[inline] 2026-02-26 09:46:11 -08:00
3690954042 common v0.8.3 Time into float 2026-02-26 08:54:10 -08:00
bfe4b491d7 conveniently convert Time into float 2026-02-26 08:53:42 -08:00
36f44ffd43 common v0.8.2: Ratio64 fixes 2026-02-25 08:56:34 -08:00
7f16468104 common: integer: test Ratio64 from float 2026-02-25 08:48:02 -08:00
a870899743 common: integer: fix Ratio64::mul_int with large numbers 2026-02-25 08:48:02 -08:00
b5431c0732 common: v0.8.1 pub RunState 2026-02-20 07:01:34 -08:00
36ccbdc6b2 print when zero size mesh 2026-02-19 14:40:56 -08:00
a68f009658 ignore empty face 2026-02-19 14:37:15 -08:00
118a1639a7 common: tweak run 2026-02-19 10:11:02 -08:00
3212fb1d41 graphics v0.0.2 2026-02-18 09:20:01 -08:00
424c7ce9a6 refactor setup 2026-02-18 09:19:14 -08:00
3174f9caa9 fix divide by zero 2026-02-18 09:19:14 -08:00
b8e5343464 strafesnet_graphics v0.0.1 2026-02-10 08:54:51 -08:00
51e7703933 graphics: move images and shader 2026-02-10 08:54:51 -08:00
e46f54efe0 graphics: remove settings dep 2026-02-10 08:51:21 -08:00
b2993995bb graphics: remove session dep 2026-02-10 08:44:26 -08:00
7963c3632b PhysicsCamera::set_sensitivity 2026-02-06 09:23:42 -08:00
67680312dd fixed_wide v0.2.2 mul_sign div_sign 2026-02-05 07:36:18 -08:00
1a0a3403f0 specify crate versions in workspace 2026-02-05 07:34:19 -08:00
9e65a6eb95 physics v0.0.1 2026-02-05 07:34:19 -08:00
ebc897aad8 snf v0.3.2 sprint control 2026-02-05 07:28:37 -08:00
23a6655bb0 specify crate versions in workspace 2026-02-05 07:23:27 -08:00
0422c223cd common v0.8.0 2026-02-05 06:58:21 -08:00
5db2ce076d update glam 2026-02-05 06:51:07 -08:00
c98364b68d update deps 2026-02-05 06:42:19 -08:00
b7e8fb6e18 simplify adapter selection with iterators 2026-02-03 10:13:46 -08:00
d343056664 more correct 2026-02-03 08:01:34 -08:00
534f2a2141 fixed: silence lint 2026-01-30 07:52:52 -08:00
79ea88fc74 fixed: remove pub(crate) field visibility 2026-01-30 07:52:42 -08:00
3fd507be94 noclip 2026-01-30 07:33:04 -08:00
0fbe37e483 Rewrite MeshQuery (#33)
Splits the MeshQuery trait into MeshQuery and MeshTopology and cleans up much of the physics traits.  A notable optimization is using a closure for iterating variable length topological lists.  Intermediate allocations are avoided in this way.

Reviewed-on: #33
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2026-01-29 18:05:54 +00:00
638c2b4329 shuffle code around 2026-01-29 09:44:21 -08:00
317e1d57c7 move minkowski into module 2026-01-29 09:40:46 -08:00
562e46a87e defer body update to actual strafe tick 2026-01-28 09:57:33 -08:00
3c13d5f8ec untab 2026-01-28 08:40:14 -08:00
1f0f78f9d8 tweak Trajectory code 2026-01-28 08:39:28 -08:00
a90cb53a20 delete awful functions 2026-01-28 07:28:22 -08:00
170e2b9bce return used contacts from push_solve 2026-01-27 09:28:26 -08:00
3e0fc54852 Delete Body.acceleration Field (#30)
Acceleration is not a persistent part of the PhysicsState, and is only used for intermediate calculations along a trajectory.

Fixes several bugs:
- Walk decelerate clip into wall
- Walk accelerate clip into wall
- Fall while flying

Reviewed-on: #30
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2026-01-27 17:08:08 +00:00
acea52646a untab 2026-01-27 07:58:18 -08:00
7220506fd5 plumb sprint 2026-01-27 07:46:17 -08:00
8f94234ddc change DirectedEdge signature 2026-01-27 07:38:45 -08:00
36143b8b69 change UndirectedEdge signature 2026-01-27 07:35:43 -08:00
3893b2f44f work around reset bug 2026-01-26 09:19:31 -08:00
d62ff68baa fix comments 2026-01-26 09:06:40 -08:00
2331bef281 pretty print time 2026-01-22 09:43:35 -08:00
31b52f7c34 Conditionally Advance Body in atomic_internal_instruction StrafeTick (#14)
Closes #13, but exposes the underlying issue at all times.

Reviewed-on: #14
Co-authored-by: Quaternions <krakow20@gmail.com>
Co-committed-by: Quaternions <krakow20@gmail.com>
2026-01-22 17:00:52 +00:00
b12c495a33 it: bug 3 2026-01-22 08:57:35 -08:00
8329eadb18 use unbounded range in physics tests 2026-01-22 08:47:54 -08:00
4b2f93be66 fix algorithm setup start position 2026-01-22 08:41:54 -08:00
469ab48156 allow unbounded range 2026-01-22 08:41:35 -08:00
c2650adf54 md: simplify reduce 2026-01-21 10:32:19 -08:00
cdafbb4077 Implement MinimumDifference Algorithm (#25)
Completely replace the janky closest fev crawl from infinity algorithm with a dedicated purpose-built algorithm.  Finding the closest point on a MinkowskiMesh is equivalent to finding the closest point between two meshes.

Reviewed-on: #25
Co-authored-by: Rhys Lloyd <krakow20@gmail.com>
Co-committed-by: Rhys Lloyd <krakow20@gmail.com>
2026-01-21 17:31:52 +00:00
087e95b1f7 delete TogglePaused 2025-12-22 13:54:35 -08:00
e46a51319f delete unused models 2025-12-20 16:31:05 -08:00
a3b0306430 rbx_loader: fix regex 2025-12-19 13:10:04 -08:00
e024f37843 update deps 2025-12-18 10:58:50 -08:00
4059cfa527 update wgpu 2025-12-18 10:57:19 -08:00
e4f3418bc6 document PhysicsMesh 2025-12-11 09:36:21 -08:00
6ca6d5e484 expect dead code 2025-12-10 18:05:16 -08:00
0668ac2def use allow instead of expect 2025-12-09 14:39:42 -08:00
73e3181d0c roblox_emulator: v0.5.2 2025-11-27 16:42:01 -08:00
19ba8f2445 update deps 2025-11-27 15:50:19 -08:00
0495d07e26 update rbx-dom 2025-11-27 15:48:17 -08:00
0ea353b27d common: fixed_wide: min max 2025-11-24 13:04:44 -08:00
99706079d9 common: fixed_wide: add mul_sign div_sign 2025-11-24 13:04:44 -08:00
730c5fb7dd common: integer: generic zero 2025-11-22 08:47:16 -08:00
d1b61bb997 push_solve: remove epsilon 2025-11-21 10:52:34 -08:00
0343ad19cf MeshQuery::hint_point returns any point inside the mesh 2025-11-20 10:59:08 -08:00
43210b1417 less access to TouchingState private fields 2025-11-19 13:39:07 -08:00
e9d28cf15f document jank 2025-11-19 13:15:31 -08:00
452bac4049 change collision_end_contact & collision_end_intersect fn signatures 2025-11-19 10:57:44 -08:00
48aad78f59 change contact_normal function signature to reduce copies 2025-11-19 10:20:33 -08:00
d45a42f5aa change ContactCollision struct layout
Match TouchingState contacts HashMap K,V layout to try to get lucky with compiler optimization.
2025-11-19 10:20:33 -08:00
c219fec3bc specialize touching member access 2025-11-19 10:08:40 -08:00
2a05d50abb check touching before testing collision 2025-11-19 10:08:40 -08:00
fbb047f8d4 combine call chain 2025-11-19 09:01:51 -08:00
c4d837a552 Fix infinite loop with intersects when allowing 0s collisions 2025-11-19 09:01:51 -08:00
a08bd44789 Generic ConvexMeshId 2025-11-19 09:01:51 -08:00
4ae5359046 rename not_spawn_at to is_not_spawn_at 2025-11-19 09:01:27 -08:00
15ecaf602a deep match 2025-11-18 12:29:46 -08:00
1e0511a7ba remove intermediate allocation 2025-11-18 12:23:05 -08:00
a9e4705d89 remove (some) fixed point implicit conversion
They may be convenient, but they cannot be done at compile-time.
TODO: remove more of them i.e. impl_multiplicative_operator
2025-11-18 11:53:52 -08:00
98069859b5 Gracefully handle 0 acceleration for walking targets 2025-11-18 19:47:04 +00:00
64d3996fa9 use From instead of Into 2025-11-18 11:46:32 -08:00
49c0c16e35 Use a From implementation instead of manual conversion
If the contacts and intersects map ever change in the future to not be 1:1 with gaps but instead something else, this guarantees that this implicit use of the relationship will flag at a compiler level
2025-11-18 19:25:44 +00:00
255bed4803 Ensure the PhysicsData's bvh respects the original model ordering
There's no importance in worrying about the core HashMap ordering since it's not used as an iterator except for outside of this very function for bvh purposes
2025-11-18 19:25:44 +00:00
128e137829 remove redundant code 2025-11-17 13:22:46 -08:00
1e19f804cc custom hex Debug print for Fixed 2025-11-17 12:45:55 -08:00
f6f35c5f54 fix lints 2025-11-17 12:41:34 -08:00
4e7d580918 add lints to workspace 2025-11-16 14:53:23 -08:00
8d5a100a2e update deps 2025-11-09 05:48:31 -08:00
91208db706 drop lazy_regex dep 2025-11-09 05:47:33 -08:00
5a320b514e fix style 2025-11-07 16:52:50 -08:00
661d706a22 common: aabb: area_weight fn 2025-10-17 15:48:59 +01:00
5550d5771e common: bvh: reduce variable scope 2025-10-17 15:03:54 +01:00
c834d1d1ca common: bvh: name constant 2025-10-17 14:57:53 +01:00
ca9d2238a7 common: bvh: tweak code style 2025-10-17 14:57:53 +01:00
f3bb8dd067 update deps 2025-10-01 23:37:07 -07:00
e58f9b9ff2 rbx_loader: silently filter vertices which fail to convert 2025-09-29 19:45:16 -07:00
54c4ed6bad update deps 2025-08-30 15:20:23 -07:00
9aceafa0df roblox_emulator: fix lints 2025-08-30 15:15:23 -07:00
d065bac130 Revert "roblox_emulator: use extended instances"
This reverts commit bb8e131464.
2025-08-30 15:13:28 -07:00
a4d0393556 update rbx-dom 2025-08-30 15:12:22 -07:00
3692d7f79e it: fix bug 3 test 2025-08-29 19:07:57 -07:00
7e49840768 it: set gravity to 0 2025-08-29 19:06:51 -07:00
4ecdd547c6 it: use instruction iter 2025-08-29 18:30:28 -07:00
b0365165e8 physics: create iterator over internal instructions 2025-08-29 18:30:18 -07:00
c2ff52a2ae instruction: iterator 2025-08-29 18:30:18 -07:00
6e778869e8 it: bug 3 test scene 2025-08-29 18:30:18 -07:00
6509bef070 it: add test scene 2025-08-29 16:47:46 -07:00
0fa097a004 aabb: tweak Aabb.contains 2025-08-29 15:40:00 -07:00
55d4b1d264 physics: PhysicsData is immutable after construction 2025-08-28 16:37:48 -07:00
ea28663e95 physics: move code 2025-08-28 16:02:23 -07:00
bac9be9684 physics: add edge case tests 2025-08-28 14:49:36 -07:00
7e76f3309b ignore debugger config 2025-08-26 16:54:36 -07:00
98 changed files with 6764 additions and 16788 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target
.zed

2655
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,3 +24,34 @@ resolver = "2"
#lto = true
strip = true
codegen-units = 1
[workspace.lints.rust]
# unsafe_code = "forbid"
# missing_docs = "warn"
# missing_debug_implementations = "warn"
single_use_lifetimes = "warn"
trivial_casts = "warn"
unused_lifetimes = "warn"
unused_qualifications = "warn"
# variant_size_differences = "warn"
unexpected_cfgs = "warn"
[workspace.dependencies]
glam = "0.32.0"
wgpu = "29.0.0"
# engine
strafesnet_graphics = { path = "engine/graphics", registry = "strafesnet" }
strafesnet_physics = { path = "engine/physics", registry = "strafesnet" }
strafesnet_session = { path = "engine/session", registry = "strafesnet" }
strafesnet_settings = { path = "engine/settings", registry = "strafesnet" }
# lib
fixed_wide = { version = "0.3.0", path = "lib/fixed_wide", registry = "strafesnet" }
linear_ops = { version = "0.2.0", path = "lib/linear_ops", registry = "strafesnet" }
ratio_ops = { version = "0.1.0", path = "lib/ratio_ops", registry = "strafesnet" }
strafesnet_bsp_loader = { version = "0.5.0", path = "lib/bsp_loader", registry = "strafesnet" }
strafesnet_common = { version = "0.9.0", path = "lib/common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.6.0", path = "lib/deferred_loader", registry = "strafesnet" }
strafesnet_rbx_loader = { version = "0.10.2", path = "lib/rbx_loader", registry = "strafesnet" }
strafesnet_snf = { version = "0.4.0", path = "lib/snf", registry = "strafesnet" }

View File

@@ -1,14 +1,15 @@
[package]
name = "strafesnet_graphics"
version = "0.1.0"
version = "0.0.11-depth2"
edition = "2024"
[dependencies]
bytemuck = { version = "1.13.1", features = ["derive"] }
ddsfile = "0.5.1"
glam = "0.30.0"
glam.workspace = true
id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
strafesnet_session = { path = "../session", registry = "strafesnet" }
strafesnet_settings = { path = "../settings", registry = "strafesnet" }
wgpu = "26.0.1"
strafesnet_common.workspace = true
wgpu.workspace = true
[lints]
workspace = true

View File

@@ -44,11 +44,11 @@ struct ModelInstance{
transform:mat4x4<f32>,
normal_transform:mat3x3<f32>,
color:vec4<f32>,
texture_color:vec4<f32>,
}
//my fancy idea is to create a megatexture for each model that includes all the textures each intance will need
//the texture transform then maps the texture coordinates to the location of the specific texture
//group 1 is the model
const MAX_MODEL_INSTANCES=512;
@group(2)
@binding(0)
var<uniform> model_instances: array<ModelInstance, MAX_MODEL_INSTANCES>;
@@ -66,6 +66,7 @@ struct EntityOutputTexture {
@location(3) view: vec3<f32>,
@location(4) color: vec4<f32>,
@location(5) @interpolate(flat) model_color: vec4<f32>,
@location(6) @interpolate(flat) texture_color: vec4<f32>,
};
@vertex
fn vs_entity_texture(
@@ -81,6 +82,7 @@ fn vs_entity_texture(
result.texture = texture;
result.color = color;
result.model_color = model_instances[instance].color;
result.texture_color = model_instances[instance].texture_color;
result.view = position.xyz - camera.view_inv[3].xyz;//col(3)
result.position = camera.proj * camera.view * position;
return result;
@@ -106,7 +108,9 @@ fn fs_entity_texture(vertex: EntityOutputTexture) -> @location(0) vec4<f32> {
let d = dot(normal, incident);
let reflected = incident - 2.0 * d * normal;
let fragment_color = textureSample(model_texture, model_sampler, vertex.texture)*vertex.color;
let fragment_color = textureSample(model_texture, model_sampler, vertex.texture)*vertex.texture_color;
let reflected_color = textureSample(cube_texture, cube_sampler, reflected).rgb;
return mix(vec4<f32>(vec3<f32>(0.05) + 0.2 * reflected_color,1.0),mix(vertex.model_color,vec4<f32>(fragment_color.rgb,1.0),fragment_color.a),0.5+0.5*abs(d));
let entity_color = mix(vertex.model_color*vertex.color,vec4<f32>(fragment_color.rgb,1.0),fragment_color.a);
let sky_color = vec4<f32>(vec3<f32>(0.05) + 0.2 * reflected_color,1.0);
return mix(sky_color,entity_color,0.5+0.5*abs(d));
}

View File

@@ -1,15 +1,9 @@
use std::borrow::Cow;
use std::collections::{HashSet,HashMap};
use strafesnet_common::map;
use strafesnet_settings::settings;
use strafesnet_session::session;
use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId};
use wgpu::{util::DeviceExt,AstcBlock,AstcChannel};
use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex};
pub fn required_limits()->wgpu::Limits{
wgpu::Limits::default()
}
use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelOwned,GraphicsVertex};
struct Indices{
count:u32,
@@ -54,6 +48,10 @@ struct GraphicsPipelines{
model:wgpu::RenderPipeline,
}
pub fn view_inv(pos:glam::Vec3,angles:glam::Vec2)->glam::Mat4{
//f32 good enough for view matrix
glam::Mat4::from_mat3_translation(glam::Mat3::from_euler(glam::EulerRot::YXZ,angles.x,angles.y,0f32),pos)
}
struct GraphicsCamera{
screen_size:glam::UVec2,
fov:glam::Vec2,//slope
@@ -75,15 +73,10 @@ impl GraphicsCamera{
pub fn proj(&self)->glam::Mat4{
perspective_rh(self.fov.x,self.fov.y,0.4,4000.0)
}
pub fn world(&self,pos:glam::Vec3,angles:glam::Vec2)->glam::Mat4{
//f32 good enough for view matrix
glam::Mat4::from_translation(pos)*glam::Mat4::from_euler(glam::EulerRot::YXZ,angles.x,angles.y,0f32)
}
pub fn to_uniform_data(&self,pos:glam::Vec3,angles:glam::Vec2)->[f32;16*4]{
pub fn to_uniform_data(&self,view_inv:glam::Mat4)->[f32;16*4]{
let proj=self.proj();
let proj_inv=proj.inverse();
let view_inv=self.world(pos,angles);
let view=view_inv.inverse();
let mut raw=[0f32; 16 * 4];
@@ -94,7 +87,7 @@ impl GraphicsCamera{
raw
}
}
impl std::default::Default for GraphicsCamera{
impl Default for GraphicsCamera{
fn default()->Self{
Self{
screen_size:glam::UVec2::ONE,
@@ -103,7 +96,7 @@ impl std::default::Default for GraphicsCamera{
}
}
const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4;//let size=std::mem::size_of::<ModelInstance>();
const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4 + 4;//let size=std::mem::size_of::<ModelInstance>();
const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*4;
fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
let mut raw=Vec::with_capacity(MODEL_BUFFER_SIZE*instances.len());
@@ -118,7 +111,9 @@ fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.z_axis));
raw.extend_from_slice(&[0.0]);
//color
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color.get()));
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color));
//texture color
raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.texture_color));
}
raw
}
@@ -132,42 +127,45 @@ pub struct GraphicsState{
camera_buf:wgpu::Buffer,
temp_squid_texture_view:wgpu::TextureView,
models:Vec<GraphicsModel>,
depth:wgpu::Texture,
depth_view:wgpu::TextureView,
staging_belt:wgpu::util::StagingBelt,
model_instances_uniform_len:usize,
}
impl GraphicsState{
const DEPTH_FORMAT:wgpu::TextureFormat=wgpu::TextureFormat::Depth24Plus;
const DEPTH_FORMAT:wgpu::TextureFormat=wgpu::TextureFormat::Depth32Float;
fn create_depth_texture(
config:&wgpu::SurfaceConfiguration,
size:glam::UVec2,
device:&wgpu::Device,
)->wgpu::TextureView{
let depth_texture=device.create_texture(&wgpu::TextureDescriptor{
)->wgpu::Texture{
device.create_texture(&wgpu::TextureDescriptor{
size:wgpu::Extent3d{
width:config.width,
height:config.height,
width:size.x,
height:size.y,
depth_or_array_layers:1,
},
mip_level_count:1,
sample_count:1,
dimension:wgpu::TextureDimension::D2,
format:Self::DEPTH_FORMAT,
usage:wgpu::TextureUsages::RENDER_ATTACHMENT,
label:None,
usage:wgpu::TextureUsages::RENDER_ATTACHMENT|wgpu::TextureUsages::COPY_SRC,
label:Some("Depth Texture"),
view_formats:&[],
});
depth_texture.create_view(&wgpu::TextureViewDescriptor::default())
})
}
pub const fn depth_texture(&self)->&wgpu::Texture{
&self.depth
}
pub const fn depth_texture_view(&self)->&wgpu::TextureView{
&self.depth_view
}
pub fn clear(&mut self){
self.models.clear();
}
pub fn load_user_settings(&mut self,user_settings:&settings::UserSettings){
self.camera.fov=user_settings.calculate_fov(1.0,&self.camera.screen_size).as_vec2();
}
pub fn generate_models(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&map::CompleteMap){
//generate texture view per texture
let texture_views:HashMap<strafesnet_common::model::TextureId,wgpu::TextureView>=map.textures.iter().enumerate().filter_map(|(texture_id,texture_data)|{
let texture_views:HashMap<model::TextureId,wgpu::TextureView>=map.textures.iter().enumerate().filter_map(|(texture_id,texture_data)|{
let texture_id=model::TextureId::new(texture_id as u32);
let image=match ddsfile::Dds::read(std::io::Cursor::new(texture_data)){
Ok(image)=>image,
@@ -239,7 +237,8 @@ impl GraphicsState{
let instance=GraphicsModelOwned{
transform:model.transform.into(),
normal_transform:glam::Mat3::from_cols_array_2d(&model.transform.matrix3.to_array().map(|row|row.map(Into::into))).inverse().transpose(),
color:GraphicsModelColor4::new(model.color),
color:model.color,
texture_color:glam::Vec4::ONE,
};
//get or create owned mesh map
let owned_mesh_map=owned_mesh_id_from_mesh_id_render_config_id
@@ -269,16 +268,15 @@ impl GraphicsState{
owned_mesh_id
});
let owned_mesh=unique_render_config_models.get_mut(owned_mesh_id.get() as usize).unwrap();
match &mut owned_mesh.polys{
model::PolygonGroup::PolygonList(polygon_list)=>polygon_list.extend(
graphics_group.groups.iter().flat_map(|polygon_group_id|{
mesh.polygon_groups[polygon_group_id.get() as usize].polys()
})
.map(|vertex_id_slice|
vertex_id_slice.to_vec()
)
),
}
let model::PolygonGroup::PolygonList(polygon_list)=&mut owned_mesh.polys;
polygon_list.extend(
graphics_group.groups.iter().flat_map(|polygon_group_id|{
mesh.polygon_groups[polygon_group_id.get() as usize].polys()
})
.map(|vertex_id_slice|
vertex_id_slice.to_vec()
)
);
}
}
owned_mesh_map
@@ -286,10 +284,20 @@ impl GraphicsState{
for owned_mesh_id in owned_mesh_map.values(){
let owned_mesh=unique_render_config_models.get_mut(owned_mesh_id.get() as usize).unwrap();
let render_config=&map.render_configs[owned_mesh.render_config.get() as usize];
if model.color.w==0.0&&render_config.texture.is_none(){
continue;
}
owned_mesh.instances.push(instance.clone());
let no_texture=render_config.texture.is_none();
let instance_clone=if no_texture{
// this model has no chance of affecting pixels on the screen
if model.color.w==0.0{
continue;
}
// hack texture_color to zero to make the texture invisible
let mut instance=instance.clone();
instance.texture_color=glam::Vec4::ZERO;
instance
}else{
instance.clone()
};
owned_mesh.instances.push(instance_clone);
}
}
//check every model to see if it's using the same (texture,color) but has few instances,if it is combine it into one model
@@ -315,7 +323,7 @@ impl GraphicsState{
//separate instances by color
for (instance_id,instance) in model.instances.iter().enumerate(){
let model_instance_list=unique_color
.entry(instance.color)
.entry(instance.hashable_color())
.or_insert_with(||Vec::new());
//add model instance to list
model_instance_list.push((model_id,instance_id));
@@ -404,6 +412,7 @@ impl GraphicsState{
).collect()
));
}
let (color,texture_color)=color.color();
//push model into dedup
deduplicated_models.push(IndexedGraphicsMeshOwnedRenderConfig{
unique_pos,
@@ -416,7 +425,8 @@ impl GraphicsState{
instances:vec![GraphicsModelOwned{
transform:glam::Mat4::IDENTITY,
normal_transform:glam::Mat3::IDENTITY,
color
color,
texture_color,
}],
});
}
@@ -474,16 +484,14 @@ impl GraphicsState{
//.into_iter() the modeldata vec so entities can be /moved/ to models.entities
let mut model_count=0;
let mut instance_count=0;
let uniform_buffer_binding_size=required_limits().max_uniform_buffer_binding_size as usize;
let chunk_size=uniform_buffer_binding_size/MODEL_BUFFER_SIZE_BYTES;
self.models.reserve(models.len());
for model in models.into_iter(){
instance_count+=model.instances.len();
for instances_chunk in model.instances.rchunks(chunk_size){
for instances_chunk in model.instances.rchunks(self.model_instances_uniform_len){
model_count+=1;
let mut model_uniforms=get_instances_buffer_data(instances_chunk);
//TEMP: fill with zeroes to pass validation
model_uniforms.resize(MODEL_BUFFER_SIZE*512,0.0f32);
model_uniforms.resize(MODEL_BUFFER_SIZE*self.model_instances_uniform_len,0.0f32);
let model_buf=device.create_buffer_init(&wgpu::util::BufferInitDescriptor{
label:Some(format!("Model{} Buf",model_count).as_str()),
contents:bytemuck::cast_slice(&model_uniforms),
@@ -539,7 +547,9 @@ impl GraphicsState{
pub fn new(
device:&wgpu::Device,
queue:&wgpu::Queue,
config:&wgpu::SurfaceConfiguration,
size:glam::UVec2,
view_format:wgpu::TextureFormat,
limits:wgpu::Limits,
)->Self{
let camera_bind_group_layout=device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor{
label:None,
@@ -616,7 +626,7 @@ impl GraphicsState{
address_mode_w:wgpu::AddressMode::ClampToEdge,
mag_filter:wgpu::FilterMode::Linear,
min_filter:wgpu::FilterMode::Linear,
mipmap_filter:wgpu::FilterMode::Linear,
mipmap_filter:wgpu::MipmapFilterMode::Linear,
..Default::default()
});
let repeat_sampler=device.create_sampler(&wgpu::SamplerDescriptor{
@@ -626,15 +636,23 @@ impl GraphicsState{
address_mode_w:wgpu::AddressMode::Repeat,
mag_filter:wgpu::FilterMode::Linear,
min_filter:wgpu::FilterMode::Linear,
mipmap_filter:wgpu::FilterMode::Linear,
mipmap_filter:wgpu::MipmapFilterMode::Linear,
anisotropy_clamp:16,
..Default::default()
});
let model_instances_uniform_len=limits.max_uniform_buffer_binding_size as usize/MODEL_BUFFER_SIZE_BYTES;
// write dynamic value into shader
let shader=format!("
// This is equal to the CHUNK_SIZE constant from graphics.rs
const MAX_MODEL_INSTANCES={model_instances_uniform_len};
")+include_str!("../shaders/shader.wgsl");
// Create the render pipeline
let shader=device.create_shader_module(wgpu::ShaderModuleDescriptor{
label:None,
source:wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("../../../strafe-client/src/shader.wgsl"))),
source:wgpu::ShaderSource::Wgsl(Cow::Owned(shader)),
});
//load textures
@@ -662,10 +680,10 @@ impl GraphicsState{
wgpu::TextureFormat::Astc{
block:AstcBlock::B4x4,
channel:AstcChannel::UnormSrgb,
}=>&include_bytes!("../../../strafe-client/images/astc.dds")[..],
wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../../../strafe-client/images/etc2.dds")[..],
wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../../../strafe-client/images/bc1.dds")[..],
wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../../../strafe-client/images/bgra.dds")[..],
}=>&include_bytes!("../images/astc.dds")[..],
wgpu::TextureFormat::Etc2Rgb8UnormSrgb=>&include_bytes!("../images/etc2.dds")[..],
wgpu::TextureFormat::Bc1RgbaUnormSrgb=>&include_bytes!("../images/bc1.dds")[..],
wgpu::TextureFormat::Bgra8UnormSrgb=>&include_bytes!("../images/bgra.dds")[..],
_=>unreachable!(),
};
@@ -708,7 +726,7 @@ impl GraphicsState{
//squid
let squid_texture_view={
let bytes=include_bytes!("../../../strafe-client/images/squid.dds");
let bytes=include_bytes!("../images/squid.dds");
let image=ddsfile::Dds::read(&mut std::io::Cursor::new(bytes)).unwrap();
@@ -750,19 +768,19 @@ impl GraphicsState{
let model_pipeline_layout=device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor{
label:None,
bind_group_layouts:&[
&camera_bind_group_layout,
&skybox_texture_bind_group_layout,
&model_bind_group_layout,
Some(&camera_bind_group_layout),
Some(&skybox_texture_bind_group_layout),
Some(&model_bind_group_layout),
],
push_constant_ranges:&[],
immediate_size:0,
});
let sky_pipeline_layout=device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor{
label:None,
bind_group_layouts:&[
&camera_bind_group_layout,
&skybox_texture_bind_group_layout,
Some(&camera_bind_group_layout),
Some(&skybox_texture_bind_group_layout),
],
push_constant_ranges:&[],
immediate_size:0,
});
// Create the render pipelines
@@ -778,7 +796,7 @@ impl GraphicsState{
fragment:Some(wgpu::FragmentState{
module:&shader,
entry_point:Some("fs_sky"),
targets:&[Some(config.view_formats[0].into())],
targets:&[Some(view_format.into())],
compilation_options:wgpu::PipelineCompilationOptions::default(),
}),
primitive:wgpu::PrimitiveState{
@@ -787,13 +805,13 @@ impl GraphicsState{
},
depth_stencil:Some(wgpu::DepthStencilState{
format:Self::DEPTH_FORMAT,
depth_write_enabled:false,
depth_compare:wgpu::CompareFunction::LessEqual,
depth_write_enabled:Some(false),
depth_compare:Some(wgpu::CompareFunction::LessEqual),
stencil:wgpu::StencilState::default(),
bias:wgpu::DepthBiasState::default(),
}),
multisample:wgpu::MultisampleState::default(),
multiview:None,
multiview_mask:None,
cache:None,
});
let model_pipeline=device.create_render_pipeline(&wgpu::RenderPipelineDescriptor{
@@ -803,7 +821,7 @@ impl GraphicsState{
module:&shader,
entry_point:Some("vs_entity_texture"),
buffers:&[wgpu::VertexBufferLayout{
array_stride:std::mem::size_of::<GraphicsVertex>() as wgpu::BufferAddress,
array_stride:size_of::<GraphicsVertex>() as wgpu::BufferAddress,
step_mode:wgpu::VertexStepMode::Vertex,
attributes:&wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Float32x3,3=>Float32x4],
}],
@@ -812,7 +830,7 @@ impl GraphicsState{
fragment:Some(wgpu::FragmentState{
module:&shader,
entry_point:Some("fs_entity_texture"),
targets:&[Some(config.view_formats[0].into())],
targets:&[Some(view_format.into())],
compilation_options:wgpu::PipelineCompilationOptions::default(),
}),
primitive:wgpu::PrimitiveState{
@@ -822,18 +840,18 @@ impl GraphicsState{
},
depth_stencil:Some(wgpu::DepthStencilState{
format:Self::DEPTH_FORMAT,
depth_write_enabled:true,
depth_compare:wgpu::CompareFunction::LessEqual,
depth_write_enabled:Some(true),
depth_compare:Some(wgpu::CompareFunction::LessEqual),
stencil:wgpu::StencilState::default(),
bias:wgpu::DepthBiasState::default(),
}),
multisample:wgpu::MultisampleState::default(),
multiview:None,
multiview_mask:None,
cache:None,
});
let camera=GraphicsCamera::default();
let camera_uniforms=camera.to_uniform_data(glam::Vec3::ZERO,glam::Vec2::ZERO);
let camera_uniforms=camera.to_uniform_data(view_inv(glam::Vec3::ZERO,glam::Vec2::ZERO));
let camera_buf=device.create_buffer_init(&wgpu::util::BufferInitDescriptor{
label:Some("Camera"),
contents:bytemuck::cast_slice(&camera_uniforms),
@@ -865,7 +883,8 @@ impl GraphicsState{
label:Some("Sky Texture"),
});
let depth_view=Self::create_depth_texture(config,device);
let depth=Self::create_depth_texture(size,device);
let depth_view=depth.create_view(&wgpu::TextureViewDescriptor::default());
Self{
pipelines:GraphicsPipelines{
@@ -879,46 +898,44 @@ impl GraphicsState{
camera,
camera_buf,
models:Vec::new(),
depth,
depth_view,
staging_belt:wgpu::util::StagingBelt::new(0x100),
staging_belt:wgpu::util::StagingBelt::new(device.clone(),0x100),
bind_group_layouts:GraphicsBindGroupLayouts{model:model_bind_group_layout},
samplers:GraphicsSamplers{repeat:repeat_sampler},
temp_squid_texture_view:squid_texture_view,
model_instances_uniform_len,
}
}
pub fn resize(
&mut self,
device:&wgpu::Device,
config:&wgpu::SurfaceConfiguration,
user_settings:&settings::UserSettings,
size:glam::UVec2,
fov:glam::Vec2,
){
self.depth_view=Self::create_depth_texture(config,device);
self.camera.screen_size=glam::uvec2(config.width,config.height);
self.load_user_settings(user_settings);
self.depth=Self::create_depth_texture(size,device);
self.depth_view=self.depth.create_view(&wgpu::TextureViewDescriptor::default());
self.camera.screen_size=size;
self.camera.fov=fov;
}
pub fn render(
pub fn encode_commands(
&mut self,
encoder:&mut wgpu::CommandEncoder,
view:&wgpu::TextureView,
device:&wgpu::Device,
queue:&wgpu::Queue,
frame_state:session::FrameState,
camera:glam::Mat4,
){
//TODO:use scheduled frame times to create beautiful smoothing simulation physics extrapolation assuming no input
let mut encoder=device.create_command_encoder(&wgpu::CommandEncoderDescriptor{label:None});
// TODO: find a way to call this directly after queue.submit()
self.staging_belt.recall();
// update rotation
let camera_uniforms=self.camera.to_uniform_data(
frame_state.body.extrapolated_position(frame_state.time).map(Into::<f32>::into).to_array().into(),
frame_state.camera.simulate_move_angles(glam::IVec2::ZERO)
);
let camera_uniforms=self.camera.to_uniform_data(camera);
self.staging_belt
.write_buffer(
&mut encoder,
encoder,
&self.camera_buf,
0,
wgpu::BufferSize::new((camera_uniforms.len() * 4) as wgpu::BufferAddress).unwrap(),
device,
)
.copy_from_slice(bytemuck::cast_slice(&camera_uniforms));
//This code only needs to run when the uniforms change
@@ -959,12 +976,13 @@ impl GraphicsState{
view:&self.depth_view,
depth_ops:Some(wgpu::Operations{
load:wgpu::LoadOp::Clear(1.0),
store:wgpu::StoreOp::Discard,
store:wgpu::StoreOp::Store,
}),
stencil_ops:None,
}),
timestamp_writes:Default::default(),
occlusion_query_set:Default::default(),
multiview_mask:None,
});
rpass.set_bind_group(0,&self.bind_groups.camera,&[]);
@@ -982,9 +1000,5 @@ impl GraphicsState{
rpass.set_pipeline(&self.pipelines.skybox);
rpass.draw(0..3,0..1);
}
queue.submit(std::iter::once(encoder.finish()));
self.staging_belt.recall();
}
}

View File

@@ -1,2 +1,4 @@
pub mod model;
pub mod setup;
pub mod surface;
pub mod graphics;

View File

@@ -30,19 +30,23 @@ pub struct GraphicsMeshOwnedRenderConfig{
pub render_config:RenderConfigId,
pub instances:Vec<GraphicsModelOwned>,
}
#[derive(Clone,Copy,PartialEq,id::Id)]
pub struct GraphicsModelColor4(glam::Vec4);
impl std::hash::Hash for GraphicsModelColor4{
fn hash<H:std::hash::Hasher>(&self,state:&mut H) {
for &f in self.0.as_ref(){
bytemuck::cast::<f32,u32>(f).hash(state);
}
}
}
impl Eq for GraphicsModelColor4{}
#[derive(Clone)]
pub struct GraphicsModelOwned{
pub transform:glam::Mat4,
pub normal_transform:glam::Mat3,
pub color:GraphicsModelColor4,
pub color:glam::Vec4,
pub texture_color:glam::Vec4,
}
impl GraphicsModelOwned{
pub fn hashable_color(&self)->HashableColor{
HashableColor([bytemuck::cast::<[f32;4],[u32;4]>(self.color.to_array()),bytemuck::cast::<[f32;4],[u32;4]>(self.texture_color.to_array())])
}
}
#[derive(Clone,Copy,Eq,Hash,PartialEq)]
pub struct HashableColor([[u32;4];2]);
impl HashableColor{
pub fn color(self)->(glam::Vec4,glam::Vec4){
let [color,texture_color]=bytemuck::cast::<[[u32;4];2],[[f32;4];2]>(self.0);
(glam::Vec4::from_array(color),glam::Vec4::from_array(texture_color))
}
}

View File

@@ -0,0 +1,116 @@
fn optional_features()->wgpu::Features{
wgpu::Features::TEXTURE_COMPRESSION_ASTC
|wgpu::Features::TEXTURE_COMPRESSION_ETC2
}
fn required_features()->wgpu::Features{
wgpu::Features::TEXTURE_COMPRESSION_BC
}
fn required_downlevel_capabilities()->wgpu::DownlevelCapabilities{
wgpu::DownlevelCapabilities{
flags:wgpu::DownlevelFlags::empty(),
shader_model:wgpu::ShaderModel::Sm5,
..wgpu::DownlevelCapabilities::default()
}
}
pub mod step1{
pub fn create_instance()->wgpu::Instance{
Default::default()
}
}
pub mod step2{
pub fn create_surface<'window>(instance:&wgpu::Instance,target:impl Into<wgpu::SurfaceTarget<'window>>)->Result<wgpu::Surface<'window>,wgpu::CreateSurfaceError>{
instance.create_surface(target)
}
}
pub mod step3{
pub async fn pick_adapter(instance:&wgpu::Instance,surface:&wgpu::Surface<'_>)->Option<wgpu::Adapter>{
let backends=wgpu::Backends::from_env().unwrap_or_default();
//TODO: prefer adapter that implements optional features
//let optional_features=optional_features();
let required_features=super::required_features();
let required_downlevel_capabilities=super::required_downlevel_capabilities();
//no helper function smh gotta write it myself
let adapters=instance.enumerate_adapters(backends).await;
let adapter=adapters.into_iter()
// reverse because we want to select adapters that appear first in ties,
// and max_by_key selects the last equal element in the iterator.
.rev()
.filter(|adapter|
adapter.is_surface_supported(surface)
&&adapter.features().contains(required_features)
&&{
let downlevel_capabilities=adapter.get_downlevel_capabilities();
downlevel_capabilities.shader_model>=required_downlevel_capabilities.shader_model
&&downlevel_capabilities.flags.contains(required_downlevel_capabilities.flags)
}
)
.max_by_key(|adapter|match adapter.get_info().device_type{
wgpu::DeviceType::IntegratedGpu=>3,
wgpu::DeviceType::DiscreteGpu=>4,
wgpu::DeviceType::VirtualGpu=>2,
wgpu::DeviceType::Other|wgpu::DeviceType::Cpu=>1,
})?;
Some(adapter)
}
}
pub mod step4{
pub async fn request_device(adapter:&wgpu::Adapter,limits:wgpu::Limits)->Result<(wgpu::Device,wgpu::Queue),wgpu::RequestDeviceError>{
let optional_features=super::optional_features();
let required_features=super::required_features();
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the surface.
let needed_limits=limits.using_resolution(adapter.limits());
let (device, queue)=adapter
.request_device(
&wgpu::DeviceDescriptor{
label:None,
required_features:(optional_features&adapter.features())|required_features,
required_limits:needed_limits,
memory_hints:wgpu::MemoryHints::Performance,
trace:wgpu::Trace::Off,
experimental_features:wgpu::ExperimentalFeatures::disabled(),
},
).await?;
Ok((
device,
queue,
))
}
}
pub mod step5{
use crate::surface::Surface;
#[derive(Debug)]
pub struct ErrorSurfaceNotSupported;
impl std::fmt::Display for ErrorSurfaceNotSupported{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"Surface isn't supported by the adapter.")
}
}
pub fn configure_surface<'window>(
adapter:&wgpu::Adapter,
device:&wgpu::Device,
surface:wgpu::Surface<'window>,
(width,height):(u32,u32),
)->Result<Surface<'window>,ErrorSurfaceNotSupported>{
let mut config=surface
.get_default_config(adapter, width, height)
.ok_or(ErrorSurfaceNotSupported)?;
let surface_view_format=config.format.add_srgb_suffix();
config.view_formats.push(surface_view_format);
config.present_mode=wgpu::PresentMode::AutoNoVsync;
surface.configure(device,&config);
Ok(Surface::new(surface,config))
}
}

View File

@@ -0,0 +1,75 @@
/// A texture view which can be targeted by draw calls in the command buffer, and then presented to the surface texture.
pub struct Frame{
surface_texture:wgpu::SurfaceTexture,
view:wgpu::TextureView,
}
impl Frame{
pub const fn view(&self)->&wgpu::TextureView{
&self.view
}
pub fn present(self){
self.surface_texture.present();
}
}
#[derive(Debug)]
pub enum FrameError{
Skip,
DeviceLost,
}
impl core::fmt::Display for FrameError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl core::error::Error for FrameError{}
/// A render surface configuration, containing information such as resolution and pixel format
pub struct Surface<'window>{
surface:wgpu::Surface<'window>,
config:wgpu::SurfaceConfiguration,
}
impl<'window> Surface<'window>{
pub(crate) fn new(
surface:wgpu::Surface<'window>,
config:wgpu::SurfaceConfiguration,
)->Self{
Self{surface,config}
}
#[must_use]
pub fn new_frame(&self,device:&wgpu::Device)->Result<Frame,FrameError>{
let frame=loop{
match self.surface.get_current_texture(){
wgpu::CurrentSurfaceTexture::Success(surface_texture)=>break surface_texture,
// Suboptimal -> surface_texture must be dropped and surface reconfigured
wgpu::CurrentSurfaceTexture::Suboptimal(_)
|wgpu::CurrentSurfaceTexture::Outdated=>{},
wgpu::CurrentSurfaceTexture::Timeout
|wgpu::CurrentSurfaceTexture::Occluded=>return Err(FrameError::Skip),
wgpu::CurrentSurfaceTexture::Lost=>return Err(FrameError::DeviceLost),
wgpu::CurrentSurfaceTexture::Validation=>unreachable!(),
};
self.surface.configure(device,&self.config);
};
let view=frame.texture.create_view(&wgpu::TextureViewDescriptor{
format:Some(self.config.view_formats[0]),
..wgpu::TextureViewDescriptor::default()
});
Ok(Frame{
surface_texture:frame,
view,
})
}
pub const fn size(&self)->glam::UVec2{
glam::uvec2(self.config.width,self.config.height)
}
pub fn view_format(&self)->wgpu::TextureFormat{
self.config.view_formats[0]
}
pub fn configure(&mut self,device:&wgpu::Device,size:glam::UVec2){
self.config.width=size.x.max(1);
self.config.height=size.y.max(1);
self.surface.configure(device,&self.config);
}
}

View File

@@ -1,10 +1,13 @@
[package]
name = "strafesnet_physics"
version = "0.1.0"
version = "0.0.2"
edition = "2024"
[dependencies]
arrayvec = "0.7.6"
glam = "0.30.0"
glam.workspace = true
id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
strafesnet_common.workspace = true
[lints]
workspace = true

View File

@@ -2,12 +2,18 @@ use strafesnet_common::aabb;
use strafesnet_common::integer::{self,vec3,Time,Planar64,Planar64Vec3};
#[derive(Clone,Copy,Debug,Hash)]
pub struct Body<T>{
pub position:Planar64Vec3,//I64 where 2^32 = 1 u
pub velocity:Planar64Vec3,//I64 where 2^32 = 1 u/s
pub acceleration:Planar64Vec3,//I64 where 2^32 = 1 u/s/s
pub time:Time<T>,//nanoseconds x xxxxD!
pub position:Planar64Vec3,
pub velocity:Planar64Vec3,
pub time:Time<T>,
}
impl<T> std::ops::Neg for Body<T>{
#[derive(Clone,Copy,Debug,Hash)]
pub struct Trajectory<T>{
pub position:Planar64Vec3,
pub velocity:Planar64Vec3,
pub acceleration:Planar64Vec3,
pub time:Time<T>,
}
impl<T> std::ops::Neg for Trajectory<T>{
type Output=Self;
fn neg(self)->Self::Output{
Self{
@@ -18,10 +24,10 @@ impl<T> std::ops::Neg for Body<T>{
}
}
}
impl<T:Copy> std::ops::Neg for &Body<T>{
type Output=Body<T>;
impl<T:Copy> std::ops::Neg for &Trajectory<T>{
type Output=Trajectory<T>;
fn neg(self)->Self::Output{
Body{
Trajectory{
position:self.position,
velocity:-self.velocity,
acceleration:self.acceleration,
@@ -33,7 +39,33 @@ impl<T:Copy> std::ops::Neg for &Body<T>{
impl<T> Body<T>
where Time<T>:Copy,
{
pub const ZERO:Self=Self::new(vec3::ZERO,vec3::ZERO,vec3::ZERO,Time::ZERO);
pub const ZERO:Self=Self::new(vec3::zero(),vec3::zero(),Time::ZERO);
pub const fn new(position:Planar64Vec3,velocity:Planar64Vec3,time:Time<T>)->Self{
Self{
position,
velocity,
time,
}
}
pub const fn with_acceleration(self,acceleration:Planar64Vec3)->Trajectory<T>{
let Body{
position,
velocity,
time,
}=self;
Trajectory{
position,
velocity,
acceleration,
time,
}
}
}
impl<T> Trajectory<T>
where Time<T>:Copy,
{
pub const ZERO:Self=Self::new(vec3::zero(),vec3::zero(),vec3::zero(),Time::ZERO);
pub const fn new(position:Planar64Vec3,velocity:Planar64Vec3,acceleration:Planar64Vec3,time:Time<T>)->Self{
Self{
position,
@@ -42,28 +74,31 @@ impl<T> Body<T>
time,
}
}
pub const fn relative_to<'a>(&'a self,body0:&'a Body<T>)->VirtualBody<'a,T>{
pub fn relative_to(&self,trj0:&Self,time:Time<T>)->Self{
//(p0,v0,a0,t0)
//(p1,v1,a1,t1)
VirtualBody{
body0,
body1:self,
}
Trajectory::new(
self.extrapolated_position(time)-trj0.extrapolated_position(time),
self.extrapolated_velocity(time)-trj0.extrapolated_velocity(time),
self.acceleration-trj0.acceleration,
time)
}
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
let dt=time-self.time;
self.position
+(self.velocity*dt).map(|elem|elem.divide().clamp_1())
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().clamp_1())
+(self.velocity*dt).map(|elem|elem.divide().clamp_64())
+self.acceleration.map(|elem|(dt*dt*elem/2).divide().clamp_64())
}
pub fn extrapolated_velocity(&self,time:Time<T>)->Planar64Vec3{
let dt=time-self.time;
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().clamp_1())
self.velocity+(self.acceleration*dt).map(|elem|elem.divide().clamp_64())
}
pub fn advance_time(&mut self,time:Time<T>){
self.position=self.extrapolated_position(time);
self.velocity=self.extrapolated_velocity(time);
self.time=time;
pub fn extrapolated_body(&self,time:Time<T>)->Body<T>{
Body::new(
self.extrapolated_position(time),
self.extrapolated_velocity(time),
time,
)
}
pub fn extrapolated_position_ratio_dt<Num,Den,N1,D1,N2,N3,D2,N4,T1>(&self,dt:integer::Ratio<Num,Den>)->Planar64Vec3
where
@@ -101,14 +136,16 @@ impl<T> Body<T>
// a*dt + v
self.acceleration.map(|elem|(dt*elem).divide().clamp())+self.velocity
}
pub fn advance_time_ratio_dt(&mut self,dt:crate::model::GigaTime){
self.position=self.extrapolated_position_ratio_dt(dt);
self.velocity=self.extrapolated_velocity_ratio_dt(dt);
self.time+=dt.into();
pub fn extrapolated_body_ratio_dt(&self,dt:crate::model::GigaTime)->Body<T>{
Body::new(
self.extrapolated_position_ratio_dt(dt),
self.extrapolated_velocity_ratio_dt(dt),
self.time+dt.into(),
)
}
pub fn infinity_dir(&self)->Option<Planar64Vec3>{
if self.velocity==vec3::ZERO{
if self.acceleration==vec3::ZERO{
if self.velocity==vec3::zero(){
if self.acceleration==vec3::zero(){
None
}else{
Some(self.acceleration)
@@ -144,28 +181,12 @@ impl<T> Body<T>
}
impl<T> std::fmt::Display for Body<T>{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"p({}) v({}) t({})",self.position,self.velocity,self.time)
}
}
impl<T> std::fmt::Display for Trajectory<T>{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"p({}) v({}) a({}) t({})",self.position,self.velocity,self.acceleration,self.time)
}
}
pub struct VirtualBody<'a,T>{
body0:&'a Body<T>,
body1:&'a Body<T>,
}
impl<T> VirtualBody<'_,T>
where Time<T>:Copy,
{
pub fn extrapolated_position(&self,time:Time<T>)->Planar64Vec3{
self.body1.extrapolated_position(time)-self.body0.extrapolated_position(time)
}
pub fn extrapolated_velocity(&self,time:Time<T>)->Planar64Vec3{
self.body1.extrapolated_velocity(time)-self.body0.extrapolated_velocity(time)
}
pub fn acceleration(&self)->Planar64Vec3{
self.body1.acceleration-self.body0.acceleration
}
pub fn body(&self,time:Time<T>)->Body<T>{
Body::new(self.extrapolated_position(time),self.extrapolated_velocity(time),self.acceleration(),time)
}
}

View File

@@ -1,32 +1,29 @@
use crate::model::{into_giga_time,GigaTime,FEV,MeshQuery,DirectedEdge};
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
use crate::physics::{Time,Body};
use crate::model::{into_giga_time,GigaTime};
use strafesnet_common::integer::fixed_types::{F64_32,F128_64,F256_128};
use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::integer::{Ratio,Planar64Vec3};
use crate::physics::{Time,Trajectory};
use crate::mesh_query::{FEV,DirectedEdge,MeshQuery,MeshTopology};
use core::ops::Bound;
enum Transition<M:MeshQuery>{
enum Transition<M:MeshTopology>{
Miss,
Next(FEV<M>,GigaTime),
Hit(M::Face,GigaTime),
}
pub enum CrawlResult<M:MeshQuery>{
pub enum CrawlResult<M:MeshTopology>{
Miss(FEV<M>),
Hit(M::Face,GigaTime),
}
impl<M:MeshQuery> CrawlResult<M>{
impl<M:MeshTopology> CrawlResult<M>{
pub fn hit(self)->Option<(M::Face,GigaTime)>{
match self{
CrawlResult::Miss(_)=>None,
CrawlResult::Hit(face,time)=>Some((face,time)),
}
}
pub fn miss(self)->Option<FEV<M>>{
match self{
CrawlResult::Miss(fev)=>Some(fev),
CrawlResult::Hit(_,_)=>None,
}
}
}
// TODO: move predict_collision_face_out algorithm in here or something
@@ -70,17 +67,18 @@ where
}
}
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=F256_128,Position=Planar64Vec3,Direction=Planar64Vec3>> FEV<M>
where
// This is hardcoded for MinkowskiMesh lol
M::Face:Copy,
M::Edge:Copy,
M::DirectedEdge:Copy,
M::Vert:Copy,
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>,
<F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum,
M::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
F:core::ops::Mul<F64_32,Output=F256_128>,
<F as core::ops::Mul<F64_32>>::Output:core::iter::Sum,
M::Offset:core::ops::Sub<<F as std::ops::Mul<F64_32>>::Output>,
{
fn next_transition(&self,mesh:&M,body:&Body,lower_bound:Bound<GigaTime>,mut upper_bound:Bound<GigaTime>)->Transition<M>{
fn next_transition(&self,mesh:&M,trajectory:&Trajectory,lower_bound:Bound<GigaTime>,mut upper_bound:Bound<GigaTime>)->Transition<M>{
//conflicting derivative means it crosses in the wrong direction.
//if the transition time is equal to an already tested transition, do not replace the current best.
let mut best_transition=Transition::Miss;
@@ -92,29 +90,29 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
let (n,d)=mesh.face_nd(face_id);
//TODO: use higher precision d value?
//use the mesh transform translation instead of baking it into the d value.
for dt in Fixed::<4,128>::zeroes2((n.dot(body.position)-d)*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
for dt in F256_128::zeroes2((n.dot(trajectory.position)-d)*2,n.dot(trajectory.velocity)*2,n.dot(trajectory.acceleration)){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
upper_bound=Bound::Included(dt);
best_transition=Transition::Hit(face_id,dt);
break;
}
}
//test each edge collision time, ignoring roots with zero or conflicting derivative
for &directed_edge_id in mesh.face_edges(face_id).as_ref(){
mesh.for_each_face_edge(face_id,|directed_edge_id|{
let edge_n=mesh.directed_edge_n(directed_edge_id);
let n=n.cross(edge_n);
let &[v0,v1]=mesh.edge_verts(directed_edge_id.as_undirected()).as_ref();
//WARNING: d is moved out of the *2 block because of adding two vertices!
//WARNING: precision is swept under the rug!
//wrap for speed
for dt in Fixed::<4,128>::zeroes2(n.dot(body.position*2-(mesh.vert(v0)+mesh.vert(v1))).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
for dt in F256_128::zeroes2(n.dot(trajectory.position*2-(mesh.vert(v0)+mesh.vert(v1))).wrap_256(),n.dot(trajectory.velocity).wrap_256()*2,n.dot(trajectory.acceleration).wrap_256()){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break;
}
}
}
});
//if none:
},
&FEV::Edge(edge_id)=>{
@@ -123,15 +121,15 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
let &[ev0,ev1]=edge_verts.as_ref();
let (v0,v1)=(mesh.vert(ev0),mesh.vert(ev1));
let edge_n=v1-v0;
let delta_pos=body.position*2-(v0+v1);
let delta_pos=trajectory.position*2-(v0+v1);
for (i,&edge_face_id) in mesh.edge_faces(edge_id).as_ref().iter().enumerate(){
let face_n=mesh.face_nd(edge_face_id).0;
//edge_n gets parity from the order of edge_faces
let n=face_n.cross(edge_n)*((i as i64)*2-1);
//WARNING yada yada d *2
//wrap for speed
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
for dt in F256_128::zeroes2(n.dot(delta_pos).wrap_256(),n.dot(trajectory.velocity).wrap_256()*2,n.dot(trajectory.acceleration).wrap_256()){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Face(edge_face_id),dt);
break;
@@ -142,9 +140,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
for (i,&vert_id) in edge_verts.as_ref().iter().enumerate(){
//vertex normal gets parity from vert index
let n=edge_n*(1-2*(i as i64));
for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
for dt in F128_64::zeroes2((n.dot(trajectory.position-mesh.vert(vert_id)))*2,n.dot(trajectory.velocity)*2,n.dot(trajectory.acceleration)){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.widen_256(),dt.den.widen_256());
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Vert(vert_id),dt);
break;
@@ -155,28 +153,28 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
},
&FEV::Vert(vert_id)=>{
//test each edge collision time, ignoring roots with zero or conflicting derivative
for &directed_edge_id in mesh.vert_edges(vert_id).as_ref(){
mesh.for_each_vert_edge(vert_id,|directed_edge_id|{
//edge is directed away from vertex, but we want the dot product to turn out negative
let n=-mesh.directed_edge_n(directed_edge_id);
for dt in Fixed::<2,64>::zeroes2((n.dot(body.position-mesh.vert(vert_id)))*2,n.dot(body.velocity)*2,n.dot(body.acceleration)){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.widen_4(),dt.den.widen_4());
for dt in F128_64::zeroes2((n.dot(trajectory.position-mesh.vert(vert_id)))*2,n.dot(trajectory.velocity)*2,n.dot(trajectory.acceleration)){
if low(&lower_bound,&dt)&&upp(&dt,&upper_bound)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
let dt=Ratio::new(dt.num.widen_256(),dt.den.widen_256());
upper_bound=Bound::Included(dt);
best_transition=Transition::Next(FEV::Edge(directed_edge_id.as_undirected()),dt);
break;
}
}
}
});
//if none:
},
}
best_transition
}
pub fn crawl(mut self,mesh:&M,relative_body:&Body,lower_bound:Bound<&Time>,upper_bound:Bound<&Time>)->CrawlResult<M>{
let mut lower_bound=lower_bound.map(|&t|into_giga_time(t,relative_body.time));
let upper_bound=upper_bound.map(|&t|into_giga_time(t,relative_body.time));
pub fn crawl(mut self,mesh:&M,trajectory:&Trajectory,lower_bound:Bound<&Time>,upper_bound:Bound<&Time>)->CrawlResult<M>{
let mut lower_bound=lower_bound.map(|&t|into_giga_time(t,trajectory.time));
let upper_bound=upper_bound.map(|&t|into_giga_time(t,trajectory.time));
for _ in 0..20{
match self.next_transition(mesh,relative_body,lower_bound,upper_bound){
match self.next_transition(mesh,trajectory,lower_bound,upper_bound){
Transition::Miss=>return CrawlResult::Miss(self),
Transition::Next(next_fev,next_time)=>(self,lower_bound)=(next_fev,Bound::Included(next_time)),
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),

View File

@@ -1,7 +1,10 @@
mod body;
mod push_solve;
mod face_crawler;
mod mesh_query;
mod minkowski;
mod model;
mod push_solve;
mod minimum_difference;
pub mod physics;

View File

@@ -0,0 +1,57 @@
#[derive(Debug)]
pub enum FEV<M:MeshTopology>{
Vert(M::Vert),
Edge(M::Edge),
Face(M::Face),
}
pub trait UndirectedEdge{
type DirectedEdge:DirectedEdge<UndirectedEdge=Self>;
fn as_directed(self,parity:bool)->Self::DirectedEdge;
}
pub trait DirectedEdge{
type UndirectedEdge:UndirectedEdge<DirectedEdge=Self>;
fn as_undirected(self)->Self::UndirectedEdge;
fn parity(&self)->bool;
fn reverse(self)->Self
where
Self:Sized
{
let parity=!self.parity();
self.as_undirected().as_directed(parity)
}
}
pub trait MeshTopology{
type Face;
type Edge:UndirectedEdge<DirectedEdge=Self::DirectedEdge>;
type DirectedEdge:DirectedEdge<UndirectedEdge=Self::Edge>;
type Vert;
fn for_each_vert_edge(&self,vert_id:Self::Vert,f:impl FnMut(Self::DirectedEdge));
fn for_each_vert_face(&self,vert_id:Self::Vert,f:impl FnMut(Self::Face));
fn edge_faces(&self,edge_id:Self::Edge)->impl AsRef<[Self::Face;2]>;
fn edge_verts(&self,edge_id:Self::Edge)->impl AsRef<[Self::Vert;2]>;
#[expect(unused)]
fn for_each_face_vert(&self,face_id:Self::Face,f:impl FnMut(Self::Vert));
fn for_each_face_edge(&self,face_id:Self::Face,f:impl FnMut(Self::DirectedEdge));
}
// Make face_nd d value relative
// euclidean point?
// Simplex physics
// Directed edge necessary?
// recursive for_each function calls
// define faces from vertices (Fixed<2> vs Fixed<3>)
pub trait MeshQuery:MeshTopology{
type Position;
type Direction;
type Normal;
type Offset;
fn vert(&self,vert_id:Self::Vert)->Self::Position;
fn farthest_vert(&self,dir:Self::Direction)->Self::Vert;
/// This must return a point inside the mesh.
fn hint_point(&self)->Self::Position;
fn face_nd(&self,face_id:Self::Face)->(Self::Normal,Self::Offset);
fn edge_n(&self,edge_id:Self::Edge)->Self::Direction;
fn directed_edge_n(&self,directed_edge_id:Self::DirectedEdge)->Self::Direction;
}

View File

@@ -0,0 +1,924 @@
use strafesnet_common::integer::fixed_types::{F128_64,F192_96,F256_128};
use strafesnet_common::integer::vec3;
use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::integer::{Fixed,Planar64,Planar64Vec3};
use crate::mesh_query::{FEV,DirectedEdge,MeshQuery,MeshTopology};
// TODO: remove mesh invert
use crate::minkowski::{MinkowskiMesh,MinkowskiVert};
// This algorithm is based on Lua code
// written by Trey Reynolds in 2021
type Simplex<const N:usize,Vert>=[Vert;N];
#[derive(Clone,Copy)]
enum Simplex1_3<Vert>{
Simplex1(Simplex<1,Vert>),
Simplex2(Simplex<2,Vert>),
Simplex3(Simplex<3,Vert>),
}
impl<Vert> Simplex1_3<Vert>{
fn push_front(self,v:Vert)->Simplex2_4<Vert>{
match self{
Simplex1_3::Simplex1([v0])=>Simplex2_4::Simplex2([v,v0]),
Simplex1_3::Simplex2([v0,v1])=>Simplex2_4::Simplex3([v,v0,v1]),
Simplex1_3::Simplex3([v0,v1,v2])=>Simplex2_4::Simplex4([v,v0,v1,v2]),
}
}
}
#[derive(Clone,Copy)]
enum Simplex2_4<Vert>{
Simplex2(Simplex<2,Vert>),
Simplex3(Simplex<3,Vert>),
Simplex4(Simplex<4,Vert>),
}
/*
local function absDet(r, u, v, w)
if w then
return math.abs((u - r):Cross(v - r):Dot(w - r))
elseif v then
return (u - r):Cross(v - r).magnitude
elseif u then
return (u - r).magnitude
else
return 1
end
end
*/
impl<Vert> Simplex2_4<Vert>{
fn det_is_zero<M:MeshQuery<Vert=Vert,Position=Planar64Vec3>>(self,mesh:&M)->bool{
match self{
Self::Simplex4([p0,p1,p2,p3])=>{
let p0=mesh.vert(p0);
let p1=mesh.vert(p1);
let p2=mesh.vert(p2);
let p3=mesh.vert(p3);
(p1-p0).cross(p2-p0).dot(p3-p0)==Fixed::ZERO
},
Self::Simplex3([p0,p1,p2])=>{
let p0=mesh.vert(p0);
let p1=mesh.vert(p1);
let p2=mesh.vert(p2);
(p1-p0).cross(p2-p0)==vec3::zero()
},
Self::Simplex2([p0,p1])=>{
let p0=mesh.vert(p0);
let p1=mesh.vert(p1);
p1-p0==vec3::zero()
}
}
}
}
/*
local function choosePerpendicularDirection(d)
local x, y, z = d.x, d.y, d.z
local best = math.min(x*x, y*y, z*z)
if x*x == best then
return Vector3.new(y*y + z*z, -x*y, -x*z)
elseif y*y == best then
return Vector3.new(-x*y, x*x + z*z, -y*z)
else
return Vector3.new(-x*z, -y*z, x*x + y*y)
end
end
*/
fn choose_perpendicular_direction(d:Planar64Vec3)->Planar64Vec3{
let x=d.x.abs();
let y=d.y.abs();
let z=d.z.abs();
if x<y&&x<z{
Vector3::new([Fixed::ZERO,-d.z,d.y])
}else if y<z{
Vector3::new([d.z,Fixed::ZERO,-d.x])
}else{
Vector3::new([-d.y,d.x,Fixed::ZERO])
}
}
const fn choose_any_direction()->Planar64Vec3{
vec3::X
}
fn narrow_dir2(dir:Vector3<F128_64>)->Planar64Vec3{
if dir==vec3::zero(){
return dir.narrow_64().unwrap();
}
let x=dir.x.as_bits().unsigned_abs().bit_width();
let y=dir.y.as_bits().unsigned_abs().bit_width();
let z=dir.z.as_bits().unsigned_abs().bit_width();
let big=x.max(y).max(z);
const MAX_BITS:u32=64+31;
if MAX_BITS<big{
dir>>(big-MAX_BITS)
}else{
dir
}.narrow_64().unwrap()
}
fn narrow_dir3(dir:Vector3<F192_96>)->Planar64Vec3{
if dir==vec3::zero(){
return dir.narrow_64().unwrap();
}
let x=dir.x.as_bits().unsigned_abs().bit_width();
let y=dir.y.as_bits().unsigned_abs().bit_width();
let z=dir.z.as_bits().unsigned_abs().bit_width();
let big=x.max(y).max(z);
const MAX_BITS:u32=96+31;
if MAX_BITS<big{
dir>>(big-MAX_BITS)
}else{
dir
}.narrow_64().unwrap()
}
fn reduce1<M:MeshQuery<Position=Planar64Vec3>>(
[v0]:Simplex<1,M::Vert>,
mesh:&M,
point:Planar64Vec3,
)->Reduced<M::Vert>
where M::Vert:Copy,
{
// --debug.profilebegin("reduceSimplex0")
// local a = a1 - a0
let p0=mesh.vert(v0);
// local p = -a
let p=-(p0+point);
// local direction = p
let mut dir=p;
// if direction.magnitude == 0 then
// direction = chooseAnyDirection()
if dir==vec3::zero(){
dir=choose_any_direction();
}
// return direction, a0, a1
Reduced{
dir,
simplex:Simplex1_3::Simplex1([v0]),
}
}
// local function reduceSimplex1(a0, a1, b0, b1)
fn reduce2<M:MeshQuery<Position=Planar64Vec3>>(
[v0,v1]:Simplex<2,M::Vert>,
mesh:&M,
point:Planar64Vec3,
)->Reduced<M::Vert>
where
M::Vert:Copy
{
// --debug.profilebegin("reduceSimplex1")
// local a = a1 - a0
// local b = b1 - b0
let p0=mesh.vert(v0);
let p1=mesh.vert(v1);
// local p = -a
// local u = b - a
let p=-(p0+point);
let u=p1-p0;
// -- modify to take into account the radiuses
// local p_u = p:Dot(u)
let p_u=p.dot(u);
// if p_u >= 0 then
if !p_u.is_negative(){
// local direction = u:Cross(p):Cross(u)
let direction=u.cross(p).cross(u);
// if direction.magnitude == 0 then
if direction==vec3::zero(){
return Reduced{
dir:choose_perpendicular_direction(u),
simplex:Simplex1_3::Simplex2([v0,v1]),
};
}
// -- modify the direction to take into account a0R and b0R
// return direction, a0, a1, b0, b1
return Reduced{
dir:narrow_dir3(direction),
simplex:Simplex1_3::Simplex2([v0,v1]),
};
}
// local direction = p
let mut dir=p;
// if direction.magnitude == 0 then
if dir==vec3::zero(){
dir=choose_perpendicular_direction(u);
}
// return direction, a0, a1
Reduced{
dir,
simplex:Simplex1_3::Simplex1([v0]),
}
}
// local function reduceSimplex2(a0, a1, b0, b1, c0, c1)
fn reduce3<M:MeshQuery<Position=Planar64Vec3>>(
[v0,mut v1,v2]:Simplex<3,M::Vert>,
mesh:&M,
point:Planar64Vec3,
)->Reduced<M::Vert>
where
M::Vert:Copy
{
// --debug.profilebegin("reduceSimplex2")
// local a = a1 - a0
// local b = b1 - b0
// local c = c1 - c0
let p0=mesh.vert(v0);
let p1=mesh.vert(v1);
let p2=mesh.vert(v2);
// local p = -a
// local u = b - a
// local v = c - a
let p=-(p0+point);
let mut u=p1-p0;
let v=p2-p0;
// local uv = u:Cross(v)
// local up = u:Cross(p)
// local pv = p:Cross(v)
// local uv_up = uv:Dot(up)
// local uv_pv = uv:Dot(pv)
let mut uv=u.cross(v);
let mut up=u.cross(p);
let pv=p.cross(v);
let uv_up=uv.dot(up);
let uv_pv=uv.dot(pv);
// if uv_up >= 0 and uv_pv >= 0 then
if !uv_up.is_negative()&&!uv_pv.is_negative(){
// local uvp = uv:Dot(p)
let uvp=uv.dot(p);
// local direction = uvp < 0 and -uv or uv
let direction=if uvp.is_negative(){
-uv
}else{
uv
};
// return direction, a0, a1, b0, b1, c0, c1
return Reduced{
dir:narrow_dir2(direction),
simplex:Simplex1_3::Simplex3([v0,v1,v2]),
};
}
// local u_u = u:Dot(u)
// local v_v = v:Dot(v)
// local uDist = uv_up/(u_u*v.magnitude)
// local vDist = uv_pv/(v_v*u.magnitude)
// local minDist2 = math.min(uDist, vDist)
let u_dist=uv_up*v.length();
let v_dist=uv_pv*u.length();
// if vDist == minDist2 then
if v_dist<u_dist{
u=v;
up=-pv;
uv=-uv;
// b0 = c0
// b1 = c1
v1=v2;
}
// local p_u = p:Dot(u)
let p_u=p.dot(u);
// if p_u >= 0 then
if !p_u.is_negative(){
// local direction = up:Cross(u)
let direction=up.cross(u);
// if direction.magnitude == 0 then
if direction==vec3::zero(){
// direction = uv
return Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex2([v0,v1]),
};
}
// return direction, a0, a1, b0, b1
return Reduced{
dir:narrow_dir3(direction),
simplex:Simplex1_3::Simplex2([v0,v1]),
};
}
// local direction = p
let dir=p;
// if direction.magnitude == 0 then
if dir==vec3::zero(){
// direction = uv
return Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex1([v0]),
};
}
// return direction, a0, a0
Reduced{
dir,
simplex:Simplex1_3::Simplex1([v0]),
}
}
// local function reduceSimplex3(a0, a1, b0, b1, c0, c1, d0, d1)
fn reduce4<M:MeshQuery<Position=Planar64Vec3>>(
[v0,mut v1,mut v2,v3]:Simplex<4,M::Vert>,
mesh:&M,
point:Planar64Vec3,
)->Reduce<M::Vert>
where
M::Vert:Copy
{
// --debug.profilebegin("reduceSimplex3")
// local a = a1 - a0
// local b = b1 - b0
// local c = c1 - c0
// local d = d1 - d0
let p0=mesh.vert(v0);
let p1=mesh.vert(v1);
let p2=mesh.vert(v2);
let p3=mesh.vert(v3);
// local p = -a
// local u = b - a
// local v = c - a
// local w = d - a
let p=-(p0+point);
let mut u=p1-p0;
let mut v=p2-p0;
let w=p3-p0;
// local uv = u:Cross(v)
// local vw = v:Cross(w)
// local wu = w:Cross(u)
// local uvw = uv:Dot(w)
// local pvw = vw:Dot(p)
// local upw = wu:Dot(p)
// local uvp = uv:Dot(p)
let mut uv=u.cross(v);
let vw=v.cross(w);
let wu=w.cross(u);
let uv_w=uv.dot(w);
let pv_w=vw.dot(p);
let up_w=wu.dot(p);
let uv_p=uv.dot(p);
// if pvw/uvw >= 0 and upw/uvw >= 0 and uvp/uvw >= 0 then
if !pv_w.div_sign(uv_w).is_negative()
&&!up_w.div_sign(uv_w).is_negative()
&&!uv_p.div_sign(uv_w).is_negative(){
// origin is contained, this is a positive detection
// local direction = Vector3.new(0, 0, 0)
// return direction, a0, a1, b0, b1, c0, c1, d0, d1
return Reduce::Escape([v0,v1,v2,v3]);
}
// local uvwSign = uvw < 0 and -1 or uvw > 0 and 1 or 0
// local uvDist = uvp*uvwSign/uv.magnitude
// local vwDist = pvw*uvwSign/vw.magnitude
// local wuDist = upw*uvwSign/wu.magnitude
// local minDist3 = math.min(uvDist, vwDist, wuDist)
let uv_dist=uv_p.mul_sign(uv_w);
let vw_dist=pv_w.mul_sign(uv_w);
let wu_dist=up_w.mul_sign(uv_w);
let wu_len=wu.length();
let uv_len=uv.length();
let vw_len=vw.length();
if vw_dist*wu_len<wu_dist*vw_len{
// if vwDist == minDist3 then
if vw_dist*uv_len<uv_dist*vw_len{
(u,v)=(v,w);
uv=vw;
// uv_p=pv_w; // unused
// b0, c0 = c0, d0
// b1, c1 = c1, d1
(v1,v2)=(v2,v3);
}
}else{
// elseif wuDist == minDist3 then
if wu_dist*uv_len<uv_dist*wu_len{
(u,v)=(w,u);
uv=wu;
// uv_p=up_w; // unused
// b0, c0 = d0, b0
// b1, c1 = d1, b1
// before [a,b,c,d]
(v1,v2)=(v3,v1);
// after [a,d,b]
}
}
// local up = u:Cross(p)
// local pv = p:Cross(v)
// local uv_up = uv:Dot(up)
// local uv_pv = uv:Dot(pv)
let mut up=u.cross(p);
let pv=p.cross(v);
let uv_up=uv.dot(up);
let uv_pv=uv.dot(pv);
// if uv_up >= 0 and uv_pv >= 0 then
if !uv_up.is_negative()&&!uv_pv.is_negative(){
// local direction = uvw < 0 and uv or -uv
// return direction, a0, a1, b0, b1, c0, c1
let dir=if uv_w.is_negative(){
narrow_dir2(uv)
}else{
narrow_dir2(-uv)
};
return Reduce::Reduced(Reduced{
dir,
simplex:Simplex1_3::Simplex3([v0,v1,v2]),
});
}
// local u_u = u:Dot(u)
// local v_v = v:Dot(v)
// local uDist = uv_up/(u_u*v.magnitude)
// local vDist = uv_pv/(v_v*u.magnitude)
// local minDist2 = math.min(uDist, vDist)
let u_dist=uv_up*v.length();
let v_dist=uv_pv*u.length();
// if vDist == minDist2 then
if v_dist<u_dist{
u=v;
up=-pv;
uv=-uv;
// b0 = c0
// b1 = c1
v1=v2;
}
// local p_u = p:Dot(u)
let p_u=p.dot(u);
// if p_u >= 0 then
if !p_u.is_negative(){
// local direction = up:Cross(u)
let direction=up.cross(u);
// if direction.magnitude == 0 then
if direction==vec3::zero(){
// direction = uvw < 0 and uv or -uv
// return direction, a0, a1, b0, b1
let dir=if uv_w.is_negative(){
narrow_dir2(uv)
}else{
narrow_dir2(-uv)
};
return Reduce::Reduced(Reduced{
dir,
simplex:Simplex1_3::Simplex2([v0,v1]),
});
}
// return direction, a0, a1, b0, b1
return Reduce::Reduced(Reduced{
dir:narrow_dir3(direction),
simplex:Simplex1_3::Simplex2([v0,v1]),
});
}
// local direction = p
let dir=p;
// if direction.magnitude == 0 then
if dir==vec3::zero(){
// direction = uvw < 0 and uv or -uv
let dir=if uv_w.is_negative(){
narrow_dir2(uv)
}else{
narrow_dir2(-uv)
};
return Reduce::Reduced(Reduced{
dir,
simplex:Simplex1_3::Simplex1([v0]),
});
}
// return direction, a0, a1
Reduce::Reduced(Reduced{
dir,
simplex:Simplex1_3::Simplex1([v0]),
})
}
struct Reduced<Vert>{
dir:Planar64Vec3,
simplex:Simplex1_3<Vert>,
}
enum Reduce<Vert>{
Escape(Simplex<4,Vert>),
Reduced(Reduced<Vert>),
}
impl<Vert> Simplex2_4<Vert>{
fn reduce<M:MeshQuery<Vert=Vert,Position=Planar64Vec3>>(self,mesh:&M,point:Planar64Vec3)->Reduce<Vert>
where
M::Vert:Copy
{
match self{
Self::Simplex2(simplex)=>Reduce::Reduced(reduce2(simplex,mesh,point)),
Self::Simplex3(simplex)=>Reduce::Reduced(reduce3(simplex,mesh,point)),
Self::Simplex4(simplex)=>reduce4(simplex,mesh,point),
}
}
}
//infinity fev algorithm state transition
#[derive(Debug)]
enum Transition<Vert>{
Done,//found closest vert, no edges are better
Vert(Vert),//transition to vert
}
enum EV<M:MeshTopology>{
Vert(M::Vert),
Edge(M::Edge),
}
impl<M:MeshTopology> From<EV<M>> for FEV<M>{
fn from(value:EV<M>)->Self{
match value{
EV::Vert(minkowski_vert)=>FEV::Vert(minkowski_vert),
EV::Edge(minkowski_edge)=>FEV::Edge(minkowski_edge),
}
}
}
trait Contains{
fn contains(&self,point:Planar64Vec3)->bool;
}
// convenience type to check if a point is within some threshold of a plane.
struct ThickPlane{
point:Planar64Vec3,
normal:Vector3<F128_64>,
epsilon:F192_96,
}
impl ThickPlane{
fn new<M:MeshQuery<Position=Planar64Vec3>>(mesh:&M,[v0,v1,v2]:Simplex<3,M::Vert>)->Self{
let p0=mesh.vert(v0);
let p1=mesh.vert(v1);
let p2=mesh.vert(v2);
let point=p0;
let normal=(p1-p0).cross(p2-p0);
// Allow ~ 2*sqrt(3) units of thickness on the plane
// This is to account for the variance of two voxels across the longest diagonal
let epsilon=(normal.length()*(Planar64::EPSILON*3)).wrap_192();
Self{point,normal,epsilon}
}
}
impl Contains for ThickPlane{
fn contains(&self,point:Planar64Vec3)->bool{
(point-self.point).dot(self.normal).abs()<=self.epsilon
}
}
struct ThickLine{
point:Planar64Vec3,
dir:Planar64Vec3,
epsilon:F256_128,
}
impl ThickLine{
fn new<M:MeshQuery<Position=Planar64Vec3>>(mesh:&M,[v0,v1]:Simplex<2,M::Vert>)->Self{
let p0=mesh.vert(v0);
let p1=mesh.vert(v1);
let point=p0;
let dir=p1-p0;
// Allow ~ 2*sqrt(3) units of thickness on the plane
// This is to account for the variance of two voxels across the longest diagonal
let epsilon=(dir.length_squared()*(Planar64::EPSILON*3)).widen_256();
Self{point,dir,epsilon}
}
}
impl Contains for ThickLine{
fn contains(&self,point:Planar64Vec3)->bool{
(point-self.point).cross(self.dir).length_squared()<=self.epsilon
}
}
struct EVFinder<'a,M,C>{
mesh:&'a M,
constraint:C,
best_distance_squared:F128_64,
}
impl<M:MeshQuery<Position=Planar64Vec3>,C:Contains> EVFinder<'_,M,C>
where
M::Vert:Copy,
M::DirectedEdge:Copy,
{
fn next_transition_vert(&mut self,vert_id:M::Vert,point:Planar64Vec3)->Transition<M::Vert>{
let mut best_transition=Transition::Done;
self.mesh.for_each_vert_edge(vert_id,|directed_edge_id|{
//test if this edge's opposite vertex closer
let edge_verts=self.mesh.edge_verts(directed_edge_id.as_undirected());
//select opposite vertex
let test_vert_id=edge_verts.as_ref()[directed_edge_id.parity() as usize];
let test_pos=self.mesh.vert(test_vert_id);
let diff=point-test_pos;
let distance_squared=diff.dot(diff);
// ensure test_vert_id is coplanar to simplex
if distance_squared<self.best_distance_squared&&self.constraint.contains(test_pos){
best_transition=Transition::Vert(test_vert_id);
self.best_distance_squared=distance_squared;
}
});
best_transition
}
fn final_ev(&mut self,vert_id:M::Vert,point:Planar64Vec3)->EV<M>{
let mut best_transition=EV::Vert(vert_id);
let vert_pos=self.mesh.vert(vert_id);
let diff=point-vert_pos;
self.mesh.for_each_vert_edge(vert_id,|directed_edge_id|{
//test if this edge is closer
let edge_verts=self.mesh.edge_verts(directed_edge_id.as_undirected());
let test_vert_id=edge_verts.as_ref()[directed_edge_id.parity() as usize];
let test_pos=self.mesh.vert(test_vert_id);
let edge_n=test_pos-vert_pos;
let d=edge_n.dot(diff);
//test the edge
let edge_nn=edge_n.dot(edge_n);
// ensure edge contains closest point and directed_edge_id is coplanar to simplex
if !d.is_negative()&&d<=edge_nn&&self.constraint.contains(test_pos){
let distance_squared={
let c=diff.cross(edge_n);
//wrap for speed
(c.dot(c)/edge_nn).divide().wrap_128()
};
if distance_squared<=self.best_distance_squared{
best_transition=EV::Edge(directed_edge_id.as_undirected());
self.best_distance_squared=distance_squared;
}
}
});
best_transition
}
fn crawl_boundaries(&mut self,mut vert_id:M::Vert,point:Planar64Vec3)->EV<M>
where
M::Vert:Copy
{
loop{
match self.next_transition_vert(vert_id,point){
Transition::Done=>return self.final_ev(vert_id,point),
Transition::Vert(new_vert_id)=>vert_id=new_vert_id,
}
}
}
}
/// This function hops along parallel vertices until it finds the EV which contains the closest point to `point`.
fn crawl_to_closest_ev<M:MeshQuery<Position=Planar64Vec3>>(mesh:&M,simplex:Simplex<2,M::Vert>,point:Planar64Vec3)->EV<M>
where
M::Vert:Copy,
M::DirectedEdge:Copy,
{
// naively start at the closest vertex
// the closest vertex is not necessarily the one with the fewest boundary hops
// but it doesn't matter, we will get there regardless.
let (vert_id,best_distance_squared)=simplex.into_iter().map(|vert_id|{
let diff=point-mesh.vert(vert_id);
(vert_id,diff.dot(diff))
}).min_by_key(|&(_,d)|d).unwrap();
let constraint=ThickLine::new(mesh,simplex);
let mut finder=EVFinder{constraint,mesh,best_distance_squared};
//start on any vertex
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
//cross edge-face boundary if it's uncrossable
finder.crawl_boundaries(vert_id,point)
}
/// This function hops along connected vertices until it finds the FEV which contains the closest point to `point`.
fn crawl_to_closest_fev<'a>(mesh:&MinkowskiMesh<'a>,simplex:Simplex<3,MinkowskiVert>,point:Planar64Vec3)->FEV::<MinkowskiMesh<'a>>{
// naively start at the closest vertex
// the closest vertex is not necessarily the one with the fewest boundary hops
// but it doesn't matter, we will get there regardless.
let (vert_id,best_distance_squared)=simplex.into_iter().map(|vert_id|{
let diff=point-mesh.vert(vert_id);
(vert_id,diff.dot(diff))
}).min_by_key(|&(_,d)|d).unwrap();
let constraint=ThickPlane::new(mesh,simplex);
let mut finder=EVFinder{constraint,mesh,best_distance_squared};
//start on any vertex
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
//cross edge-face boundary if it's uncrossable
match finder.crawl_boundaries(vert_id,point){
//if a vert is returned, it is the closest point to the infinity point
EV::Vert(vert_id)=>FEV::Vert(vert_id),
EV::Edge(edge_id)=>{
//cross to face if we are on the wrong side
let edge_n=mesh.edge_n(edge_id);
// point is multiplied by two because vert_sum sums two vertices.
let delta_pos=point*2-{
let &[v0,v1]=mesh.edge_verts(edge_id).as_ref();
mesh.vert(v0)+mesh.vert(v1)
};
for (i,&face_id) in mesh.edge_faces(edge_id).as_ref().iter().enumerate(){
//test if this face is closer
let (face_n,d)=mesh.face_nd(face_id);
//if test point is behind face, the face is invalid
// TODO: find out why I thought of this backwards
if !(face_n.dot(point)-d).is_positive(){
continue;
}
//edge-face boundary nd, n facing out of the face towards the edge
let boundary_n=face_n.cross(edge_n)*(i as i64*2-1);
let boundary_d=boundary_n.dot(delta_pos);
//is test point behind edge, i.e. contained in the face
if !boundary_d.is_positive(){
//both faces cannot pass this condition, return early if one does.
return FEV::Face(face_id);
}
}
FEV::Edge(edge_id)
},
}
}
pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Option<FEV<MinkowskiMesh<'a>>>{
const ENABLE_FAST_FAIL:bool=false;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
// on_exact
|is_intersecting,simplex|{
if is_intersecting{
return None;
}
// Convert simplex to FEV
// Vertices must be inverted since the mesh is inverted
Some(match simplex{
Simplex1_3::Simplex1([v0])=>FEV::Vert(-v0),
Simplex1_3::Simplex2([v0,v1])=>{
// invert
let (v0,v1)=(-v0,-v1);
crawl_to_closest_ev(mesh,[v0,v1],point).into()
},
Simplex1_3::Simplex3([v0,v1,v2])=>{
// invert
let (v0,v1,v2)=(-v0,-v1,-v2);
// Shimmy to the side until you find a face that contains the closest point
// it's ALWAYS representable as a face, but this algorithm may
// return E or V in edge cases but I don't think that will break the face crawler
crawl_to_closest_fev(mesh,[v0,v1,v2],point)
},
})
},
// on_escape
|_simplex|{
// intersection is guaranteed at this point
// local norm, dist, u0, u1, v0, v1, w0, w1 = expand(queryP, queryQ, a0, a1, b0, b1, c0, c1, d0, d1, 1e-5)
// let simplex=refine_to_exact(mesh,simplex);
None
},
// fast_fail value is irrelevant and will never be returned!
||unreachable!()
)
}
pub fn contains_point(mesh:&MinkowskiMesh<'_>,point:Planar64Vec3)->bool{
const ENABLE_FAST_FAIL:bool=true;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
// on_exact
|is_intersecting,_simplex|{
is_intersecting
},
// on_escape
|_simplex|{
// intersection is guaranteed at this point
true
},
// fast_fail value
||false
)
}
// local function minimumDifference(
// queryP, radiusP,
// queryQ, radiusQ,
// exitRadius, testIntersection
// )
fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery<Position=Planar64Vec3,Direction=Planar64Vec3>>(
mesh:&M,
point:Planar64Vec3,
on_exact:impl FnOnce(bool,Simplex1_3<M::Vert>)->T,
on_escape:impl FnOnce(Simplex<4,M::Vert>)->T,
on_fast_fail:impl FnOnce()->T,
)->T
where
M::Vert:Copy
{
// local initialAxis = queryQ() - queryP()
// local new_point_p = queryP(initialAxis)
// local new_point_q = queryQ(-initialAxis)
// local direction, a0, a1, b0, b1, c0, c1, d0, d1
let mut initial_axis=mesh.hint_point()+point;
// degenerate case
if initial_axis==vec3::zero(){
initial_axis=choose_any_direction();
}
let last_point=mesh.farthest_vert(-initial_axis);
// this represents the 'a' value in the commented code
let mut last_pos=mesh.vert(last_point);
let Reduced{dir:mut direction,simplex:mut simplex_small}=reduce1([last_point],mesh,point);
// exitRadius = testIntersection and 0 or exitRadius or 1/0
// for _ = 1, 100 do
loop{
// new_point_p = queryP(-direction)
// new_point_q = queryQ(direction)
// local next_point = new_point_q - new_point_p
let next_point=mesh.farthest_vert(direction);
let next_pos=mesh.vert(next_point);
// if -direction:Dot(next_point) > (exitRadius + radiusP + radiusQ)*direction.magnitude then
if ENABLE_FAST_FAIL&&direction.dot(next_pos+point).is_negative(){
return on_fast_fail();
}
let simplex_big=simplex_small.push_front(next_point);
// if
// direction:Dot(next_point - a) <= 0 or
// absDet(next_point, a, b, c) < 1e-6
if !direction.dot(next_pos-last_pos).is_positive()
||simplex_big.det_is_zero(mesh){
// Found enough information to compute the exact closest point.
// local norm = direction.unit
// local dist = a:Dot(norm)
// local hits = -dist < radiusP + radiusQ
let is_intersecting=(last_pos+point).dot(direction).is_positive();
return on_exact(is_intersecting,simplex_small);
}
// direction, a0, a1, b0, b1, c0, c1, d0, d1 = reduceSimplex(new_point_p, new_point_q, a0, a1, b0, b1, c0, c1)
match simplex_big.reduce(mesh,point){
// if a and b and c and d then
Reduce::Escape(simplex)=>{
// Enough information to conclude that the meshes are intersecting.
// Topology information is computed if needed.
return on_escape(simplex);
},
Reduce::Reduced(reduced)=>{
direction=reduced.dir;
simplex_small=reduced.simplex;
},
}
// next loop this will be a
last_pos=next_pos;
}
}
#[cfg(test)]
mod test{
use super::*;
use crate::model::{PhysicsMesh,PhysicsMeshView};
fn mesh_contains_point(mesh:PhysicsMeshView<'_>,point:Planar64Vec3)->bool{
const ENABLE_FAST_FAIL:bool=true;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&mesh,point,
// on_exact
|is_intersecting,_simplex|{
is_intersecting
},
// on_escape
|_simplex|{
// intersection is guaranteed at this point
true
},
// fast_fail value
||false
)
}
#[test]
fn test_cube_points(){
let mesh=PhysicsMesh::unit_cube();
let mesh_view=mesh.complete_mesh_view();
for x in -2..=2{
for y in -2..=2{
for z in -2..=2{
let point=vec3::int(x,y,z)>>1;
assert!(mesh_contains_point(mesh_view,point),"Mesh did not contain point {point}");
}
}
}
}
}

View File

@@ -0,0 +1,409 @@
use core::ops::{Bound,RangeBounds};
use strafesnet_common::integer::fixed_types::{F192_96,F256_128,F512_256};
use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::integer::{Planar64Vec3,Ratio,Fixed};
use crate::model::into_giga_time;
use crate::model::{SubmeshVertId,SubmeshEdgeId,SubmeshDirectedEdgeId,SubmeshFaceId,TransformedMesh,GigaTime};
use crate::mesh_query::{MeshQuery,MeshTopology,DirectedEdge,UndirectedEdge};
use crate::physics::{Time,Trajectory};
struct AsRefHelper<T>(T);
impl<T> AsRef<T> for AsRefHelper<T>{
fn as_ref(&self)->&T{
&self.0
}
}
//Note that a face on a minkowski mesh refers to a pair of fevs on the meshes it's summed from
//(face,vertex)
//(edge,edge)
//(vertex,face)
#[derive(Clone,Copy,Debug,Eq,PartialEq)]
pub enum MinkowskiVert{
VertVert(SubmeshVertId,SubmeshVertId),
}
// TODO: remove this
impl core::ops::Neg for MinkowskiVert{
type Output=Self;
fn neg(self)->Self::Output{
match self{
MinkowskiVert::VertVert(v0,v1)=>MinkowskiVert::VertVert(v1,v0),
}
}
}
#[derive(Clone,Copy,Debug)]
pub enum MinkowskiEdge{
VertEdge(SubmeshVertId,SubmeshEdgeId),
EdgeVert(SubmeshEdgeId,SubmeshVertId),
//EdgeEdge when edges are parallel
}
impl UndirectedEdge for MinkowskiEdge{
type DirectedEdge=MinkowskiDirectedEdge;
fn as_directed(self,parity:bool)->Self::DirectedEdge{
match self{
MinkowskiEdge::VertEdge(v0,e1)=>MinkowskiDirectedEdge::VertEdge(v0,e1.as_directed(parity)),
MinkowskiEdge::EdgeVert(e0,v1)=>MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),v1),
}
}
}
#[derive(Clone,Copy,Debug)]
pub enum MinkowskiDirectedEdge{
VertEdge(SubmeshVertId,SubmeshDirectedEdgeId),
EdgeVert(SubmeshDirectedEdgeId,SubmeshVertId),
//EdgeEdge when edges are parallel
}
impl DirectedEdge for MinkowskiDirectedEdge{
type UndirectedEdge=MinkowskiEdge;
fn as_undirected(self)->Self::UndirectedEdge{
match self{
MinkowskiDirectedEdge::VertEdge(v0,e1)=>MinkowskiEdge::VertEdge(v0,e1.as_undirected()),
MinkowskiDirectedEdge::EdgeVert(e0,v1)=>MinkowskiEdge::EdgeVert(e0.as_undirected(),v1),
}
}
fn parity(&self)->bool{
match self{
MinkowskiDirectedEdge::VertEdge(_,e)
|MinkowskiDirectedEdge::EdgeVert(e,_)=>e.parity(),
}
}
}
#[derive(Clone,Copy,Debug,Hash)]
pub enum MinkowskiFace{
VertFace(SubmeshVertId,SubmeshFaceId),
EdgeEdge(SubmeshEdgeId,SubmeshEdgeId,bool),
FaceVert(SubmeshFaceId,SubmeshVertId),
//EdgeFace
//FaceEdge
//FaceFace
}
#[derive(Debug)]
pub struct MinkowskiMesh<'a>{
mesh0:TransformedMesh<'a>,
mesh1:TransformedMesh<'a>,
}
// TODO: remove this
impl<'a> core::ops::Neg for &MinkowskiMesh<'a>{
type Output=MinkowskiMesh<'a>;
fn neg(self)->Self::Output{
MinkowskiMesh::minkowski_sum(self.mesh1,self.mesh0)
}
}
impl MinkowskiMesh<'_>{
pub fn minkowski_sum<'a>(mesh0:TransformedMesh<'a>,mesh1:TransformedMesh<'a>)->MinkowskiMesh<'a>{
MinkowskiMesh{
mesh0,
mesh1,
}
}
pub fn predict_collision_in(&self,trajectory:&Trajectory,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
let start_position=match range.start_bound(){
Bound::Included(time)=>trajectory.extrapolated_position(*time),
Bound::Excluded(time)=>trajectory.extrapolated_position(*time),
Bound::Unbounded=>trajectory.position,
};
let fev=crate::minimum_difference::closest_fev_not_inside(self,start_position)?;
//continue forwards along the body parabola
fev.crawl(self,trajectory,range.start_bound(),range.end_bound()).hit()
}
pub fn predict_collision_out(&self,trajectory:&Trajectory,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
// TODO: handle unbounded collision using infinity fev
let start_position=match upper_bound{
Bound::Included(time)=>trajectory.extrapolated_position(*time),
Bound::Excluded(time)=>trajectory.extrapolated_position(*time),
Bound::Unbounded=>trajectory.position,
};
let fev=crate::minimum_difference::closest_fev_not_inside(self,start_position)?;
// swap and negate bounds to do a time inversion
let (lower_bound,upper_bound)=(upper_bound.map(|&t|-t),lower_bound.map(|&t|-t));
let time_reversed_trajectory=-trajectory;
//continue backwards along the body parabola
fev.crawl(self,&time_reversed_trajectory,lower_bound.as_ref(),upper_bound.as_ref()).hit()
//no need to test -time<time_limit because of the first step
.map(|(face,time)|(face,-time))
}
pub fn predict_collision_face_out(&self,trajectory:&Trajectory,range:impl RangeBounds<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiDirectedEdge,GigaTime)>{
// TODO: make better
use crate::face_crawler::{low,upp};
//no algorithm needed, there is only one state and two cases (Edge,None)
//determine when it passes an edge ("sliding off" case)
let start_time=range.start_bound().map(|&t|(t-trajectory.time).to_ratio());
let mut best_time=range.end_bound().map(|&t|into_giga_time(t,trajectory.time));
let mut best_edge=None;
let face_n=self.face_nd(contact_face_id).0;
self.for_each_face_edge(contact_face_id,|directed_edge_id|{
let edge_n=self.directed_edge_n(directed_edge_id);
//f x e points in
let n=face_n.cross(edge_n);
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
let d=n.dot(self.vert(v0)+self.vert(v1));
//WARNING! d outside of *2
//WARNING: truncated precision
//wrap for speed
for dt in F256_128::zeroes2(((n.dot(trajectory.position))*2-d).wrap_256(),n.dot(trajectory.velocity).wrap_256()*2,n.dot(trajectory.acceleration).wrap_256()){
if low(&start_time,&dt)&&upp(&dt,&best_time)&&n.dot(trajectory.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=Bound::Included(dt);
best_edge=Some((directed_edge_id,dt));
break;
}
}
});
best_edge
}
pub fn contains_point(&self,point:Planar64Vec3)->bool{
crate::minimum_difference::contains_point(self,point)
}
}
impl MeshQuery for MinkowskiMesh<'_>{
type Direction=Planar64Vec3;
type Position=Planar64Vec3;
type Normal=Vector3<F192_96>;
type Offset=F256_128;
// TODO: relative d
fn face_nd(&self,face_id:MinkowskiFace)->(Self::Normal,Self::Offset){
match face_id{
MinkowskiFace::VertFace(v0,f1)=>{
let (n,d)=self.mesh1.face_nd(f1);
(-n,d-n.dot(self.mesh0.vert(v0)))
},
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
let edge0_n=self.mesh0.edge_n(e0);
let edge1_n=self.mesh1.edge_n(e1);
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
let n=edge0_n.cross(edge1_n);
let e0d=n.dot(self.mesh0.vert(e0v0)+self.mesh0.vert(e0v1));
let e1d=n.dot(self.mesh1.vert(e1v0)+self.mesh1.vert(e1v1));
((n*(parity as i64*4-2)).widen_192(),((e0d-e1d)*(parity as i64*2-1)).widen_256())
},
MinkowskiFace::FaceVert(f0,v1)=>{
let (n,d)=self.mesh0.face_nd(f0);
(n,d-n.dot(self.mesh1.vert(v1)))
},
}
}
fn vert(&self,vert_id:MinkowskiVert)->Planar64Vec3{
match vert_id{
MinkowskiVert::VertVert(v0,v1)=>{
self.mesh0.vert(v0)-self.mesh1.vert(v1)
},
}
}
fn hint_point(&self)->Planar64Vec3{
self.mesh0.hint_point()-self.mesh1.hint_point()
}
fn farthest_vert(&self,dir:Planar64Vec3)->MinkowskiVert{
MinkowskiVert::VertVert(self.mesh0.farthest_vert(dir),self.mesh1.farthest_vert(-dir))
}
fn edge_n(&self,edge_id:Self::Edge)->Self::Direction{
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
self.vert(v1)-self.vert(v0)
}
fn directed_edge_n(&self,directed_edge_id:Self::DirectedEdge)->Self::Direction{
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
}
}
impl MeshTopology for MinkowskiMesh<'_>{
type Face=MinkowskiFace;
type Edge=MinkowskiEdge;
type DirectedEdge=MinkowskiDirectedEdge;
type Vert=MinkowskiVert;
fn for_each_vert_edge(&self,vert_id:Self::Vert,mut f:impl FnMut(Self::DirectedEdge)){
match vert_id{
MinkowskiVert::VertVert(v0,v1)=>{
//detect shared volume when the other mesh is mirrored along a test edge dir
let v0f={
let mut faces=Vec::new();
self.mesh0.for_each_vert_face(v0,|face|faces.push(face));
faces
};
let v1f={
let mut faces=Vec::new();
self.mesh1.for_each_vert_face(v1,|face|faces.push(face));
faces
};
let v0f_n:Vec<_>=v0f.iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
let v1f_n:Vec<_>=v1f.iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
// scratch vector
let mut face_normals=Vec::with_capacity(v0f.len()+v1f.len());
face_normals.clone_from(&v0f_n);
self.mesh0.for_each_vert_edge(v0,|directed_edge_id|{
let n=self.mesh0.directed_edge_n(directed_edge_id);
let nn=n.dot(n);
// TODO: there's gotta be a better way to do this
// drop faces beyond v0f_n
face_normals.truncate(v0f.len());
// make a set of faces from mesh0's perspective
for face_n in &v1f_n{
//add reflected mesh1 faces
//wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_192());
}
if is_empty_volume(&face_normals){
f(MinkowskiDirectedEdge::EdgeVert(directed_edge_id,v1));
}
});
face_normals.clone_from(&v1f_n);
self.mesh1.for_each_vert_edge(v1,|directed_edge_id|{
let n=self.mesh1.directed_edge_n(directed_edge_id);
let nn=n.dot(n);
// drop faces beyond v1f_n
face_normals.truncate(v1f.len());
// make a set of faces from mesh1's perspective
for face_n in &v0f_n{
//wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_192());
}
if is_empty_volume(&face_normals){
f(MinkowskiDirectedEdge::VertEdge(v0,directed_edge_id));
}
});
},
}
}
fn for_each_vert_face(&self,_vert_id:Self::Vert,_f:impl FnMut(Self::Face)){
unimplemented!()
}
fn edge_faces(&self,edge_id:Self::Edge)->impl AsRef<[Self::Face;2]>{
match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>{
//faces are listed backwards from the minkowski mesh
let v0e={
let mut edges=Vec::new();
self.mesh0.for_each_vert_edge(v0,|edge|edges.push(edge));
edges
};
let &[e1f0,e1f1]=self.mesh1.edge_faces(e1).as_ref();
AsRefHelper([(e1f1,false),(e1f0,true)].map(|(edge_face_id1,face_parity)|{
let mut best_edge=None;
let mut best_d:Ratio<F512_256,F512_256>=Ratio::new(Fixed::ZERO,Fixed::ONE);
let edge_face1_n=self.mesh1.face_nd(edge_face_id1).0;
let edge_face1_nn=edge_face1_n.dot(edge_face1_n);
for &directed_edge_id0 in &v0e{
let edge0_n=self.mesh0.directed_edge_n(directed_edge_id0);
//must be behind other face.
let d=edge_face1_n.dot(edge0_n);
if d.is_negative(){
let edge0_nn=edge0_n.dot(edge0_n);
// Assume not every number is huge
// TODO: revisit this
let dd=(d*d)/(edge_face1_nn*edge0_nn);
if best_d<dd{
best_d=dd;
best_edge=Some(directed_edge_id0);
}
}
}
best_edge.map_or(
MinkowskiFace::VertFace(v0,edge_face_id1),
|directed_edge_id0|MinkowskiFace::EdgeEdge(directed_edge_id0.as_undirected(),e1,directed_edge_id0.parity()^face_parity)
)
}))
},
MinkowskiEdge::EdgeVert(e0,v1)=>{
//tracking index with an external variable because .enumerate() is not available
let v1e={
let mut edges=Vec::new();
self.mesh1.for_each_vert_edge(v1,|edge|edges.push(edge));
edges
};
let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).as_ref();
AsRefHelper([(e0f0,true),(e0f1,false)].map(|(edge_face_id0,face_parity)|{
let mut best_edge=None;
let mut best_d:Ratio<F512_256,F512_256>=Ratio::new(Fixed::ZERO,Fixed::ONE);
let edge_face0_n=self.mesh0.face_nd(edge_face_id0).0;
let edge_face0_nn=edge_face0_n.dot(edge_face0_n);
for &directed_edge_id1 in &v1e{
let edge1_n=self.mesh1.directed_edge_n(directed_edge_id1);
let d=edge_face0_n.dot(edge1_n);
if d.is_negative(){
let edge1_nn=edge1_n.dot(edge1_n);
let dd=(d*d)/(edge_face0_nn*edge1_nn);
if best_d<dd{
best_d=dd;
best_edge=Some(directed_edge_id1);
}
}
}
best_edge.map_or(
MinkowskiFace::FaceVert(edge_face_id0,v1),
|directed_edge_id1|MinkowskiFace::EdgeEdge(e0,directed_edge_id1.as_undirected(),directed_edge_id1.parity()^face_parity)
)
}))
},
}
}
fn edge_verts(&self,edge_id:Self::Edge)->impl AsRef<[Self::Vert;2]>{
AsRefHelper(match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>self.mesh1.edge_verts(e1).as_ref().map(|vert_id1|
MinkowskiVert::VertVert(v0,vert_id1)
),
MinkowskiEdge::EdgeVert(e0,v1)=>self.mesh0.edge_verts(e0).as_ref().map(|vert_id0|
MinkowskiVert::VertVert(vert_id0,v1)
),
})
}
fn for_each_face_vert(&self,_face_id:Self::Face,_f:impl FnMut(Self::Vert)){
unimplemented!()
}
fn for_each_face_edge(&self,face_id:Self::Face,mut f:impl FnMut(Self::DirectedEdge)){
match face_id{
MinkowskiFace::VertFace(v0,f1)=>{
self.mesh1.for_each_face_edge(f1,|edge_id1|
f(MinkowskiDirectedEdge::VertEdge(v0,edge_id1.reverse()))
)
},
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
//could sort this if ordered edges are needed
//probably just need to reverse this list according to parity
f(MinkowskiDirectedEdge::VertEdge(e0v0,e1.as_directed(parity)));
f(MinkowskiDirectedEdge::EdgeVert(e0.as_directed(!parity),e1v0));
f(MinkowskiDirectedEdge::VertEdge(e0v1,e1.as_directed(!parity)));
f(MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),e1v1));
},
MinkowskiFace::FaceVert(f0,v1)=>{
self.mesh0.for_each_face_edge(f0,|edge_id0|
f(MinkowskiDirectedEdge::EdgeVert(edge_id0,v1))
)
},
}
}
}
fn is_empty_volume(normals:&[Vector3<F192_96>])->bool{
let len=normals.len();
for i in 0..len-1{
for j in i+1..len{
let n=normals[i].cross(normals[j]);
let mut d_comp=None;
for k in 0..len{
if k!=i&&k!=j{
let d=n.dot(normals[k]).is_negative();
if let &Some(comp)=&d_comp{
// This is testing if d_comp*d < 0
if comp^d{
return true;
}
}else{
d_comp=Some(d);
}
}
}
}
}
return false;
}
#[test]
fn test_is_empty_volume(){
use strafesnet_common::integer::vec3;
assert!(!is_empty_volume(&[vec3::X.widen_192(),vec3::Y.widen_192(),vec3::Z.widen_192()]));
assert!(is_empty_volume(&[vec3::X.widen_192(),vec3::Y.widen_192(),vec3::Z.widen_192(),vec3::NEG_X.widen_192()]));
}

View File

@@ -1,11 +1,10 @@
use std::collections::{HashSet,HashMap};
use core::ops::{Bound,RangeBounds};
use strafesnet_common::integer::fixed_types::{F128_64,F192_96,F256_128};
use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::model::{self,MeshId,PolygonIter};
use strafesnet_common::integer::{self,vec3,Fixed,Planar64,Planar64Vec3,Ratio};
use strafesnet_common::physics::Time;
type Body=crate::body::Body<strafesnet_common::physics::TimeInner>;
use crate::mesh_query::{MeshQuery,MeshTopology,DirectedEdge,UndirectedEdge};
struct AsRefHelper<T>(T);
impl<T> AsRef<T> for AsRefHelper<T>{
@@ -14,20 +13,6 @@ impl<T> AsRef<T> for AsRefHelper<T>{
}
}
pub trait UndirectedEdge{
type DirectedEdge:Copy+DirectedEdge;
fn as_directed(&self,parity:bool)->Self::DirectedEdge;
}
pub trait DirectedEdge{
type UndirectedEdge:Copy+std::fmt::Debug+UndirectedEdge;
fn as_undirected(&self)->Self::UndirectedEdge;
fn parity(&self)->bool;
//this is stupid but may work fine
fn reverse(&self)-><<Self as DirectedEdge>::UndirectedEdge as UndirectedEdge>::DirectedEdge{
self.as_undirected().as_directed(!self.parity())
}
}
#[derive(Debug,Clone,Copy,Hash,id::Id,Eq,PartialEq)]
pub struct MeshVertId(u32);
#[derive(Debug,Clone,Copy,Hash,id::Id,Eq,PartialEq)]
@@ -45,13 +30,13 @@ pub struct SubmeshFaceId(u32);
impl UndirectedEdge for SubmeshEdgeId{
type DirectedEdge=SubmeshDirectedEdgeId;
fn as_directed(&self,parity:bool)->SubmeshDirectedEdgeId{
fn as_directed(self,parity:bool)->SubmeshDirectedEdgeId{
SubmeshDirectedEdgeId(self.0|((parity as u32)<<(u32::BITS-1)))
}
}
impl DirectedEdge for SubmeshDirectedEdgeId{
type UndirectedEdge=SubmeshEdgeId;
fn as_undirected(&self)->SubmeshEdgeId{
fn as_undirected(self)->SubmeshEdgeId{
SubmeshEdgeId(self.0&!(1<<(u32::BITS-1)))
}
fn parity(&self)->bool{
@@ -59,14 +44,6 @@ impl DirectedEdge for SubmeshDirectedEdgeId{
}
}
//Vertex <-> Edge <-> Face -> Collide
#[derive(Debug)]
pub enum FEV<M:MeshQuery>{
Face(M::Face),
Edge(<M::Edge as DirectedEdge>::UndirectedEdge),
Vert(M::Vert),
}
//use Unit32 #[repr(C)] for map files
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
struct Face{
@@ -75,42 +52,23 @@ struct Face{
}
#[derive(Debug)]
struct Vert(Planar64Vec3);
pub trait MeshQuery{
type Face:Copy;
type Edge:Copy+DirectedEdge;
type Vert:Copy;
// Vertex must be Planar64Vec3 because it represents an actual position
type Normal;
type Offset;
fn edge_n(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->Planar64Vec3{
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
self.vert(v1)-self.vert(v0)
}
fn directed_edge_n(&self,directed_edge_id:Self::Edge)->Planar64Vec3{
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
}
fn vert(&self,vert_id:Self::Vert)->Planar64Vec3;
fn face_nd(&self,face_id:Self::Face)->(Self::Normal,Self::Offset);
fn face_edges(&self,face_id:Self::Face)->impl AsRef<[Self::Edge]>;
fn edge_faces(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->impl AsRef<[Self::Face;2]>;
fn edge_verts(&self,edge_id:<Self::Edge as DirectedEdge>::UndirectedEdge)->impl AsRef<[Self::Vert;2]>;
fn vert_edges(&self,vert_id:Self::Vert)->impl AsRef<[Self::Edge]>;
fn vert_faces(&self,vert_id:Self::Vert)->impl AsRef<[Self::Face]>;
}
#[derive(Debug)]
struct FaceRefs{
// I didn't write it down, but I assume the edges are directed
// clockwise when looking towards the face normal, i.e. right hand rule.
edges:Vec<SubmeshDirectedEdgeId>,
//verts are redundant, use edge[i].verts[0]
//verts:Vec<VertId>,
}
#[derive(Debug)]
struct EdgeRefs{
faces:[SubmeshFaceId;2],//left, right
verts:[SubmeshVertId;2],//bottom, top
verts:[SubmeshVertId;2],//start, end
}
#[derive(Debug)]
struct VertRefs{
faces:Vec<SubmeshFaceId>,
// edges are always directed away from the vert
edges:Vec<SubmeshDirectedEdgeId>,
}
#[derive(Debug)]
@@ -392,8 +350,8 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
}
//assume face hash is stable, and there are no flush faces...
let face=Face{
normal:(normal/len as i64).divide().narrow_1().unwrap(),
dot:(dot/(len*len) as i64).narrow_1().unwrap(),
normal:(normal/len as i64).divide().narrow_64().unwrap(),
dot:(dot/(len*len) as i64).narrow_64().unwrap(),
};
let face_id=*face_id_from_face.entry(face).or_insert_with(||{
let face_id=MeshFaceId::new(faces.len() as u32);
@@ -432,48 +390,80 @@ impl TryFrom<&model::Mesh> for PhysicsMesh{
}
}
#[derive(Debug)]
#[derive(Debug,Clone,Copy)]
pub struct PhysicsMeshView<'a>{
data:&'a PhysicsMeshData,
topology:&'a PhysicsMeshTopology,
}
impl MeshQuery for PhysicsMeshView<'_>{
type Face=SubmeshFaceId;
type Edge=SubmeshDirectedEdgeId;
type Vert=SubmeshVertId;
type Position=Planar64Vec3;
type Direction=Planar64Vec3;
type Normal=Planar64Vec3;
type Offset=Planar64;
fn face_nd(&self,face_id:SubmeshFaceId)->(Planar64Vec3,Planar64){
let face_idx=self.topology.faces[face_id.get() as usize].get() as usize;
(self.data.faces[face_idx].normal,self.data.faces[face_idx].dot)
}
fn hint_point(&self)->Planar64Vec3{
// invariant: meshes always encompass the origin
vec3::zero()
}
fn farthest_vert(&self,dir:Planar64Vec3)->SubmeshVertId{
//this happens to be well-defined. there are no virtual virtices
SubmeshVertId::new(
self.topology.verts.iter()
.enumerate()
.max_by_key(|&(_,&vert_id)|
dir.dot(self.data.verts[vert_id.get() as usize].0)
)
//assume there is more than zero vertices.
.unwrap().0 as u32
)
}
//ideally I never calculate the vertex position, but I have to for the graphical meshes...
fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{
let vert_idx=self.topology.verts[vert_id.get() as usize].get() as usize;
self.data.verts[vert_idx].0
}
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.topology.face_topology[face_id.get() as usize].edges.as_slice()
fn edge_n(&self,edge_id:Self::Edge)->Self::Direction{
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
self.vert(v1)-self.vert(v0)
}
fn edge_faces(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshFaceId;2]>{
fn directed_edge_n(&self,directed_edge_id:Self::DirectedEdge)->Self::Direction{
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
}
}
impl MeshTopology for PhysicsMeshView<'_>{
type Face=SubmeshFaceId;
type Edge=SubmeshEdgeId;
type DirectedEdge=SubmeshDirectedEdgeId;
type Vert=SubmeshVertId;
fn for_each_vert_edge(&self,vert_id:Self::Vert,f:impl FnMut(Self::DirectedEdge)){
self.topology.vert_topology[vert_id.get() as usize].edges.iter().copied().for_each(f);
}
fn for_each_vert_face(&self,vert_id:Self::Vert,f:impl FnMut(Self::Face)){
self.topology.vert_topology[vert_id.get() as usize].faces.iter().copied().for_each(f);
}
fn edge_faces(&self,edge_id:Self::Edge)->impl AsRef<[Self::Face;2]>{
AsRefHelper(self.topology.edge_topology[edge_id.get() as usize].faces)
}
fn edge_verts(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshVertId;2]>{
fn edge_verts(&self,edge_id:Self::Edge)->impl AsRef<[Self::Vert;2]>{
AsRefHelper(self.topology.edge_topology[edge_id.get() as usize].verts)
}
fn vert_edges(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.topology.vert_topology[vert_id.get() as usize].edges.as_slice()
fn for_each_face_vert(&self,_face_id:Self::Face,_f:impl FnMut(Self::Vert)){
unimplemented!()
}
fn vert_faces(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshFaceId]>{
self.topology.vert_topology[vert_id.get() as usize].faces.as_slice()
fn for_each_face_edge(&self,face_id:Self::Face,f:impl FnMut(Self::DirectedEdge)){
self.topology.face_topology[face_id.get() as usize].edges.iter().copied().for_each(f);
}
}
#[derive(Debug)]
pub struct PhysicsMeshTransform{
pub vertex:integer::Planar64Affine3,
pub normal:integer::mat3::Matrix3<Fixed<2,64>>,
pub det:Fixed<3,96>,
pub normal:integer::mat3::Matrix3<F128_64>,
pub det:F192_96,
}
impl PhysicsMeshTransform{
pub fn new(transform:integer::Planar64Affine3)->Self{
@@ -485,7 +475,7 @@ impl PhysicsMeshTransform{
}
}
#[derive(Debug)]
#[derive(Debug,Clone,Copy)]
pub struct TransformedMesh<'a>{
view:PhysicsMeshView<'a>,
transform:&'a PhysicsMeshTransform,
@@ -500,9 +490,28 @@ impl TransformedMesh<'_>{
transform,
}
}
pub fn verts<'a>(&'a self)->impl Iterator<Item=vec3::Vector3<Fixed<2,64>>>+'a{
pub fn verts<'a>(&'a self)->impl Iterator<Item=Vector3<F128_64>>+'a{
self.view.data.verts.iter().map(|&Vert(pos)|self.transform.vertex.transform_point3(pos))
}
}
impl MeshQuery for TransformedMesh<'_>{
type Direction=Planar64Vec3;
type Position=Planar64Vec3;
type Normal=Vector3<F192_96>;
type Offset=F256_128;
fn face_nd(&self,face_id:SubmeshFaceId)->(Self::Normal,Self::Offset){
let (n,d)=self.view.face_nd(face_id);
let transformed_n=self.transform.normal*n;
let transformed_d=d*self.transform.det+transformed_n.dot(self.transform.vertex.translation);
(transformed_n,transformed_d)
}
fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{
// wrap for speed
self.transform.vertex.transform_point3(self.view.vert(vert_id)).wrap_64()
}
fn hint_point(&self)->Planar64Vec3{
self.transform.vertex.translation
}
fn farthest_vert(&self,dir:Planar64Vec3)->SubmeshVertId{
//this happens to be well-defined. there are no virtual virtices
SubmeshVertId::new(
@@ -515,526 +524,47 @@ impl TransformedMesh<'_>{
.unwrap().0 as u32
)
}
fn edge_n(&self,edge_id:Self::Edge)->Self::Direction{
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
self.vert(v1)-self.vert(v0)
}
fn directed_edge_n(&self,directed_edge_id:Self::DirectedEdge)->Self::Direction{
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
(self.vert(v1)-self.vert(v0))*((directed_edge_id.parity() as i64)*2-1)
}
}
impl MeshQuery for TransformedMesh<'_>{
impl MeshTopology for TransformedMesh<'_>{
type Face=SubmeshFaceId;
type Edge=SubmeshDirectedEdgeId;
type Edge=SubmeshEdgeId;
type DirectedEdge=SubmeshDirectedEdgeId;
type Vert=SubmeshVertId;
type Normal=Vector3<Fixed<3,96>>;
type Offset=Fixed<4,128>;
fn face_nd(&self,face_id:SubmeshFaceId)->(Self::Normal,Self::Offset){
let (n,d)=self.view.face_nd(face_id);
let transformed_n=self.transform.normal*n;
let transformed_d=d*self.transform.det+transformed_n.dot(self.transform.vertex.translation);
(transformed_n,transformed_d)
}
fn vert(&self,vert_id:SubmeshVertId)->Planar64Vec3{
// wrap for speed
self.transform.vertex.transform_point3(self.view.vert(vert_id)).wrap_1()
#[inline]
fn for_each_vert_edge(&self,vert_id:Self::Vert,f:impl FnMut(Self::DirectedEdge)){
self.view.for_each_vert_edge(vert_id,f)
}
#[inline]
fn face_edges(&self,face_id:SubmeshFaceId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.view.face_edges(face_id)
fn for_each_vert_face(&self,vert_id:Self::Vert,f:impl FnMut(Self::Face)){
self.view.for_each_vert_face(vert_id,f)
}
#[inline]
fn edge_faces(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshFaceId;2]>{
fn edge_faces(&self,edge_id:Self::Edge)->impl AsRef<[Self::Face;2]>{
self.view.edge_faces(edge_id)
}
#[inline]
fn edge_verts(&self,edge_id:SubmeshEdgeId)->impl AsRef<[SubmeshVertId;2]>{
fn edge_verts(&self,edge_id:Self::Edge)->impl AsRef<[Self::Vert;2]>{
self.view.edge_verts(edge_id)
}
#[inline]
fn vert_edges(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshDirectedEdgeId]>{
self.view.vert_edges(vert_id)
fn for_each_face_vert(&self,_face_id:Self::Face,_f:impl FnMut(Self::Vert)){
unimplemented!()
}
#[inline]
fn vert_faces(&self,vert_id:SubmeshVertId)->impl AsRef<[SubmeshFaceId]>{
self.view.vert_faces(vert_id)
fn for_each_face_edge(&self,face_id:Self::Face,f:impl FnMut(Self::DirectedEdge)){
self.view.for_each_face_edge(face_id,f)
}
}
//Note that a face on a minkowski mesh refers to a pair of fevs on the meshes it's summed from
//(face,vertex)
//(edge,edge)
//(vertex,face)
#[derive(Clone,Copy,Debug)]
pub enum MinkowskiVert{
VertVert(SubmeshVertId,SubmeshVertId),
}
#[derive(Clone,Copy,Debug)]
pub enum MinkowskiEdge{
VertEdge(SubmeshVertId,SubmeshEdgeId),
EdgeVert(SubmeshEdgeId,SubmeshVertId),
//EdgeEdge when edges are parallel
}
impl UndirectedEdge for MinkowskiEdge{
type DirectedEdge=MinkowskiDirectedEdge;
fn as_directed(&self,parity:bool)->Self::DirectedEdge{
match self{
MinkowskiEdge::VertEdge(v0,e1)=>MinkowskiDirectedEdge::VertEdge(*v0,e1.as_directed(parity)),
MinkowskiEdge::EdgeVert(e0,v1)=>MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),*v1),
}
}
}
#[derive(Clone,Copy,Debug)]
pub enum MinkowskiDirectedEdge{
VertEdge(SubmeshVertId,SubmeshDirectedEdgeId),
EdgeVert(SubmeshDirectedEdgeId,SubmeshVertId),
//EdgeEdge when edges are parallel
}
impl DirectedEdge for MinkowskiDirectedEdge{
type UndirectedEdge=MinkowskiEdge;
fn as_undirected(&self)->Self::UndirectedEdge{
match self{
MinkowskiDirectedEdge::VertEdge(v0,e1)=>MinkowskiEdge::VertEdge(*v0,e1.as_undirected()),
MinkowskiDirectedEdge::EdgeVert(e0,v1)=>MinkowskiEdge::EdgeVert(e0.as_undirected(),*v1),
}
}
fn parity(&self)->bool{
match self{
MinkowskiDirectedEdge::VertEdge(_,e)
|MinkowskiDirectedEdge::EdgeVert(e,_)=>e.parity(),
}
}
}
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
pub enum MinkowskiFace{
VertFace(SubmeshVertId,SubmeshFaceId),
EdgeEdge(SubmeshEdgeId,SubmeshEdgeId,bool),
FaceVert(SubmeshFaceId,SubmeshVertId),
//EdgeFace
//FaceEdge
//FaceFace
}
#[derive(Debug)]
pub struct MinkowskiMesh<'a>{
mesh0:TransformedMesh<'a>,
mesh1:TransformedMesh<'a>,
}
//infinity fev algorithm state transition
#[derive(Debug)]
enum Transition{
Done,//found closest vert, no edges are better
Vert(MinkowskiVert),//transition to vert
}
enum EV{
Vert(MinkowskiVert),
Edge(MinkowskiEdge),
}
pub type GigaTime=Ratio<Fixed<4,128>,Fixed<4,128>>;
pub type GigaTime=Ratio<F256_128,F256_128>;
pub fn into_giga_time(time:Time,relative_to:Time)->GigaTime{
let r=(time-relative_to).to_ratio();
Ratio::new(r.num.widen_4(),r.den.widen_4())
}
impl MinkowskiMesh<'_>{
pub fn minkowski_sum<'a>(mesh0:TransformedMesh<'a>,mesh1:TransformedMesh<'a>)->MinkowskiMesh<'a>{
MinkowskiMesh{
mesh0,
mesh1,
}
}
fn farthest_vert(&self,dir:Planar64Vec3)->MinkowskiVert{
MinkowskiVert::VertVert(self.mesh0.farthest_vert(dir),self.mesh1.farthest_vert(-dir))
}
fn next_transition_vert(&self,vert_id:MinkowskiVert,best_distance_squared:&mut Fixed<2,64>,infinity_dir:Planar64Vec3,point:Planar64Vec3)->Transition{
let mut best_transition=Transition::Done;
for &directed_edge_id in self.vert_edges(vert_id).as_ref(){
let edge_n=self.directed_edge_n(directed_edge_id);
//is boundary uncrossable by a crawl from infinity
let edge_verts=self.edge_verts(directed_edge_id.as_undirected());
//select opposite vertex
let test_vert_id=edge_verts.as_ref()[directed_edge_id.parity() as usize];
//test if it's closer
let diff=point-self.vert(test_vert_id);
if edge_n.dot(infinity_dir).is_zero(){
let distance_squared=diff.dot(diff);
if distance_squared<*best_distance_squared{
best_transition=Transition::Vert(test_vert_id);
*best_distance_squared=distance_squared;
}
}
}
best_transition
}
fn final_ev(&self,vert_id:MinkowskiVert,best_distance_squared:&mut Fixed<2,64>,infinity_dir:Planar64Vec3,point:Planar64Vec3)->EV{
let mut best_transition=EV::Vert(vert_id);
let diff=point-self.vert(vert_id);
for &directed_edge_id in self.vert_edges(vert_id).as_ref(){
let edge_n=self.directed_edge_n(directed_edge_id);
//is boundary uncrossable by a crawl from infinity
//check if time of collision is outside Time::MIN..Time::MAX
if edge_n.dot(infinity_dir).is_zero(){
let d=edge_n.dot(diff);
//test the edge
let edge_nn=edge_n.dot(edge_n);
if !d.is_negative()&&d<=edge_nn{
let distance_squared={
let c=diff.cross(edge_n);
//wrap for speed
(c.dot(c)/edge_nn).divide().wrap_2()
};
if distance_squared<=*best_distance_squared{
best_transition=EV::Edge(directed_edge_id.as_undirected());
*best_distance_squared=distance_squared;
}
}
}
}
best_transition
}
fn crawl_boundaries(&self,mut vert_id:MinkowskiVert,infinity_dir:Planar64Vec3,point:Planar64Vec3)->EV{
let mut best_distance_squared={
let diff=point-self.vert(vert_id);
diff.dot(diff)
};
loop{
match self.next_transition_vert(vert_id,&mut best_distance_squared,infinity_dir,point){
Transition::Done=>return self.final_ev(vert_id,&mut best_distance_squared,infinity_dir,point),
Transition::Vert(new_vert_id)=>vert_id=new_vert_id,
}
}
}
/// This function drops a vertex down to an edge or a face if the path from infinity did not cross any vertex-edge boundaries but the point is supposed to have already crossed a boundary down from a vertex
fn infinity_fev(&self,infinity_dir:Planar64Vec3,point:Planar64Vec3)->FEV::<MinkowskiMesh<'_>>{
//start on any vertex
//cross uncrossable vertex-edge boundaries until you find the closest vertex or edge
//cross edge-face boundary if it's uncrossable
match self.crawl_boundaries(self.farthest_vert(infinity_dir),infinity_dir,point){
//if a vert is returned, it is the closest point to the infinity point
EV::Vert(vert_id)=>FEV::Vert(vert_id),
EV::Edge(edge_id)=>{
//cross to face if the boundary is not crossable and we are on the wrong side
let edge_n=self.edge_n(edge_id);
// point is multiplied by two because vert_sum sums two vertices.
let delta_pos=point*2-{
let &[v0,v1]=self.edge_verts(edge_id).as_ref();
self.vert(v0)+self.vert(v1)
};
for (i,&face_id) in self.edge_faces(edge_id).as_ref().iter().enumerate(){
let face_n=self.face_nd(face_id).0;
//edge-face boundary nd, n facing out of the face towards the edge
let boundary_n=face_n.cross(edge_n)*(i as i64*2-1);
let boundary_d=boundary_n.dot(delta_pos);
//check if time of collision is outside Time::MIN..Time::MAX
//infinity_dir can always be treated as a velocity
if !boundary_d.is_positive()&&boundary_n.dot(infinity_dir).is_zero(){
//both faces cannot pass this condition, return early if one does.
return FEV::Face(face_id);
}
}
FEV::Edge(edge_id)
},
}
}
// TODO: fundamentally improve this algorithm.
// All it needs to do is find the closest point on the mesh
// and return the FEV which the point resides on.
//
// What it actually does is use the above functions to trace a ray in from infinity,
// crawling the closest point along the mesh surface until the ray reaches
// the starting point to discover the final FEV.
//
// The actual collision prediction probably does a single test
// and then immediately returns with 0 FEV transitions on average,
// because of the strict time_limit constraint.
//
// Most of the calculation time is just calculating the starting point
// for the "actual" crawling algorithm below (predict_collision_{in|out}).
fn closest_fev_not_inside(&self,mut infinity_body:Body,start_time:Bound<&Time>)->Option<FEV<MinkowskiMesh<'_>>>{
infinity_body.infinity_dir().and_then(|dir|{
let infinity_fev=self.infinity_fev(-dir,infinity_body.position);
//a line is simpler to solve than a parabola
infinity_body.velocity=dir;
infinity_body.acceleration=vec3::ZERO;
//crawl in from negative infinity along a tangent line to get the closest fev
infinity_fev.crawl(self,&infinity_body,Bound::Unbounded,start_time).miss()
})
}
pub fn predict_collision_in(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
self.closest_fev_not_inside(*relative_body,range.start_bound()).and_then(|fev|{
//continue forwards along the body parabola
fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit()
})
}
pub fn predict_collision_out(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
// swap and negate bounds to do a time inversion
let (lower_bound,upper_bound)=(upper_bound.map(|&t|-t),lower_bound.map(|&t|-t));
let infinity_body=-relative_body;
self.closest_fev_not_inside(infinity_body,lower_bound.as_ref()).and_then(|fev|{
//continue backwards along the body parabola
fev.crawl(self,&infinity_body,lower_bound.as_ref(),upper_bound.as_ref()).hit()
//no need to test -time<time_limit because of the first step
.map(|(face,time)|(face,-time))
})
}
pub fn predict_collision_face_out(&self,relative_body:&Body,range:impl RangeBounds<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiDirectedEdge,GigaTime)>{
// TODO: make better
use crate::face_crawler::{low,upp};
//no algorithm needed, there is only one state and two cases (Edge,None)
//determine when it passes an edge ("sliding off" case)
let start_time=range.start_bound().map(|&t|{
let r=(t-relative_body.time).to_ratio();
Ratio::new(r.num,r.den)
});
let mut best_time=range.end_bound().map(|&t|into_giga_time(t,relative_body.time));
let mut best_edge=None;
let face_n=self.face_nd(contact_face_id).0;
for &directed_edge_id in self.face_edges(contact_face_id).as_ref(){
let edge_n=self.directed_edge_n(directed_edge_id);
//f x e points in
let n=face_n.cross(edge_n);
let &[v0,v1]=self.edge_verts(directed_edge_id.as_undirected()).as_ref();
let d=n.dot(self.vert(v0)+self.vert(v1));
//WARNING! d outside of *2
//WARNING: truncated precision
//wrap for speed
for dt in Fixed::<4,128>::zeroes2(((n.dot(relative_body.position))*2-d).wrap_4(),n.dot(relative_body.velocity).wrap_4()*2,n.dot(relative_body.acceleration).wrap_4()){
if low(&start_time,&dt)&&upp(&dt,&best_time)&&n.dot(relative_body.extrapolated_velocity_ratio_dt(dt)).is_negative(){
best_time=Bound::Included(dt);
best_edge=Some((directed_edge_id,dt));
break;
}
}
}
best_edge
}
fn infinity_in(&self,infinity_body:Body)->Option<(MinkowskiFace,GigaTime)>{
let infinity_fev=self.infinity_fev(-infinity_body.velocity,infinity_body.position);
// Bound::Included means that the surface of the mesh is included in the mesh
infinity_fev.crawl(self,&infinity_body,Bound::Unbounded,Bound::Included(&infinity_body.time)).hit()
}
pub fn is_point_in_mesh(&self,point:Planar64Vec3)->bool{
let infinity_body=Body::new(point,vec3::Y,vec3::ZERO,Time::ZERO);
//movement must escape the mesh forwards and backwards in time,
//otherwise the point is not inside the mesh
self.infinity_in(infinity_body)
.is_some_and(|_|
self.infinity_in(-infinity_body)
.is_some()
)
}
}
impl MeshQuery for MinkowskiMesh<'_>{
type Face=MinkowskiFace;
type Edge=MinkowskiDirectedEdge;
type Vert=MinkowskiVert;
type Normal=Vector3<Fixed<3,96>>;
type Offset=Fixed<4,128>;
// TODO: relative d
fn face_nd(&self,face_id:MinkowskiFace)->(Self::Normal,Self::Offset){
match face_id{
MinkowskiFace::VertFace(v0,f1)=>{
let (n,d)=self.mesh1.face_nd(f1);
(-n,d-n.dot(self.mesh0.vert(v0)))
},
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
let edge0_n=self.mesh0.edge_n(e0);
let edge1_n=self.mesh1.edge_n(e1);
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
let n=edge0_n.cross(edge1_n);
let e0d=n.dot(self.mesh0.vert(e0v0)+self.mesh0.vert(e0v1));
let e1d=n.dot(self.mesh1.vert(e1v0)+self.mesh1.vert(e1v1));
((n*(parity as i64*4-2)).widen_3(),((e0d-e1d)*(parity as i64*2-1)).widen_4())
},
MinkowskiFace::FaceVert(f0,v1)=>{
let (n,d)=self.mesh0.face_nd(f0);
(n,d-n.dot(self.mesh1.vert(v1)))
},
}
}
fn vert(&self,vert_id:MinkowskiVert)->Planar64Vec3{
match vert_id{
MinkowskiVert::VertVert(v0,v1)=>{
self.mesh0.vert(v0)-self.mesh1.vert(v1)
},
}
}
fn face_edges(&self,face_id:MinkowskiFace)->impl AsRef<[MinkowskiDirectedEdge]>{
match face_id{
MinkowskiFace::VertFace(v0,f1)=>{
self.mesh1.face_edges(f1).as_ref().iter().map(|&edge_id1|
MinkowskiDirectedEdge::VertEdge(v0,edge_id1.reverse())
).collect()
},
MinkowskiFace::EdgeEdge(e0,e1,parity)=>{
let &[e0v0,e0v1]=self.mesh0.edge_verts(e0).as_ref();
let &[e1v0,e1v1]=self.mesh1.edge_verts(e1).as_ref();
//could sort this if ordered edges are needed
//probably just need to reverse this list according to parity
vec![
MinkowskiDirectedEdge::VertEdge(e0v0,e1.as_directed(parity)),
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(!parity),e1v0),
MinkowskiDirectedEdge::VertEdge(e0v1,e1.as_directed(!parity)),
MinkowskiDirectedEdge::EdgeVert(e0.as_directed(parity),e1v1),
]
},
MinkowskiFace::FaceVert(f0,v1)=>{
self.mesh0.face_edges(f0).as_ref().iter().map(|&edge_id0|
MinkowskiDirectedEdge::EdgeVert(edge_id0,v1)
).collect()
},
}
}
fn edge_faces(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiFace;2]>{
match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>{
//faces are listed backwards from the minkowski mesh
let v0e=self.mesh0.vert_edges(v0);
let &[e1f0,e1f1]=self.mesh1.edge_faces(e1).as_ref();
AsRefHelper([(e1f1,false),(e1f0,true)].map(|(edge_face_id1,face_parity)|{
let mut best_edge=None;
let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE);
let edge_face1_n=self.mesh1.face_nd(edge_face_id1).0;
let edge_face1_nn=edge_face1_n.dot(edge_face1_n);
for &directed_edge_id0 in v0e.as_ref(){
let edge0_n=self.mesh0.directed_edge_n(directed_edge_id0);
//must be behind other face.
let d=edge_face1_n.dot(edge0_n);
if d.is_negative(){
let edge0_nn=edge0_n.dot(edge0_n);
// Assume not every number is huge
// TODO: revisit this
let dd=(d*d)/(edge_face1_nn*edge0_nn);
if best_d<dd{
best_d=dd;
best_edge=Some(directed_edge_id0);
}
}
}
best_edge.map_or(
MinkowskiFace::VertFace(v0,edge_face_id1),
|directed_edge_id0|MinkowskiFace::EdgeEdge(directed_edge_id0.as_undirected(),e1,directed_edge_id0.parity()^face_parity)
)
}))
},
MinkowskiEdge::EdgeVert(e0,v1)=>{
//tracking index with an external variable because .enumerate() is not available
let v1e=self.mesh1.vert_edges(v1);
let &[e0f0,e0f1]=self.mesh0.edge_faces(e0).as_ref();
AsRefHelper([(e0f0,true),(e0f1,false)].map(|(edge_face_id0,face_parity)|{
let mut best_edge=None;
let mut best_d:Ratio<Fixed<8,256>,Fixed<8,256>>=Ratio::new(Fixed::ZERO,Fixed::ONE);
let edge_face0_n=self.mesh0.face_nd(edge_face_id0).0;
let edge_face0_nn=edge_face0_n.dot(edge_face0_n);
for &directed_edge_id1 in v1e.as_ref(){
let edge1_n=self.mesh1.directed_edge_n(directed_edge_id1);
let d=edge_face0_n.dot(edge1_n);
if d.is_negative(){
let edge1_nn=edge1_n.dot(edge1_n);
let dd=(d*d)/(edge_face0_nn*edge1_nn);
if best_d<dd{
best_d=dd;
best_edge=Some(directed_edge_id1);
}
}
}
best_edge.map_or(
MinkowskiFace::FaceVert(edge_face_id0,v1),
|directed_edge_id1|MinkowskiFace::EdgeEdge(e0,directed_edge_id1.as_undirected(),directed_edge_id1.parity()^face_parity)
)
}))
},
}
}
fn edge_verts(&self,edge_id:MinkowskiEdge)->impl AsRef<[MinkowskiVert;2]>{
AsRefHelper(match edge_id{
MinkowskiEdge::VertEdge(v0,e1)=>self.mesh1.edge_verts(e1).as_ref().map(|vert_id1|
MinkowskiVert::VertVert(v0,vert_id1)
),
MinkowskiEdge::EdgeVert(e0,v1)=>self.mesh0.edge_verts(e0).as_ref().map(|vert_id0|
MinkowskiVert::VertVert(vert_id0,v1)
),
})
}
fn vert_edges(&self,vert_id:MinkowskiVert)->impl AsRef<[MinkowskiDirectedEdge]>{
match vert_id{
MinkowskiVert::VertVert(v0,v1)=>{
let mut edges=Vec::new();
//detect shared volume when the other mesh is mirrored along a test edge dir
let v0f_thing=self.mesh0.vert_faces(v0);
let v1f_thing=self.mesh1.vert_faces(v1);
let v0f=v0f_thing.as_ref();
let v1f=v1f_thing.as_ref();
let v0f_n:Vec<_>=v0f.iter().map(|&face_id|self.mesh0.face_nd(face_id).0).collect();
let v1f_n:Vec<_>=v1f.iter().map(|&face_id|self.mesh1.face_nd(face_id).0).collect();
// scratch vector
let mut face_normals=Vec::with_capacity(v0f.len()+v1f.len());
face_normals.clone_from(&v0f_n);
for &directed_edge_id in self.mesh0.vert_edges(v0).as_ref(){
let n=self.mesh0.directed_edge_n(directed_edge_id);
let nn=n.dot(n);
// TODO: there's gotta be a better way to do this
// drop faces beyond v0f_n
face_normals.truncate(v0f.len());
// make a set of faces from mesh0's perspective
for face_n in &v1f_n{
//add reflected mesh1 faces
//wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
}
if is_empty_volume(&face_normals){
edges.push(MinkowskiDirectedEdge::EdgeVert(directed_edge_id,v1));
}
}
face_normals.clone_from(&v1f_n);
for &directed_edge_id in self.mesh1.vert_edges(v1).as_ref(){
let n=self.mesh1.directed_edge_n(directed_edge_id);
let nn=n.dot(n);
// drop faces beyond v1f_n
face_normals.truncate(v1f.len());
// make a set of faces from mesh1's perspective
for face_n in &v0f_n{
//wrap for speed
face_normals.push(*face_n-(n*face_n.dot(n)*2/nn).divide().wrap_3());
}
if is_empty_volume(&face_normals){
edges.push(MinkowskiDirectedEdge::VertEdge(v0,directed_edge_id));
}
}
edges
},
}
}
fn vert_faces(&self,_vert_id:MinkowskiVert)->impl AsRef<[MinkowskiFace]>{
unimplemented!();
#[expect(unreachable_code)]
Vec::new()
}
}
fn is_empty_volume(normals:&[Vector3<Fixed<3,96>>])->bool{
let len=normals.len();
for i in 0..len-1{
for j in i+1..len{
let n=normals[i].cross(normals[j]);
let mut d_comp=None;
for k in 0..len{
if k!=i&&k!=j{
let d=n.dot(normals[k]).is_negative();
if let &Some(comp)=&d_comp{
// This is testing if d_comp*d < 0
if comp^d{
return true;
}
}else{
d_comp=Some(d);
}
}
}
}
}
return false;
}
#[test]
fn test_is_empty_volume(){
assert!(!is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3()]));
assert!(is_empty_volume(&[vec3::X.widen_3(),vec3::Y.widen_3(),vec3::Z.widen_3(),vec3::NEG_X.widen_3()]));
Ratio::new(r.num.widen_256(),r.den.widen_256())
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
use strafesnet_common::integer::fixed_types::{F64_32,F128_64,F192_96,F256_128,F320_160};
use strafesnet_common::integer::vec3::{self,Vector3};
use strafesnet_common::integer::{Fixed,Planar64Vec3,Ratio};
use strafesnet_common::ray::Ray;
@@ -12,9 +13,10 @@ use strafesnet_common::ray::Ray;
type Conts<'a>=arrayvec::ArrayVec<&'a Contact,4>;
// hack to allow comparing ratios to zero
const RATIO_ZERO:Ratio<Fixed<1,32>,Fixed<1,32>>=Ratio::new(Fixed::ZERO,Fixed::EPSILON);
const RATIO_ZERO:Ratio<F64_32,F64_32>=Ratio::new(Fixed::ZERO,Fixed::EPSILON);
/// Information about a contact restriction
#[derive(Debug,PartialEq)]
pub struct Contact{
pub position:Planar64Vec3,
pub velocity:Planar64Vec3,
@@ -28,42 +30,39 @@ impl Contact{
normal:self.normal,
}
}
fn relative_dot(&self,direction:Planar64Vec3)->Fixed<2,64>{
fn relative_dot(&self,direction:Planar64Vec3)->F128_64{
(direction-self.velocity).dot(self.normal)
}
/// Calculate the time of intersection. (previously get_touch_time)
fn solve(&self,ray:&Ray)->Ratio<Fixed<2,64>,Fixed<2,64>>{
fn solve(&self,ray:&Ray)->Ratio<F128_64,F128_64>{
(self.position-ray.origin).dot(self.normal)/(ray.direction-self.velocity).dot(self.normal)
}
}
//note that this is horrible with fixed point arithmetic
fn solve1(c0:&Contact)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<2,64>>>{
const EPSILON:Fixed<2,64>=Fixed::from_bits(Fixed::<2,64>::ONE.to_bits().shr(10));
fn solve1(c0:&Contact)->Option<Ratio<Vector3<F192_96>,F128_64>>{
let det=c0.normal.dot(c0.velocity);
if det.abs()<EPSILON{
if det.abs()==Fixed::ZERO{
return None;
}
let d0=c0.normal.dot(c0.position);
Some(c0.normal*d0/det)
}
fn solve2(c0:&Contact,c1:&Contact)->Option<Ratio<Vector3<Fixed<5,160>>,Fixed<4,128>>>{
const EPSILON:Fixed<4,128>=Fixed::from_bits(Fixed::<4,128>::ONE.to_bits().shr(10));
fn solve2(c0:&Contact,c1:&Contact)->Option<Ratio<Vector3<F320_160>,F256_128>>{
let u0_u1=c0.velocity.cross(c1.velocity);
let n0_n1=c0.normal.cross(c1.normal);
let det=u0_u1.dot(n0_n1);
if det.abs()<EPSILON{
if det.abs()==Fixed::ZERO{
return None;
}
let d0=c0.normal.dot(c0.position);
let d1=c1.normal.dot(c1.position);
Some((c1.normal.cross(u0_u1)*d0+u0_u1.cross(c0.normal)*d1)/det)
}
fn solve3(c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ratio<Vector3<Fixed<4,128>>,Fixed<3,96>>>{
const EPSILON:Fixed<3,96>=Fixed::from_bits(Fixed::<3,96>::ONE.to_bits().shr(10));
fn solve3(c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ratio<Vector3<F256_128>,F192_96>>{
let n0_n1=c0.normal.cross(c1.normal);
let det=c2.normal.dot(n0_n1);
if det.abs()<EPSILON{
if det.abs()==Fixed::ZERO{
return None;
}
let d0=c0.normal.dot(c0.position);
@@ -72,7 +71,7 @@ fn solve3(c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ratio<Vector3<Fixed<4,128
Some((c1.normal.cross(c2.normal)*d0+c2.normal.cross(c0.normal)*d1+c0.normal.cross(c1.normal)*d2)/det)
}
fn decompose1(point:Planar64Vec3,u0:Planar64Vec3)->Option<[Ratio<Fixed<2,64>,Fixed<2,64>>;1]>{
fn decompose1(point:Planar64Vec3,u0:Planar64Vec3)->Option<[Ratio<F128_64,F128_64>;1]>{
let det=u0.dot(u0);
if det==Fixed::ZERO{
return None;
@@ -80,7 +79,7 @@ fn decompose1(point:Planar64Vec3,u0:Planar64Vec3)->Option<[Ratio<Fixed<2,64>,Fix
let s0=u0.dot(point)/det;
Some([s0])
}
fn decompose2(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3)->Option<[Ratio<Fixed<4,128>,Fixed<4,128>>;2]>{
fn decompose2(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3)->Option<[Ratio<F256_128,F256_128>;2]>{
let u0_u1=u0.cross(u1);
let det=u0_u1.dot(u0_u1);
if det==Fixed::ZERO{
@@ -90,7 +89,7 @@ fn decompose2(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3)->Option<[Ratio
let s1=u0_u1.dot(u0.cross(point))/det;
Some([s0,s1])
}
fn decompose3(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3,u2:Planar64Vec3)->Option<[Ratio<Fixed<3,96>,Fixed<3,96>>;3]>{
fn decompose3(point:Planar64Vec3,u0:Planar64Vec3,u1:Planar64Vec3,u2:Planar64Vec3)->Option<[Ratio<F192_96,F192_96>;3]>{
let det=u0.cross(u1).dot(u2);
if det==Fixed::ZERO{
return None;
@@ -149,22 +148,22 @@ fn is_space_enclosed_4(
}
const fn get_push_ray_0(point:Planar64Vec3)->Ray{
Ray{origin:point,direction:vec3::ZERO}
Ray{origin:point,direction:vec3::zero()}
}
fn get_push_ray_1(point:Planar64Vec3,c0:&Contact)->Option<Ray>{
//wrap for speed
let direction=solve1(c0)?.divide().wrap_1();
let direction=solve1(c0)?.divide().wrap_64();
let [s0]=decompose1(direction,c0.velocity)?;
if s0.lt_ratio(RATIO_ZERO){
return None;
}
let origin=point+solve1(
&c0.relative_to(point),
)?.divide().wrap_1();
)?.divide().wrap_64();
Some(Ray{origin,direction})
}
fn get_push_ray_2(point:Planar64Vec3,c0:&Contact,c1:&Contact)->Option<Ray>{
let direction=solve2(c0,c1)?.divide().wrap_1();
let direction=solve2(c0,c1)?.divide().wrap_64();
let [s0,s1]=decompose2(direction,c0.velocity,c1.velocity)?;
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO){
return None;
@@ -172,11 +171,11 @@ fn get_push_ray_2(point:Planar64Vec3,c0:&Contact,c1:&Contact)->Option<Ray>{
let origin=point+solve2(
&c0.relative_to(point),
&c1.relative_to(point),
)?.divide().wrap_1();
)?.divide().wrap_64();
Some(Ray{origin,direction})
}
fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Option<Ray>{
let direction=solve3(c0,c1,c2)?.divide().wrap_1();
let direction=solve3(c0,c1,c2)?.divide().wrap_64();
let [s0,s1,s2]=decompose3(direction,c0.velocity,c1.velocity,c2.velocity)?;
if s0.lt_ratio(RATIO_ZERO)||s1.lt_ratio(RATIO_ZERO)||s2.lt_ratio(RATIO_ZERO){
return None;
@@ -185,7 +184,7 @@ fn get_push_ray_3(point:Planar64Vec3,c0:&Contact,c1:&Contact,c2:&Contact)->Optio
&c0.relative_to(point),
&c1.relative_to(point),
&c2.relative_to(point),
)?.divide().wrap_1();
)?.divide().wrap_64();
Some(Ray{origin,direction})
}
@@ -274,26 +273,26 @@ fn get_best_push_ray_and_conts<'a>(
}
}
fn get_first_touch<'a>(contacts:&'a [Contact],ray:&Ray,conts:&Conts)->Option<(Ratio<Fixed<2,64>,Fixed<2,64>>,&'a Contact)>{
fn get_first_touch<'a>(contacts:&'a [Contact],ray:&Ray,conts:&Conts)->Option<(Ratio<F128_64,F128_64>,&'a Contact)>{
contacts.iter()
.filter(|&contact|
!conts.iter().any(|&c|std::ptr::eq(c,contact))
!conts.iter().any(|&c|core::ptr::eq(c,contact))
&&contact.relative_dot(ray.direction).is_negative()
)
.map(|contact|(contact.solve(ray),contact))
.min_by_key(|&(t,_)|t)
}
pub fn push_solve(contacts:&[Contact],point:Planar64Vec3)->Planar64Vec3{
pub fn push_solve(contacts:&[Contact],point:Planar64Vec3)->(Planar64Vec3,Conts<'_>){
let (mut ray,mut conts)=get_best_push_ray_and_conts_0(point);
loop{
let (next_t,next_cont)=match get_first_touch(contacts,&ray,&conts){
Some((t,cont))=>(t,cont),
None=>return ray.origin,
None=>return (ray.origin,conts),
};
if RATIO_ZERO.le_ratio(next_t){
return ray.origin;
return (ray.origin,conts);
}
//push_front
@@ -309,7 +308,7 @@ pub fn push_solve(contacts:&[Contact],point:Planar64Vec3)->Planar64Vec3{
let meet_point=ray.extrapolate(next_t);
match get_best_push_ray_and_conts(meet_point,conts.as_slice()){
Some((new_ray,new_conts))=>(ray,conts)=(new_ray,new_conts),
None=>return meet_point,
None=>return (meet_point,conts),
}
}
}
@@ -321,14 +320,13 @@ mod tests{
fn test_push_solve(){
let contacts=vec![
Contact{
position:vec3::ZERO,
position:vec3::zero(),
velocity:vec3::Y,
normal:vec3::Y,
}
];
assert_eq!(
vec3::ZERO,
push_solve(&contacts,vec3::NEG_Y)
);
let (point,conts)=push_solve(&contacts,vec3::NEG_Y);
assert_eq!(point,vec3::zero());
assert_eq!(conts.as_slice(),[&contacts[0]].as_slice());
}
}

View File

@@ -4,9 +4,12 @@ version = "0.1.0"
edition = "2024"
[dependencies]
glam = "0.30.0"
glam.workspace = true
replace_with = "0.1.7"
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
strafesnet_physics = { path = "../physics", registry = "strafesnet" }
strafesnet_settings = { path = "../settings", registry = "strafesnet" }
strafesnet_snf = { path = "../../lib/snf", registry = "strafesnet" }
strafesnet_common.workspace = true
strafesnet_physics.workspace = true
strafesnet_settings.workspace = true
strafesnet_snf.workspace = true
[lints]
workspace = true

View File

@@ -31,7 +31,7 @@ pub enum SessionInputInstruction{
Mouse(glam::IVec2),
SetControl(strafesnet_common::physics::SetControlInstruction),
Mode(ImplicitModeInstruction),
Misc(strafesnet_common::physics::MiscInstruction),
Misc(MiscInstruction),
}
/// Implicit mode instruction are fed separately to session.
/// Session generates the explicit mode instructions interlaced with a SetSensitivity instruction
@@ -52,16 +52,23 @@ pub enum SessionControlInstruction{
pub enum SessionPlaybackInstruction{
SkipForward,
SkipBack,
TogglePaused,
DecreaseTimescale,
IncreaseTimescale,
}
pub struct FrameState{
pub body:physics::Body,
pub trajectory:physics::Trajectory,
pub camera:physics::PhysicsCamera,
pub time:PhysicsTime,
}
impl FrameState{
pub fn pos(&self)->glam::Vec3{
self.trajectory.extrapolated_position(self.time).map(Into::<f32>::into).to_array().into()
}
pub fn angles(&self)->glam::Vec2{
self.camera.simulate_move_angles(glam::IVec2::ZERO)
}
}
pub struct Simulation{
timer:Timer<Scaled<SessionTimeInner,PhysicsTimeInner>>,
@@ -77,9 +84,9 @@ impl Simulation{
physics,
}
}
pub fn get_frame_state(&self,time:SessionTime)->FrameState{
pub fn get_frame_state(&self,time:SessionTime,data:&PhysicsData)->FrameState{
FrameState{
body:self.physics.camera_body(),
trajectory:self.physics.camera_trajectory(data),
camera:self.physics.camera(),
time:self.timer.time(time),
}
@@ -152,10 +159,10 @@ enum ViewState{
pub struct Session{
directories:Directories,
user_settings:UserSettings,
mouse_interpolator:crate::mouse_interpolator::MouseInterpolator,
mouse_interpolator:MouseInterpolator,
view_state:ViewState,
//gui:GuiState
geometry_shared:physics::PhysicsData,
geometry_shared:PhysicsData,
simulation:Simulation,
// below fields not included in lite session
recording:Recording,
@@ -172,7 +179,7 @@ impl Session{
user_settings,
directories,
mouse_interpolator:MouseInterpolator::new(),
geometry_shared:Default::default(),
geometry_shared:PhysicsData::empty(),
simulation,
view_state:ViewState::Play,
recording:Default::default(),
@@ -184,13 +191,13 @@ impl Session{
}
fn change_map(&mut self,map:&strafesnet_common::map::CompleteMap){
self.simulation.physics.clear();
self.geometry_shared.generate_models(map);
self.geometry_shared=PhysicsData::new(map);
}
pub fn get_frame_state(&self,time:SessionTime)->Option<FrameState>{
match &self.view_state{
ViewState::Play=>Some(self.simulation.get_frame_state(time)),
ViewState::Play=>Some(self.simulation.get_frame_state(time,&self.geometry_shared)),
ViewState::Replay(bot_id)=>self.replays.get(bot_id).map(|replay|
replay.simulation.get_frame_state(time)
replay.simulation.get_frame_state(time,&self.geometry_shared)
),
}
}
@@ -239,12 +246,16 @@ impl InstructionConsumer<Instruction<'_>> for Session{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Restart(mode_id)));
// TODO: think about this harder. This works around a bug where you fall infinitely when you reset.
self.simulation.timer.set_time(ins.time,PhysicsTime::ZERO);
},
Instruction::Input(SessionInputInstruction::Mode(ImplicitModeInstruction::ResetAndSpawn(mode_id,spawn_id)))=>{
self.clear_recording();
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Reset));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(MiscInstruction::SetSensitivity(self.user_settings().calculate_sensitivity())));
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Mode(ModeInstruction::Spawn(mode_id,spawn_id)));
// TODO: think about this harder. This works around a bug where you fall infinitely when you reset.
self.simulation.timer.set_time(ins.time,PhysicsTime::ZERO);
},
Instruction::Input(SessionInputInstruction::Misc(misc_instruction))=>{
run_mouse_interpolator_instruction!(MouseInterpolatorInstruction::Misc(misc_instruction));
@@ -253,7 +264,14 @@ impl InstructionConsumer<Instruction<'_>> for Session{
// don't flush the buffered instructions in the mouse interpolator
// until the mouse is confirmed to be not moving at a later time
// what if they pause for 5ms lmao
_=self.simulation.timer.set_paused(ins.time,paused);
match &self.view_state{
ViewState::Play=>{
_=self.simulation.timer.set_paused(ins.time,paused);
},
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
_=replay.simulation.timer.set_paused(ins.time,paused);
},
}
},
Instruction::Control(SessionControlInstruction::CopyRecordingIntoReplayAndSpectate)=> if let ViewState::Play=self.view_state{
// Bind: B
@@ -374,14 +392,6 @@ impl InstructionConsumer<Instruction<'_>> for Session{
},
}
},
Instruction::Playback(SessionPlaybackInstruction::TogglePaused)=>{
match &self.view_state{
ViewState::Play=>(),
ViewState::Replay(bot_id)=>if let Some(replay)=self.replays.get_mut(bot_id){
_=replay.simulation.timer.set_paused(ins.time,!replay.simulation.timer.is_paused());
},
}
}
Instruction::ChangeMap(complete_map)=>{
self.clear_recording();
self.change_map(complete_map);

View File

@@ -6,5 +6,8 @@ edition = "2024"
[dependencies]
configparser = "3.0.2"
directories = "6.0.0"
glam = "0.30.0"
strafesnet_common = { path = "../../lib/common", registry = "strafesnet" }
glam.workspace = true
strafesnet_common.workspace = true
[lints]
workspace = true

View File

@@ -4,6 +4,12 @@ version = "0.1.0"
edition = "2024"
[dependencies]
strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet" }
glam.workspace = true
strafesnet_common.workspace = true
strafesnet_physics.workspace = true
strafesnet_snf.workspace = true
# this is just for the primitive constructor
strafesnet_rbx_loader.workspace = true
[lints]
workspace = true

View File

@@ -3,6 +3,8 @@ mod util;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod test_scenes;
use std::time::Instant;
@@ -29,9 +31,8 @@ fn run_replay()->Result<(),ReplayError>{
let bot=strafesnet_snf::read_bot(data)?.read_all()?;
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
let physics_data=PhysicsData::new(&map);
println!("simulating...");
let mut physics=PhysicsState::default();
for ins in bot.instructions{
@@ -70,16 +71,16 @@ fn segment_determinism(bot:strafesnet_snf::bot::Segment,physics_data:&PhysicsDat
for (i,ins) in bot.instructions.into_iter().enumerate(){
let state_deterministic=physics_deterministic.clone();
let state_filtered=physics_filtered.clone();
PhysicsContext::run_input_instruction(&mut physics_deterministic,&physics_data,ins.clone());
PhysicsContext::run_input_instruction(&mut physics_deterministic,physics_data,ins.clone());
match ins{
strafesnet_common::instruction::TimedInstruction{instruction:strafesnet_common::physics::Instruction::Idle,..}=>(),
other=>{
non_idle_count+=1;
// run
PhysicsContext::run_input_instruction(&mut physics_filtered,&physics_data,other.clone());
PhysicsContext::run_input_instruction(&mut physics_filtered,physics_data,other.clone());
// check if position matches
let b0=physics_deterministic.camera_body();
let b1=physics_filtered.camera_body();
let b0=physics_deterministic.camera_trajectory(physics_data);
let b1=physics_filtered.camera_trajectory(physics_data);
if b0.position!=b1.position{
let nanoseconds=start.elapsed().as_nanos() as u64;
println!("desync at instruction #{}",i);
@@ -139,9 +140,8 @@ fn test_determinism()->Result<(),ReplayError>{
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
let physics_data=PhysicsData::new(&map);
let (send,recv)=std::sync::mpsc::channel();

View File

@@ -0,0 +1,113 @@
use strafesnet_physics::physics::{InternalInstruction,PhysicsData,PhysicsState,PhysicsContext};
use strafesnet_common::gameplay_modes::NormalizedModes;
use strafesnet_common::gameplay_attributes::{CollisionAttributes,CollisionAttributesId};
use strafesnet_common::integer::{vec3,mat3,Planar64Affine3,Time};
use strafesnet_common::model::{Mesh,Model,MeshId,ModelId,RenderConfigId};
use strafesnet_common::map::CompleteMap;
use strafesnet_rbx_loader::primitives::{unit_cube,CubeFaceDescription};
struct TestSceneBuilder{
meshes:Vec<Mesh>,
models:Vec<Model>,
}
impl TestSceneBuilder{
fn new()->Self{
Self{
meshes:Vec::new(),
models:Vec::new(),
}
}
fn push_mesh(&mut self,mesh:Mesh)->MeshId{
let mesh_id=self.meshes.len();
self.meshes.push(mesh);
MeshId::new(mesh_id as u32)
}
fn push_mesh_instance(&mut self,mesh:MeshId,transform:Planar64Affine3)->ModelId{
let model=Model{
mesh,
attributes:CollisionAttributesId::new(0),
color:glam::Vec4::ONE,
transform,
};
let model_id=self.models.len();
self.models.push(model);
ModelId::new(model_id as u32)
}
fn build(self)->PhysicsData{
let modes=NormalizedModes::new(Vec::new());
let attributes=vec![CollisionAttributes::contact_default()];
let meshes=self.meshes;
let models=self.models;
let textures=Vec::new();
let render_configs=Vec::new();
PhysicsData::new(&CompleteMap{
modes,
attributes,
meshes,
models,
textures,
render_configs,
})
}
}
fn test_scene()->PhysicsData{
let mut builder=TestSceneBuilder::new();
let cube_face_description=CubeFaceDescription::new(Default::default(),RenderConfigId::new(0));
let mesh=builder.push_mesh(unit_cube(cube_face_description));
// place two 5x5x5 cubes.
builder.push_mesh_instance(mesh,Planar64Affine3::new(
mat3::from_diagonal(vec3::int(5,5,5)>>1),
vec3::int(0,0,0)
));
builder.push_mesh_instance(mesh,Planar64Affine3::new(
mat3::from_diagonal(vec3::int(5,5,5)>>1),
vec3::int(5,-5,0)
));
builder.build()
}
#[test]
fn simultaneous_collision(){
let physics_data=test_scene();
let body=strafesnet_physics::physics::Body::new(
(vec3::int(5+2,0,0)>>1)+vec3::int(1,1,0),
vec3::int(-1,-1,0),
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
physics.style_mut().gravity=vec3::zero();
let mut phys_iter=PhysicsContext::iter_internal(&mut physics,&physics_data,Time::from_secs(2))
.filter(|ins|!matches!(ins.instruction,InternalInstruction::StrafeTick));
// the order that they hit does matter, but we aren't currently worrying about that.
// See multi-collision branch
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(1));
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(1));
assert!(phys_iter.next().is_none());
let body=physics.body();
assert_eq!(body.position,vec3::int(5,0,0));
assert_eq!(body.velocity,vec3::int(0,0,0));
assert_eq!(body.time,Time::from_secs(1));
}
#[test]
fn bug_3(){
let physics_data=test_scene();
let body=strafesnet_physics::physics::Body::new(
(vec3::int(5+2,0,0)>>1)+vec3::int(1,2,0),
vec3::int(-1,-1,0),
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
physics.style_mut().gravity=vec3::zero();
let mut phys_iter=PhysicsContext::iter_internal(&mut physics,&physics_data,Time::from_secs(3))
.filter(|ins|!matches!(ins.instruction,InternalInstruction::StrafeTick));
// touch side of part at 0,0,0
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(1));
// touch top of part at 5,-5,0
assert_eq!(phys_iter.next().unwrap().time,Time::from_secs(2));
assert!(phys_iter.next().is_none());
let body=physics.body();
assert_eq!(body.position,vec3::int(5+2,0,0)>>1);
assert_eq!(body.velocity,vec3::int(0,0,0));
assert_eq!(body.time,Time::from_secs(2));
}

View File

@@ -10,9 +10,8 @@ fn physics_bug_2()->Result<(),ReplayError>{
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
let physics_data=PhysicsData::new(&map);
println!("simulating...");
//teleport to bug
@@ -23,14 +22,13 @@ fn physics_bug_2()->Result<(),ReplayError>{
let body=strafesnet_physics::physics::Body::new(
vec3::raw_xyz(555690659654,1490485868773,1277783839382),
vec3::int(0,0,0),
vec3::int(0,-100,0),
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
// wait one second to activate the bug
// hit=Some(ModelId(2262))
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:strafesnet_common::integer::Time::from_millis(500),
time:Time::from_millis(500),
instruction:strafesnet_common::physics::Instruction::Idle,
});
@@ -45,9 +43,8 @@ fn physics_bug_3()->Result<(),ReplayError>{
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
let physics_data=PhysicsData::new(&map);
println!("simulating...");
//teleport to bug
@@ -62,15 +59,18 @@ fn physics_bug_3()->Result<(),ReplayError>{
// vec3::raw_xyz(0,-96915585363,1265),
// vec3::raw_xyz(0,-429496729600,0),
// corner setup before wall hits
vec3::raw_xyz(-1392580080675,3325402529458,-2444727738679),
vec3::raw_xyz(-30259028820,-22950929553,-71141663007),
vec3::raw_xyz(0,-429496729600,0),
// vec3::raw_xyz(-1392580080675,3325402529458,-2444727738679),
// vec3::raw_xyz(-30259028820,-22950929553,-71141663007),
// vec3::raw_xyz(0,-429496729600,0),
// Actual bug 3 repro
vec3::raw_xyz(-2505538624455,3357963283914,557275711118),
vec3::raw_xyz(204188283920,-282280474198,166172785440),
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
// wait one second to activate the bug
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:strafesnet_common::integer::Time::from_millis(500),
time:Time::from_millis(500),
instruction:strafesnet_common::physics::Instruction::Idle,
});

View File

@@ -1,6 +1,6 @@
[package]
name = "strafesnet_bsp_loader"
version = "0.3.1"
version = "0.5.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -10,10 +10,13 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
glam = "0.30.0"
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.1", path = "../deferred_loader", registry = "strafesnet" }
glam.workspace = true
strafesnet_common.workspace = true
strafesnet_deferred_loader.workspace = true
vbsp = "0.9.1"
vbsp-entities-css = "0.6.0"
vmdl = "0.2.0"
vpk = "0.3.0"
[lints]
workspace = true

View File

@@ -1,21 +1,22 @@
use strafesnet_common::integer::Planar64;
use strafesnet_common::{model,integer};
use strafesnet_common::integer::{vec3::Vector3,Fixed,Ratio};
use strafesnet_common::integer::fixed_types::F192_96;
use strafesnet_common::integer::vec3::Vector3;
use strafesnet_common::integer::{Planar64,Planar64Vec3,Ratio};
use crate::{valve_transform_normal,valve_transform_dist};
#[derive(Hash,Eq,PartialEq)]
struct Face{
normal:integer::Planar64Vec3,
dot:integer::Planar64,
normal:Planar64Vec3,
dot:Planar64,
}
#[derive(Debug)]
struct Faces{
faces:Vec<Vec<integer::Planar64Vec3>>,
faces:Vec<Vec<Planar64Vec3>>,
}
fn solve3(c0:&Face,c1:&Face,c2:&Face)->Option<Ratio<Vector3<Fixed<3,96>>,Fixed<3,96>>>{
fn solve3(c0:&Face,c1:&Face,c2:&Face)->Option<Ratio<Vector3<F192_96>,F192_96>>{
let n0_n1=c0.normal.cross(c1.normal);
let det=c2.normal.dot(n0_n1);
if det.abs().is_zero(){
@@ -82,12 +83,12 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
// test if any *other* faces occlude the intersection
for new_face in &face_list{
// new face occludes intersection point
if (new_face.dot.widen_2()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
if (new_face.dot.widen_128()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
// replace one of the faces with the new face
// dont' try to replace face0 because we are exploring that face in particular
if let Some(new_intersection)=solve3(face0,new_face,face2){
// face1 does not occlude (or intersect) the new intersection
if (face1.dot.widen_2()/Planar64::ONE).gt_ratio(face1.normal.dot(new_intersection.num)/new_intersection.den){
if (face1.dot.widen_128()/Planar64::ONE).gt_ratio(face1.normal.dot(new_intersection.num)/new_intersection.den){
face1=new_face;
intersection=new_intersection;
continue 'find;
@@ -95,7 +96,7 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
}
if let Some(new_intersection)=solve3(face0,face1,new_face){
// face2 does not occlude (or intersect) the new intersection
if (face2.dot.widen_2()/Planar64::ONE).gt_ratio(face2.normal.dot(new_intersection.num)/new_intersection.den){
if (face2.dot.widen_128()/Planar64::ONE).gt_ratio(face2.normal.dot(new_intersection.num)/new_intersection.den){
face2=new_face;
intersection=new_intersection;
continue 'find;
@@ -120,7 +121,7 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
continue;
}
// new_face occludes intersection meaning intersection is not on convex solid and face0 is degenrate
if (new_face.dot.widen_2()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
if (new_face.dot.widen_128()/Planar64::ONE).lt_ratio(new_face.normal.dot(intersection.num)/intersection.den){
// abort! reject face0 entirely
continue 'face;
}
@@ -138,7 +139,7 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
loop{
// push point onto vertices
// problem: this may push a vertex that does not fit in the fixed point range and is thus meaningless
face.push(intersection.divide().narrow_1().unwrap());
face.push(intersection.divide().narrow_64().unwrap());
// we looped back around to face1, we're done!
if core::ptr::eq(face1,face2){
@@ -204,13 +205,13 @@ impl std::fmt::Display for BrushToMeshError{
}
impl core::error::Error for BrushToMeshError{}
pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
pub fn faces_to_mesh(faces:Vec<Vec<Planar64Vec3>>)->model::Mesh{
// generate the mesh
let mut mb=model::MeshBuilder::new();
let color=mb.acquire_color_id(glam::Vec4::ONE);
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
// normals are ignored by physics
let normal=mb.acquire_normal_id(integer::vec3::ZERO);
let normal=mb.acquire_normal_id(integer::vec3::zero());
let polygon_list=faces.into_iter().map(|face|{
face.into_iter().map(|pos|{

View File

@@ -105,7 +105,7 @@ pub fn convert<'a>(
water:Some(attr::IntersectingWater{
viscosity:integer::Planar64::ONE,
density:integer::Planar64::ONE,
velocity:integer::vec3::ZERO,
velocity:integer::vec3::zero(),
}),
},
general:attr::GeneralAttributes::default(),
@@ -295,7 +295,7 @@ pub fn convert<'a>(
attributes,
transform:integer::Planar64Affine3::new(
integer::mat3::identity(),
integer::vec3::ZERO,
integer::vec3::zero(),
),
color:glam::Vec4::ONE,
});
@@ -347,7 +347,7 @@ pub struct PartialMap1{
modes:NormalizedModes,
}
impl PartialMap1{
pub fn add_prop_meshes<'a>(
pub fn add_prop_meshes(
self,
prop_meshes:Meshes<model::Mesh>,
)->PartialMap2{

View File

@@ -1,9 +1,6 @@
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
mod bsp;
mod mesh;
pub mod bsp;
pub mod mesh;
mod brush;
pub mod loader;
const VALVE_SCALE:f32=1.0/16.0;
pub(crate) fn valve_transform_dist(d:f32)->strafesnet_common::integer::Planar64{
@@ -28,28 +25,6 @@ impl std::fmt::Display for ReadError{
}
impl std::error::Error for ReadError{}
#[derive(Debug)]
pub enum LoadError{
Texture(loader::TextureError),
Mesh(loader::MeshError),
}
impl std::fmt::Display for LoadError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for LoadError{}
impl From<loader::TextureError> for LoadError{
fn from(value:loader::TextureError)->Self{
Self::Texture(value)
}
}
impl From<loader::MeshError> for LoadError{
fn from(value:loader::MeshError)->Self{
Self::Mesh(value)
}
}
pub struct Bsp{
bsp:vbsp::Bsp,
case_folded_file_names:std::collections::HashMap<String,String>,
@@ -84,28 +59,6 @@ impl Bsp{
None=>Ok(None),
}
}
pub fn to_snf(&self,failure_mode:LoadFailureMode,vpk_list:&[Vpk])->Result<strafesnet_common::map::CompleteMap,LoadError>{
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
let mut mesh_deferred_loader=MeshDeferredLoader::new();
let map_step1=bsp::convert(
self,
&mut texture_deferred_loader,
&mut mesh_deferred_loader,
);
let mut mesh_loader=loader::MeshLoader::new(loader::BspFinder{bsp:self,vpks:vpk_list},&mut texture_deferred_loader);
let prop_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,failure_mode).map_err(LoadError::Mesh)?;
let map_step2=map_step1.add_prop_meshes(prop_meshes);
let mut texture_loader=loader::TextureLoader::new();
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,failure_mode).map_err(LoadError::Texture)?;
let map=map_step2.add_render_configs_and_textures(render_configs);
Ok(map)
}
}
pub struct Vpk{
vpk:vpk::VPK,

View File

@@ -5,6 +5,8 @@ use strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader;
use crate::valve_transform;
pub use model::Mesh;
fn ingest_vertex(mb:&mut model::MeshBuilder,vertex:&vmdl::vvd::Vertex,color:model::ColorId)->model::VertexId{
let pos=mb.acquire_pos_id(valve_transform(vertex.position.into()));
let normal=mb.acquire_normal_id(valve_transform(vertex.normal.into()));
@@ -17,7 +19,7 @@ fn ingest_vertex(mb:&mut model::MeshBuilder,vertex:&vmdl::vvd::Vertex,color:mode
})
}
pub fn convert_mesh(model:vmdl::Model,deferred_loader:&mut RenderConfigDeferredLoader<Cow<str>>)->model::Mesh{
pub fn convert_mesh(model:vmdl::Model,deferred_loader:&mut RenderConfigDeferredLoader<Cow<str>>)->Mesh{
let texture_paths=model.texture_directories();
if texture_paths.len()!=1{
println!("WARNING: multiple texture paths");
@@ -61,7 +63,7 @@ pub fn convert_mesh(model:vmdl::Model,deferred_loader:&mut RenderConfigDeferredL
_=>None,
}
})
}).flat_map(|[v1,v2,v3]|{
}).filter_map(|[v1,v2,v3]|{
// this should probably be a fatal error :D
let v1=model_vertices.get(v1)?;
let v2=model_vertices.get(v2)?;

View File

@@ -1,6 +1,6 @@
[package]
name = "strafesnet_common"
version = "0.7.0"
version = "0.9.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -12,8 +12,11 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
arrayvec = "0.7.4"
bitflags = "2.6.0"
fixed_wide = { version = "0.2.0", path = "../fixed_wide", registry = "strafesnet", features = ["deferred-division","zeroes","wide-mul"] }
linear_ops = { version = "0.1.1", path = "../linear_ops", registry = "strafesnet", features = ["deferred-division","named-fields"] }
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet" }
glam = "0.30.0"
fixed_wide = { workspace = true, features = ["deferred-division","zeroes","wide-mul"] }
linear_ops = { workspace = true, features = ["deferred-division","named-fields"] }
ratio_ops = { workspace = true }
glam.workspace = true
id = { version = "0.1.0", registry = "strafesnet" }
[lints]
workspace = true

View File

@@ -2,7 +2,9 @@ use crate::integer::{vec3,Planar64Vec3};
#[derive(Clone)]
pub struct Aabb{
// min is inclusive
min:Planar64Vec3,
// max is not inclusive
max:Planar64Vec3,
}
@@ -43,7 +45,7 @@ impl Aabb{
}
#[inline]
pub fn contains(&self,point:Planar64Vec3)->bool{
let bvec=self.min.lt(point)&point.lt(self.max);
let bvec=self.min.le(point)&point.lt(self.max);
bvec.all()
}
#[inline]
@@ -59,11 +61,11 @@ impl Aabb{
pub fn center(&self)->Planar64Vec3{
self.min.map_zip(self.max,|(min,max)|min.midpoint(max))
}
//probably use floats for area & volume because we don't care about precision
// pub fn area_weight(&self)->f32{
// let d=self.max-self.min;
// d.x*d.y+d.y*d.z+d.z*d.x
// }
#[inline]
pub fn area_weight(&self)->fixed_wide::types::F128_64{
let d=self.max-self.min;
d.x*d.y+d.y*d.z+d.z*d.x
}
// pub fn volume(&self)->f32{
// let d=self.max-self.min;
// d.x*d.y*d.z

View File

@@ -3,7 +3,7 @@ use std::collections::BTreeMap;
use crate::aabb::Aabb;
use crate::ray::Ray;
use crate::integer::{Ratio,Planar64};
use crate::integer::{Ratio,Planar64,Planar64Vec3};
use crate::instruction::{InstructionCollector,TimedInstruction};
//da algaritum
@@ -140,33 +140,34 @@ impl<L> BvhNode<L>{
},
}
}
fn populate_nodes<'a,T,F>(
fn populate_nodes<'a,T,IntersectLeaf,IntersectAabb>(
&'a self,
collector:&mut InstructionCollector<&'a L,Ratio<Planar64,Planar64>>,
nodes:&mut BTreeMap<Ratio<Planar64,Planar64>,&'a BvhNode<L>>,
ray:&Ray,
start_time:Ratio<Planar64,Planar64>,
f:&F,
start_point:Planar64Vec3,
collector:&mut InstructionCollector<&'a L,T>,
nodes:&mut BTreeMap<T,&'a BvhNode<L>>,
start_time:T,
intersect_leaf:&IntersectLeaf,
intersect_aabb:&IntersectAabb,
)
where
T:Ord+Copy,
Ratio<Planar64,Planar64>:From<T>,
F:Fn(&L,&Ray)->Option<T>,
IntersectLeaf:Fn(&L)->Option<T>,
IntersectAabb:Fn(&Aabb)->Option<T>,
{
match &self.content{
RecursiveContent::Leaf(leaf)=>if let Some(time)=f(leaf,ray){
let ins=TimedInstruction{time:time.into(),instruction:leaf};
if start_time.lt_ratio(ins.time)&&ins.time.lt_ratio(collector.time()){
RecursiveContent::Leaf(leaf)=>if let Some(time)=intersect_leaf(leaf){
let ins=TimedInstruction{time,instruction:leaf};
if start_time<ins.time&&ins.time<collector.time(){
collector.collect(Some(ins));
}
},
RecursiveContent::Branch(children)=>for child in children{
if child.aabb.contains(ray.origin){
child.populate_nodes(collector,nodes,ray,start_time,f);
if child.aabb.contains(start_point){
child.populate_nodes(start_point,collector,nodes,start_time,intersect_leaf,intersect_aabb);
}else{
// Am I an upcoming superstar?
if let Some(t)=intersect_aabb(ray,&child.aabb){
if start_time.lt_ratio(t)&&t.lt_ratio(collector.time()){
if let Some(t)=intersect_aabb(&child.aabb){
if start_time<t&&t<collector.time(){
nodes.insert(t,child);
}
}
@@ -174,27 +175,29 @@ impl<L> BvhNode<L>{
},
}
}
pub fn sample_ray<T,F>(
/// Traverse the BVH using the given sampling functions.
/// Nodes are tested in order of T returned by IntersectAabb.
/// The algorithm ends when T for the next node to test is
/// greater than the current best collected T from IntersectLeaf.
pub fn traverse<T,IntersectLeaf,IntersectAabb>(
&self,
ray:&Ray,
start_point:Planar64Vec3,
start_time:T,
time_limit:T,
f:F,
intersect_leaf:IntersectLeaf,
intersect_aabb:IntersectAabb,
)->Option<(T,&L)>
where
T:Ord+Copy,
T:From<Ratio<Planar64,Planar64>>,
Ratio<Planar64,Planar64>:From<T>,
F:Fn(&L,&Ray)->Option<T>,
IntersectLeaf:Fn(&L)->Option<T>,
IntersectAabb:Fn(&Aabb)->Option<T>,
{
// source of nondeterminism when Aabb boundaries are coplanar
let mut nodes=BTreeMap::new();
let start_time=start_time.into();
let time_limit=time_limit.into();
let mut collector=InstructionCollector::new(time_limit);
// break open all nodes that contain ray.origin and populate nodes with future intersection times
self.populate_nodes(&mut collector,&mut nodes,ray,start_time,&f);
self.populate_nodes(start_point,&mut collector,&mut nodes,start_time,&intersect_leaf,&intersect_aabb);
// swim through nodes one at a time
while let Some((t,node))=nodes.pop_first(){
@@ -202,18 +205,18 @@ impl<L> BvhNode<L>{
break;
}
match &node.content{
RecursiveContent::Leaf(leaf)=>if let Some(time)=f(leaf,ray){
RecursiveContent::Leaf(leaf)=>if let Some(time)=intersect_leaf(leaf){
let ins=TimedInstruction{time:time.into(),instruction:leaf};
// this lower bound can also be omitted
// but it causes type inference errors lol
if start_time.lt_ratio(ins.time)&&ins.time.lt_ratio(collector.time()){
if start_time<ins.time&&ins.time<collector.time(){
collector.collect(Some(ins));
}
},
// break open the node and predict collisions with the child nodes
RecursiveContent::Branch(children)=>for child in children{
// Am I an upcoming superstar?
if let Some(t)=intersect_aabb(ray,&child.aabb){
if let Some(t)=intersect_aabb(&child.aabb){
// we don't need to check the lower bound
// because child aabbs are guaranteed to be within the parent bounds.
if t<collector.time(){
@@ -245,18 +248,19 @@ pub fn generate_bvh<T>(boxen:Vec<(T,Aabb)>)->BvhNode<T>{
fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
let n=boxen.len();
if force||n<20{
let mut aabb=Aabb::default();
let nodes=boxen.into_iter().map(|b|{
aabb.join(&b.1);
const MAX_TERMINAL_BRANCH_LEAF_NODES:usize=20;
if force||n<MAX_TERMINAL_BRANCH_LEAF_NODES{
let mut aabb_outer=Aabb::default();
let nodes=boxen.into_iter().map(|(data,aabb)|{
aabb_outer.join(&aabb);
BvhNode{
content:RecursiveContent::Leaf(b.0),
aabb:b.1,
content:RecursiveContent::Leaf(data),
aabb,
}
}).collect();
BvhNode{
content:RecursiveContent::Branch(nodes),
aabb,
aabb:aabb_outer,
}
}else{
let mut sort_x=Vec::with_capacity(n);
@@ -272,9 +276,9 @@ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
sort_y.sort_by_key(|&(_,c)|c);
sort_z.sort_by_key(|&(_,c)|c);
let h=n/2;
let median_x=sort_x[h].1;
let median_y=sort_y[h].1;
let median_z=sort_z[h].1;
let (_,median_x)=sort_x[h];
let (_,median_y)=sort_y[h];
let (_,median_z)=sort_z[h];
//locate a run of values equal to the median
//partition point gives the first index for which the predicate evaluates to false
let first_index_eq_median_x=sort_x.partition_point(|&(_,x)|x<median_x);
@@ -313,10 +317,10 @@ fn generate_bvh_node<T>(boxen:Vec<(T,Aabb)>,force:bool)->BvhNode<T>{
};
list_list[list_id].push((data,aabb));
}
let mut aabb=Aabb::default();
if list_list.len()==1{
generate_bvh_node(list_list.remove(0),true)
}else{
let mut aabb=Aabb::default();
BvhNode{
content:RecursiveContent::Branch(
list_list.into_iter().map(|b|{

View File

@@ -34,7 +34,7 @@ pub struct StyleModifiers{
//unused
pub mass:Planar64,
}
impl std::default::Default for StyleModifiers{
impl Default for StyleModifiers{
fn default()->Self{
Self::roblox_bhop()
}
@@ -66,7 +66,7 @@ impl JumpImpulse{
_mass:Planar64,
)->Planar64Vec3{
match self{
&JumpImpulse::Time(time)=>velocity-(*gravity*time).map(|t|t.divide().clamp_1()),
&JumpImpulse::Time(time)=>velocity-(*gravity*time).map(|t|t.divide().clamp_64()),
&JumpImpulse::Height(height)=>{
//height==-v.y*v.y/(2*g.y);
//use energy to determine max height
@@ -74,10 +74,10 @@ impl JumpImpulse{
let g=gg.sqrt();
let v_g=gravity.dot(velocity);
//do it backwards
let radicand=v_g*v_g+(g*height*2).widen_4();
velocity-(*gravity*(radicand.sqrt().wrap_2()+v_g)/gg).divide().clamp_1()
let radicand=v_g*v_g+(g*height*2).widen_256();
velocity-(*gravity*(radicand.sqrt().wrap_128()+v_g)/gg).divide().clamp_64()
},
&JumpImpulse::Linear(jump_speed)=>velocity+(jump_dir*jump_speed/jump_dir.length()).divide().clamp_1(),
&JumpImpulse::Linear(jump_speed)=>velocity+(jump_dir*jump_speed/jump_dir.length()).divide().clamp_64(),
&JumpImpulse::Energy(_energy)=>{
//calculate energy
//let e=gravity.dot(velocity);
@@ -91,10 +91,10 @@ impl JumpImpulse{
pub fn get_jump_deltav(&self,gravity:&Planar64Vec3,mass:Planar64)->Planar64{
//gravity.length() is actually the proper calculation because the jump is always opposite the gravity direction
match self{
&JumpImpulse::Time(time)=>(gravity.length().wrap_1()*time/2).divide().clamp_1(),
&JumpImpulse::Height(height)=>(gravity.length()*height*2).sqrt().wrap_1(),
&JumpImpulse::Time(time)=>(gravity.length().wrap_64()*time/2).divide().clamp_64(),
&JumpImpulse::Height(height)=>(gravity.length()*height*2).sqrt().wrap_64(),
&JumpImpulse::Linear(deltav)=>deltav,
&JumpImpulse::Energy(energy)=>(energy.sqrt()*2/mass.sqrt()).divide().clamp_1(),
&JumpImpulse::Energy(energy)=>(energy.sqrt()*2/mass.sqrt()).divide().clamp_64(),
}
}
}
@@ -126,10 +126,10 @@ impl JumpSettings{
None=>rel_velocity,
};
let j=boost_vel.dot(jump_dir);
let js=jump_speed.widen_2();
let js=jump_speed.widen_128();
if j<js{
//weak booster: just do a regular jump
boost_vel+jump_dir.with_length(js-j).divide().wrap_1()
boost_vel+jump_dir.with_length(js-j).divide().wrap_64()
}else{
//activate booster normally, jump does nothing
boost_vel
@@ -142,13 +142,13 @@ impl JumpSettings{
None=>rel_velocity,
};
let j=boost_vel.dot(jump_dir);
let js=jump_speed.widen_2();
let js=jump_speed.widen_128();
if j<js{
//speed in direction of jump cannot be lower than amount
boost_vel+jump_dir.with_length(js-j).divide().wrap_1()
boost_vel+jump_dir.with_length(js-j).divide().wrap_64()
}else{
//boost and jump add together
boost_vel+jump_dir.with_length(js).divide().wrap_1()
boost_vel+jump_dir.with_length(js).divide().wrap_64()
}
}
(false,JumpCalculation::Max)=>{
@@ -159,10 +159,10 @@ impl JumpSettings{
None=>rel_velocity,
};
let boost_dot=boost_vel.dot(jump_dir);
let js=jump_speed.widen_2();
let js=jump_speed.widen_128();
if boost_dot<js{
//weak boost is extended to jump speed
boost_vel+jump_dir.with_length(js-boost_dot).divide().wrap_1()
boost_vel+jump_dir.with_length(js-boost_dot).divide().wrap_64()
}else{
//activate booster normally, jump does nothing
boost_vel
@@ -174,7 +174,7 @@ impl JumpSettings{
Some(booster)=>booster.boost(rel_velocity),
None=>rel_velocity,
};
boost_vel+jump_dir.with_length(jump_speed).divide().wrap_1()
boost_vel+jump_dir.with_length(jump_speed).divide().wrap_64()
},
}
}
@@ -267,9 +267,9 @@ pub struct StrafeSettings{
impl StrafeSettings{
pub fn tick_velocity(&self,velocity:Planar64Vec3,control_dir:Planar64Vec3)->Option<Planar64Vec3>{
let d=velocity.dot(control_dir);
let mv=self.mv.widen_2();
let mv=self.mv.widen_128();
match d<mv{
true=>Some(velocity+(control_dir*self.air_accel_limit.map_or(mv-d,|limit|limit.widen_2().min(mv-d))).wrap_1()),
true=>Some(velocity+(control_dir*self.air_accel_limit.map_or(mv-d,|limit|limit.widen_128().min(mv-d))).wrap_64()),
false=>None,
}
}
@@ -290,7 +290,7 @@ pub struct PropulsionSettings{
}
impl PropulsionSettings{
pub fn acceleration(&self,control_dir:Planar64Vec3)->Planar64Vec3{
(control_dir*self.magnitude).clamp_1()
(control_dir*self.magnitude).clamp_64()
}
}
@@ -310,16 +310,16 @@ pub struct WalkSettings{
impl WalkSettings{
pub fn accel(&self,target_diff:Planar64Vec3,gravity:Planar64Vec3)->Planar64{
//TODO: fallible walk accel
let diff_len=target_diff.length().wrap_1();
let diff_len=target_diff.length().wrap_64();
let friction=if diff_len<self.accelerate.topspeed{
self.static_friction
}else{
self.kinetic_friction
};
self.accelerate.accel.min((-gravity.y*friction).clamp_1())
self.accelerate.accel.min((-gravity.y*friction).clamp_64())
}
pub fn get_walk_target_velocity(&self,control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{
if control_dir==crate::integer::vec3::ZERO{
if control_dir==crate::integer::vec3::zero(){
return control_dir;
}
let nn=normal.length_squared();
@@ -329,19 +329,19 @@ impl WalkSettings{
let dd=d*d;
if dd<nnmm{
let cr=normal.cross(control_dir);
if cr==crate::integer::vec3::ZERO_2{
crate::integer::vec3::ZERO
if cr==crate::integer::vec3::zero(){
crate::integer::vec3::zero()
}else{
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_64()
}
}else{
crate::integer::vec3::ZERO
crate::integer::vec3::zero()
}
}
pub fn is_slope_walkable(&self,normal:Planar64Vec3,up:Planar64Vec3)->bool{
//normal is not guaranteed to be unit length
let ny=normal.dot(up);
let h=normal.length().wrap_1();
let h=normal.length().wrap_64();
//remember this is a normal vector
ny.is_positive()&&h*self.surf_dot<ny
}
@@ -360,7 +360,7 @@ impl LadderSettings{
self.accelerate.accel
}
pub fn get_ladder_target_velocity(&self,mut control_dir:Planar64Vec3,normal:Planar64Vec3)->Planar64Vec3{
if control_dir==crate::integer::vec3::ZERO{
if control_dir==crate::integer::vec3::zero(){
return control_dir;
}
let nn=normal.length_squared();
@@ -368,13 +368,13 @@ impl LadderSettings{
let nnmm=nn*mm;
let d=normal.dot(control_dir);
let mut dd=d*d;
if (self.dot*self.dot*nnmm).clamp_4()<dd{
if (self.dot*self.dot*nnmm).clamp_256()<dd{
if d.is_negative(){
control_dir=Planar64Vec3::new([Planar64::ZERO,mm.clamp_1(),Planar64::ZERO]);
control_dir=Planar64Vec3::new([Planar64::ZERO,mm.clamp_64(),Planar64::ZERO]);
}else{
control_dir=Planar64Vec3::new([Planar64::ZERO,-mm.clamp_1(),Planar64::ZERO]);
control_dir=Planar64Vec3::new([Planar64::ZERO,-mm.clamp_64(),Planar64::ZERO]);
}
dd=(normal.y*normal.y).widen_4();
dd=(normal.y*normal.y).widen_256();
}
//n=d if you are standing on top of a ladder and press E.
//two fixes:
@@ -382,13 +382,13 @@ impl LadderSettings{
//- fix the underlying issue
if dd<nnmm{
let cr=normal.cross(control_dir);
if cr==crate::integer::vec3::ZERO_2{
crate::integer::vec3::ZERO
if cr==crate::integer::vec3::zero(){
crate::integer::vec3::zero()
}else{
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_1()
(cr.cross(normal)*self.accelerate.topspeed/((nn*(nnmm-dd)).sqrt())).divide().clamp_64()
}
}else{
crate::integer::vec3::ZERO
crate::integer::vec3::zero()
}
}
}
@@ -417,7 +417,7 @@ impl Hitbox{
}
pub fn source()->Self{
Self{
halfsize:((int3(33,73,33)>>1)*VALVE_SCALE).narrow_1().unwrap(),
halfsize:((int3(33,73,33)>>1)*VALVE_SCALE).narrow_64().unwrap(),
mesh:HitboxMesh::Box,
}
}
@@ -538,11 +538,11 @@ impl StyleModifiers{
tick_rate:Ratio64::new(100,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_1().unwrap()),
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_64().unwrap()),
calculation:JumpCalculation::JumpThenBoost,
limit_minimum:true,
}),
gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_1().unwrap(),
gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_64().unwrap(),
mass:int(1),
rocket:None,
walk:Some(WalkSettings{
@@ -565,7 +565,7 @@ impl StyleModifiers{
magnitude:int(12),//?
}),
hitbox:Hitbox::source(),
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_1().unwrap(),
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_64().unwrap(),
}
}
pub fn source_surf()->Self{
@@ -574,16 +574,16 @@ impl StyleModifiers{
controls_mask_state:Controls::all(),
strafe:Some(StrafeSettings{
enable:ControlsActivation::full_2d(),
air_accel_limit:Some((int(150)*66*VALVE_SCALE).narrow_1().unwrap()),
air_accel_limit:Some((int(150)*66*VALVE_SCALE).narrow_64().unwrap()),
mv:Planar64::raw(30<<28),
tick_rate:Ratio64::new(66,AbsoluteTime::ONE_SECOND.get() as u64).unwrap(),
}),
jump:Some(JumpSettings{
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_1().unwrap()),
impulse:JumpImpulse::Height((int(52)*VALVE_SCALE).narrow_64().unwrap()),
calculation:JumpCalculation::JumpThenBoost,
limit_minimum:true,
}),
gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_1().unwrap(),
gravity:(int3(0,-800,0)*VALVE_SCALE).narrow_64().unwrap(),
mass:int(1),
rocket:None,
walk:Some(WalkSettings{
@@ -606,7 +606,7 @@ impl StyleModifiers{
magnitude:int(12),//?
}),
hitbox:Hitbox::source(),
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_1().unwrap(),
camera_offset:((int3(0,64,0)-(int3(0,73,0)>>1))*VALVE_SCALE).narrow_64().unwrap(),
}
}
}

View File

@@ -34,12 +34,41 @@ pub trait InstructionFeedback<I,T>:InstructionEmitter<I,Time=T>+InstructionConsu
self.process_instruction(instruction);
}
}
#[inline]
fn into_iter(self,time_limit:T)->InstructionIter<I,T,Self>
where
Self:Sized
{
InstructionIter{
time_limit,
feedback:self,
_phantom:core::marker::PhantomData,
}
}
}
impl<I,T,X> InstructionFeedback<I,T> for X
impl<I,T,F> InstructionFeedback<I,T> for F
where
T:Copy,
X:InstructionEmitter<I,Time=T>+InstructionConsumer<I,Time=T>,
F:InstructionEmitter<I,Time=T>+InstructionConsumer<I,Time=T>,
{}
pub struct InstructionIter<I,T:Copy,F:InstructionFeedback<I,T>>{
time_limit:T,
feedback:F,
_phantom:core::marker::PhantomData<I>,
}
impl<I,T,F> Iterator for InstructionIter<I,T,F>
where
I:Clone,
T:Clone+Copy,
F:InstructionFeedback<I,T>,
{
type Item=TimedInstruction<I,T>;
fn next(&mut self)->Option<Self::Item>{
let instruction=self.feedback.next_instruction(self.time_limit)?;
self.feedback.process_instruction(instruction.clone());
Some(instruction)
}
}
//PROPER PRIVATE FIELDS!!!
pub struct InstructionCollector<I,T>{

View File

@@ -1,6 +1,12 @@
pub use fixed_wide::fixed::*;
pub use ratio_ops::ratio::{Ratio,Divide,Parity};
pub mod fixed_types{
pub use fixed_wide::types::*;
}
use fixed_wide::types::F128_64;
//integer units
/// specific example of a "default" time type
@@ -56,11 +62,19 @@ impl<T> Time<T>{
pub const fn coerce<U>(self)->Time<U>{
Time::raw(self.0)
}
#[inline]
pub const fn div_euclid(self,other:Self)->Self{
Time::raw(self.0.div_euclid(other.0))
}
#[inline]
pub const fn rem_euclid(self,other:Self)->Self{
Time::raw(self.0.rem_euclid(other.0))
}
}
impl<T> From<Planar64> for Time<T>{
#[inline]
fn from(value:Planar64)->Self{
Self::raw((value*Planar64::raw(1_000_000_000)).clamp_1().to_raw())
Self::raw((value*Planar64::raw(1_000_000_000)).clamp_64().to_raw())
}
}
impl<T> From<Time<T>> for Ratio<Planar64,Planar64>{
@@ -86,7 +100,7 @@ impl<T> std::fmt::Display for Time<T>{
write!(f,"{}s+{:09}ns",self.0/Self::ONE_SECOND.0,self.0%Self::ONE_SECOND.0)
}
}
impl<T> std::default::Default for Time<T>{
impl<T> Default for Time<T>{
fn default()->Self{
Self::raw(0)
}
@@ -126,10 +140,10 @@ impl_time_additive_assign_operator!(core::ops::AddAssign,add_assign);
impl_time_additive_assign_operator!(core::ops::SubAssign,sub_assign);
impl_time_additive_assign_operator!(core::ops::RemAssign,rem_assign);
impl<T> std::ops::Mul for Time<T>{
type Output=Ratio<fixed_wide::fixed::Fixed<2,64>,fixed_wide::fixed::Fixed<2,64>>;
type Output=Ratio<F128_64,F128_64>;
#[inline]
fn mul(self,rhs:Self)->Self::Output{
Ratio::new(Fixed::raw(self.0)*Fixed::raw(rhs.0),Fixed::raw_digit(1_000_000_000i64.pow(2)))
Ratio::new(Fixed::raw(self.0)*Fixed::raw(rhs.0),Fixed::from_u64(1_000_000_000u64.pow(2)))
}
}
macro_rules! impl_time_i64_rhs_operator {
@@ -148,15 +162,29 @@ impl_time_i64_rhs_operator!(Mul,mul);
impl_time_i64_rhs_operator!(Shr,shr);
impl_time_i64_rhs_operator!(Shl,shl);
impl<T> core::ops::Mul<Time<T>> for Planar64{
type Output=Ratio<Fixed<2,64>,Planar64>;
type Output=Ratio<F128_64,Planar64>;
#[inline]
fn mul(self,rhs:Time<T>)->Self::Output{
Ratio::new(self*Fixed::raw(rhs.0),Planar64::raw(1_000_000_000))
}
}
impl<T> From<Time<T>> for f32{
#[inline]
fn from(value:Time<T>)->Self{
value.get() as f32/Time::<T>::ONE_SECOND.get() as f32
}
}
impl<T> From<Time<T>> for f64{
#[inline]
fn from(value:Time<T>)->Self{
value.get() as f64/Time::<T>::ONE_SECOND.get() as f64
}
}
#[cfg(test)]
mod test_time{
use super::*;
type Time=super::AbsoluteTime;
use fixed_wide::types::F64_32;
type Time=AbsoluteTime;
#[test]
fn time_from_planar64(){
let a:Time=Planar64::from(1).into();
@@ -170,13 +198,13 @@ mod test_time{
#[test]
fn time_squared(){
let a=Time::from_secs(2);
assert_eq!(a*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))*4,Fixed::<2,64>::raw_digit(1_000_000_000i64.pow(2))));
assert_eq!(a*a,Ratio::new(F128_64::from_u64(1_000_000_000u64.pow(2))*4,F128_64::from_u64(1_000_000_000u64.pow(2))));
}
#[test]
fn time_times_planar64(){
let a=Time::from_secs(2);
let b=Planar64::from(2);
assert_eq!(b*a,Ratio::new(Fixed::<2,64>::raw_digit(1_000_000_000*(1<<32))<<2,Fixed::<1,32>::raw_digit(1_000_000_000)));
assert_eq!(b*a,Ratio::new(F128_64::from_u64(1_000_000_000*(1<<32))<<2,F64_32::from_u64(1_000_000_000)));
}
}
@@ -214,11 +242,11 @@ impl Ratio64{
}
#[inline]
pub const fn mul_int(&self,rhs:i64)->i64{
rhs*self.num/(self.den as i64)
(rhs as i128*self.num as i128/self.den as i128) as i64
}
#[inline]
pub const fn rhs_div_int(&self,rhs:i64)->i64{
rhs*(self.den as i64)/self.num
(rhs as i128*self.den as i128/self.num as i128) as i64
}
#[inline]
pub const fn mul_ref(&self,rhs:&Ratio64)->Ratio64{
@@ -263,7 +291,6 @@ fn integer_decode_f64(f: f64) -> (u64, i16, i8) {
pub enum Ratio64TryFromFloatError{
Nan,
Infinite,
Subnormal,
HighlyNegativeExponent(i16),
HighlyPositiveExponent(i16),
}
@@ -298,8 +325,10 @@ fn ratio64_from_mes((m,e,s):(u64,i16,i8))->Result<Ratio64,Ratio64TryFromFloatErr
Ok(Ratio64::new(num as i64,den as u64).unwrap())
}else if e<0{
// simple exact representation
Ok(Ratio64::new((m as i64)*(s as i64),1<<-e).unwrap())
}else if (64-m.leading_zeros() as i16)+e<64{
// integer
Ok(Ratio64::new((m as i64)*(s as i64)*(1<<e),1).unwrap())
}else{
Err(Ratio64TryFromFloatError::HighlyPositiveExponent(e))
@@ -331,6 +360,29 @@ impl TryFrom<f64> for Ratio64{
}
}
}
#[cfg(test)]
fn req(r0:Ratio64,r1:Ratio64){
println!("r0={r0:?} r1={r1:?}");
assert_eq!(r0.num(),r1.num(),"Nums not eq");
assert_eq!(r0.den(),r1.den(),"Dens not eq");
}
#[test]
fn test_ratio64_from_float(){
req(2.0.try_into().unwrap(),Ratio64::new(2,1).unwrap());
req(1.0.try_into().unwrap(),Ratio64::new(1,1).unwrap());
req(0.5.try_into().unwrap(),Ratio64::new(1,2).unwrap());
req(1.1.try_into().unwrap(),Ratio64::new(2476979795053773,2251799813685248).unwrap());
req(0.8.try_into().unwrap(),Ratio64::new(3602879701896397,4503599627370496).unwrap());
req(0.61.try_into().unwrap(),Ratio64::new(5494391545392005,9007199254740992).unwrap());
req(0.01.try_into().unwrap(),Ratio64::new(5764607523034235,576460752303423488).unwrap());
req(0.001.try_into().unwrap(),Ratio64::new(1152921504606847,1152921504606846976).unwrap());
req(0.00001.try_into().unwrap(),Ratio64::new(89605456633725,8960545663372499267).unwrap());
req(0.00000000001.try_into().unwrap(),Ratio64::new(35204848,3520484800000000213).unwrap());
req(0.000000000000000001.try_into().unwrap(),Ratio64::new(11,10999999999999999213).unwrap());
req(2222222222222.0.try_into().unwrap(),Ratio64::new(2222222222222,1).unwrap());
req(core::f64::consts::PI.try_into().unwrap(),Ratio64::new(884279719003555,281474976710656).unwrap());
}
impl std::ops::Mul<Ratio64> for Ratio64{
type Output=Ratio64;
#[inline]
@@ -520,8 +572,8 @@ fn angle_sin_cos(){
println!("cordic s={} c={}",(s/h).divide(),(c/h).divide());
let (fs,fc)=f.sin_cos();
println!("float s={} c={}",fs,fc);
assert!(close_enough((c/h).divide().wrap_1(),Planar64::raw((fc*((1u64<<32) as f64)) as i64)));
assert!(close_enough((s/h).divide().wrap_1(),Planar64::raw((fs*((1u64<<32) as f64)) as i64)));
assert!(close_enough((c/h).divide().wrap_64(),Planar64::raw((fc*((1u64<<32) as f64)) as i64)));
assert!(close_enough((s/h).divide().wrap_64(),Planar64::raw((fs*((1u64<<32) as f64)) as i64)));
}
test_angle(1.0);
test_angle(std::f64::consts::PI/4.0);
@@ -552,8 +604,8 @@ impl TryFrom<[f32;3]> for Unit32Vec3{
}
*/
pub type Planar64TryFromFloatError=fixed_wide::fixed::FixedFromFloatError;
pub type Planar64=fixed_wide::types::I32F32;
pub type Planar64TryFromFloatError=FixedFromFloatError;
pub type Planar64=fixed_wide::types::F64_32;
pub type Planar64Vec3=linear_ops::types::Vector3<Planar64>;
pub type Planar64Mat3=linear_ops::types::Matrix3<Planar64>;
pub mod vec3{
@@ -561,12 +613,6 @@ pub mod vec3{
pub use linear_ops::types::Vector3;
pub const MIN:Planar64Vec3=Planar64Vec3::new([Planar64::MIN;3]);
pub const MAX:Planar64Vec3=Planar64Vec3::new([Planar64::MAX;3]);
pub const ZERO:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO;3]);
pub const ZERO_2:linear_ops::types::Vector3<Fixed::<2,64>>=linear_ops::types::Vector3::new([Fixed::<2,64>::ZERO;3]);
pub const ZERO_3:linear_ops::types::Vector3<Fixed::<3,96>>=linear_ops::types::Vector3::new([Fixed::<3,96>::ZERO;3]);
pub const ZERO_4:linear_ops::types::Vector3<Fixed::<4,128>>=linear_ops::types::Vector3::new([Fixed::<4,128>::ZERO;3]);
pub const ZERO_5:linear_ops::types::Vector3<Fixed::<5,160>>=linear_ops::types::Vector3::new([Fixed::<5,160>::ZERO;3]);
pub const ZERO_6:linear_ops::types::Vector3<Fixed::<6,192>>=linear_ops::types::Vector3::new([Fixed::<6,192>::ZERO;3]);
pub const X:Planar64Vec3=Planar64Vec3::new([Planar64::ONE,Planar64::ZERO,Planar64::ZERO]);
pub const Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ONE,Planar64::ZERO]);
pub const Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::ONE]);
@@ -575,6 +621,10 @@ pub mod vec3{
pub const NEG_Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::NEG_ONE,Planar64::ZERO]);
pub const NEG_Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::NEG_ONE]);
pub const NEG_ONE:Planar64Vec3=Planar64Vec3::new([Planar64::NEG_ONE,Planar64::NEG_ONE,Planar64::NEG_ONE]);
// TODO: use #![feature(generic_const_items)] when stabilized https://github.com/rust-lang/rust/issues/113521
pub const fn zero<const N:usize,const F:usize>()->Vector3<Fixed<N,F>>{
Vector3::new([Fixed::ZERO;3])
}
#[inline]
pub const fn int(x:i32,y:i32,z:i32)->Planar64Vec3{
Planar64Vec3::new([Planar64::raw((x as i64)<<32),Planar64::raw((y as i64)<<32),Planar64::raw((z as i64)<<32)])
@@ -634,8 +684,8 @@ pub mod mat3{
let (yc,ys)=y.cos_sin();
Planar64Mat3::from_cols([
Planar64Vec3::new([xc,Planar64::ZERO,-xs]),
Planar64Vec3::new([(xs*ys).wrap_1(),yc,(xc*ys).wrap_1()]),
Planar64Vec3::new([(xs*yc).wrap_1(),-ys,(xc*yc).wrap_1()]),
Planar64Vec3::new([(xs*ys).wrap_64(),yc,(xc*ys).wrap_64()]),
Planar64Vec3::new([(xs*yc).wrap_64(),-ys,(xc*yc).wrap_64()]),
])
}
#[inline]
@@ -663,7 +713,7 @@ pub struct Planar64Affine3{
pub translation:Planar64Vec3,
}
impl Planar64Affine3{
pub const IDENTITY:Self=Self::new(mat3::identity(),vec3::ZERO);
pub const IDENTITY:Self=Self::new(mat3::identity(),vec3::zero());
#[inline]
pub const fn new(matrix3:Planar64Mat3,translation:Planar64Vec3)->Self{
Self{matrix3,translation}
@@ -676,8 +726,8 @@ impl Planar64Affine3{
}
}
#[inline]
pub fn transform_point3(&self,point:Planar64Vec3)->vec3::Vector3<Fixed<2,64>>{
self.translation.widen_2()+self.matrix3*point
pub fn transform_point3(&self,point:Planar64Vec3)->vec3::Vector3<F128_64>{
self.translation.widen_128()+self.matrix3*point
}
}
impl Into<glam::Mat4> for Planar64Affine3{

View File

@@ -36,6 +36,7 @@ pub enum SetControlInstruction{
SetMoveForward(bool),
SetJump(bool),
SetZoom(bool),
SetSprint(bool),
}
#[derive(Clone,Debug)]
pub enum ModeInstruction{

View File

@@ -53,36 +53,23 @@ impl std::fmt::Display for Error{
impl std::error::Error for Error{}
#[derive(Clone,Copy,Debug)]
enum RunState{
pub enum RunState{
Created,
Started{timer:TimerFixed<Realtime<PhysicsTimeInner,TimeInner>,Unpaused>},
Finished{timer:TimerFixed<Realtime<PhysicsTimeInner,TimeInner>,Paused>},
}
#[derive(Clone,Copy,Debug)]
pub struct Run{
state:RunState,
flagged:Option<FlagReason>,
}
impl Run{
pub fn new()->Self{
Self{
state:RunState::Created,
flagged:None,
}
}
impl RunState{
pub fn time(&self,time:PhysicsTime)->Time{
match &self.state{
match &self{
RunState::Created=>Time::ZERO,
RunState::Started{timer}=>timer.time(time),
RunState::Finished{timer}=>timer.time(),
}
}
pub fn start(&mut self,time:PhysicsTime)->Result<(),Error>{
match &self.state{
match &self{
RunState::Created=>{
self.state=RunState::Started{
*self=RunState::Started{
timer:TimerFixed::new(time,Time::ZERO),
};
Ok(())
@@ -93,10 +80,10 @@ impl Run{
}
pub fn finish(&mut self,time:PhysicsTime)->Result<(),Error>{
//this uses Copy
match &self.state{
match &self{
RunState::Created=>Err(Error::NotStarted),
RunState::Started{timer}=>{
self.state=RunState::Finished{
*self=RunState::Finished{
timer:timer.into_paused(time),
};
Ok(())
@@ -104,12 +91,39 @@ impl Run{
RunState::Finished{..}=>Err(Error::AlreadyFinished),
}
}
}
#[derive(Clone,Copy,Debug)]
pub struct Run{
state:RunState,
flag_reason:Option<FlagReason>,
}
impl Run{
pub fn new()->Self{
Self{
state:RunState::Created,
flag_reason:None,
}
}
pub fn time(&self,time:PhysicsTime)->Time{
self.state.time(time)
}
pub fn start(&mut self,time:PhysicsTime)->Result<(),Error>{
self.state.start(time)
}
pub fn finish(&mut self,time:PhysicsTime)->Result<(),Error>{
self.state.finish(time)
}
pub fn flag(&mut self,flag_reason:FlagReason){
//don't replace the first reason the run was flagged
if self.flagged.is_none(){
self.flagged=Some(flag_reason);
if self.flag_reason.is_none(){
self.flag_reason=Some(flag_reason);
}
}
pub fn flag_reason(&self)->Option<FlagReason>{
self.flag_reason
}
pub fn get_finish_time(&self)->Option<Time>{
match &self.state{
RunState::Finished{timer}=>Some(timer.time()),

View File

@@ -69,10 +69,8 @@ impl<In,Out> Scaled<In,Out>
const fn get_scale(&self)->Ratio64{
self.scale
}
fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
let new_time=self.get_time(time);
const fn set_scale(&mut self,new_scale:Ratio64){
self.scale=new_scale;
self.set_time(time,new_time);
}
}
@@ -132,7 +130,7 @@ pub struct TimerFixed<T:TimerState,P:PauseState>{
_paused:P,
}
//scaled timer methods are generic across PauseState
//some scaled timer methods are generic across PauseState
impl<P:PauseState,In,Out> TimerFixed<Scaled<In,Out>,P>
where Time<In>:Copy,
{
@@ -147,8 +145,22 @@ impl<P:PauseState,In,Out> TimerFixed<Scaled<In,Out>,P>
pub const fn get_scale(&self)->Ratio64{
self.state.get_scale()
}
}
// setting the scale of an unpaused timer is different than a paused timer
impl<In,Out> TimerFixed<Scaled<In,Out>,Unpaused>
where Time<In>:Copy,
{
pub fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
self.state.set_scale(time,new_scale)
let new_time=self.state.get_time(time);
self.state.set_scale(new_scale);
self.state.set_time(time,new_time);
}
}
impl<In,Out> TimerFixed<Scaled<In,Out>,Paused>
where Time<In>:Copy,
{
pub fn set_scale(&mut self,new_scale:Ratio64){
self.state.set_scale(new_scale);
}
}
@@ -305,7 +317,7 @@ impl<In,Out> Timer<Scaled<In,Out>>
}
pub fn set_scale(&mut self,time:Time<In>,new_scale:Ratio64){
match self{
Self::Paused(timer)=>timer.set_scale(time,new_scale),
Self::Paused(timer)=>timer.set_scale(new_scale),
Self::Unpaused(timer)=>timer.set_scale(time,new_scale),
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "strafesnet_deferred_loader"
version = "0.5.1"
version = "0.6.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -10,4 +10,7 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
strafesnet_common.workspace = true
[lints]
workspace = true

View File

@@ -4,5 +4,5 @@ pub trait Loader{
type Error:Error;
type Index<'a> where Self:'a;
type Resource;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>;
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>;
}

View File

@@ -1,6 +1,6 @@
[package]
name = "fixed_wide"
version = "0.2.1"
version = "0.3.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -14,7 +14,10 @@ wide-mul=[]
zeroes=["dep:arrayvec"]
[dependencies]
bnum = "0.13.0"
bnum = "0.14.3"
arrayvec = { version = "0.7.6", optional = true }
paste = "1.0.15"
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true }
ratio_ops = { workspace = true, optional = true }
[lints]
workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -1,208 +1,273 @@
use crate::types::I32F32;
use crate::types::I256F256;
use crate::fixed::Fixed;
use crate::types::{F64_32,F128_64,F192_96,F512_256};
#[test]
fn you_can_add_numbers(){
let a=I256F256::from((3i128*2).pow(4));
assert_eq!(a+a,I256F256::from((3i128*2).pow(4)*2));
let a=F512_256::from((3i128*2).pow(4));
assert_eq!(a+a,F512_256::from((3i128*2).pow(4)*2));
}
macro_rules! test_bit_by_bit{
($n:expr,$float:ty,$mantissa_bits:expr)=>{{
const MANT:u64=(1<<$mantissa_bits)-1;
// all bits in range
for i in 0..$n-$mantissa_bits{
let a=Fixed::<{$n/8},{$n>>1}>::from_bits(bnum::cast::As::as_::<bnum::Int::<{$n/8}>>(MANT).shl(i));
let b=(MANT as $float)*(2.0 as $float).powi(i as i32-{$n>>1});
let f:$float=a.into();
assert_eq!(f,b,"F{}_{} Into float {i}",$n,$n>>1);
assert_eq!(a,b.try_into().unwrap(),"F{}_{} From float {i}",$n,$n>>1);
}
// underflow
for i in 0u32..$mantissa_bits{
let a=Fixed::<{$n/8},{$n>>1}>::from_bits(bnum::cast::As::as_::<bnum::Int::<{$n/8}>>(MANT>>i));
let b=((MANT>>i) as $float)*(2.0 as $float).powi(-{$n>>1});
let f:$float=a.into();
assert_eq!(f,b,"Underflow F{}_{} Into float {i}",$n,$n>>1);
assert_eq!(a,b.try_into().unwrap(),"Underflow F{}_{} From float {i}",$n,$n>>1);
}
}};
}
#[test]
fn test_many(){
test_bit_by_bit!(64,f32,24);
test_bit_by_bit!(128,f32,24);
// f32 is reaching its limits here
// test_bit_by_bit!(256,f32,24);
// test_bit_by_bit!(512,f32,24);
test_bit_by_bit!(64,f64,53);
test_bit_by_bit!(128,f64,53);
test_bit_by_bit!(256,f64,53);
test_bit_by_bit!(512,f64,53);
}
#[test]
fn to_f32(){
let a=I256F256::from(1)>>2;
let a=F64_32::ZERO;
let f:f32=a.into();
assert_eq!(f,0.0f32);
let a=F64_32::from(1)>>2;
let f:f32=a.into();
assert_eq!(f,0.25f32);
let f:f32=(-a).into();
assert_eq!(f,-0.25f32);
let a=I256F256::from(0);
let a=F64_32::MIN;
let f:f32=a.into();
assert_eq!(f,i32::MIN as f32);
let a=F512_256::from(1)>>2;
let f:f32=a.into();
assert_eq!(f,0.25f32);
let f:f32=(-a).into();
assert_eq!(f,-0.25f32);
let a=F512_256::from(0);
let f:f32=(-a).into();
assert_eq!(f,0f32);
let a=I256F256::from(237946589723468975i64)<<16;
let a=F512_256::from(237946589723468975i64)<<16;
let f:f32=a.into();
assert_eq!(f,237946589723468975f32*2.0f32.powi(16));
}
#[test]
fn to_f64(){
let a=I256F256::from(1)>>2;
let a=F64_32::ZERO;
let f:f64=a.into();
assert_eq!(f,0.0f64);
let a=F64_32::from(1)>>2;
let f:f64=a.into();
assert_eq!(f,0.25f64);
let f:f64=(-a).into();
assert_eq!(f,-0.25f64);
let a=I256F256::from(0);
let a=F64_32::MIN;
let f:f64=a.into();
assert_eq!(f,i32::MIN as f64);
let a=F512_256::from(1)>>2;
let f:f64=a.into();
assert_eq!(f,0.25f64);
let f:f64=(-a).into();
assert_eq!(f,-0.25f64);
let a=F512_256::from(0);
let f:f64=(-a).into();
assert_eq!(f,0f64);
let a=I256F256::from(237946589723468975i64)<<16;
let a=F512_256::from(237946589723468975i64)<<16;
let f:f64=a.into();
assert_eq!(f,237946589723468975f64*2.0f64.powi(16));
}
#[test]
fn from_f32(){
let a=I256F256::from(1)>>2;
let b:Result<I256F256,_>=0.25f32.try_into();
let a=F64_32::ZERO;
let b:Result<F64_32,_>=0.0f32.try_into();
assert_eq!(b,Ok(a));
let a=I256F256::from(-1)>>2;
let b:Result<I256F256,_>=(-0.25f32).try_into();
let a=F512_256::from(1)>>2;
let b:Result<F512_256,_>=0.25f32.try_into();
assert_eq!(b,Ok(a));
let a=I256F256::from(0);
let b:Result<I256F256,_>=0.try_into();
let a=F512_256::from(-1)>>2;
let b:Result<F512_256,_>=(-0.25f32).try_into();
assert_eq!(b,Ok(a));
let a=I256F256::from(0b101011110101001010101010000000000000000000000000000i64)<<16;
let b:Result<I256F256,_>=(0b101011110101001010101010000000000000000000000000000u64 as f32*2.0f32.powi(16)).try_into();
let a=F512_256::from(0);
let b:Result<F512_256,_>=0.try_into();
assert_eq!(b,Ok(a));
let a=F512_256::from(0b101011110101001010101010000000000000000000000000000i64)<<16;
let b:Result<F512_256,_>=(0b101011110101001010101010000000000000000000000000000u64 as f32*2.0f32.powi(16)).try_into();
assert_eq!(b,Ok(a));
//I32F32::MAX into f32 is truncated into this value
let a=I32F32::raw(0b111111111111111111111111000000000000000000000000000000000000000i64);
let b:Result<I32F32,_>=Into::<f32>::into(I32F32::MAX).try_into();
let a=F64_32::raw(0b111111111111111111111111000000000000000000000000000000000000000i64);
let b:Result<F64_32,_>=Into::<f32>::into(F64_32::MAX).try_into();
assert_eq!(b,Ok(a));
//I32F32::MIN hits a special case since it's not representable as a positive signed integer
//TODO: don't return an overflow because this is technically possible
let _a=I32F32::MIN;
let b:Result<I32F32,_>=Into::<f32>::into(I32F32::MIN).try_into();
let a=F64_32::MIN;
let f:f32=a.into();
let b:Result<F64_32,_>=f.try_into();
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow));
//16 is within the 24 bits of float precision
let b:Result<I32F32,_>=Into::<f32>::into(-I32F32::MIN.widen_2()).try_into();
let a=-F64_32::MIN.widen_128();
let f:f32=a.into();
let b:Result<F64_32,_>=f.try_into();
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Overflow));
let b:Result<I32F32,_>=f32::MIN_POSITIVE.try_into();
let b:Result<F64_32,_>=f32::MIN_POSITIVE.try_into();
assert_eq!(b,Err(crate::fixed::FixedFromFloatError::Underflow));
//test many cases
for i in 0..64{
let a=crate::fixed::Fixed::<2,64>::raw_digit(0b111111111111111111111111000000000000000000000000000000000000000i64)<<i;
let a=F128_64::from_u64(0b111111111111111111111111000000000000000000000000000000000000000u64)<<i;
let f:f32=a.into();
let b:Result<crate::fixed::Fixed<2,64>,_>=f.try_into();
let b:Result<F128_64,_>=f.try_into();
assert_eq!(b,Ok(a));
}
}
#[test]
fn from_f64(){
let a=I256F256::from(1)>>2;
let b:Result<I256F256,_>=0.25f64.try_into();
let a=F64_32::ZERO;
let b:Result<F64_32,_>=0.0f64.try_into();
assert_eq!(b,Ok(a));
let a=I256F256::from(-1)>>2;
let b:Result<I256F256,_>=(-0.25f64).try_into();
let a=F512_256::from(1)>>2;
let b:Result<F512_256,_>=0.25f64.try_into();
assert_eq!(b,Ok(a));
let a=I256F256::from(0);
let b:Result<I256F256,_>=0.try_into();
let a=F512_256::from(-1)>>2;
let b:Result<F512_256,_>=(-0.25f64).try_into();
assert_eq!(b,Ok(a));
let a=I256F256::from(0b101011110101001010101010000000000000000000000000000i64)<<16;
let b:Result<I256F256,_>=(0b101011110101001010101010000000000000000000000000000u64 as f64*2.0f64.powi(16)).try_into();
let a=F512_256::from(0);
let b:Result<F512_256,_>=0.try_into();
assert_eq!(b,Ok(a));
let a=F512_256::from(0b101011110101001010101010000000000000000000000000000i64)<<16;
let b:Result<F512_256,_>=(0b101011110101001010101010000000000000000000000000000u64 as f64*2.0f64.powi(16)).try_into();
assert_eq!(b,Ok(a));
}
#[test]
fn you_can_shr_numbers(){
let a=I32F32::from(4);
assert_eq!(a>>1,I32F32::from(2));
let a=F64_32::from(4);
assert_eq!(a>>1,F64_32::from(2));
}
#[test]
fn test_wide_mul(){
let a=I32F32::ONE;
let aa=a.wide_mul_1_1(a);
assert_eq!(aa,crate::types::I64F64::ONE);
let a=F64_32::ONE;
let aa=a.wide_mul_64_64(a);
assert_eq!(aa,F128_64::ONE);
}
#[test]
fn test_wide_div(){
let a=I32F32::ONE*4;
let b=I32F32::ONE*2;
let wide_a=a.wide_mul_1_1(I32F32::ONE);
let wide_b=b.wide_mul_1_1(I32F32::ONE);
let ab=a.wide_div_1_1(b);
assert_eq!(ab,crate::types::I64F64::ONE*2);
let wab=wide_a.wide_div_2_1(b);
assert_eq!(wab,crate::fixed::Fixed::<3,96>::ONE*2);
let awb=a.wide_div_1_2(wide_b);
assert_eq!(awb,crate::fixed::Fixed::<3,96>::ONE*2);
let a=F64_32::ONE*4;
let b=F64_32::ONE*2;
let wide_a=a.wide_mul_64_64(F64_32::ONE);
let wide_b=b.wide_mul_64_64(F64_32::ONE);
let ab=a.wide_div_64_64(b);
assert_eq!(ab,F128_64::ONE*2);
let wab=wide_a.wide_div_128_64(b);
assert_eq!(wab,F192_96::ONE*2);
let awb=a.wide_div_64_128(wide_b);
assert_eq!(awb,F192_96::ONE*2);
}
#[test]
fn test_wide_mul_repeated() {
let a=I32F32::from(2);
let b=I32F32::from(3);
let a=F64_32::from(2);
let b=F64_32::from(3);
let w1=a.wide_mul_1_1(b);
let w2=w1.wide_mul_2_2(w1);
let w3=w2.wide_mul_4_4(w2);
let w1=a.wide_mul_64_64(b);
let w2=w1.wide_mul_128_128(w1);
let w3=w2.wide_mul_256_256(w2);
assert_eq!(w3,I256F256::from((3i128*2).pow(4)));
assert_eq!(w3,F512_256::from((3i128*2).pow(4)));
}
#[test]
fn test_bint(){
let a=I32F32::ONE;
assert_eq!(a*2,I32F32::from(2));
let a=F64_32::ONE;
assert_eq!(a*2,F64_32::from(2));
}
#[test]
fn test_wrap(){
assert_eq!(I32F32::ONE,I256F256::ONE.wrap_1());
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.wrap_1());
assert_eq!(F64_32::ONE,F512_256::ONE.wrap_64());
assert_eq!(F64_32::NEG_ONE,F512_256::NEG_ONE.wrap_64());
}
#[test]
fn test_narrow(){
assert_eq!(Ok(I32F32::ONE),I256F256::ONE.narrow_1());
assert_eq!(Ok(I32F32::NEG_ONE),I256F256::NEG_ONE.narrow_1());
assert_eq!(Ok(F64_32::ONE),F512_256::ONE.narrow_64());
assert_eq!(Ok(F64_32::NEG_ONE),F512_256::NEG_ONE.narrow_64());
}
#[test]
fn test_widen(){
assert_eq!(I32F32::ONE.widen_8(),I256F256::ONE);
assert_eq!(I32F32::NEG_ONE.widen_8(),I256F256::NEG_ONE);
assert_eq!(F64_32::ONE.widen_512(),F512_256::ONE);
assert_eq!(F64_32::NEG_ONE.widen_512(),F512_256::NEG_ONE);
}
#[test]
fn test_clamp(){
assert_eq!(I32F32::ONE,I256F256::ONE.clamp_1());
assert_eq!(I32F32::NEG_ONE,I256F256::NEG_ONE.clamp_1());
assert_eq!(F64_32::ONE,F512_256::ONE.clamp_64());
assert_eq!(F64_32::NEG_ONE,F512_256::NEG_ONE.clamp_64());
}
#[test]
fn test_sqrt(){
let a=I32F32::ONE*4;
assert_eq!(a.sqrt(),I32F32::from(2));
let a=F64_32::ONE*4;
assert_eq!(a.sqrt(),F64_32::from(2));
}
#[test]
fn test_sqrt_zero(){
let a=I32F32::ZERO;
assert_eq!(a.sqrt(),I32F32::ZERO);
let a=F64_32::ZERO;
assert_eq!(a.sqrt(),F64_32::ZERO);
}
#[test]
fn test_sqrt_low(){
let a=I32F32::HALF;
let a=F64_32::HALF;
let b=a.fixed_mul(a);
assert_eq!(b.sqrt(),a);
}
fn find_equiv_sqrt_via_f64(n:I32F32)->I32F32{
fn find_equiv_sqrt_via_f64(n:F64_32)->F64_32{
//GIMME THEM BITS BOY
let &[bits]=n.to_bits().to_bits().digits();
let ibits=bits as i64;
let ibits=i64::from_le_bytes(n.to_bits().to_bytes());
let f=(ibits as f64)/((1u64<<32) as f64);
let f_ans=f.sqrt();
let i=(f_ans*((1u64<<32) as f64)) as i64;
let r=I32F32::from_bits(bnum::BInt::<1>::from(i));
let i=(f_ans*((1u64<<32) as f64)) as u64;
let r=F64_32::from_u64(i);
//mimic the behaviour of the algorithm,
//return the result if it truncates to the exact answer
if (r+I32F32::EPSILON).wide_mul_1_1(r+I32F32::EPSILON)==n.wide_mul_1_1(I32F32::ONE){
return r+I32F32::EPSILON;
if (r+F64_32::EPSILON).wide_mul_64_64(r+F64_32::EPSILON)==n.wide_mul_64_64(F64_32::ONE){
return r+F64_32::EPSILON;
}
if (r-I32F32::EPSILON).wide_mul_1_1(r-I32F32::EPSILON)==n.wide_mul_1_1(I32F32::ONE){
return r-I32F32::EPSILON;
if (r-F64_32::EPSILON).wide_mul_64_64(r-F64_32::EPSILON)==n.wide_mul_64_64(F64_32::ONE){
return r-F64_32::EPSILON;
}
return r;
}
fn test_exact(n:I32F32){
fn test_exact(n:F64_32){
assert_eq!(n.sqrt(),find_equiv_sqrt_via_f64(n));
}
#[test]
fn test_sqrt_exact(){
//43
for i in 0..((i64::MAX as f32).ln() as u32){
let n=I32F32::from_bits(bnum::BInt::<1>::from((i as f32).exp() as i64));
let n=F64_32::from_u64((i as f32).exp() as u64);
test_exact(n);
}
}
#[test]
fn test_sqrt_max(){
let a=I32F32::MAX;
let a=F64_32::MAX;
test_exact(a);
}
#[test]
@@ -210,9 +275,9 @@ fn test_sqrt_max(){
fn test_zeroes_normal(){
// (x-1)*(x+1)
// x^2-1
let zeroes=I32F32::zeroes2(I32F32::NEG_ONE,I32F32::ZERO,I32F32::ONE);
let zeroes=F64_32::zeroes2(F64_32::NEG_ONE,F64_32::ZERO,F64_32::ONE);
assert_eq!(zeroes,arrayvec::ArrayVec::from_iter([I32F32::NEG_ONE,I32F32::ONE]));
let zeroes=I32F32::zeroes2(I32F32::NEG_ONE*3,I32F32::ONE*2,I32F32::ONE);
let zeroes=F64_32::zeroes2(F64_32::NEG_ONE*3,F64_32::ONE*2,F64_32::ONE);
assert_eq!(zeroes,arrayvec::ArrayVec::from_iter([I32F32::NEG_ONE*3,I32F32::ONE]));
}
#[test]
@@ -220,12 +285,25 @@ fn test_zeroes_normal(){
fn test_zeroes_deferred_division(){
// (x-1)*(x+1)
// x^2-1
let zeroes=I32F32::zeroes2(I32F32::NEG_ONE,I32F32::ZERO,I32F32::ONE);
let zeroes=F64_32::zeroes2(F64_32::NEG_ONE,F64_32::ZERO,F64_32::ONE);
assert_eq!(
zeroes,
arrayvec::ArrayVec::from_iter([
ratio_ops::ratio::Ratio::new(I32F32::ONE*2,I32F32::NEG_ONE*2),
ratio_ops::ratio::Ratio::new(I32F32::ONE*2,I32F32::ONE*2),
ratio_ops::ratio::Ratio::new(F64_32::ONE*2,F64_32::NEG_ONE*2),
ratio_ops::ratio::Ratio::new(F64_32::ONE*2,F64_32::ONE*2),
])
);
}
#[test]
fn test_debug(){
assert_eq!(format!("{:?}",F64_32::EPSILON),"0.00000001");
assert_eq!(format!("{:?}",F64_32::ONE),"1.00000000");
assert_eq!(format!("{:?}",F64_32::TWO),"2.00000000");
assert_eq!(format!("{:?}",F64_32::MAX),"7fffffff.ffffffff");
assert_eq!(format!("{:?}",F64_32::try_from(core::f64::consts::PI).unwrap()),"3.243f6a88");
assert_eq!(format!("{:?}",F64_32::NEG_EPSILON),"-0.00000001");
assert_eq!(format!("{:?}",F64_32::NEG_ONE),"-1.00000000");
assert_eq!(format!("{:?}",F64_32::NEG_TWO),"-2.00000000");
assert_eq!(format!("{:?}",F64_32::MIN),"-80000000.00000000");
}

View File

@@ -1,4 +1,7 @@
pub type I32F32=crate::fixed::Fixed<1,32>;
pub type I64F64=crate::fixed::Fixed<2,64>;
pub type I128F128=crate::fixed::Fixed<4,128>;
pub type I256F256=crate::fixed::Fixed<8,256>;
use crate::fixed::BNUM_DIGIT_WIDTH;
pub type F64_32=crate::fixed::Fixed<{64/BNUM_DIGIT_WIDTH},32>;
pub type F128_64=crate::fixed::Fixed<{128/BNUM_DIGIT_WIDTH},64>;
pub type F192_96=crate::fixed::Fixed<{192/BNUM_DIGIT_WIDTH},96>;
pub type F256_128=crate::fixed::Fixed<{256/BNUM_DIGIT_WIDTH},128>;
pub type F320_160=crate::fixed::Fixed<{320/BNUM_DIGIT_WIDTH},160>;
pub type F512_256=crate::fixed::Fixed<{512/BNUM_DIGIT_WIDTH},256>;

View File

@@ -1,18 +1,19 @@
use crate::fixed::Fixed;
use crate::fixed::BNUM_DIGIT_WIDTH;
use arrayvec::ArrayVec;
use std::cmp::Ordering;
macro_rules! impl_zeroes{
($n:expr)=>{
impl Fixed<$n,{$n*32}>{
impl Fixed<{$n/BNUM_DIGIT_WIDTH},{$n>>1}>{
#[inline]
pub fn zeroes2(a0:Self,a1:Self,a2:Self)->ArrayVec<<Self as core::ops::Div>::Output,2>{
let a2pos=match a2.cmp(&Self::ZERO){
Ordering::Greater=>true,
Ordering::Equal=>return ArrayVec::from_iter(Self::zeroes1(a0,a1).into_iter()),
Ordering::Equal=>return ArrayVec::from_iter(Self::zeroes1(a0,a1)),
Ordering::Less=>false,
};
let radicand=a1*a1-a2*a0*4;
let radicand=a1*a1-((a2*a0)<<2);
match radicand.cmp(&<Self as core::ops::Mul>::Output::ZERO){
Ordering::Greater=>{
// using wrap because sqrt always halves the number of leading digits.
@@ -21,21 +22,21 @@ macro_rules! impl_zeroes{
let planar_radicand=radicand.sqrt().[<wrap_ $n>]();
}
//sort roots ascending and avoid taking the difference of large numbers
let zeroes=match (a2pos,Self::ZERO<a1){
(true, true )=>[(-a1-planar_radicand)/(a2*2),(a0*2)/(-a1-planar_radicand)],
(true, false)=>[(a0*2)/(-a1+planar_radicand),(-a1+planar_radicand)/(a2*2)],
(false,true )=>[(a0*2)/(-a1-planar_radicand),(-a1-planar_radicand)/(a2*2)],
(false,false)=>[(-a1+planar_radicand)/(a2*2),(a0*2)/(-a1+planar_radicand)],
let zeroes=match (a2pos,a1.is_positive()){
(true, true )=>[(-a1-planar_radicand)/(a2<<1),(a0<<1)/(-a1-planar_radicand)],
(true, false)=>[(a0<<1)/(-a1+planar_radicand),(-a1+planar_radicand)/(a2<<1)],
(false,true )=>[(a0<<1)/(-a1-planar_radicand),(-a1-planar_radicand)/(a2<<1)],
(false,false)=>[(-a1+planar_radicand)/(a2<<1),(a0<<1)/(-a1+planar_radicand)],
};
ArrayVec::from_iter(zeroes)
},
Ordering::Equal=>ArrayVec::from_iter([(a1)/(a2*-2)]),
Ordering::Equal=>ArrayVec::from_iter([(a1)/(-a2<<1)]),
Ordering::Less=>ArrayVec::new_const(),
}
}
#[inline]
pub fn zeroes1(a0:Self,a1:Self)->ArrayVec<<Self as core::ops::Div>::Output,1>{
if a1==Self::ZERO{
if a1.is_zero(){
ArrayVec::new_const()
}else{
ArrayVec::from_iter([(-a0)/(a1)])
@@ -44,10 +45,10 @@ macro_rules! impl_zeroes{
}
};
}
impl_zeroes!(1);
impl_zeroes!(2);
impl_zeroes!(3);
impl_zeroes!(4);
impl_zeroes!(64);
impl_zeroes!(128);
impl_zeroes!(192);
impl_zeroes!(256);
//sqrt doubles twice!
//impl_zeroes!(5);
//impl_zeroes!(6);

View File

@@ -1,6 +1,6 @@
[package]
name = "linear_ops"
version = "0.1.1"
version = "0.2.0"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -14,9 +14,12 @@ fixed-wide=["dep:fixed_wide","dep:paste"]
deferred-division=["dep:ratio_ops"]
[dependencies]
ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet", optional = true }
fixed_wide = { version = "0.2.0", path = "../fixed_wide", registry = "strafesnet", optional = true }
ratio_ops = { workspace = true, optional = true }
fixed_wide = { workspace = true, optional = true }
paste = { version = "1.0.15", optional = true }
[dev-dependencies]
fixed_wide = { path = "../fixed_wide", registry = "strafesnet", features = ["wide-mul"] }
fixed_wide = { workspace = true, features = ["wide-mul"] }
[lints]
workspace = true

View File

@@ -5,17 +5,17 @@ macro_rules! impl_fixed_wide_vector_not_const_generic {
(),
$n:expr
) => {
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<$n,{$n*32}>>{
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<{$n>>3},{$n>>1}>>{
#[inline]
pub fn length(self)-><fixed_wide::fixed::Fixed::<$n,{$n*32}> as core::ops::Mul>::Output{
pub fn length(self)-><fixed_wide::fixed::Fixed::<{$n>>3},{$n>>1}> as core::ops::Mul>::Output{
self.length_squared().sqrt_unchecked()
}
#[inline]
pub fn with_length<U,V>(self,length:U)-><Vector<N,V> as core::ops::Div<<fixed_wide::fixed::Fixed::<$n,{$n*32}> as core::ops::Mul>::Output>>::Output
pub fn with_length<U,V>(self,length:U)-><Vector<N,V> as core::ops::Div<<fixed_wide::fixed::Fixed::<{$n>>3},{$n>>1}> as core::ops::Mul>::Output>>::Output
where
fixed_wide::fixed::Fixed<$n,{$n*32}>:core::ops::Mul<U,Output=V>,
fixed_wide::fixed::Fixed<{$n>>3},{$n>>1}>:core::ops::Mul<U,Output=V>,
U:Copy,
V:core::ops::Div<<fixed_wide::fixed::Fixed::<$n,{$n*32}> as core::ops::Mul>::Output>,
V:core::ops::Div<<fixed_wide::fixed::Fixed::<{$n>>3},{$n>>1}> as core::ops::Mul>::Output>,
{
self*length/self.length()
}
@@ -27,7 +27,7 @@ macro_rules! impl_fixed_wide_vector_not_const_generic {
#[macro_export(local_inner_macros)]
macro_rules! macro_4 {
( $macro: ident, $any:tt ) => {
$crate::macro_repeated!($macro,$any,1,2,3,4);
$crate::macro_repeated!($macro,$any,64,128,192,256);
}
}
@@ -39,40 +39,40 @@ macro_rules! impl_fixed_wide_vector {
// I LOVE NOT BEING ABLE TO USE CONST GENERICS
$crate::macro_repeated!(
impl_narrow_not_const_generic,(),
(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1),(11,1),(12,1),(13,1),(14,1),(15,1),(16,1),(17,1),
(3,2),(4,2),(5,2),(6,2),(7,2),(8,2),(9,2),(10,2),(11,2),(12,2),(13,2),(14,2),(15,2),(16,2),
(4,3),(5,3),(6,3),(7,3),(8,3),(9,3),(10,3),(11,3),(12,3),(13,3),(14,3),(15,3),(16,3),
(5,4),(6,4),(7,4),(8,4),(9,4),(10,4),(11,4),(12,4),(13,4),(14,4),(15,4),(16,4),
(6,5),(7,5),(8,5),(9,5),(10,5),(11,5),(12,5),(13,5),(14,5),(15,5),(16,5),
(7,6),(8,6),(9,6),(10,6),(11,6),(12,6),(13,6),(14,6),(15,6),(16,6),
(8,7),(9,7),(10,7),(11,7),(12,7),(13,7),(14,7),(15,7),(16,7),
(9,8),(10,8),(11,8),(12,8),(13,8),(14,8),(15,8),(16,8),
(10,9),(11,9),(12,9),(13,9),(14,9),(15,9),(16,9),
(11,10),(12,10),(13,10),(14,10),(15,10),(16,10),
(12,11),(13,11),(14,11),(15,11),(16,11),
(13,12),(14,12),(15,12),(16,12),
(14,13),(15,13),(16,13),
(15,14),(16,14),
(16,15)
(128,64),(192,64),(256,64),(320,64),(384,64),(448,64),(512,64),(576,64),(640,64),(704,64),(768,64),(832,64),(896,64),(960,64),(1024,64),(1088,64),
(192,128),(256,128),(320,128),(384,128),(448,128),(512,128),(576,128),(640,128),(704,128),(768,128),(832,128),(896,128),(960,128),(1024,128),
(256,192),(320,192),(384,192),(448,192),(512,192),(576,192),(640,192),(704,192),(768,192),(832,192),(896,192),(960,192),(1024,192),
(320,256),(384,256),(448,256),(512,256),(576,256),(640,256),(704,256),(768,256),(832,256),(896,256),(960,256),(1024,256),
(384,320),(448,320),(512,320),(576,320),(640,320),(704,320),(768,320),(832,320),(896,320),(960,320),(1024,320),
(448,384),(512,384),(576,384),(640,384),(704,384),(768,384),(832,384),(896,384),(960,384),(1024,384),
(512,448),(576,448),(640,448),(704,448),(768,448),(832,448),(896,448),(960,448),(1024,448),
(576,512),(640,512),(704,512),(768,512),(832,512),(896,512),(960,512),(1024,512),
(640,576),(704,576),(768,576),(832,576),(896,576),(960,576),(1024,576),
(704,640),(768,640),(832,640),(896,640),(960,640),(1024,640),
(768,704),(832,704),(896,704),(960,704),(1024,704),
(832,768),(896,768),(960,768),(1024,768),
(896,832),(960,832),(1024,832),
(960,896),(1024,896),
(1024,960)
);
$crate::macro_repeated!(
impl_widen_not_const_generic,(),
(1,2),
(1,3),(2,3),
(1,4),(2,4),(3,4),
(1,5),(2,5),(3,5),(4,5),
(1,6),(2,6),(3,6),(4,6),(5,6),
(1,7),(2,7),(3,7),(4,7),(5,7),(6,7),
(1,8),(2,8),(3,8),(4,8),(5,8),(6,8),(7,8),
(1,9),(2,9),(3,9),(4,9),(5,9),(6,9),(7,9),(8,9),
(1,10),(2,10),(3,10),(4,10),(5,10),(6,10),(7,10),(8,10),(9,10),
(1,11),(2,11),(3,11),(4,11),(5,11),(6,11),(7,11),(8,11),(9,11),(10,11),
(1,12),(2,12),(3,12),(4,12),(5,12),(6,12),(7,12),(8,12),(9,12),(10,12),(11,12),
(1,13),(2,13),(3,13),(4,13),(5,13),(6,13),(7,13),(8,13),(9,13),(10,13),(11,13),(12,13),
(1,14),(2,14),(3,14),(4,14),(5,14),(6,14),(7,14),(8,14),(9,14),(10,14),(11,14),(12,14),(13,14),
(1,15),(2,15),(3,15),(4,15),(5,15),(6,15),(7,15),(8,15),(9,15),(10,15),(11,15),(12,15),(13,15),(14,15),
(1,16),(2,16),(3,16),(4,16),(5,16),(6,16),(7,16),(8,16),(9,16),(10,16),(11,16),(12,16),(13,16),(14,16),(15,16),
(1,17)
(64,128),
(64,192),(128,192),
(64,256),(128,256),(192,256),
(64,320),(128,320),(192,320),(256,320),
(64,384),(128,384),(192,384),(256,384),(320,384),
(64,448),(128,448),(192,448),(256,448),(320,448),(384,448),
(64,512),(128,512),(192,512),(256,512),(320,512),(384,512),(448,512),
(64,576),(128,576),(192,576),(256,576),(320,576),(384,576),(448,576),(512,576),
(64,640),(128,640),(192,640),(256,640),(320,640),(384,640),(448,640),(512,640),(576,640),
(64,704),(128,704),(192,704),(256,704),(320,704),(384,704),(448,704),(512,704),(576,704),(640,704),
(64,768),(128,768),(192,768),(256,768),(320,768),(384,768),(448,768),(512,768),(576,768),(640,768),(704,768),
(64,832),(128,832),(192,832),(256,832),(320,832),(384,832),(448,832),(512,832),(576,832),(640,832),(704,832),(768,832),
(64,896),(128,896),(192,896),(256,896),(320,896),(384,896),(448,896),(512,896),(576,896),(640,896),(704,896),(768,896),(832,896),
(64,960),(128,960),(192,960),(256,960),(320,960),(384,960),(448,960),(512,960),(576,960),(640,960),(704,960),(768,960),(832,960),(896,960),
(64,1024),(128,1024),(192,1024),(256,1024),(320,1024),(384,1024),(448,1024),(512,1024),(576,1024),(640,1024),(704,1024),(768,1024),(832,1024),(896,1024),(960,1024),
(64,1088)
);
impl<const N:usize,T:fixed_wide::fixed::Wrap<U>,U> fixed_wide::fixed::Wrap<Vector<N,U>> for Vector<N,T>
{
@@ -98,17 +98,17 @@ macro_rules! impl_narrow_not_const_generic{
($lhs:expr,$rhs:expr)
)=>{
paste::item!{
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<$lhs,{$lhs*32}>>{
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<{$lhs>>3},{$lhs>>1}>>{
#[inline]
pub fn [<wrap_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
pub fn [<wrap_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<{$rhs>>3},{$rhs>>1}>>{
self.map(|t|t.[<wrap_ $rhs>]())
}
#[inline]
pub fn [<narrow_ $rhs>](self)->Vector<N,Result<fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>,fixed_wide::fixed::NarrowError>>{
pub fn [<narrow_ $rhs>](self)->Vector<N,Result<fixed_wide::fixed::Fixed<{$rhs>>3},{$rhs>>1}>,fixed_wide::fixed::NarrowError>>{
self.map(|t|t.[<narrow_ $rhs>]())
}
#[inline]
pub fn [<clamp_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
pub fn [<clamp_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<{$rhs>>3},{$rhs>>1}>>{
self.map(|t|t.[<clamp_ $rhs>]())
}
}
@@ -123,9 +123,9 @@ macro_rules! impl_widen_not_const_generic{
($lhs:expr,$rhs:expr)
)=>{
paste::item!{
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<$lhs,{$lhs*32}>>{
impl<const N:usize> Vector<N,fixed_wide::fixed::Fixed<{$lhs>>3},{$lhs>>1}>>{
#[inline]
pub fn [<widen_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<$rhs,{$rhs*32}>>{
pub fn [<widen_ $rhs>](self)->Vector<N,fixed_wide::fixed::Fixed<{$rhs>>3},{$rhs>>1}>>{
self.map(|t|t.[<widen_ $rhs>]())
}
}

View File

@@ -205,7 +205,8 @@ macro_rules! impl_matrix_named_fields_shape {
#[inline]
fn deref(&self)->&Self::Target{
// This cast is valid because Matrix has #[repr(transparent)]
let ptr=&self.array as *const [[T;$size_inner];$size_outer] as *const Self::Target;
let ptr:*const [[T;$size_inner];$size_outer]=&self.array;
let ptr=ptr as *const Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&*ptr}
}
@@ -214,7 +215,8 @@ macro_rules! impl_matrix_named_fields_shape {
#[inline]
fn deref_mut(&mut self)->&mut Self::Target{
// This cast is valid because Matrix has #[repr(transparent)]
let ptr=&mut self.array as *mut [[T;$size_inner];$size_outer] as *mut Self::Target;
let ptr:*mut [[T;$size_inner];$size_outer]=&mut self.array;
let ptr=ptr as *mut Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&mut*ptr}
}

View File

@@ -331,7 +331,8 @@ macro_rules! impl_vector_named_fields {
#[inline]
fn deref(&self)->&Self::Target{
// This cast is valid because Vector has #[repr(transparent)]
let ptr=&self.array as *const [T;$size] as *const Self::Target;
let ptr:*const [T;$size]=&self.array;
let ptr=ptr as *const Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&*ptr}
}
@@ -340,7 +341,8 @@ macro_rules! impl_vector_named_fields {
#[inline]
fn deref_mut(&mut self)->&mut Self::Target{
// This cast is valid because Vector has #[repr(transparent)]
let ptr=&mut self.array as *mut [T;$size] as *mut Self::Target;
let ptr:*mut [T;$size]=&mut self.array;
let ptr=ptr as *mut Self::Target;
// SAFETY: this pointer is non-null because it comes from a reference
unsafe{&mut*ptr}
}

View File

@@ -1,9 +1,9 @@
use crate::types::{Matrix3,Matrix3x2,Matrix3x4,Matrix4x2,Vector3};
type Planar64=fixed_wide::types::I32F32;
type Planar64Wide1=fixed_wide::types::I64F64;
type Planar64=fixed_wide::types::F64_32;
type Planar64Wide1=fixed_wide::types::F128_64;
//type Planar64Wide2=fixed_wide::types::I128F128;
type Planar64Wide3=fixed_wide::types::I256F256;
type Planar64Wide3=fixed_wide::types::F512_256;
#[test]
fn wide_vec3(){
@@ -72,7 +72,7 @@ fn wide_matrix_det(){
]);
// In[2]:= Det[{{1, 2, 3}, {4, 5, 7}, {6, 8, 9}}]
// Out[2]= 7
assert_eq!(m.det(),fixed_wide::fixed::Fixed::<3,96>::from(7));
assert_eq!(m.det(),fixed_wide::types::F192_96::from(7));
}
#[test]

View File

@@ -8,3 +8,6 @@ description = "Ratio operations using trait bounds for avoiding division like th
authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
[lints]
workspace = true

View File

@@ -1,6 +1,6 @@
[package]
name = "strafesnet_rbx_loader"
version = "0.7.0"
version = "0.10.2"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -11,15 +11,18 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
bytemuck = "1.14.3"
glam = "0.30.0"
lazy-regex = "3.1.0"
rbx_binary = { version = "1.1.0-sn4", registry = "strafesnet" }
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] }
rbx_mesh = "0.5.0"
rbx_reflection = "5.0.0"
rbx_reflection_database = "1.0.0"
rbx_xml = { version = "1.1.0-sn4", registry = "strafesnet" }
glam.workspace = true
regex = { version = "1.11.3", default-features = false, features = ["unicode-perl"] }
rbx_mesh = "0.6.0"
rbxassetid = { version = "0.1.0", path = "../rbxassetid", registry = "strafesnet" }
roblox_emulator = { version = "0.5.1", path = "../roblox_emulator", default-features = false, registry = "strafesnet" }
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.1", path = "../deferred_loader", registry = "strafesnet" }
strafesnet_common.workspace = true
strafesnet_deferred_loader.workspace = true
rbx_binary = "2.0.1"
rbx_dom_weak = "4.1.0"
rbx_reflection = "6.1.0"
rbx_reflection_database = "2.0.2"
rbx_xml = "2.0.1"
[lints]
workspace = true

View File

@@ -25,6 +25,8 @@ pub struct RecoverableErrors{
pub unsupported_class:HashSet<String>,
/// A decal has an invalid / missing property.
pub decal_property:Vec<InstancePath>,
/// A SurfaceAppearance has an invalid / missing property.
pub surface_appearance_property:Vec<InstancePath>,
/// A decal has an invalid normal_id.
pub normal_id:Vec<NormalIdError>,
/// A texture has an invalid / missing property.
@@ -58,6 +60,7 @@ impl RecoverableErrors{
self.meshpart_content.len()+
self.unsupported_class.len()+
self.decal_property.len()+
self.surface_appearance_property.len()+
self.normal_id.len()+
self.texture_property.len()+
self.mode_id_parse_int.len()+
@@ -145,6 +148,7 @@ impl core::fmt::Display for RecoverableErrors{
}
}
write_instance_path_error!(f,self,decal_property,"Decal is","Decals are","missing a property");
write_instance_path_error!(f,self,surface_appearance_property,"SurfaceAppearance is","SurfaceAppearances are","missing a property");
write_bespoke_error!(f,self,normal_id,"Decal","Decals","NormalId is invalid",path,normal_id);
write_instance_path_error!(f,self,texture_property,"Texture is","Textures are","missing a property");
write_bespoke_error!(f,self,mode_id_parse_int,"ModeId","ModeIds","failed to parse",context,error);

View File

@@ -1,41 +1,22 @@
use std::io::Read;
use rbx_dom_weak::WeakDom;
use roblox_emulator::context::Context;
use strafesnet_common::map::CompleteMap;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
pub use error::RecoverableErrors;
pub use roblox_emulator::runner::Error as RunnerError;
mod rbx;
mod mesh;
pub mod rbx;
pub mod mesh;
mod error;
mod union;
pub mod loader;
mod primitives;
pub mod data{
pub struct RobloxMeshBytes(Vec<u8>);
impl RobloxMeshBytes{
pub fn new(bytes:Vec<u8>)->Self{
Self(bytes)
}
pub(crate) fn cursor(self)->std::io::Cursor<Vec<u8>>{
std::io::Cursor::new(self.0)
}
}
}
pub mod union;
pub mod primitives;
pub struct Model{
dom:WeakDom,
}
impl Model{
fn new(dom:WeakDom)->Self{
pub fn new(dom:WeakDom)->Self{
Self{dom}
}
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
to_snf(self,failure_mode)
}
}
impl AsRef<WeakDom> for Model{
fn as_ref(&self)->&WeakDom{
@@ -66,9 +47,6 @@ impl Place{
}
Ok(errors)
}
pub fn to_snf(&self,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
to_snf(self,failure_mode)
}
}
impl AsRef<WeakDom> for Place{
fn as_ref(&self)->&WeakDom{
@@ -83,72 +61,3 @@ impl From<Model> for Place{
}
}
}
#[derive(Debug)]
pub enum ReadError{
RbxBinary(rbx_binary::DecodeError),
RbxXml(rbx_xml::DecodeError),
Io(std::io::Error),
UnknownFileFormat,
}
impl std::fmt::Display for ReadError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ReadError{}
pub fn read<R:Read>(input:R)->Result<Model,ReadError>{
let mut buf=std::io::BufReader::new(input);
let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?;
match peek.get(0..8){
Some(b"<roblox!")=>rbx_binary::from_reader(buf).map(Model::new).map_err(ReadError::RbxBinary),
Some(b"<roblox ")=>rbx_xml::from_reader_default(buf).map(Model::new).map_err(ReadError::RbxXml),
_=>Err(ReadError::UnknownFileFormat),
}
}
#[derive(Debug)]
pub enum LoadError{
Texture(loader::TextureError),
Mesh(loader::MeshError),
}
impl std::fmt::Display for LoadError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for LoadError{}
impl From<loader::TextureError> for LoadError{
fn from(value:loader::TextureError)->Self{
Self::Texture(value)
}
}
impl From<loader::MeshError> for LoadError{
fn from(value:loader::MeshError)->Self{
Self::Mesh(value)
}
}
fn to_snf(dom:impl AsRef<WeakDom>,failure_mode:LoadFailureMode)->Result<(CompleteMap,RecoverableErrors),LoadError>{
let dom=dom.as_ref();
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
let mut mesh_deferred_loader=MeshDeferredLoader::new();
let map_step1=rbx::convert(
dom,
&mut texture_deferred_loader,
&mut mesh_deferred_loader,
);
let mut mesh_loader=loader::MeshLoader::new();
let meshpart_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,failure_mode).map_err(LoadError::Mesh)?;
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(meshpart_meshes);
let mut texture_loader=loader::TextureLoader::new();
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,failure_mode).map_err(LoadError::Texture)?;
Ok(map_step2.add_render_configs_and_textures(render_configs))
}

View File

@@ -5,12 +5,73 @@ use strafesnet_common::aabb::Aabb;
use strafesnet_common::integer::vec3;
use strafesnet_common::model::{self,ColorId,IndexedVertex,PolygonGroup,PolygonList,RenderConfigId,VertexId};
use crate::loader::MeshWithSize;
use crate::rbx::RobloxPartDescription;
#[derive(Hash,Eq,PartialEq)]
pub enum MeshType<'a>{
FileMesh,
Union{
mesh_data:&'a [u8],
physics_data:&'a [u8],
size_float_bits:[u32;3],
part_texture_description:RobloxPartDescription,
},
}
#[derive(Hash,Eq,PartialEq)]
pub struct MeshIndex<'a>{
pub mesh_type:MeshType<'a>,
pub content:&'a str,
}
impl MeshIndex<'_>{
pub fn file_mesh(content:&str)->MeshIndex<'_>{
MeshIndex{
mesh_type:MeshType::FileMesh,
content,
}
}
pub fn union<'a>(
content:&'a str,
mesh_data:&'a [u8],
physics_data:&'a [u8],
size:&rbx_dom_weak::types::Vector3,
part_texture_description:RobloxPartDescription,
)->MeshIndex<'a>{
MeshIndex{
mesh_type:MeshType::Union{
mesh_data,
physics_data,
size_float_bits:[size.x.to_bits(),size.y.to_bits(),size.z.to_bits()],
part_texture_description,
},
content,
}
}
}
#[derive(Clone)]
pub struct MeshWithSize{
mesh:model::Mesh,
size:strafesnet_common::integer::Planar64Vec3,
}
impl MeshWithSize{
pub(crate) const fn new(
mesh:model::Mesh,
size:strafesnet_common::integer::Planar64Vec3,
)->Self{
Self{mesh,size}
}
pub const fn mesh(&self)->&model::Mesh{
&self.mesh
}
pub const fn size(&self)->strafesnet_common::integer::Planar64Vec3{
self.size
}
}
#[derive(Debug)]
pub enum Error{
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
RbxMesh(rbx_mesh::mesh::Error)
NoPolygons,
RbxMesh(rbx_mesh::mesh::Error),
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
@@ -22,41 +83,47 @@ impl std::error::Error for Error{}
fn ingest_vertices2(
vertices:Vec<Vertex2>,
mb:&mut model::MeshBuilder,
)->Result<HashMap<rbx_mesh::mesh::VertexId2,VertexId>,Error>{
)->HashMap<rbx_mesh::mesh::VertexId2,VertexId>{
//this monster is collecting a map of old_vertices_index -> unique_vertices_index
//while also doing the inserting unique entries into lists simultaneously
Ok(vertices.into_iter().enumerate().map(|(vertex_id,vertex)|Ok((
// vertex positions that fail to convert are DROPPED
vertices.into_iter().enumerate().filter_map(|(vertex_id,vertex)|Some((
rbx_mesh::mesh::VertexId2(vertex_id as u32),
{
let vertex=IndexedVertex{
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?),
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos).ok()?),
tex:mb.acquire_tex_id(glam::Vec2::from_array(vertex.tex)),
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?),
color:mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32)))
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm).ok()?),
color:mb.acquire_color_id(glam::Vec4::from_array(vertex.color.map(|f|f as f32/255.0f32))),
};
mb.acquire_vertex_id(vertex)
}
))).collect::<Result<_,_>>().map_err(Error::Planar64Vec3)?)
))).collect()
}
fn ingest_vertices_truncated2(
vertices:Vec<Vertex2Truncated>,
mb:&mut model::MeshBuilder,
static_color_id:ColorId,//pick one color and fill everything with it
)->Result<HashMap<rbx_mesh::mesh::VertexId2,VertexId>,Error>{
)->HashMap<rbx_mesh::mesh::VertexId2,VertexId>{
//this monster is collecting a map of old_vertices_index -> unique_vertices_index
//while also doing the inserting unique entries into lists simultaneously
Ok(vertices.into_iter().enumerate().map(|(vertex_id,vertex)|Ok((
// vertex positions that fail to convert are DROPPED
vertices.into_iter().enumerate().filter_map(|(vertex_id,vertex)|Some((
rbx_mesh::mesh::VertexId2(vertex_id as u32),
{
let vertex=IndexedVertex{
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?),
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos).ok()?),
tex:mb.acquire_tex_id(glam::Vec2::from_array(vertex.tex)),
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?),
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm).ok()?),
color:static_color_id,
};
mb.acquire_vertex_id(vertex)
}
))).collect::<Result<_,_>>().map_err(Error::Planar64Vec3)?)
))).collect()
}
fn new_polygon_list_checked(list:Vec<model::IndexedVertexList>)->Option<PolygonList>{
(!list.is_empty()).then_some(PolygonList::new(list))
}
fn ingest_faces2_lods3(
@@ -66,32 +133,35 @@ fn ingest_faces2_lods3(
lods:&[rbx_mesh::mesh::Lod3],
){
//faces have to be split into polygon groups based on lod
polygon_groups.extend(lods.windows(2).map(|lod_pair|
PolygonGroup::PolygonList(PolygonList::new(faces[lod_pair[0].0 as usize..lod_pair[1].0 as usize].iter().map(|rbx_mesh::mesh::Face2(v0,v1,v2)|
vec![vertex_id_map[&v0],vertex_id_map[&v1],vertex_id_map[&v2]]
).collect()))
polygon_groups.extend(lods.windows(2).filter_map(|lod_pair|
Some(PolygonGroup::PolygonList(new_polygon_list_checked(faces[lod_pair[0].0 as usize..lod_pair[1].0 as usize].iter().filter_map(|rbx_mesh::mesh::Face2(v0,v1,v2)|
Some(vec![*vertex_id_map.get(&v0)?,*vertex_id_map.get(&v1)?,*vertex_id_map.get(&v2)?])
).collect())?))
))
}
pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithSize,Error>{
pub fn convert(roblox_mesh_bytes:&[u8])->Result<MeshWithSize,Error>{
//generate that mesh boi
let mut polygon_groups=Vec::new();
let mut mb=model::MeshBuilder::new();
match rbx_mesh::read_versioned(roblox_mesh_bytes.cursor()).map_err(Error::RbxMesh)?{
match rbx_mesh::read_versioned(std::io::Cursor::new(roblox_mesh_bytes)).map_err(Error::RbxMesh)?{
rbx_mesh::mesh::Mesh::V1(mesh)=>{
let color_id=mb.acquire_color_id(glam::Vec4::ONE);
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.vertices.chunks_exact(3).map(|trip|{
let polygon_list=new_polygon_list_checked(mesh.vertices.chunks_exact(3).filter_map(|trip|{
let mut ingest_vertex1=|vertex:&rbx_mesh::mesh::Vertex1|{
let vertex=IndexedVertex{
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos)?),
pos:mb.acquire_pos_id(vec3::try_from_f32_array(vertex.pos).ok()?),
tex:mb.acquire_tex_id(glam::vec2(vertex.tex[0],vertex.tex[1])),
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm)?),
normal:mb.acquire_normal_id(vec3::try_from_f32_array(vertex.norm).ok()?),
color:color_id,
};
Ok(mb.acquire_vertex_id(vertex))
Some(mb.acquire_vertex_id(vertex))
};
Ok(vec![ingest_vertex1(&trip[0])?,ingest_vertex1(&trip[1])?,ingest_vertex1(&trip[2])?])
}).collect::<Result<_,_>>().map_err(Error::Planar64Vec3)?)));
Some(vec![ingest_vertex1(&trip[0])?,ingest_vertex1(&trip[1])?,ingest_vertex1(&trip[2])?])
}).collect());
if let Some(polygon_list)=polygon_list{
polygon_groups.push(PolygonGroup::PolygonList(polygon_list));
}
},
rbx_mesh::mesh::Mesh::V2(mesh)=>{
let vertex_id_map=match mesh.header.sizeof_vertex{
@@ -101,11 +171,14 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithS
ingest_vertices_truncated2(mesh.vertices_truncated,&mut mb,color_id)
},
rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut mb),
}?;
};
//one big happy group for all the faces
polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|face|
vec![vertex_id_map[&face.0],vertex_id_map[&face.1],vertex_id_map[&face.2]]
).collect())));
let polygon_list=new_polygon_list_checked(mesh.faces.into_iter().filter_map(|face|
Some(vec![*vertex_id_map.get(&face.0)?,*vertex_id_map.get(&face.1)?,*vertex_id_map.get(&face.2)?])
).collect());
if let Some(polygon_list)=polygon_list{
polygon_groups.push(PolygonGroup::PolygonList(polygon_list));
}
},
rbx_mesh::mesh::Mesh::V3(mesh)=>{
let vertex_id_map=match mesh.header.sizeof_vertex{
@@ -114,18 +187,21 @@ pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result<MeshWithS
ingest_vertices_truncated2(mesh.vertices_truncated,&mut mb,color_id)
},
rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut mb),
}?;
};
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
},
rbx_mesh::mesh::Mesh::V4(mesh)=>{
let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb)?;
let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb);
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
},
rbx_mesh::mesh::Mesh::V5(mesh)=>{
let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb)?;
let vertex_id_map=ingest_vertices2(mesh.vertices,&mut mb);
ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods);
},
}
if polygon_groups.is_empty(){
return Err(Error::NoPolygons);
}
let mesh=mb.build(
polygon_groups,
//these should probably be moved to the model...

View File

@@ -519,7 +519,7 @@ pub fn unit_cylinder(face_descriptions:CubeFaceDescription)->Mesh{
(glam::vec2(-x as f32,y as f32).normalize()+1.0)/2.0
)
);
let pos=mb.acquire_pos_id($end+vec3::int(0,-x,y).with_length(Planar64::ONE).divide().wrap_1());
let pos=mb.acquire_pos_id($end+vec3::int(0,-x,y).with_length(Planar64::ONE).divide().wrap_64());
mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color})
}).collect();
@@ -560,9 +560,9 @@ pub fn unit_cylinder(face_descriptions:CubeFaceDescription)->Mesh{
let mut polygon_list=Vec::with_capacity(CubeFaceDescription::FACES);
for $loop in -GON..GON{
// lo Z
let lz_dir=$lo_dir.with_length(Planar64::ONE).divide().wrap_1();
let lz_dir=$lo_dir.with_length(Planar64::ONE).divide().wrap_64();
// hi Z
let hz_dir=$hi_dir.with_length(Planar64::ONE).divide().wrap_1();
let hz_dir=$hi_dir.with_length(Planar64::ONE).divide().wrap_64();
// pos
let lx_lz_pos=mb.acquire_pos_id(vec3::NEG_X+lz_dir);

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use crate::error::{RecoverableErrors,CFrameError,CFrameErrorType,DuplicateStageError,InstancePath,NormalIdError,Planar64ConvertError,ParseIntContext,ShapeError};
use crate::loader::{MeshWithSize,MeshIndex};
use crate::mesh::{MeshWithSize,MeshIndex};
use crate::primitives::{self,CubeFace,CubeFaceDescription,WedgeFaceDescription,CornerWedgeFaceDescription,FaceDescription,Primitives};
use strafesnet_common::map;
use strafesnet_common::model;
@@ -18,15 +18,24 @@ fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}
macro_rules! lazy_regex{
($r:literal)=>{{
use regex::Regex;
use std::sync::LazyLock;
static RE:LazyLock<Regex>=LazyLock::new(||Regex::new($r).unwrap());
&RE
}};
}
fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_weak::types::Vector3)->Result<Planar64Affine3,Planar64TryFromFloatError>{
Ok(Planar64Affine3::new(
Planar64Mat3::from_cols([
(vec3::try_from_f32_array([cf.orientation.x.x,cf.orientation.y.x,cf.orientation.z.x])?
*integer::try_from_f32(size.x/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
*integer::try_from_f32(size.x/2.0)?).narrow_64().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
(vec3::try_from_f32_array([cf.orientation.x.y,cf.orientation.y.y,cf.orientation.z.y])?
*integer::try_from_f32(size.y/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
*integer::try_from_f32(size.y/2.0)?).narrow_64().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
(vec3::try_from_f32_array([cf.orientation.x.z,cf.orientation.y.z,cf.orientation.z.z])?
*integer::try_from_f32(size.z/2.0)?).narrow_1().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
*integer::try_from_f32(size.z/2.0)?).narrow_64().unwrap(),//.map_err(Planar64ConvertError::Narrow)?
]),
vec3::try_from_f32_array([cf.position.x,cf.position.y,cf.position.z])?
))
@@ -118,7 +127,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
);
},
other=>{
let regman=lazy_regex::regex!(r"^(BonusStart|WormholeOut)(\d+)$");
let regman=lazy_regex!(r"^(BonusStart|WormholeOut)(\d+)$");
if let Some(captures)=regman.captures(other){
match &captures[1]{
"BonusStart"=>{
@@ -144,7 +153,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
},
_=>(),
}
}else if let Some(captures)=lazy_regex::regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$")
}else if let Some(captures)=lazy_regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$")
.captures(other){
force_intersecting=true;
let stage_id=StageId::new(ParseIntContext::parse(&captures[3]).map_err(GetAttributesError::StageIdParseInt)?);
@@ -185,7 +194,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
stage_element,
),
);
}else if let Some(captures)=lazy_regex::regex!(r"^(Jump|WormholeIn)(\d+)$")
}else if let Some(captures)=lazy_regex!(r"^(Jump|WormholeIn)(\d+)$")
.captures(other){
match &captures[1]{
"Jump"=>modes_builder.push_mode_update(
@@ -210,7 +219,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
},
_=>unreachable!("regex2[1] messed up bad"),
}
}else if let Some(captures)=lazy_regex::regex!(r"^Bonus(Finish|Anticheat)(\d+)$")
}else if let Some(captures)=lazy_regex!(r"^Bonus(Finish|Anticheat)(\d+)$")
.captures(other){
force_can_collide=false;
force_intersecting=true;
@@ -242,7 +251,7 @@ fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:mode
}
}
//need some way to skip this
if allow_booster&&velocity!=vec3::ZERO{
if allow_booster&&velocity!=vec3::zero(){
general.booster=Some(attr::Booster::Velocity(velocity));
}
Ok(match force_can_collide{
@@ -335,12 +344,12 @@ pub struct RobloxFaceTextureDescription{
pub color:glam::Vec4,
pub transform:RobloxTextureTransform,
}
impl core::cmp::PartialEq for RobloxFaceTextureDescription{
impl PartialEq for RobloxFaceTextureDescription{
fn eq(&self,other:&Self)->bool{
self.to_bits().eq(&other.to_bits())
}
}
impl core::cmp::Eq for RobloxFaceTextureDescription{}
impl Eq for RobloxFaceTextureDescription{}
impl core::hash::Hash for RobloxFaceTextureDescription{
fn hash<H:core::hash::Hasher>(&self,state:&mut H){
self.to_bits().hash(state);
@@ -499,6 +508,43 @@ fn get_texture_description<'a>(
}
part_texture_description
}
fn get_surface_appearance<'a>(
recoverable_errors:&mut RecoverableErrors,
db:&rbx_reflection::ReflectionDatabase,
dom:&'a rbx_dom_weak::WeakDom,
object:&rbx_dom_weak::Instance,
)->Option<&'a str>{
//use the biggest one and cut it down later...
let decal=&db.classes["SurfaceAppearance"];
let surface_appearances=object.children().iter().filter_map(|&referent|{
let instance=dom.get_by_ref(referent)?;
db.classes.get(instance.class.as_str()).is_some_and(|class|
db.has_superclass(class,decal)
).then_some(instance)
});
for surface_appearance in surface_appearances{
// SurfaceAppearance should always have these properties,
// but it is not guaranteed by the rbx_dom_weak data structure.
let (
Some(rbx_dom_weak::types::Variant::Content(color_map)),
// Some(rbx_dom_weak::types::Variant::Content(_metalness_map)),
// Some(rbx_dom_weak::types::Variant::Content(_normal_map)),
// Some(rbx_dom_weak::types::Variant::Content(_roughness_map)),
)=(
surface_appearance.properties.get(&static_ustr("ColorMapContent")),
// surface_appearance.properties.get(&static_ustr("MetalnessMapContent")),
// surface_appearance.properties.get(&static_ustr("NormalMapContent")),
// surface_appearance.properties.get(&static_ustr("RoughnessMapContent")),
)else{
recoverable_errors.surface_appearance_property.push(InstancePath::new(dom,surface_appearance));
continue;
};
if let Some(texture_id)=get_content_url(color_map){
return Some(texture_id);
}
}
None
}
enum Shape{
Primitive(Primitives),
MeshPart,
@@ -550,7 +596,7 @@ pub fn convert<'a>(
//just going to leave it like this for now instead of reworking the data structures for this whole thing
let textureless_render_group=render_config_deferred_loader.acquire_render_config_id(None);
let db=rbx_reflection_database::get();
let db=rbx_reflection_database::get().unwrap();
let basepart=&db.classes["BasePart"];
let baseparts=dom.descendants().filter(|&instance|
db.classes.get(instance.class.as_str()).is_some_and(|class|
@@ -697,7 +743,8 @@ pub fn convert<'a>(
""
}
};
let texture_asset_id=get_content_url(texture_content);
// load SurfaceAppearance and then fall back to mesh texture
let texture_asset_id=get_surface_appearance(&mut recoverable_errors,db,dom,object).or_else(||get_content_url(texture_content));
(
MeshAvailability::DeferredMesh(render_config_deferred_loader.acquire_render_config_id(texture_asset_id)),
mesh_deferred_loader.acquire_mesh_id(MeshIndex::file_mesh(mesh_asset_id)),
@@ -766,19 +813,19 @@ struct MeshIdWithSize{
mesh:model::MeshId,
size:Planar64Vec3,
}
fn acquire_mesh_id_from_render_config_id<'a>(
fn acquire_mesh_id_from_render_config_id(
primitive_meshes:&mut Vec<model::Mesh>,
mesh_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RenderConfigId,model::MeshId>>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithSize>,
loaded_meshes:&HashMap<model::MeshId,MeshWithSize>,
old_mesh_id:model::MeshId,
render:RenderConfigId,
)->Option<MeshIdWithSize>{
//ignore meshes that fail to load completely for now
loaded_meshes.get(&old_mesh_id).map(|&MeshWithSize{ref mesh,size}|MeshIdWithSize{
loaded_meshes.get(&old_mesh_id).map(|mesh_with_size|MeshIdWithSize{
mesh:*mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new())
.entry(render).or_insert_with(||{
let mesh_id=model::MeshId::new(primitive_meshes.len() as u32);
let mut mesh_clone=mesh.clone();
let mut mesh_clone=mesh_with_size.mesh().clone();
//set the render group lool
if let Some(graphics_group)=mesh_clone.graphics_groups.first_mut(){
graphics_group.render=render;
@@ -786,22 +833,22 @@ fn acquire_mesh_id_from_render_config_id<'a>(
primitive_meshes.push(mesh_clone);
mesh_id
}),
size,
size:mesh_with_size.size(),
})
}
fn acquire_union_id_from_render_config_id<'a>(
fn acquire_union_id_from_render_config_id(
primitive_meshes:&mut Vec<model::Mesh>,
union_id_from_render_config_id:&mut HashMap<model::MeshId,HashMap<RobloxPartDescription,model::MeshId>>,
loaded_meshes:&'a HashMap<model::MeshId,MeshWithSize>,
loaded_meshes:&HashMap<model::MeshId,MeshWithSize>,
old_union_id:model::MeshId,
part_texture_description:RobloxPartDescription,
)->Option<MeshIdWithSize>{
//ignore uniones that fail to load completely for now
loaded_meshes.get(&old_union_id).map(|&MeshWithSize{ref mesh,size}|MeshIdWithSize{
loaded_meshes.get(&old_union_id).map(|mesh_with_size|MeshIdWithSize{
mesh:*union_id_from_render_config_id.entry(old_union_id).or_insert_with(||HashMap::new())
.entry(part_texture_description.clone()).or_insert_with(||{
let union_id=model::MeshId::new(primitive_meshes.len() as u32);
let mut union_clone=mesh.clone();
let mut union_clone=mesh_with_size.mesh().clone();
//set the render groups
for (graphics_group,maybe_face_texture_description) in union_clone.graphics_groups.iter_mut().zip(part_texture_description.0){
if let Some(face_texture_description)=maybe_face_texture_description{
@@ -811,7 +858,7 @@ fn acquire_union_id_from_render_config_id<'a>(
primitive_meshes.push(union_clone);
union_id
}),
size,
size:mesh_with_size.size(),
})
}
pub struct PartialMap1<'a>{
@@ -858,19 +905,19 @@ impl PartialMap1<'_>{
deferred_model_deferred_attributes.model.mesh,
deferred_model_deferred_attributes.render
)?;
Some(ModelDeferredAttributes{
mesh,
deferred_attributes:deferred_model_deferred_attributes.model.deferred_attributes,
color:deferred_model_deferred_attributes.model.color,
transform:Planar64Affine3::new(
Planar64Mat3::from_cols([
(deferred_model_deferred_attributes.model.transform.matrix3.x_axis*2/mesh_size.x).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.y_axis*2/mesh_size.y).divide().narrow_1().unwrap(),
(deferred_model_deferred_attributes.model.transform.matrix3.z_axis*2/mesh_size.z).divide().narrow_1().unwrap(),
]),
deferred_model_deferred_attributes.model.transform.translation
),
})
let mut model=deferred_model_deferred_attributes.model;
model.mesh=mesh;
// avoid devide by zero but introduce more edge cases. not sure what the correct thing to do here is.
if mesh_size.x!=integer::Fixed::ZERO{
model.transform.matrix3.x_axis=(model.transform.matrix3.x_axis*2/mesh_size.x).divide().narrow_64().unwrap();
}
if mesh_size.y!=integer::Fixed::ZERO{
model.transform.matrix3.y_axis=(model.transform.matrix3.y_axis*2/mesh_size.y).divide().narrow_64().unwrap();
}
if mesh_size.z!=integer::Fixed::ZERO{
model.transform.matrix3.z_axis=(model.transform.matrix3.z_axis*2/mesh_size.z).divide().narrow_64().unwrap();
}
Some(model)
}).chain(self.deferred_unions_deferred_attributes.into_iter().flat_map(|deferred_union_deferred_attributes|{
//meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id
//insert into primitive_meshes
@@ -881,19 +928,19 @@ impl PartialMap1<'_>{
deferred_union_deferred_attributes.model.mesh,
deferred_union_deferred_attributes.render
)?;
Some(ModelDeferredAttributes{
mesh,
deferred_attributes:deferred_union_deferred_attributes.model.deferred_attributes,
color:deferred_union_deferred_attributes.model.color,
transform:Planar64Affine3::new(
Planar64Mat3::from_cols([
(deferred_union_deferred_attributes.model.transform.matrix3.x_axis*2/size.x).divide().narrow_1().unwrap(),
(deferred_union_deferred_attributes.model.transform.matrix3.y_axis*2/size.y).divide().narrow_1().unwrap(),
(deferred_union_deferred_attributes.model.transform.matrix3.z_axis*2/size.z).divide().narrow_1().unwrap(),
]),
deferred_union_deferred_attributes.model.transform.translation
),
})
let mut model=deferred_union_deferred_attributes.model;
model.mesh=mesh;
// avoid devide by zero but introduce more edge cases. not sure what the correct thing to do here is.
if size.x!=integer::Fixed::ZERO{
model.transform.matrix3.x_axis=(model.transform.matrix3.x_axis*2/size.x).divide().narrow_64().unwrap();
}
if size.y!=integer::Fixed::ZERO{
model.transform.matrix3.y_axis=(model.transform.matrix3.y_axis*2/size.y).divide().narrow_64().unwrap();
}
if size.z!=integer::Fixed::ZERO{
model.transform.matrix3.z_axis=(model.transform.matrix3.z_axis*2/size.z).divide().narrow_64().unwrap();
}
Some(model)
}))
.chain(self.primitive_models_deferred_attributes.into_iter())
.filter_map(|model_deferred_attributes|{

View File

@@ -1,4 +1,4 @@
use crate::loader::MeshWithSize;
use crate::mesh::MeshWithSize;
use crate::rbx::RobloxPartDescription;
use crate::primitives::{CUBE_DEFAULT_VERTICES,CUBE_DEFAULT_POLYS,FaceDescription};
@@ -104,7 +104,7 @@ fn build_mesh2(
if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
polygon_groups_normal_id[normal_id as usize-1].push(face);
}else{
panic!("Empty face!");
print!("[union] Empty face!");
}
}
Ok(())
@@ -158,12 +158,16 @@ fn build_mesh5(
if let Some(normal_id)=normal_agreement_checker.into_agreed_normal(){
polygon_groups_normal_id[normal_id as usize-1].push(face);
}else{
panic!("Empty face!");
print!("[union] Empty face!");
}
}
Ok(())
}
fn new_polygon_list_checked(list:Vec<model::IndexedVertexList>)->Option<PolygonList>{
(!list.is_empty()).then_some(PolygonList::new(list))
}
const NORMAL_FACES:usize=6;
impl std::error::Error for Error{}
pub fn convert(
@@ -172,12 +176,12 @@ pub fn convert(
size:glam::Vec3,
RobloxPartDescription(part_texture_description):RobloxPartDescription,
)->Result<MeshWithSize,Error>{
let mut polygon_groups_normal_id:[_;NORMAL_FACES]=[vec![],vec![],vec![],vec![],vec![],vec![]];
// build graphics and physics meshes
let mut mb=MeshBuilder::new();
// graphics
let graphics_groups=if !roblox_mesh_data.is_empty(){
let (polygon_groups_normal_id,graphics_groups)=if !roblox_mesh_data.is_empty(){
let mut polygon_groups_normal_id:[_;NORMAL_FACES]=[vec![],vec![],vec![],vec![],vec![],vec![]];
// create per-face texture coordinate affine transforms
let cube_face_description=part_texture_description.map(|opt|opt.map(|mut t|{
t.transform.set_size(1.0,1.0);
@@ -193,20 +197,26 @@ pub fn convert(
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V4(mesh_data4))=>build_mesh2(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data4.mesh)?,
rbx_mesh::mesh_data::MeshData::CSGMDL(rbx_mesh::mesh_data::CSGMDL::V5(mesh_data4))=>build_mesh5(&mut mb,&mut polygon_groups_normal_id,&cube_face_description,mesh_data4)?,
};
(0..NORMAL_FACES).map(|polygon_group_id|{
model::IndexedGraphicsGroup{
render:cube_face_description[polygon_group_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render),
groups:vec![PolygonGroupId::new(polygon_group_id as u32)]
}
}).collect()
let graphics_groups=polygon_groups_normal_id
.iter()
.enumerate()
.filter(|&(_,group)|!group.is_empty())
.enumerate()
.map(|(polygon_group_id,(normal_id,_))|{
model::IndexedGraphicsGroup{
render:cube_face_description[normal_id].as_ref().map_or(RenderConfigId::new(0),|face_description|face_description.render),
groups:vec![PolygonGroupId::new(polygon_group_id as u32)]
}
}).collect();
(polygon_groups_normal_id,graphics_groups)
}else{
Vec::new()
([vec![],vec![],vec![],vec![],vec![],vec![]],Vec::new())
};
//physics
let polygon_groups_normal_it=polygon_groups_normal_id.into_iter().map(|faces|
let polygon_groups_normal_it=polygon_groups_normal_id.into_iter().filter_map(|faces|
// graphics polygon groups (to be rendered)
Ok(PolygonGroup::PolygonList(PolygonList::new(faces)))
Some(PolygonGroup::PolygonList(new_polygon_list_checked(faces)?))
);
let polygon_groups:Vec<PolygonGroup>=if !roblox_physics_data.is_empty(){
let physics_data=rbx_mesh::read_physics_data_versioned(
@@ -225,12 +235,12 @@ pub fn convert(
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::V7(meshes))
=>meshes.meshes,
};
let physics_convex_meshes_it=physics_convex_meshes.into_iter().map(|mesh|{
let physics_convex_meshes_it=physics_convex_meshes.into_iter().filter_map(|mesh|{
// this can be factored out of the loop but I am lazy
let color=mb.acquire_color_id(glam::Vec4::ONE);
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
// physics polygon groups (to do physics)
Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[PhysicsDataVertexId(vertex_id0),PhysicsDataVertexId(vertex_id1),PhysicsDataVertexId(vertex_id2)]|{
let polygons=mesh.faces.into_iter().map(|[PhysicsDataVertexId(vertex_id0),PhysicsDataVertexId(vertex_id1),PhysicsDataVertexId(vertex_id2)]|{
let face=[
mesh.vertices.get(vertex_id0 as usize).ok_or(Error::MissingVertexId(vertex_id0))?,
mesh.vertices.get(vertex_id1 as usize).ok_or(Error::MissingVertexId(vertex_id1))?,
@@ -243,21 +253,26 @@ pub fn convert(
let pos=mb.acquire_pos_id(vec3::try_from_f32_array(vertex_pos.to_array()).map_err(Error::Planar64Vec3)?);
Ok(mb.acquire_vertex_id(IndexedVertex{pos,tex,normal,color}))
}).collect()
}).collect::<Result<_,_>>()?)))
}).collect::<Result<_,_>>();
let polygon_list=match polygons{
Ok(polygons)=>new_polygon_list_checked(polygons)?,
Err(e)=>return Some(Err(e)),
};
Some(Ok(PolygonGroup::PolygonList(polygon_list)))
});
polygon_groups_normal_it.chain(physics_convex_meshes_it).collect::<Result<_,_>>()?
polygon_groups_normal_it.map(Ok).chain(physics_convex_meshes_it).collect::<Result<_,_>>()?
}else{
// generate a unit cube as default physics
let pos_list=CUBE_DEFAULT_VERTICES.map(|pos|mb.acquire_pos_id(pos>>1));
let tex=mb.acquire_tex_id(glam::Vec2::ZERO);
let normal=mb.acquire_normal_id(vec3::ZERO);
let normal=mb.acquire_normal_id(vec3::zero());
let color=mb.acquire_color_id(glam::Vec4::ONE);
let polygon_group=PolygonGroup::PolygonList(PolygonList::new(CUBE_DEFAULT_POLYS.map(|poly|poly.map(|[pos_id,_]|
mb.acquire_vertex_id(IndexedVertex{pos:pos_list[pos_id as usize],tex,normal,color})
).to_vec()).to_vec()));
polygon_groups_normal_it.chain([Ok(polygon_group)]).collect::<Result<_,_>>()?
polygon_groups_normal_it.chain([polygon_group]).collect()
};
let physics_groups=(NORMAL_FACES..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
let physics_groups=(graphics_groups.len()..polygon_groups.len()).map(|id|model::IndexedPhysicsGroup{
groups:vec![PolygonGroupId::new(id as u32)]
}).collect();
let mesh=mb.build(
@@ -265,8 +280,5 @@ pub fn convert(
graphics_groups,
physics_groups,
);
Ok(MeshWithSize{
mesh,
size:vec3::ONE,
})
Ok(MeshWithSize::new(mesh,vec3::ONE))
}

View File

@@ -9,3 +9,6 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
[dependencies]
url = "2.5.4"
[lints]
workspace = true

View File

@@ -1,6 +1,6 @@
[package]
name = "roblox_emulator"
version = "0.5.1"
version = "0.5.3"
edition = "2024"
repository = "https://git.itzana.me/StrafesNET/strafe-project"
license = "MIT OR Apache-2.0"
@@ -12,10 +12,13 @@ default=["run-service"]
run-service=[]
[dependencies]
glam = "0.30.0"
mlua = { version = "0.10.1", features = ["luau"] }
phf = { version = "0.12.1", features = ["macros"] }
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet", features = ["instance-userdata"] }
rbx_reflection = "5.0.0"
rbx_reflection_database = "1.0.0"
rbx_types = "2.0.0"
glam.workspace = true
mlua = { version = "0.11.3", features = ["luau"] }
phf = { version = "0.13.1", features = ["macros"] }
rbx_dom_weak = "4.1.0"
rbx_reflection = "6.1.0"
rbx_reflection_database = "2.0.2"
rbx_types = "3.1.0"
[lints]
workspace = true

View File

@@ -52,7 +52,7 @@ impl Context{
}
/// Creates an iterator over all items of a particular class.
pub fn superclass_iter<'a>(&'a self,superclass:&'a str)->impl Iterator<Item=Ref>+'a{
let db=rbx_reflection_database::get();
let db=rbx_reflection_database::get().unwrap();
let Some(superclass)=db.classes.get(superclass)else{
panic!("Invalid class");
};

View File

@@ -8,7 +8,7 @@ impl<'a> EnumItem<'a>{
Self{name:Some(name.as_ref()),value}
}
}
impl<'a> From<rbx_types::Enum> for EnumItem<'a>{
impl From<rbx_types::Enum> for EnumItem<'_>{
fn from(e:rbx_types::Enum)->Self{
EnumItem{
name:None,
@@ -37,7 +37,7 @@ impl PartialEq for EnumItem<'_>{
pub struct Enums;
impl Enums{
pub fn get(&self,index:&str)->Option<EnumItems<'static>>{
let db=rbx_reflection_database::get();
let db=rbx_reflection_database::get().unwrap();
db.enums.get(index).map(|ed|EnumItems{ed})
}
}
@@ -65,7 +65,7 @@ impl<'a> EnumItems<'a>{
}
pub enum CoerceEnum<'a>{
Integer(i32),
Integer(i64),
String(mlua::String),
Enum(EnumItem<'a>),
}

View File

@@ -5,12 +5,12 @@ use rbx_types::Ref;
use rbx_dom_weak::{Ustr,InstanceBuilder,WeakDom};
use crate::util::static_ustr;
use crate::runner::vector3::Vector3;
use crate::runner::number::Number;
pub fn set_globals(lua:&mlua::Lua,globals:&mlua::Table)->Result<(),mlua::Error>{
//class functions store
lua.set_app_data(ClassMethodsStore::default());
lua.set_app_data(InstanceValueStore::default());
let table=lua.create_table()?;
@@ -37,13 +37,13 @@ pub fn dom_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut WeakDom)->mlua::Result<T>
}
pub fn class_is_a(class:&str,superclass:&str)->bool{
let db=rbx_reflection_database::get();
let db=rbx_reflection_database::get().unwrap();
let (Some(class),Some(superclass))=(db.classes.get(class),db.classes.get(superclass))else{
return false;
};
db.has_superclass(class,superclass)
}
fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance)->String{
fn get_full_name(dom:&WeakDom,instance:&rbx_dom_weak::Instance)->String{
let mut full_name=instance.name.clone();
let mut pref=instance.parent();
while let Some(parent)=dom.get_by_ref(pref){
@@ -65,29 +65,29 @@ pub fn get_name_source(lua:&mlua::Lua,script:Instance)->Result<(String,String),m
})
}
pub fn find_first_child<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
pub fn find_first_child<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.name==name)
}
pub fn find_first_descendant<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
pub fn find_first_descendant<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,name:&str)->Option<&'a rbx_dom_weak::Instance>{
dom.descendants_of(instance.referent()).find(|&inst|inst.name==name)
}
pub fn find_first_child_of_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
pub fn find_first_child_of_class<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|inst.class==class)
}
pub fn find_first_descendant_of_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
pub fn find_first_descendant_of_class<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,class:&str)->Option<&'a rbx_dom_weak::Instance>{
dom.descendants_of(instance.referent()).find(|&inst|inst.class==class)
}
pub fn find_first_child_which_is_a<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get();
pub fn find_first_child_which_is_a<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get().unwrap();
let superclass_descriptor=db.classes.get(superclass)?;
instance.children().iter().filter_map(|&r|dom.get_by_ref(r)).find(|inst|{
db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
})
}
pub fn find_first_descendant_which_is_a<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get();
pub fn find_first_descendant_which_is_a<'a>(dom:&'a WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str)->Option<&'a rbx_dom_weak::Instance>{
let db=rbx_reflection_database::get().unwrap();
let superclass_descriptor=db.classes.get(superclass)?;
dom.descendants_of(instance.referent()).find(|inst|{
db.classes.get(inst.class.as_str()).is_some_and(|descriptor|db.has_superclass(descriptor,superclass_descriptor))
@@ -282,7 +282,7 @@ impl mlua::UserData for Instance{
dom_mut(lua,|dom|{
let instance=this.get(dom)?;
//println!("__index t={} i={index:?}",instance.name);
let db=rbx_reflection_database::get();
let db=rbx_reflection_database::get().unwrap();
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
// Find existing property
// Interestingly, ustr can know ahead of time if
@@ -325,13 +325,16 @@ impl mlua::UserData for Instance{
}
//find or create an associated userdata object
let instance=this.get_mut(dom)?;
if let Some(value)=get_or_create_userdata(instance,lua,index_str)?{
if let Some(value)=instance_value_store_mut(lua,|ivs|{
//TODO: walk class tree somehow
match ivs.get_or_create_instance_values(&instance){
Some(mut instance_values)=>instance_values.get_or_create_value(lua,index_str),
None=>Ok(None)
}
})?{
return value.into_lua(lua);
}
// drop mutable borrow
//find a child with a matching name
let instance=this.get(dom)?;
find_first_child(dom,instance,index_str)
.map(|instance|Instance::new_unchecked(instance.referent()))
.into_lua(lua)
@@ -341,7 +344,7 @@ impl mlua::UserData for Instance{
let index_str=&*index.to_str()?;
dom_mut(lua,|dom|{
let instance=this.get_mut(dom)?;
let db=rbx_reflection_database::get();
let db=rbx_reflection_database::get().unwrap();
let class=db.classes.get(instance.class.as_str()).ok_or_else(||mlua::Error::runtime("Class missing"))?;
let property=db.superclasses_iter(class).find_map(|cls|
cls.properties.get(index_str)
@@ -419,7 +422,7 @@ impl mlua::UserData for Instance{
rbx_types::Variant::CFrame(typed_value.clone().into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::ContentId)=>{
let typed_value=value.as_str().ok_or_else(||mlua::Error::runtime("Expected string"))?.to_owned();
let typed_value=value.as_string().ok_or_else(||mlua::Error::runtime("Expected string"))?.to_str()?.to_owned();
rbx_types::Variant::ContentId(typed_value.into())
},
rbx_reflection::DataType::Value(rbx_types::VariantType::Ref)=>{
@@ -484,8 +487,8 @@ static CLASS_FUNCTION_DATABASE:CFD=phf::phf_map!{
"GetService"=>GET_SERVICE,
},
"Terrain"=>phf::phf_map!{
"FillBall"=>cf!(|_lua,_,_:(Vector3,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Vector3,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"FillBall"=>cf!(|_lua,_,_:(crate::runner::vector3::Vector3,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"FillBlock"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,crate::runner::vector3::Vector3,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"FillCylinder"=>cf!(|_lua,_,_:(crate::runner::cframe::CFrame,Number,Number,crate::runner::r#enum::CoerceEnum)|mlua::Result::Ok(())),
"SetMaterialColor"=>cf!(|_lua,_,_:(crate::runner::r#enum::CoerceEnum,crate::runner::color3::Color3)|mlua::Result::Ok(())),
},
@@ -606,6 +609,8 @@ fn find_virtual_property(
}
// lazy-loaded per-instance userdata values
// This whole thing is a bad idea and a garbage collection nightmare.
// TODO: recreate rbx_dom_weak with my own instance type that owns this data.
type CreateUserData=fn(&mlua::Lua)->mlua::Result<mlua::AnyUserData>;
type LUD=phf::Map<&'static str,// Class name
phf::Map<&'static str,// Value name
@@ -638,22 +643,47 @@ static LAZY_USER_DATA:LUD=phf::phf_map!{
"MouseClick"=>create_script_signal,
},
};
fn get_or_create_userdata(instance:&mut rbx_dom_weak::Instance,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{
use std::collections::hash_map::Entry;
let db=rbx_reflection_database::get();
let Some(class)=db.classes.get(instance.class.as_str())else{
return Ok(None)
};
if let Some((&static_str,create_userdata))=db.superclasses_iter(class).find_map(|superclass|
// find pair (class,index)
LAZY_USER_DATA.get(&superclass.name)
.and_then(|map|map.get_entry(index))
){
let index_ustr=static_ustr(static_str);
return Ok(Some(match instance.userdata.entry(index_ustr){
Entry::Occupied(entry)=>entry.get().clone(),
Entry::Vacant(entry)=>entry.insert(create_userdata(lua)?).clone(),
}));
}
Ok(None)
#[derive(Default)]
pub struct InstanceValueStore{
values:HashMap<Ref,
HashMap<&'static str,
mlua::AnyUserData
>
>,
}
pub struct InstanceValues<'a>{
named_values:&'static phf::Map<&'static str,CreateUserData>,
values:&'a mut HashMap<&'static str,mlua::AnyUserData>,
}
impl InstanceValueStore{
pub fn get_or_create_instance_values(&mut self,instance:&rbx_dom_weak::Instance)->Option<InstanceValues<'_>>{
LAZY_USER_DATA.get(instance.class.as_str())
.map(|named_values|
InstanceValues{
named_values,
values:self.values.entry(instance.referent())
.or_insert_with(||HashMap::new()),
}
)
}
}
impl InstanceValues<'_>{
pub fn get_or_create_value(&mut self,lua:&mlua::Lua,index:&str)->mlua::Result<Option<mlua::AnyUserData>>{
Ok(match self.named_values.get_entry(index){
Some((&static_index_str,&function_pointer))=>Some(
match self.values.entry(static_index_str){
Entry::Occupied(entry)=>entry.get().clone(),
Entry::Vacant(entry)=>entry.insert(
function_pointer(lua)?
).clone(),
}
),
None=>None,
})
}
}
pub fn instance_value_store_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut InstanceValueStore)->mlua::Result<T>)->mlua::Result<T>{
let mut cf=lua.app_data_mut::<InstanceValueStore>().ok_or_else(||mlua::Error::runtime("InstanceValueStore missing"))?;
f(&mut *cf)
}

View File

@@ -4,7 +4,7 @@
#[derive(Clone,Copy)]
pub enum Number{
Integer(i32),
Integer(i64),
Number(f64),
}
macro_rules! impl_ty{

View File

@@ -1,5 +1,4 @@
use crate::context::Context;
use crate::util::static_ustr;
#[cfg(feature="run-service")]
use crate::scheduler::scheduler_mut;
@@ -49,6 +48,11 @@ fn init(lua:&mlua::Lua)->mlua::Result<()>{
Ok(())
}
unsafe fn extend_lifetime_mut<'a,T>(src:&mut T)->&'a mut T{
let ptr:*mut T=src;
unsafe{&mut*ptr}
}
impl Runner{
pub fn new()->Result<Self,Error>{
let runner=Self{
@@ -66,8 +70,7 @@ impl Runner{
// SAFETY: This is not a &'static mut WeakDom,
// but as long as Runnable<'a> holds the lifetime of &'a mut Context
// it is a valid unique reference.
let ptr=&mut context.dom as *mut rbx_dom_weak::WeakDom;
self.lua.set_app_data::<crate::context::LuaAppData>(unsafe{&mut*ptr});
self.lua.set_app_data::<crate::context::LuaAppData>(unsafe{extend_lifetime_mut(&mut context.dom)});
#[cfg(feature="run-service")]
self.lua.set_app_data::<crate::scheduler::Scheduler>(crate::scheduler::Scheduler::default());
Ok(Runnable{
@@ -98,7 +101,19 @@ impl Runnable<'_>{
.set_name(name).into_function().map_err(Error::RustLua)?;
// TODO: set_environment without losing the ability to print from Lua
let thread=self.lua.create_thread(f).map_err(Error::RustLua)?;
// set timeout
let start=std::time::Instant::now();
self.lua.set_interrupt(move|lua|{
if std::time::Duration::from_secs(1)<start.elapsed(){
lua.remove_interrupt();
return Err(mlua::Error::runtime("timeout"));
}
Ok(mlua::VmState::Continue)
});
thread.resume::<mlua::MultiValue>(()).map_err(|error|Error::Lua{source,error})?;
self.lua.remove_interrupt();
// wait() is called from inside Lua and goes to a rust function that schedules the thread and then yields
// No need to schedule the thread here
Ok(())
@@ -126,15 +141,20 @@ impl Runnable<'_>{
}
#[cfg(feature="run-service")]
pub fn run_service_step(&self)->Result<(),mlua::Error>{
let render_stepped_signal=super::instance::instance::dom_mut(&self.lua,|dom|{
let render_stepped=super::instance::instance::dom_mut(&self.lua,|dom|{
let run_service=super::instance::instance::find_first_child_of_class(dom,dom.root(),"RunService").ok_or_else(||mlua::Error::runtime("RunService missing"))?;
Ok(match run_service.userdata.get(&static_ustr("RenderStepped")){
Some(render_stepped)=>Some(render_stepped.borrow::<super::script_signal::ScriptSignal>()?.clone()),
None=>None
super::instance::instance::instance_value_store_mut(&self.lua,|instance_value_store|{
//unwrap because I trust my find_first_child_of_class function to
let mut instance_values=instance_value_store.get_or_create_instance_values(run_service).ok_or_else(||mlua::Error::runtime("RunService InstanceValues missing"))?;
let render_stepped=instance_values.get_or_create_value(&self.lua,"RenderStepped")?;
//let stepped=instance_values.get_or_create_value(&self.lua,"Stepped")?;
//let heartbeat=instance_values.get_or_create_value(&self.lua,"Heartbeat")?;
Ok(render_stepped)
})
})?;
if let Some(render_stepped_signal)=render_stepped_signal{
render_stepped_signal.fire(&mlua::MultiValue::new());
if let Some(render_stepped)=render_stepped{
let signal:&super::script_signal::ScriptSignal=&*render_stepped.borrow()?;
signal.fire(&mlua::MultiValue::new());
}
Ok(())
}

View File

@@ -117,7 +117,7 @@ impl mlua::FromLua for ScriptSignal{
}
impl mlua::UserData for ScriptConnection{
fn add_fields<F:mlua::UserDataFields<Self>>(fields:&mut F){
fn add_fields<F:UserDataFields<Self>>(fields:&mut F){
fields.add_field_method_get("Connected",|_,this|{
Ok(this.position().is_some())
});

View File

@@ -46,8 +46,8 @@ impl Scheduler{
}
}
pub fn scheduler_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut crate::scheduler::Scheduler)->mlua::Result<T>)->mlua::Result<T>{
let mut scheduler=lua.app_data_mut::<crate::scheduler::Scheduler>().ok_or_else(||mlua::Error::runtime("Scheduler missing"))?;
pub fn scheduler_mut<T>(lua:&mlua::Lua,mut f:impl FnMut(&mut Scheduler)->mlua::Result<T>)->mlua::Result<T>{
let mut scheduler=lua.app_data_mut::<Scheduler>().ok_or_else(||mlua::Error::runtime("Scheduler missing"))?;
f(&mut *scheduler)
}

View File

@@ -1,6 +1,6 @@
[package]
name = "strafesnet_snf"
version = "0.3.1"
version = "0.4.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -8,4 +8,7 @@ edition = "2024"
[dependencies]
binrw = "0.15.0"
id = { version = "0.1.0", registry = "strafesnet" }
strafesnet_common = { version = "0.7.0", path = "../common", registry = "strafesnet" }
strafesnet_common.workspace = true
[lints]
workspace = true

View File

@@ -6,7 +6,7 @@ use strafesnet_common::physics::Time;
const VERSION:u32=0;
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,strafesnet_common::physics::Time>;
type TimedPhysicsInstruction=strafesnet_common::instruction::TimedInstruction<strafesnet_common::physics::Instruction,Time>;
#[derive(Debug)]
pub enum Error{
@@ -274,7 +274,7 @@ pub fn write_bot<W:BinWriterExt>(mut writer:W,physics_version:u32,instructions:i
//probe header length
let mut bot_header_data=Vec::new();
binrw::BinWrite::write_le(&header,&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?;
header.write_le(&mut std::io::Cursor::new(&mut bot_header_data)).map_err(Error::InvalidData)?;
// the first block location is the map header
block_location.push(offset);

View File

@@ -97,8 +97,8 @@ enum ResourceType{
}
struct ResourceMap<T>{
meshes:HashMap<strafesnet_common::model::MeshId,T>,
textures:HashMap<strafesnet_common::model::TextureId,T>,
meshes:HashMap<model::MeshId,T>,
textures:HashMap<model::TextureId,T>,
}
impl<T> Default for ResourceMap<T>{
fn default()->Self{
@@ -185,7 +185,7 @@ pub struct StreamableMap<R:BinReaderExt>{
//this is every possible attribute... need some sort of streaming system
attributes:Vec<strafesnet_common::gameplay_attributes::CollisionAttributes>,
//this is every possible render configuration... shaders and such... need streaming
render_configs:Vec<strafesnet_common::model::RenderConfig>,
render_configs:Vec<model::RenderConfig>,
//this makes sense to keep in memory for streaming, a map of which blocks occupy what space
bvh:BvhNode<BlockId>,
//something something resources hashmaps
@@ -223,7 +223,7 @@ impl<R:BinReaderExt> StreamableMap<R>{
}
Ok(Self{
file,
modes:strafesnet_common::gameplay_modes::NormalizedModes::new(modes),
modes:gameplay_modes::NormalizedModes::new(modes),
attributes,
render_configs,
bvh:strafesnet_common::bvh::generate_bvh(bvh),
@@ -366,12 +366,12 @@ fn collect_spacial_blocks(
block_location.push(sequential_block_data.position());
}else{
match bvh_node.into_content(){
strafesnet_common::bvh::RecursiveContent::Branch(bvh_node_list)=>{
RecursiveContent::Branch(bvh_node_list)=>{
for bvh_node in bvh_node_list{
collect_spacial_blocks(block_location,block_headers,sequential_block_data,bvh_node)?;
}
},
strafesnet_common::bvh::RecursiveContent::Leaf(_)=>panic!(),//bvh branches are 20 leaves minimum
RecursiveContent::Leaf(_)=>panic!(),//bvh branches are 20 leaves minimum
}
}
Ok(())
@@ -384,13 +384,13 @@ pub fn write_map<W:BinWriterExt>(mut writer:W,map:strafesnet_common::map::Comple
let boxen=map.models.into_iter().enumerate().map(|(model_id,model)|{
//grow your own aabb
let mesh=map.meshes.get(model.mesh.get() as usize).ok_or(Error::InvalidMeshId(model.mesh))?;
let mut aabb=strafesnet_common::aabb::Aabb::default();
let mut aabb=Aabb::default();
for &pos in &mesh.unique_pos{
aabb.grow(model.transform.transform_point3(pos).narrow_1().unwrap());
aabb.grow(model.transform.transform_point3(pos).narrow_64().unwrap());
}
Ok(((model::ModelId::new(model_id as u32),model.into()),aabb))
}).collect::<Result<Vec<_>,_>>()?;
let bvh=weigh_contents(strafesnet_common::bvh::generate_bvh(boxen),&|_|std::mem::size_of::<newtypes::model::Model>());
let bvh=weigh_contents(strafesnet_common::bvh::generate_bvh(boxen),&|_|size_of::<newtypes::model::Model>());
//build blocks
//block location is initialized with two values
//the first value represents the location of the first byte after the file header

View File

@@ -20,7 +20,7 @@ impl TryInto<TimedPhysicsInstruction> for TimedInstruction{
}
}
impl TryFrom<TimedPhysicsInstruction> for TimedInstruction{
type Error=super::physics::InstructionConvert;
type Error=InstructionConvert;
fn try_from(value:TimedPhysicsInstruction)->Result<Self,Self::Error>{
Ok(Self{
time:value.time.get(),
@@ -88,6 +88,11 @@ pub enum Instruction{
PracticeFly,
#[brw(magic=14u8)]
SetSensitivity(super::integer::Ratio64Vec2),
#[brw(magic=15u8)]
SetSprint(
#[br(map=bool_from_u8)]
#[bw(map=bool_into_u8)]
bool),
#[brw(magic=255u8)]
Idle,
}
@@ -116,6 +121,7 @@ impl TryInto<strafesnet_common::physics::Instruction> for Instruction{
Instruction::SetMoveForward(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state.into())),
Instruction::SetJump(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state.into())),
Instruction::SetZoom(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state.into())),
Instruction::SetSprint(state)=>strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetSprint(state.into())),
Instruction::Reset=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset),
Instruction::Restart(mode_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(strafesnet_common::gameplay_modes::ModeId::new(mode_id))),
Instruction::Spawn(mode_id,stage_id)=>strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(
@@ -142,6 +148,7 @@ impl TryFrom<strafesnet_common::physics::Instruction> for Instruction{
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetMoveForward(state))=>Ok(Instruction::SetMoveForward(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetJump(state))=>Ok(Instruction::SetJump(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetZoom(state))=>Ok(Instruction::SetZoom(state.into())),
strafesnet_common::physics::Instruction::SetControl(strafesnet_common::physics::SetControlInstruction::SetSprint(state))=>Ok(Instruction::SetSprint(state.into())),
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Reset)=>Ok(Instruction::Reset),
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Restart(mode_id))=>Ok(Instruction::Restart(mode_id.get())),
strafesnet_common::physics::Instruction::Mode(strafesnet_common::physics::ModeInstruction::Spawn(mode_id,stage_id))=>Ok(Instruction::Spawn(

View File

@@ -1,38 +1,81 @@
[package]
name = "map-tool"
version = "1.7.2"
version = "3.0.1"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["cli","source","roblox"]
roblox = [
"dep:strafesnet_rbx_loader",
"dep:rbx_asset",
"dep:rbx_binary",
"dep:rbx_dom_weak",
"dep:rbx_reflection_database",
"dep:rbx_xml",
"dep:rbxassetid",
]
source = [
"dep:strafesnet_bsp_loader",
"dep:vbsp",
"dep:vbsp-entities-css",
"dep:vmdl",
"dep:vmt-parser",
"dep:vpk",
"dep:vtf",
"tokio/sync",
]
cli = [
"dep:clap",
"dep:futures",
"tokio/macros",
"tokio/rt-multi-thread",
"tokio/fs",
]
[lib]
name = "map_tool"
[[bin]]
name = "map-tool"
required-features = ["cli"]
[dependencies]
strafesnet_deferred_loader.workspace = true
strafesnet_snf.workspace = true
anyhow = "1.0.75"
clap = { version = "4.4.2", features = ["derive"] }
clap = { version = "4.4.2", features = ["derive"], optional = true }
flate2 = "1.0.27"
futures = "0.3.31"
image = "0.25.2"
image_dds = "0.7.1"
lazy-regex = "3.1.0"
rbx_asset = { version = "0.4.4", registry = "strafesnet" }
rbx_binary = { version = "1.1.0-sn4", registry = "strafesnet" }
rbx_dom_weak = { version = "3.1.0-sn4", registry = "strafesnet" }
rbx_reflection_database = "1.0.0"
rbx_xml = { version = "1.1.0-sn4", registry = "strafesnet" }
rbxassetid = { version = "0.1.0", registry = "strafesnet" }
strafesnet_bsp_loader = { version = "0.3.1", path = "../lib/bsp_loader", registry = "strafesnet" }
strafesnet_deferred_loader = { version = "0.5.1", path = "../lib/deferred_loader", registry = "strafesnet" }
strafesnet_rbx_loader = { version = "0.7.0", path = "../lib/rbx_loader", registry = "strafesnet" }
strafesnet_snf = { version = "0.3.1", path = "../lib/snf", registry = "strafesnet" }
futures = { version = "0.3.31", optional = true }
image = { version = "0.25.2", features = ["png", "jpeg"], default-features = false }
image_dds = { version = "0.7.1", features = ["ddsfile","encode"], default-features = false }
thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "fs"] }
vbsp = "0.9.1"
vbsp-entities-css = "0.6.0"
vmdl = "0.2.0"
vmt-parser = "0.2.0"
vpk = "0.3.0"
vtf = "0.3.0"
tokio = { version = "1.43.0", features = ["time"] }
# roblox
strafesnet_rbx_loader = { workspace = true, optional = true }
rbx_asset = { version = "0.5.0", registry = "strafesnet", optional = true }
rbx_binary = { version = "2.0.1", optional = true }
rbx_dom_weak = { version = "4.1.0", optional = true }
rbx_reflection_database = { version = "2.0.2", optional = true }
rbx_xml = { version = "2.0.1", optional = true }
rbxassetid = { version = "0.1.0", registry = "strafesnet", optional = true }
# source
strafesnet_bsp_loader = { workspace = true, optional = true }
vbsp = { version = "0.9.1", optional = true }
vbsp-entities-css = { version = "0.6.0", optional = true }
vmdl = { version = "0.2.0", optional = true }
vmt-parser = { version = "0.2.0", optional = true }
vpk = { version = "0.3.0", optional = true }
vtf = { version = "0.3.0", optional = true }
#[profile.release]
#lto = true
#strip = true
#codegen-units = 1
[lints]
workspace = true

4
map-tool/src/lib.rs Normal file
View File

@@ -0,0 +1,4 @@
#[cfg(feature="roblox")]
pub mod roblox;
#[cfg(feature="source")]
pub mod source;

View File

@@ -1,30 +1,31 @@
mod roblox;
mod source;
use clap::{Parser,Subcommand};
use anyhow::Result as AResult;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[command(author,version,about,long_about=None)]
#[command(propagate_version=true)]
struct Cli{
#[command(subcommand)]
command: Commands,
command:Commands,
}
#[derive(Subcommand)]
enum Commands{
#[command(flatten)]
Roblox(roblox::Commands),
#[cfg(feature="roblox")]
Roblox(map_tool::roblox::Commands),
#[command(flatten)]
Source(source::Commands),
#[cfg(feature="source")]
Source(map_tool::source::Commands),
}
#[tokio::main]
async fn main()->AResult<()>{
let cli=Cli::parse();
match cli.command{
#[cfg(feature="roblox")]
Commands::Roblox(commands)=>commands.run().await,
#[cfg(feature="source")]
Commands::Source(commands)=>commands.run().await,
}
}

View File

@@ -1,34 +1,29 @@
use std::path::{Path,PathBuf};
use std::io::{Cursor,Read,Seek};
use std::collections::HashSet;
use clap::{Args,Subcommand};
use std::path::PathBuf;
use std::io::Cursor;
use anyhow::Result as AResult;
use rbx_dom_weak::Instance;
use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
use rbxassetid::RobloxAssetId;
use tokio::io::AsyncReadExt;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}
use super::{convert_texture_to_dds,get_unique_assets};
use super::{ConvertTextureError,UniqueAssets};
const DOWNLOAD_LIMIT:usize=16;
#[derive(Subcommand)]
#[derive(clap::Subcommand)]
pub enum Commands{
RobloxToSNF(RobloxToSNFSubcommand),
DownloadAssets(DownloadAssetsSubcommand),
ExtractUnionData(ExtractUnionData),
}
#[derive(Args)]
pub struct RobloxToSNFSubcommand {
#[derive(clap::Args)]
pub struct RobloxToSNFSubcommand{
#[arg(long)]
output_folder:PathBuf,
#[arg(required=true)]
input_files:Vec<PathBuf>,
}
#[derive(Args)]
#[derive(clap::Args)]
pub struct DownloadAssetsSubcommand{
#[arg(required=true)]
roblox_files:Vec<PathBuf>,
@@ -39,6 +34,13 @@ pub struct DownloadAssetsSubcommand{
#[arg(long,group="cookie",required=true)]
cookie_file:Option<PathBuf>,
}
#[derive(clap::Args)]
pub struct ExtractUnionData{
#[arg(long)]
output_folder:PathBuf,
#[arg(long)]
input_file:PathBuf,
}
impl Commands{
pub async fn run(self)->AResult<()>{
@@ -52,6 +54,7 @@ impl Commands{
subcommand.cookie_file,
).await?,
).await,
Commands::ExtractUnionData(subcommand)=>extract_union_data(subcommand.input_file,subcommand.output_folder).await,
}
}
}
@@ -66,148 +69,6 @@ async fn cookie_from_args(literal:Option<String>,environment:Option<String>,file
Ok(rbx_asset::cookie::Cookie::new(cookie))
}
#[expect(dead_code)]
#[derive(Debug)]
enum LoadDomError{
IO(std::io::Error),
Binary(rbx_binary::DecodeError),
Xml(rbx_xml::DecodeError),
UnknownFormat,
}
fn load_dom<R:Read+Seek>(mut input:R)->Result<rbx_dom_weak::WeakDom,LoadDomError>{
let mut first_8=[0u8;8];
input.read_exact(&mut first_8).map_err(LoadDomError::IO)?;
input.rewind().map_err(LoadDomError::IO)?;
match &first_8{
b"<roblox!"=>rbx_binary::from_reader(input).map_err(LoadDomError::Binary),
b"<roblox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(LoadDomError::Xml),
_=>Err(LoadDomError::UnknownFormat),
}
}
/* The ones I'm interested in:
Beam.Texture
Decal.Texture
FileMesh.MeshId
FileMesh.TextureId
MaterialVariant.ColorMap
MaterialVariant.MetalnessMap
MaterialVariant.NormalMap
MaterialVariant.RoughnessMap
MeshPart.MeshId
MeshPart.TextureID
ParticleEmitter.Texture
Sky.MoonTextureId
Sky.SkyboxBk
Sky.SkyboxDn
Sky.SkyboxFt
Sky.SkyboxLf
Sky.SkyboxRt
Sky.SkyboxUp
Sky.SunTextureId
SurfaceAppearance.ColorMap
SurfaceAppearance.MetalnessMap
SurfaceAppearance.NormalMap
SurfaceAppearance.RoughnessMap
SurfaceAppearance.TexturePack
*/
/* These properties now use Content
BaseWrap.CageMeshContent
Decal.TextureContent
ImageButton.ImageContent
ImageLabel.ImageContent
MeshPart.MeshContent
MeshPart.TextureContent
SurfaceAppearance.ColorMapContent
SurfaceAppearance.MetalnessMapContent
SurfaceAppearance.NormalMapContent
SurfaceAppearance.RoughnessMapContent
WrapLayer.ReferenceMeshContent
*/
fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(&static_ustr(property))else{
println!("property={} does not exist for class={}",property,object.class.as_str());
return;
};
let rbx_dom_weak::types::ContentType::Uri(uri)=content.value()else{
println!("ContentType is not Uri");
return;
};
let Ok(asset_id)=uri.parse()else{
println!("Content failed to parse into AssetID: {:?}",content);
return;
};
content_list.insert(asset_id);
}
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
let Some(rbx_dom_weak::types::Variant::ContentId(content))=object.properties.get(&static_ustr(property))else{
println!("property={} does not exist for class={}",property,object.class.as_str());
return;
};
let Ok(asset_id)=content.as_str().parse()else{
println!("Content failed to parse into AssetID: {:?}",content);
return;
};
content_list.insert(asset_id);
}
async fn read_entire_file(path:impl AsRef<Path>)->Result<Cursor<Vec<u8>>,std::io::Error>{
let mut file=tokio::fs::File::open(path).await?;
let mut data=Vec::new();
file.read_to_end(&mut data).await?;
Ok(Cursor::new(data))
}
#[derive(Default)]
struct UniqueAssets{
meshes:HashSet<RobloxAssetId>,
unions:HashSet<RobloxAssetId>,
textures:HashSet<RobloxAssetId>,
}
impl UniqueAssets{
fn collect(&mut self,object:&Instance){
match object.class.as_str(){
"Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Decal"=>accumulate_content(&mut self.textures,object,"TextureContent"),
"Texture"=>accumulate_content(&mut self.textures,object,"TextureContent"),
"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
"MeshPart"=>{
accumulate_content(&mut self.textures,object,"TextureContent");
accumulate_content(&mut self.meshes,object,"MeshContent");
},
"SpecialMesh"=>accumulate_content_id(&mut self.meshes,object,"MeshId"),
"ParticleEmitter"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Sky"=>{
accumulate_content_id(&mut self.textures,object,"MoonTextureId");
accumulate_content_id(&mut self.textures,object,"SkyboxBk");
accumulate_content_id(&mut self.textures,object,"SkyboxDn");
accumulate_content_id(&mut self.textures,object,"SkyboxFt");
accumulate_content_id(&mut self.textures,object,"SkyboxLf");
accumulate_content_id(&mut self.textures,object,"SkyboxRt");
accumulate_content_id(&mut self.textures,object,"SkyboxUp");
accumulate_content_id(&mut self.textures,object,"SunTextureId");
},
"UnionOperation"=>accumulate_content_id(&mut self.unions,object,"AssetId"),
_=>(),
}
}
}
#[expect(dead_code)]
#[derive(Debug)]
enum UniqueAssetError{
IO(std::io::Error),
LoadDom(LoadDomError),
}
async fn unique_assets(path:&Path)->Result<UniqueAssets,UniqueAssetError>{
// read entire file
let mut assets=UniqueAssets::default();
let data=read_entire_file(path).await.map_err(UniqueAssetError::IO)?;
let dom=load_dom(data).map_err(UniqueAssetError::LoadDom)?;
for object in dom.into_raw().1.into_values(){
assets.collect(&object);
}
Ok(assets)
}
enum DownloadType{
Texture(RobloxAssetId),
Mesh(RobloxAssetId),
@@ -253,7 +114,6 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,dow
let asset_id=download_instruction.asset_id();
// if not, download file
let mut retry=0;
const BACKOFF_MUL:f32=1.3956124250860895286;//exp(1/3)
let mut backoff=1000f32;
loop{
let asset_result=context.get_asset(rbx_asset::cookie::GetAssetRequest{
@@ -276,7 +136,7 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,dow
}
println!("Hit roblox rate limit, waiting {:.0}ms...",backoff);
tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await;
backoff*=BACKOFF_MUL;
backoff*=super::BACKOFF_MUL;
retry+=1;
}else{
stats.failed_downloads+=1;
@@ -292,47 +152,27 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::Context,dow
}
}
}
#[derive(Debug,thiserror::Error)]
enum ConvertTextureError{
#[error("Io error {0:?}")]
Io(#[from]std::io::Error),
#[error("Image error {0:?}")]
Image(#[from]image::ImageError),
#[error("DDS create error {0:?}")]
DDS(#[from]image_dds::CreateDdsError),
#[error("DDS write error {0:?}")]
DDSWrite(#[from]image_dds::ddsfile::Error),
}
async fn convert_texture(RobloxAssetId(asset_id):RobloxAssetId,download_result:DownloadResult)->Result<(),ConvertTextureError>{
async fn cli_convert_texture(RobloxAssetId(asset_id):RobloxAssetId,download_result:DownloadResult)->Result<(),CliConvertTextureError>{
let data=match download_result{
DownloadResult::Cached(path)=>tokio::fs::read(path).await?,
DownloadResult::Data(data)=>data,
DownloadResult::Failed=>return Ok(()),
};
// image::ImageFormat::Png
// image::ImageFormat::Jpeg
let image=image::load_from_memory(&data)?.to_rgba8();
// pick format
let format=if image.width()%4!=0||image.height()%4!=0{
image_dds::ImageFormat::Rgba8UnormSrgb
}else{
image_dds::ImageFormat::BC7RgbaUnormSrgb
};
//this fails if the image dimensions are not a multiple of 4
let dds=image_dds::dds_from_image(
&image,
format,
image_dds::Quality::Slow,
image_dds::Mipmaps::GeneratedAutomatic,
)?;
let dds=convert_texture_to_dds(&data)?;
let file_name=format!("textures/{asset_id}.dds");
let mut file=std::fs::File::create(file_name)?;
dds.write(&mut file)?;
tokio::fs::write(file_name,&dds).await?;
Ok(())
}
#[derive(Debug,thiserror::Error)]
enum CliConvertTextureError{
#[error("Io error {0:?}")]
Io(#[from]std::io::Error),
#[error("ConvertTexture error {0:?}")]
ConvertTexture(#[from]ConvertTextureError),
}
async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->AResult<()>{
tokio::try_join!(
tokio::fs::create_dir_all("downloaded_textures"),
@@ -356,27 +196,19 @@ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->A
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let send=send_assets.clone();
tokio::spawn(async move{
let result=unique_assets(path.as_path()).await;
let result=match tokio::fs::read(&path).await{
Ok(data)=>super::load_dom(&data).map(|dom|get_unique_assets(&dom)).map_err(|e|format!("{e:?}")),
Err(e)=>Err(format!("{e:?}")),
};
_=send.send(result).await;
drop(permit);
});
}
});
// download manager
// insert into global unique assets guy
// add to download queue if the asset is globally unique and does not already exist on disk
let mut stats=Stats::default();
let context=rbx_asset::cookie::Context::new(cookie);
let mut globally_unique_assets=UniqueAssets::default();
// pop a job = retry_queue.pop_front() or ingest(recv.recv().await)
// SLOW MODE:
// acquire all permits
// drop all permits
// pop one job
// if it succeeds go into fast mode
// FAST MODE:
// acquire one permit
// pop a job
let download_thread=tokio::spawn(async move{
while let Some(result)=recv_assets.recv().await{
let unique_assets=match result{
@@ -410,7 +242,7 @@ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->A
SEM.add_permits(thread_limit);
while let (Ok(permit),Some((asset_id,download_result)))=(SEM.acquire().await,recv_texture.recv().await){
tokio::spawn(async move{
let result=convert_texture(asset_id,download_result).await;
let result=cli_convert_texture(asset_id,download_result).await;
drop(permit);
result.unwrap();
});
@@ -420,52 +252,7 @@ async fn download_assets(paths:Vec<PathBuf>,cookie:rbx_asset::cookie::Cookie)->A
Ok(())
}
#[derive(Debug)]
#[expect(dead_code)]
enum ConvertError{
IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error),
RobloxRead(strafesnet_rbx_loader::ReadError),
RobloxLoad(strafesnet_rbx_loader::LoadError),
}
impl std::fmt::Display for ConvertError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ConvertError{}
struct Errors{
script_errors:Vec<strafesnet_rbx_loader::RunnerError>,
convert_errors:strafesnet_rbx_loader::RecoverableErrors,
}
fn convert_to_snf(path:&Path,output_folder:PathBuf)->Result<Errors,ConvertError>{
let entire_file=std::fs::read(path).map_err(ConvertError::IO)?;
let model=strafesnet_rbx_loader::read(
entire_file.as_slice()
).map_err(ConvertError::RobloxRead)?;
let mut place=strafesnet_rbx_loader::Place::from(model);
let script_errors=place.run_scripts().unwrap_or_else(|e|vec![e]);
let (map,convert_errors)=place.to_snf(LoadFailureMode::DefaultToNone).map_err(ConvertError::RobloxLoad)?;
let mut dest=output_folder;
dest.push(path.file_stem().unwrap());
dest.set_extension("snfm");
let file=std::fs::File::create(dest).map_err(ConvertError::IO)?;
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
Ok(Errors{
script_errors,
convert_errors,
})
}
async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->AResult<()>{
async fn roblox_to_snf(paths:Vec<PathBuf>,output_folder:PathBuf)->AResult<()>{
let start=std::time::Instant::now();
let thread_limit=std::thread::available_parallelism()?.get();
@@ -477,17 +264,27 @@ async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->ARe
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let output_folder=output_folder.clone();
tokio::task::spawn_blocking(move||{
let result=convert_to_snf(path.as_path(),output_folder);
let result=std::fs::read(&path).and_then(|data|{
super::load_dom(&data).map_err(|e|std::io::Error::other(e))
.and_then(|dom|convert_to_snf(dom).map_err(|e|std::io::Error::other(e)))
.and_then(|output|{
let mut dest=output_folder;
dest.push(path.file_stem().unwrap());
dest.set_extension("snfm");
std::fs::write(dest,&output.snf)?;
Ok(output)
})
});
drop(permit);
match result{
Ok(errors)=>{
for error in errors.script_errors{
Ok(output)=>{
for error in output.script_errors{
println!("Script error: {error}");
}
let error_count=errors.convert_errors.count();
let error_count=output.convert_errors.count();
if error_count!=0{
println!("Error count: {error_count}");
println!("Errors: {}",errors.convert_errors);
println!("Errors: {}",output.convert_errors);
}
},
Err(e)=>println!("Convert error: {e:?}"),
@@ -499,3 +296,88 @@ async fn roblox_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf)->ARe
println!("elapsed={:?}", start.elapsed());
Ok(())
}
#[expect(dead_code)]
#[derive(Debug)]
pub enum ConvertError{
IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error),
RobloxLoadMesh(super::loader::MeshError),
RobloxLoadTexture(super::loader::TextureError),
}
impl std::fmt::Display for ConvertError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ConvertError{}
pub struct ConvertOutput{
pub snf:Vec<u8>,
pub script_errors:Vec<strafesnet_rbx_loader::RunnerError>,
pub convert_errors:strafesnet_rbx_loader::RecoverableErrors,
}
pub fn convert_to_snf(dom:rbx_dom_weak::WeakDom)->Result<ConvertOutput,ConvertError>{
const FAILURE_MODE:LoadFailureMode=LoadFailureMode::DefaultToNone;
// run scripts
let model=strafesnet_rbx_loader::Model::new(dom);
let mut place=strafesnet_rbx_loader::Place::from(model);
let script_errors=place.run_scripts().unwrap_or_else(|e|vec![e]);
// convert
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
let mut mesh_deferred_loader=MeshDeferredLoader::new();
let map_step1=strafesnet_rbx_loader::rbx::convert(
place.as_ref(),
&mut texture_deferred_loader,
&mut mesh_deferred_loader,
);
let mut mesh_loader=super::loader::MeshLoader::new();
let meshpart_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,FAILURE_MODE).map_err(ConvertError::RobloxLoadMesh)?;
let map_step2=map_step1.add_meshpart_meshes_and_calculate_attributes(meshpart_meshes);
let mut texture_loader=super::loader::TextureLoader::new();
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,FAILURE_MODE).map_err(ConvertError::RobloxLoadTexture)?;
let (map,convert_errors)=map_step2.add_render_configs_and_textures(render_configs);
let mut snf_buf=Vec::new();
strafesnet_snf::map::write_map(Cursor::new(&mut snf_buf),map).map_err(ConvertError::SNFMap)?;
Ok(ConvertOutput{
snf:snf_buf,
script_errors,
convert_errors,
})
}
async fn extract_union_data(input_file:PathBuf,output_folder:PathBuf)->AResult<()>{
let data=tokio::fs::read(&input_file).await?;
let dom=rbx_binary::from_reader(Cursor::new(data))?;
let &[referent]=dom.root().children()else{
panic!("Expected one child");
};
let Some(instance)=dom.get_by_ref(referent)else{
panic!("Missing instance");
};
let output_file=output_folder.join(input_file.file_stem().unwrap());
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&rbx_dom_weak::ustr("PhysicsData")){
let mut physics_data_path=output_file.clone();
physics_data_path.set_extension("physicsdata");
tokio::fs::write(physics_data_path,data).await?;
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get(&rbx_dom_weak::ustr("MeshData")){
let mut mesh_data_path=output_file.clone();
mesh_data_path.set_extension("meshdata");
tokio::fs::write(mesh_data_path,data).await?;
}
Ok(())
}

View File

@@ -1,10 +1,8 @@
use std::io::Read;
use rbxassetid::{RobloxAssetId,RobloxAssetIdParseErr};
use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::data::RobloxMeshBytes;
use crate::rbx::RobloxPartDescription;
use strafesnet_rbx_loader::mesh::{MeshIndex,MeshType,MeshWithSize};
// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
@@ -18,6 +16,7 @@ fn read_entire_file(path:impl AsRef<std::path::Path>)->Result<Vec<u8>,std::io::E
Ok(data)
}
#[expect(dead_code)]
#[derive(Debug)]
pub enum TextureError{
Io(std::io::Error),
@@ -50,7 +49,7 @@ impl Loader for TextureLoader{
type Error=TextureError;
type Index<'a>=&'a str;
type Resource=Texture;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
let RobloxAssetId(asset_id)=index.parse()?;
let file_name=format!("textures/{}.dds",asset_id);
let data=read_entire_file(file_name)?;
@@ -58,12 +57,13 @@ impl Loader for TextureLoader{
}
}
#[expect(dead_code)]
#[derive(Debug)]
pub enum MeshError{
Io(std::io::Error),
RobloxAssetIdParse(RobloxAssetIdParseErr),
Mesh(crate::mesh::Error),
Union(crate::union::Error),
Mesh(strafesnet_rbx_loader::mesh::Error),
Union(strafesnet_rbx_loader::union::Error),
DecodeBinary(rbx_binary::DecodeError),
OneChildPolicy,
MissingInstance,
@@ -84,13 +84,13 @@ impl From<RobloxAssetIdParseErr> for MeshError{
Self::RobloxAssetIdParse(value)
}
}
impl From<crate::mesh::Error> for MeshError{
fn from(value:crate::mesh::Error)->Self{
impl From<strafesnet_rbx_loader::mesh::Error> for MeshError{
fn from(value:strafesnet_rbx_loader::mesh::Error)->Self{
Self::Mesh(value)
}
}
impl From<crate::union::Error> for MeshError{
fn from(value:crate::union::Error)->Self{
impl From<strafesnet_rbx_loader::union::Error> for MeshError{
fn from(value:strafesnet_rbx_loader::union::Error)->Self{
Self::Union(value)
}
}
@@ -100,53 +100,6 @@ impl From<rbx_binary::DecodeError> for MeshError{
}
}
#[derive(Hash,Eq,PartialEq)]
pub enum MeshType<'a>{
FileMesh,
Union{
mesh_data:&'a [u8],
physics_data:&'a [u8],
size_float_bits:[u32;3],
part_texture_description:RobloxPartDescription,
},
}
#[derive(Hash,Eq,PartialEq)]
pub struct MeshIndex<'a>{
mesh_type:MeshType<'a>,
content:&'a str,
}
impl MeshIndex<'_>{
pub fn file_mesh(content:&str)->MeshIndex<'_>{
MeshIndex{
mesh_type:MeshType::FileMesh,
content,
}
}
pub fn union<'a>(
content:&'a str,
mesh_data:&'a [u8],
physics_data:&'a [u8],
size:&rbx_dom_weak::types::Vector3,
part_texture_description:RobloxPartDescription,
)->MeshIndex<'a>{
MeshIndex{
mesh_type:MeshType::Union{
mesh_data,
physics_data,
size_float_bits:[size.x.to_bits(),size.y.to_bits(),size.z.to_bits()],
part_texture_description,
},
content,
}
}
}
#[derive(Clone)]
pub struct MeshWithSize{
pub(crate) mesh:Mesh,
pub(crate) size:strafesnet_common::integer::Planar64Vec3,
}
pub struct MeshLoader;
impl MeshLoader{
pub fn new()->Self{
@@ -157,17 +110,17 @@ impl Loader for MeshLoader{
type Error=MeshError;
type Index<'a>=MeshIndex<'a>;
type Resource=MeshWithSize;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
let mesh=match index.mesh_type{
MeshType::FileMesh=>{
let RobloxAssetId(asset_id)=index.content.parse()?;
let file_name=format!("meshes/{}",asset_id);
let data=read_entire_file(file_name)?;
crate::mesh::convert(RobloxMeshBytes::new(data))?
strafesnet_rbx_loader::mesh::convert(&data)?
},
MeshType::Union{mut physics_data,mut mesh_data,size_float_bits,part_texture_description}=>{
// decode asset
let size=glam::Vec3::from_array(size_float_bits.map(f32::from_bits));
let size=size_float_bits.map(f32::from_bits).into();
if !index.content.is_empty()&&(physics_data.is_empty()||mesh_data.is_empty()){
let RobloxAssetId(asset_id)=index.content.parse()?;
let file_name=format!("unions/{}",asset_id);
@@ -189,9 +142,9 @@ impl Loader for MeshLoader{
mesh_data=data.as_ref();
}
}
crate::union::convert(physics_data,mesh_data,size,part_texture_description)?
strafesnet_rbx_loader::union::convert(physics_data,mesh_data,size,part_texture_description)?
}else{
crate::union::convert(physics_data,mesh_data,size,part_texture_description)?
strafesnet_rbx_loader::union::convert(physics_data,mesh_data,size,part_texture_description)?
}
},
};

210
map-tool/src/roblox/mod.rs Normal file
View File

@@ -0,0 +1,210 @@
#[cfg(feature="cli")]
mod cli;
#[cfg(feature="cli")]
pub use cli::Commands;
#[cfg(feature="cli")]
mod loader;
use std::io::{Cursor,Read,Seek};
use std::collections::HashSet;
pub use rbxassetid::RobloxAssetId;
use rbx_dom_weak::Instance;
// disallow non-static lifetimes
fn static_ustr(s:&'static str)->rbx_dom_weak::Ustr{
rbx_dom_weak::ustr(s)
}
#[derive(Debug)]
pub enum LoadDomError{
IO(std::io::Error),
Binary(rbx_binary::DecodeError),
Xml(rbx_xml::DecodeError),
UnknownFormat,
}
impl std::fmt::Display for LoadDomError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for LoadDomError{}
pub fn load_dom(data:&[u8])->Result<rbx_dom_weak::WeakDom,LoadDomError>{
let mut input=Cursor::new(data);
let mut first_8=[0u8;8];
input.read_exact(&mut first_8).map_err(LoadDomError::IO)?;
input.rewind().map_err(LoadDomError::IO)?;
match &first_8{
b"<roblox!"=>rbx_binary::from_reader(input).map_err(LoadDomError::Binary),
b"<roblox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(LoadDomError::Xml),
_=>Err(LoadDomError::UnknownFormat),
}
}
fn accumulate_content(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get(&static_ustr(property))else{
println!("property={} does not exist for class={}",property,object.class.as_str());
return;
};
let rbx_dom_weak::types::ContentType::Uri(uri)=content.value()else{
println!("ContentType is not Uri");
return;
};
let Ok(asset_id)=uri.parse()else{
println!("Content failed to parse into AssetID: {:?}",content);
return;
};
content_list.insert(asset_id);
}
fn accumulate_content_id(content_list:&mut HashSet<RobloxAssetId>,object:&Instance,property:&'static str){
let Some(rbx_dom_weak::types::Variant::ContentId(content))=object.properties.get(&static_ustr(property))else{
println!("property={} does not exist for class={}",property,object.class.as_str());
return;
};
let Ok(asset_id)=content.as_str().parse()else{
println!("Content failed to parse into AssetID: {:?}",content);
return;
};
content_list.insert(asset_id);
}
#[derive(Default)]
pub struct UniqueAssets{
pub meshes:HashSet<RobloxAssetId>,
pub unions:HashSet<RobloxAssetId>,
pub textures:HashSet<RobloxAssetId>,
}
impl UniqueAssets{
fn collect(&mut self,object:&Instance){
match object.class.as_str(){
"Beam"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"Decal"=>accumulate_content(&mut self.textures,object,"TextureContent"),
"FileMesh"=>accumulate_content_id(&mut self.textures,object,"TextureId"),
"MeshPart"=>{
accumulate_content(&mut self.textures,object,"TextureContent");
accumulate_content(&mut self.meshes,object,"MeshContent");
},
"ParticleEmitter"=>accumulate_content_id(&mut self.textures,object,"Texture"),
"SpecialMesh"=>accumulate_content_id(&mut self.meshes,object,"MeshId"),
"Sky"=>{
accumulate_content_id(&mut self.textures,object,"MoonTextureId");
accumulate_content_id(&mut self.textures,object,"SkyboxBk");
accumulate_content_id(&mut self.textures,object,"SkyboxDn");
accumulate_content_id(&mut self.textures,object,"SkyboxFt");
accumulate_content_id(&mut self.textures,object,"SkyboxLf");
accumulate_content_id(&mut self.textures,object,"SkyboxRt");
accumulate_content_id(&mut self.textures,object,"SkyboxUp");
accumulate_content_id(&mut self.textures,object,"SunTextureId");
},
"SurfaceAppearance"=>{
accumulate_content(&mut self.textures,object,"ColorMapContent");
accumulate_content(&mut self.textures,object,"MetalnessMapContent");
accumulate_content(&mut self.textures,object,"NormalMapContent");
accumulate_content(&mut self.textures,object,"RoughnessMapContent");
}
"Texture"=>accumulate_content(&mut self.textures,object,"TextureContent"),
"UnionOperation"=>accumulate_content_id(&mut self.unions,object,"AssetId"),
_=>(),
}
}
}
pub fn get_unique_assets(dom:&rbx_dom_weak::WeakDom)->UniqueAssets{
let mut assets=UniqueAssets::default();
for object in dom.descendants(){
assets.collect(object);
}
assets
}
#[derive(Debug,thiserror::Error)]
pub enum ConvertTextureError{
#[error("Image error {0:?}")]
Image(#[from]image::ImageError),
#[error("DDS encode error {0:?}")]
DDSEncode(#[from]image_dds::error::SurfaceError),
#[error("DDS create error {0:?}")]
DDS(#[from]image_dds::CreateDdsError),
#[error("DDS write error {0:?}")]
DDSWrite(#[from]image_dds::ddsfile::Error),
}
pub fn convert_texture_to_dds(data:&[u8])->Result<Vec<u8>,ConvertTextureError>{
let image=image::load_from_memory(data)?.to_rgba8();
let format=if image.width()%4!=0||image.height()%4!=0{
image_dds::ImageFormat::Rgba8UnormSrgb
}else{
image_dds::ImageFormat::BC7RgbaUnormSrgb
};
let dds=image_dds::SurfaceRgba8{
width:image.width(),
height:image.height(),
depth:1,
layers:1,
mipmaps:1,
data:image.as_raw(),
}.encode(format,image_dds::Quality::Slow,image_dds::Mipmaps::GeneratedAutomatic)?.to_dds()?;
let mut buf=Vec::new();
dds.write(&mut Cursor::new(&mut buf))?;
Ok(buf)
}
#[derive(Debug)]
pub enum DownloadAssetError{
Get(rbx_asset::cookie::GetError),
IO(std::io::Error),
}
impl std::fmt::Display for DownloadAssetError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for DownloadAssetError{}
pub async fn download_asset(context:&rbx_asset::cookie::Context,asset_id:u64)->Result<Vec<u8>,DownloadAssetError>{
let data=context.get_asset(rbx_asset::cookie::GetAssetRequest{
asset_id,
version:None,
}).await.map_err(DownloadAssetError::Get)?;
Ok(data.to_vec().map_err(DownloadAssetError::IO)?)
}
pub(crate) const BACKOFF_MUL:f32=1.3956124250860895286;//exp(1/3)
const RETRY_LIMIT:u32=12;
pub enum DownloadRetryResult{
Ok(Vec<u8>),
TimedOut,
}
pub async fn download_asset_retry(context:&rbx_asset::cookie::Context,asset_id:u64)->Result<DownloadRetryResult,DownloadAssetError>{
let mut retry=0u32;
let mut backoff=1000f32;
loop{
match context.get_asset(rbx_asset::cookie::GetAssetRequest{
asset_id,
version:None,
}).await{
Ok(data)=>{
let bytes=data.to_vec().map_err(DownloadAssetError::IO)?;
break Ok(DownloadRetryResult::Ok(bytes));
},
Err(rbx_asset::cookie::GetError::Response(rbx_asset::types::ResponseError::Details{status_code,url_and_body}))=>{
if status_code.as_u16()==429{
if retry==RETRY_LIMIT{
break Ok(DownloadRetryResult::TimedOut);
}
tokio::time::sleep(std::time::Duration::from_millis(backoff as u64)).await;
backoff*=BACKOFF_MUL;
retry+=1;
}else{
break Err(DownloadAssetError::Get(rbx_asset::cookie::GetError::Response(rbx_asset::types::ResponseError::Details{status_code,url_and_body})));
}
},
Err(e)=>{
break Err(DownloadAssetError::Get(e));
},
}
}
}

View File

@@ -1,514 +0,0 @@
use std::path::{Path,PathBuf};
use std::borrow::Cow;
use clap::{Args,Subcommand};
use anyhow::Result as AResult;
use futures::StreamExt;
use strafesnet_bsp_loader::loader::BspFinder;
use strafesnet_deferred_loader::loader::Loader;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
use vbsp_entities_css::Entity;
#[derive(Subcommand)]
pub enum Commands{
SourceToSNF(SourceToSNFSubcommand),
ExtractTextures(ExtractTexturesSubcommand),
VPKContents(VPKContentsSubcommand),
BSPContents(BSPContentsSubcommand),
}
#[derive(Args)]
pub struct SourceToSNFSubcommand {
#[arg(long)]
output_folder:PathBuf,
#[arg(required=true)]
input_files:Vec<PathBuf>,
#[arg(long)]
vpks:Vec<PathBuf>,
}
#[derive(Args)]
pub struct ExtractTexturesSubcommand{
#[arg(required=true)]
bsp_files:Vec<PathBuf>,
#[arg(long)]
vpks:Vec<PathBuf>,
}
#[derive(Args)]
pub struct VPKContentsSubcommand {
#[arg(long)]
input_file:PathBuf,
}
#[derive(Args)]
pub struct BSPContentsSubcommand {
#[arg(long)]
input_file:PathBuf,
}
impl Commands{
pub async fn run(self)->AResult<()>{
match self{
Commands::SourceToSNF(subcommand)=>source_to_snf(subcommand.input_files,subcommand.output_folder,subcommand.vpks).await,
Commands::ExtractTextures(subcommand)=>extract_textures(subcommand.bsp_files,subcommand.vpks).await,
Commands::VPKContents(subcommand)=>vpk_contents(subcommand.input_file),
Commands::BSPContents(subcommand)=>bsp_contents(subcommand.input_file),
}
}
}
enum VMTContent{
VMT(String),
VTF(String),
Patch(vmt_parser::material::PatchMaterial),
Unsupported,//don't want to deal with whatever vmt variant
Unresolved,//could not locate a texture because of vmt content
}
impl VMTContent{
fn vtf(opt:Option<String>)->Self{
match opt{
Some(s)=>Self::VTF(s),
None=>Self::Unresolved,
}
}
}
fn get_some_texture(material:vmt_parser::material::Material)->VMTContent{
//just grab some texture from somewhere for now
match material{
vmt_parser::material::Material::LightMappedGeneric(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::VertexLitGeneric(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),//this just dies if there is none
vmt_parser::material::Material::VertexLitGenericDx6(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),
vmt_parser::material::Material::UnlitGeneric(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::UnlitTwoTexture(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::Water(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::WorldVertexTransition(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::EyeRefract(mat)=>VMTContent::vtf(Some(mat.cornea_texture)),
vmt_parser::material::Material::SubRect(mat)=>VMTContent::VMT(mat.material),//recursive
vmt_parser::material::Material::Sprite(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::SpriteCard(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::Cable(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::Refract(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::Modulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::DecalModulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::Sky(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::Replacements(_mat)=>VMTContent::Unsupported,
vmt_parser::material::Material::Patch(mat)=>VMTContent::Patch(mat),
_=>unreachable!(),
}
}
#[derive(Debug,thiserror::Error)]
enum GetVMTError{
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("Utf8 error {0:?}")]
Utf8(#[from]std::str::Utf8Error),
#[error("Vdf error {0:?}")]
Vdf(#[from]vmt_parser::VdfError),
#[error("Vmt not found")]
NotFound,
}
fn get_vmt(finder:BspFinder,search_name:&str)->Result<vmt_parser::material::Material,GetVMTError>{
let vmt_data=finder.find(search_name)?.ok_or(GetVMTError::NotFound)?;
//decode vmt and then write
let vmt_str=core::str::from_utf8(&vmt_data)?;
let material=vmt_parser::from_str(vmt_str)?;
//println!("vmt material={:?}",material);
Ok(material)
}
#[derive(Debug,thiserror::Error)]
enum LoadVMTError{
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("GetVMT error {0:?}")]
GetVMT(#[from]GetVMTError),
#[error("FromUtf8 error {0:?}")]
FromUtf8(#[from]std::string::FromUtf8Error),
#[error("Vdf error {0:?}")]
Vdf(#[from]vmt_parser::VdfError),
#[error("Vmt unsupported")]
Unsupported,
#[error("Vmt unresolved")]
Unresolved,
#[error("Vmt not found")]
NotFound,
}
fn recursive_vmt_loader<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,material:vmt_parser::material::Material)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
where
'bsp:'a,
'vpk:'a,
{
match get_some_texture(material){
VMTContent::VMT(mut s)=>{
s.make_ascii_lowercase();
recursive_vmt_loader(finder,get_vmt(finder,&s)?)
},
VMTContent::VTF(s)=>{
let mut texture_file_name=PathBuf::from("materials");
texture_file_name.push(s);
texture_file_name.set_extension("vtf");
texture_file_name.as_mut_os_str().make_ascii_lowercase();
Ok(finder.find(texture_file_name.to_str().unwrap())?)
},
VMTContent::Patch(mat)=>recursive_vmt_loader(finder,
mat.resolve(|search_name|{
let name_lowercase=search_name.to_lowercase();
match finder.find(&name_lowercase)?{
Some(bytes)=>Ok(String::from_utf8(bytes.into_owned())?),
None=>Err(LoadVMTError::NotFound),
}
})?
),
VMTContent::Unsupported=>Err(LoadVMTError::Unsupported),
VMTContent::Unresolved=>Err(LoadVMTError::Unresolved),
}
}
fn load_texture<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,texture_name:&str)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
where
'bsp:'a,
'vpk:'a,
{
let mut texture_file_name=PathBuf::from("materials");
//lower case
texture_file_name.push(texture_name);
texture_file_name.as_mut_os_str().make_ascii_lowercase();
//remove stem and search for both vtf and vmt files
let stem=texture_file_name.file_stem().unwrap().to_owned();
texture_file_name.pop();
texture_file_name.push(stem);
if let Some(stuff)=finder.find(texture_file_name.to_str().unwrap())?{
return Ok(Some(stuff));
}
// search for both vmt,vtf
let mut texture_file_name_vmt=texture_file_name.clone();
texture_file_name_vmt.set_extension("vmt");
let get_vmt_result=get_vmt(finder,texture_file_name_vmt.to_str().unwrap());
match get_vmt_result{
Ok(material)=>{
let vmt_result=recursive_vmt_loader(finder,material);
match vmt_result{
Ok(Some(stuff))=>return Ok(Some(stuff)),
Ok(None)
|Err(LoadVMTError::NotFound)=>(),
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
Err(e)=>return Err(e),
}
}
|Err(GetVMTError::NotFound)=>(),
Err(e)=>Err(e)?,
}
// try looking for vtf
let mut texture_file_name_vtf=texture_file_name.clone();
texture_file_name_vtf.set_extension("vtf");
let get_vtf_result=get_vmt(finder,texture_file_name_vtf.to_str().unwrap());
match get_vtf_result{
Ok(material)=>{
let vtf_result=recursive_vmt_loader(finder,material);
match vtf_result{
Ok(Some(stuff))=>return Ok(Some(stuff)),
Ok(None)
|Err(LoadVMTError::NotFound)=>(),
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
Err(e)=>return Err(e),
}
}
|Err(GetVMTError::NotFound)=>(),
Err(e)=>Err(e)?,
}
Ok(None)
}
#[derive(Debug,thiserror::Error)]
enum ExtractTextureError{
#[error("Io error {0:?}")]
Io(#[from]std::io::Error),
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("MeshLoad error {0:?}")]
MeshLoad(#[from]strafesnet_bsp_loader::loader::MeshError),
#[error("Load VMT error {0:?}")]
LoadVMT(#[from]LoadVMTError),
}
async fn gimme_them_textures(path:&Path,vpk_list:&[strafesnet_bsp_loader::Vpk],send_texture:tokio::sync::mpsc::Sender<(Vec<u8>,String)>)->Result<(),ExtractTextureError>{
let bsp=vbsp::Bsp::read(tokio::fs::read(path).await?.as_ref())?;
let loader_bsp=strafesnet_bsp_loader::Bsp::new(bsp);
let bsp=loader_bsp.as_ref();
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
for texture in bsp.textures(){
texture_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(texture.name())));
}
let mut mesh_deferred_loader=MeshDeferredLoader::new();
for name in &bsp.static_props.dict.name{
mesh_deferred_loader.acquire_mesh_id(name.as_str());
}
for raw_ent in &bsp.entities{
let model=match raw_ent.parse(){
Ok(Entity::Cycler(brush))=>brush.model,
Ok(Entity::EnvSprite(brush))=>brush.model,
Ok(Entity::FuncBreakable(brush))=>brush.model,
Ok(Entity::FuncBrush(brush))=>brush.model,
Ok(Entity::FuncButton(brush))=>brush.model,
Ok(Entity::FuncDoor(brush))=>brush.model,
Ok(Entity::FuncDoorRotating(brush))=>brush.model,
Ok(Entity::FuncIllusionary(brush))=>brush.model,
Ok(Entity::FuncMonitor(brush))=>brush.model,
Ok(Entity::FuncMovelinear(brush))=>brush.model,
Ok(Entity::FuncPhysbox(brush))=>brush.model,
Ok(Entity::FuncPhysboxMultiplayer(brush))=>brush.model,
Ok(Entity::FuncRotButton(brush))=>brush.model,
Ok(Entity::FuncRotating(brush))=>brush.model,
Ok(Entity::FuncTracktrain(brush))=>brush.model,
Ok(Entity::FuncTrain(brush))=>brush.model,
Ok(Entity::FuncWall(brush))=>brush.model,
Ok(Entity::FuncWallToggle(brush))=>brush.model,
Ok(Entity::FuncWaterAnalog(brush))=>brush.model,
Ok(Entity::PropDoorRotating(brush))=>brush.model,
Ok(Entity::PropDynamic(brush))=>brush.model,
Ok(Entity::PropDynamicOverride(brush))=>brush.model,
Ok(Entity::PropPhysics(brush))=>brush.model,
Ok(Entity::PropPhysicsMultiplayer(brush))=>brush.model,
Ok(Entity::PropPhysicsOverride(brush))=>brush.model,
Ok(Entity::PropRagdoll(brush))=>brush.model,
Ok(Entity::TriggerGravity(brush))=>brush.model,
Ok(Entity::TriggerHurt(brush))=>brush.model,
Ok(Entity::TriggerLook(brush))=>brush.model,
Ok(Entity::TriggerMultiple(brush))=>brush.model.unwrap_or_default(),
Ok(Entity::TriggerOnce(brush))=>brush.model,
Ok(Entity::TriggerProximity(brush))=>brush.model,
Ok(Entity::TriggerPush(brush))=>brush.model,
Ok(Entity::TriggerSoundscape(brush))=>brush.model,
Ok(Entity::TriggerTeleport(brush))=>brush.model.unwrap_or_default(),
Ok(Entity::TriggerVphysicsMotion(brush))=>brush.model,
Ok(Entity::TriggerWind(brush))=>brush.model,
_=>continue,
};
match model.chars().next(){
Some('*')=>(),
_=>{
mesh_deferred_loader.acquire_mesh_id(model);
},
}
}
let finder=BspFinder{
bsp:&loader_bsp,
vpks:vpk_list
};
let mut mesh_loader=strafesnet_bsp_loader::loader::ModelLoader::new(finder);
// load models and collect requested textures
for model_path in mesh_deferred_loader.into_indices(){
let model:vmdl::Model=match mesh_loader.load(model_path){
Ok(model)=>model,
Err(e)=>{
println!("Model={model_path} Load model error: {e}");
continue;
},
};
for texture in model.textures(){
for search_path in &texture.search_paths{
let mut path=PathBuf::from(search_path.as_str());
path.push(texture.name.as_str());
let path=path.to_str().unwrap().to_owned();
texture_deferred_loader.acquire_render_config_id(Some(Cow::Owned(path)));
}
}
}
for texture_path in texture_deferred_loader.into_indices(){
match load_texture(finder,&texture_path){
Ok(Some(texture))=>send_texture.send(
(texture.into_owned(),texture_path.into_owned())
).await.unwrap(),
Ok(None)=>(),
Err(e)=>println!("Texture={texture_path} Load error: {e}"),
}
}
Ok(())
}
#[derive(Debug,thiserror::Error)]
enum ConvertTextureError{
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("Vtf error {0:?}")]
Vtf(#[from]vtf::Error),
#[error("DDS create error {0:?}")]
DDS(#[from]image_dds::CreateDdsError),
#[error("DDS write error {0:?}")]
DDSWrite(#[from]image_dds::ddsfile::Error),
#[error("Io error {0:?}")]
Io(#[from]std::io::Error),
}
async fn convert_texture(texture:Vec<u8>,write_file_name:impl AsRef<Path>)->Result<(),ConvertTextureError>{
let image=vtf::from_bytes(&texture)?.highres_image.decode(0)?.to_rgba8();
let format=if image.width()%4!=0||image.height()%4!=0{
image_dds::ImageFormat::Rgba8UnormSrgb
}else{
image_dds::ImageFormat::BC7RgbaUnormSrgb
};
//this fails if the image dimensions are not a multiple of 4
let dds = image_dds::dds_from_image(
&image,
format,
image_dds::Quality::Slow,
image_dds::Mipmaps::GeneratedAutomatic,
)?;
//write dds
let mut dest=PathBuf::from("textures");
dest.push(write_file_name);
dest.set_extension("dds");
std::fs::create_dir_all(dest.parent().unwrap())?;
let mut writer=std::io::BufWriter::new(std::fs::File::create(dest)?);
dds.write(&mut writer)?;
Ok(())
}
async fn read_vpks(vpk_paths:Vec<PathBuf>,thread_limit:usize)->Vec<strafesnet_bsp_loader::Vpk>{
futures::stream::iter(vpk_paths).map(|vpk_path|async{
// idk why it doesn't want to pass out the errors but this is fatal anyways
tokio::task::spawn_blocking(move||Ok::<_,vpk::Error>(strafesnet_bsp_loader::Vpk::new(vpk::VPK::read(&vpk_path)?))).await.unwrap().unwrap()
})
.buffer_unordered(thread_limit)
.collect().await
}
async fn extract_textures(paths:Vec<PathBuf>,vpk_paths:Vec<PathBuf>)->AResult<()>{
tokio::try_join!(
tokio::fs::create_dir_all("extracted_textures"),
tokio::fs::create_dir_all("textures"),
tokio::fs::create_dir_all("meshes"),
)?;
let thread_limit=std::thread::available_parallelism()?.get();
// load vpk list and leak for static lifetime
let vpk_list:&[strafesnet_bsp_loader::Vpk]=read_vpks(vpk_paths,thread_limit).await.leak();
let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit);
let mut it=paths.into_iter();
let extract_thread=tokio::spawn(async move{
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let send=send_texture.clone();
tokio::spawn(async move{
let result=gimme_them_textures(&path,vpk_list,send).await;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Map={path:?} Decode error: {e:?}"),
}
});
}
});
// convert images
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some((data,dest)))=(SEM.acquire().await,recv_texture.recv().await){
// TODO: dedup dest?
tokio::spawn(async move{
let result=convert_texture(data,dest).await;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Convert error: {e:?}"),
}
});
}
extract_thread.await?;
_=SEM.acquire_many(thread_limit as u32).await?;
Ok(())
}
fn vpk_contents(vpk_path:PathBuf)->AResult<()>{
let vpk_index=vpk::VPK::read(&vpk_path)?;
for (label,entry) in vpk_index.tree.into_iter(){
println!("vpk label={} entry={:?}",label,entry);
}
Ok(())
}
fn bsp_contents(path:PathBuf)->AResult<()>{
let bsp=vbsp::Bsp::read(std::fs::read(path)?.as_ref())?;
for file_name in bsp.pack.into_zip().into_inner().unwrap().file_names(){
println!("file_name={:?}",file_name);
}
Ok(())
}
#[derive(Debug)]
#[expect(dead_code)]
enum ConvertError{
IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error),
BspRead(strafesnet_bsp_loader::ReadError),
BspLoad(strafesnet_bsp_loader::LoadError),
}
impl std::fmt::Display for ConvertError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ConvertError{}
async fn convert_to_snf(path:&Path,vpk_list:&[strafesnet_bsp_loader::Vpk],output_folder:PathBuf)->AResult<()>{
let entire_file=tokio::fs::read(path).await?;
let bsp=strafesnet_bsp_loader::read(
std::io::Cursor::new(entire_file)
).map_err(ConvertError::BspRead)?;
let map=bsp.to_snf(LoadFailureMode::DefaultToNone,vpk_list).map_err(ConvertError::BspLoad)?;
let mut dest=output_folder;
dest.push(path.file_stem().unwrap());
dest.set_extension("snfm");
let file=std::fs::File::create(dest).map_err(ConvertError::IO)?;
strafesnet_snf::map::write_map(file,map).map_err(ConvertError::SNFMap)?;
Ok(())
}
async fn source_to_snf(paths:Vec<std::path::PathBuf>,output_folder:PathBuf,vpk_paths:Vec<PathBuf>)->AResult<()>{
let start=std::time::Instant::now();
let thread_limit=std::thread::available_parallelism()?.get();
// load vpk list and leak for static lifetime
let vpk_list:&[strafesnet_bsp_loader::Vpk]=read_vpks(vpk_paths,thread_limit).await.leak();
let mut it=paths.into_iter();
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let output_folder=output_folder.clone();
tokio::spawn(async move{
let result=convert_to_snf(path.as_path(),vpk_list,output_folder).await;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Convert error: {e:?}"),
}
});
}
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
println!("elapsed={:?}", start.elapsed());
Ok(())
}

348
map-tool/src/source/cli.rs Normal file
View File

@@ -0,0 +1,348 @@
use std::borrow::Cow;
use std::io::Cursor;
use std::path::PathBuf;
use strafesnet_deferred_loader::loader::Loader;
use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
use vbsp_entities_css::Entity;
use anyhow::Result as AResult;
use futures::StreamExt;
use super::{convert_texture_to_dds,load_texture};
use super::{BspFinder,ConvertTextureError,LoadVMTError};
#[derive(clap::Subcommand)]
pub enum Commands{
SourceToSNF(SourceToSNFSubcommand),
ExtractTextures(ExtractTexturesSubcommand),
VPKContents(VPKContentsSubcommand),
BSPContents(BSPContentsSubcommand),
}
#[derive(clap::Args)]
pub struct SourceToSNFSubcommand{
#[arg(long)]
output_folder:PathBuf,
#[arg(required=true)]
input_files:Vec<PathBuf>,
#[arg(long)]
vpks:Vec<PathBuf>,
}
#[derive(clap::Args)]
pub struct ExtractTexturesSubcommand{
#[arg(required=true)]
bsp_files:Vec<PathBuf>,
#[arg(long)]
vpks:Vec<PathBuf>,
}
#[derive(clap::Args)]
pub struct VPKContentsSubcommand{
#[arg(long)]
input_file:PathBuf,
}
#[derive(clap::Args)]
pub struct BSPContentsSubcommand{
#[arg(long)]
input_file:PathBuf,
}
impl Commands{
pub async fn run(self)->AResult<()>{
match self{
Commands::SourceToSNF(subcommand)=>source_to_snf(subcommand.input_files,subcommand.output_folder,subcommand.vpks).await,
Commands::ExtractTextures(subcommand)=>extract_textures(subcommand.bsp_files,subcommand.vpks).await,
Commands::VPKContents(subcommand)=>vpk_contents(subcommand.input_file),
Commands::BSPContents(subcommand)=>bsp_contents(subcommand.input_file),
}
}
}
#[derive(Debug,thiserror::Error)]
enum ExtractTextureError{
#[error("Io error {0:?}")]
Io(#[from]std::io::Error),
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("MeshLoad error {0:?}")]
MeshLoad(#[from]super::loader::MeshError),
#[error("Load VMT error {0:?}")]
LoadVMT(#[from]LoadVMTError),
}
async fn gimme_them_textures(path:&std::path::Path,vpk_list:&[strafesnet_bsp_loader::Vpk],send_texture:tokio::sync::mpsc::Sender<(Vec<u8>,String)>)->Result<(),ExtractTextureError>{
let bsp=vbsp::Bsp::read(tokio::fs::read(path).await?.as_ref())?;
let loader_bsp=strafesnet_bsp_loader::Bsp::new(bsp);
let bsp=loader_bsp.as_ref();
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
for texture in bsp.textures(){
texture_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(texture.name())));
}
let mut mesh_deferred_loader=MeshDeferredLoader::new();
for name in &bsp.static_props.dict.name{
mesh_deferred_loader.acquire_mesh_id(name.as_str());
}
for raw_ent in &bsp.entities{
let model=match raw_ent.parse(){
Ok(Entity::Cycler(brush))=>brush.model,
Ok(Entity::EnvSprite(brush))=>brush.model,
Ok(Entity::FuncBreakable(brush))=>brush.model,
Ok(Entity::FuncBrush(brush))=>brush.model,
Ok(Entity::FuncButton(brush))=>brush.model,
Ok(Entity::FuncDoor(brush))=>brush.model,
Ok(Entity::FuncDoorRotating(brush))=>brush.model,
Ok(Entity::FuncIllusionary(brush))=>brush.model,
Ok(Entity::FuncMonitor(brush))=>brush.model,
Ok(Entity::FuncMovelinear(brush))=>brush.model,
Ok(Entity::FuncPhysbox(brush))=>brush.model,
Ok(Entity::FuncPhysboxMultiplayer(brush))=>brush.model,
Ok(Entity::FuncRotButton(brush))=>brush.model,
Ok(Entity::FuncRotating(brush))=>brush.model,
Ok(Entity::FuncTracktrain(brush))=>brush.model,
Ok(Entity::FuncTrain(brush))=>brush.model,
Ok(Entity::FuncWall(brush))=>brush.model,
Ok(Entity::FuncWallToggle(brush))=>brush.model,
Ok(Entity::FuncWaterAnalog(brush))=>brush.model,
Ok(Entity::PropDoorRotating(brush))=>brush.model,
Ok(Entity::PropDynamic(brush))=>brush.model,
Ok(Entity::PropDynamicOverride(brush))=>brush.model,
Ok(Entity::PropPhysics(brush))=>brush.model,
Ok(Entity::PropPhysicsMultiplayer(brush))=>brush.model,
Ok(Entity::PropPhysicsOverride(brush))=>brush.model,
Ok(Entity::PropRagdoll(brush))=>brush.model,
Ok(Entity::TriggerGravity(brush))=>brush.model,
Ok(Entity::TriggerHurt(brush))=>brush.model,
Ok(Entity::TriggerLook(brush))=>brush.model,
Ok(Entity::TriggerMultiple(brush))=>brush.model.unwrap_or_default(),
Ok(Entity::TriggerOnce(brush))=>brush.model,
Ok(Entity::TriggerProximity(brush))=>brush.model,
Ok(Entity::TriggerPush(brush))=>brush.model,
Ok(Entity::TriggerSoundscape(brush))=>brush.model,
Ok(Entity::TriggerTeleport(brush))=>brush.model.unwrap_or_default(),
Ok(Entity::TriggerVphysicsMotion(brush))=>brush.model,
Ok(Entity::TriggerWind(brush))=>brush.model,
_=>continue,
};
match model.chars().next(){
Some('*')=>(),
_=>{
mesh_deferred_loader.acquire_mesh_id(model);
},
}
}
let finder=BspFinder{
bsp:&loader_bsp,
vpks:vpk_list
};
let mut mesh_loader=super::loader::ModelLoader::new(finder);
// load models and collect requested textures
for model_path in mesh_deferred_loader.into_indices(){
let model:vmdl::Model=match mesh_loader.load(model_path){
Ok(model)=>model,
Err(e)=>{
println!("Model={model_path} Load model error: {e}");
continue;
},
};
for texture in model.textures(){
for search_path in &texture.search_paths{
let mut path=PathBuf::from(search_path.as_str());
path.push(texture.name.as_str());
let path=path.to_str().unwrap().to_owned();
texture_deferred_loader.acquire_render_config_id(Some(Cow::Owned(path)));
}
}
}
for texture_path in texture_deferred_loader.into_indices(){
match load_texture(finder,&texture_path){
Ok(Some(texture))=>send_texture.send(
(texture.into_owned(),texture_path.into_owned())
).await.unwrap(),
Ok(None)=>(),
Err(e)=>println!("Texture={texture_path} Load error: {e}"),
}
}
Ok(())
}
#[derive(Debug,thiserror::Error)]
enum CliConvertTextureError{
#[error("Io error {0:?}")]
Io(#[from]std::io::Error),
#[error("ConvertTexture error {0:?}")]
ConvertTexture(#[from]ConvertTextureError),
}
async fn cli_convert_texture(texture:Vec<u8>,write_file_name:impl AsRef<std::path::Path>)->Result<(),CliConvertTextureError>{
let dds=convert_texture_to_dds(&texture)?;
let mut dest=PathBuf::from("textures");
dest.push(write_file_name);
dest.set_extension("dds");
std::fs::create_dir_all(dest.parent().unwrap())?;
std::fs::write(&dest,&dds)?;
Ok(())
}
async fn async_read_vpks(vpk_paths:Vec<PathBuf>,thread_limit:usize)->Vec<strafesnet_bsp_loader::Vpk>{
futures::stream::iter(vpk_paths).map(|vpk_path|async{
tokio::task::spawn_blocking(move||Ok::<_,vpk::Error>(strafesnet_bsp_loader::Vpk::new(vpk::VPK::read(&vpk_path)?))).await.unwrap().unwrap()
})
.buffer_unordered(thread_limit)
.collect().await
}
async fn extract_textures(paths:Vec<PathBuf>,vpk_paths:Vec<PathBuf>)->AResult<()>{
tokio::try_join!(
tokio::fs::create_dir_all("extracted_textures"),
tokio::fs::create_dir_all("textures"),
tokio::fs::create_dir_all("meshes"),
)?;
let thread_limit=std::thread::available_parallelism()?.get();
// load vpk list and leak for static lifetime
let vpk_list:&[strafesnet_bsp_loader::Vpk]=async_read_vpks(vpk_paths,thread_limit).await.leak();
let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit);
let mut it=paths.into_iter();
let extract_thread=tokio::spawn(async move{
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let send=send_texture.clone();
tokio::spawn(async move{
let result=gimme_them_textures(&path,vpk_list,send).await;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Map={path:?} Decode error: {e:?}"),
}
});
}
});
// convert images
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some((data,dest)))=(SEM.acquire().await,recv_texture.recv().await){
// TODO: dedup dest?
tokio::spawn(async move{
let result=cli_convert_texture(data,dest).await;
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Convert error: {e:?}"),
}
});
}
extract_thread.await?;
_=SEM.acquire_many(thread_limit as u32).await?;
Ok(())
}
fn vpk_contents(vpk_path:PathBuf)->AResult<()>{
let vpk_index=vpk::VPK::read(&vpk_path)?;
for (label,entry) in vpk_index.tree.into_iter(){
println!("vpk label={} entry={:?}",label,entry);
}
Ok(())
}
fn bsp_contents(path:PathBuf)->AResult<()>{
let bsp=vbsp::Bsp::read(std::fs::read(path)?.as_ref())?;
for file_name in bsp.pack.into_zip().into_inner().unwrap().file_names(){
println!("file_name={:?}",file_name);
}
Ok(())
}
async fn source_to_snf(paths:Vec<PathBuf>,output_folder:PathBuf,vpk_paths:Vec<PathBuf>)->AResult<()>{
let start=std::time::Instant::now();
let thread_limit=std::thread::available_parallelism()?.get();
// load vpk list and leak for static lifetime
let vpk_list:&[strafesnet_bsp_loader::Vpk]=async_read_vpks(vpk_paths,thread_limit).await.leak();
let mut it=paths.into_iter();
static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
SEM.add_permits(thread_limit);
while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
let output_folder=output_folder.clone();
tokio::spawn(async move{
let result=match tokio::fs::read(&path).await{
Ok(data)=>convert_to_snf(&data,vpk_list).map_err(|e|anyhow::anyhow!("{e:?}")).and_then(|snf_buf|{
let mut dest=output_folder;
dest.push(path.file_stem().unwrap());
dest.set_extension("snfm");
std::fs::write(dest,&snf_buf)?;
Ok(())
}),
Err(e)=>Err(e.into()),
};
drop(permit);
match result{
Ok(())=>(),
Err(e)=>println!("Convert error: {e:?}"),
}
});
}
_=SEM.acquire_many(thread_limit as u32).await.unwrap();
println!("elapsed={:?}", start.elapsed());
Ok(())
}
#[expect(dead_code)]
#[derive(Debug)]
pub enum ConvertError{
IO(std::io::Error),
SNFMap(strafesnet_snf::map::Error),
BspRead(strafesnet_bsp_loader::ReadError),
BspLoadMesh(super::loader::MeshError),
BspLoadTexture(super::loader::TextureError),
}
impl std::fmt::Display for ConvertError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ConvertError{}
pub fn convert_to_snf(bsp_data:&[u8],vpk_list:&[strafesnet_bsp_loader::Vpk])->Result<Vec<u8>,ConvertError>{
const FAILURE_MODE:LoadFailureMode=LoadFailureMode::DefaultToNone;
let bsp=strafesnet_bsp_loader::read(
Cursor::new(bsp_data)
).map_err(ConvertError::BspRead)?;
let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
let mut mesh_deferred_loader=MeshDeferredLoader::new();
let map_step1=strafesnet_bsp_loader::bsp::convert(
&bsp,
&mut texture_deferred_loader,
&mut mesh_deferred_loader,
);
let mut mesh_loader=super::loader::MeshLoader::new(BspFinder{bsp:&bsp,vpks:vpk_list},&mut texture_deferred_loader);
let prop_meshes=mesh_deferred_loader.into_meshes(&mut mesh_loader,FAILURE_MODE).map_err(ConvertError::BspLoadMesh)?;
let map_step2=map_step1.add_prop_meshes(prop_meshes);
let mut texture_loader=super::loader::TextureLoader::new();
let render_configs=texture_deferred_loader.into_render_configs(&mut texture_loader,FAILURE_MODE).map_err(ConvertError::BspLoadTexture)?;
let map=map_step2.add_render_configs_and_textures(render_configs);
let mut snf_buf=Vec::new();
strafesnet_snf::map::write_map(Cursor::new(&mut snf_buf),map).map_err(ConvertError::SNFMap)?;
Ok(snf_buf)
}

View File

@@ -1,10 +1,10 @@
use std::{borrow::Cow, io::Read};
use strafesnet_common::model::Mesh;
use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
use crate::{Bsp,Vpk};
use super::BspFinder;
#[expect(dead_code)]
#[derive(Debug)]
pub enum TextureError{
Io(std::io::Error),
@@ -31,7 +31,7 @@ impl Loader for TextureLoader{
type Error=TextureError;
type Index<'a>=Cow<'a,str>;
type Resource=Texture;
fn load<'a>(&mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
fn load(&mut self,index:Self::Index<'_>)->Result<Self::Resource,Self::Error>{
let file_name=format!("textures/{}.dds",index);
let mut file=std::fs::File::open(file_name)?;
let mut data=Vec::new();
@@ -40,6 +40,7 @@ impl Loader for TextureLoader{
}
}
#[expect(dead_code)]
#[derive(Debug)]
pub enum MeshError{
Io(std::io::Error),
@@ -71,33 +72,6 @@ impl From<vbsp::BspError> for MeshError{
}
}
#[derive(Clone,Copy)]
pub struct BspFinder<'bsp,'vpk>{
pub bsp:&'bsp Bsp,
pub vpks:&'vpk [Vpk],
}
impl<'bsp,'vpk> BspFinder<'bsp,'vpk>{
pub fn find<'a>(&self,path:&str)->Result<Option<Cow<'a,[u8]>>,vbsp::BspError>
where
'bsp:'a,
'vpk:'a,
{
// search bsp
if let Some(data)=self.bsp.pack_get(path)?{
return Ok(Some(Cow::Owned(data)));
}
//search each vpk
for vpk in self.vpks{
if let Some(vpk_entry)=vpk.tree_get(path){
return Ok(Some(vpk_entry.get()?));
}
}
Ok(None)
}
}
pub struct ModelLoader<'bsp,'vpk>{
finder:BspFinder<'bsp,'vpk>,
}
@@ -111,7 +85,7 @@ impl ModelLoader<'_,'_>{
}
}
}
impl<'bsp,'vpk> Loader for ModelLoader<'bsp,'vpk>{
impl Loader for ModelLoader<'_,'_>{
type Error=MeshError;
type Index<'a>=&'a str where Self:'a;
type Resource=vmdl::Model;
@@ -151,13 +125,13 @@ impl MeshLoader<'_,'_,'_,'_>{
}
}
}
impl<'str,'bsp,'vpk,'load> Loader for MeshLoader<'bsp,'vpk,'load,'str>{
impl Loader for MeshLoader<'_,'_,'_,'_>{
type Error=MeshError;
type Index<'a>=&'a str where Self:'a;
type Resource=Mesh;
type Resource=strafesnet_bsp_loader::mesh::Mesh;
fn load<'a>(&'a mut self,index:Self::Index<'a>)->Result<Self::Resource,Self::Error>{
let model=ModelLoader::new(self.finder).load(index)?;
let mesh=crate::mesh::convert_mesh(model,&mut self.deferred_loader);
let mesh=strafesnet_bsp_loader::mesh::convert_mesh(model,&mut self.deferred_loader);
Ok(mesh)
}
}

247
map-tool/src/source/mod.rs Normal file
View File

@@ -0,0 +1,247 @@
#[cfg(feature="cli")]
mod cli;
#[cfg(feature="cli")]
pub use cli::Commands;
#[cfg(feature="cli")]
mod loader;
use std::path::PathBuf;
use std::borrow::Cow;
use std::io::Cursor;
use strafesnet_bsp_loader::{Bsp,Vpk};
#[derive(Clone,Copy)]
pub struct BspFinder<'bsp,'vpk>{
pub bsp:&'bsp Bsp,
pub vpks:&'vpk [Vpk],
}
impl<'bsp,'vpk> BspFinder<'bsp,'vpk>{
pub fn find<'a>(&self,path:&str)->Result<Option<Cow<'a,[u8]>>,vbsp::BspError>
where
'bsp:'a,
'vpk:'a,
{
// search bsp
if let Some(data)=self.bsp.pack_get(path)?{
return Ok(Some(Cow::Owned(data)));
}
//search each vpk
for vpk in self.vpks{
if let Some(vpk_entry)=vpk.tree_get(path){
return Ok(Some(vpk_entry.get()?));
}
}
Ok(None)
}
}
enum VMTContent{
VMT(String),
VTF(String),
Patch(vmt_parser::material::PatchMaterial),
Unsupported,//don't want to deal with whatever vmt variant
Unresolved,//could not locate a texture because of vmt content
}
impl VMTContent{
fn vtf(opt:Option<String>)->Self{
match opt{
Some(s)=>Self::VTF(s),
None=>Self::Unresolved,
}
}
}
fn get_some_texture(material:vmt_parser::material::Material)->VMTContent{
//just grab some texture from somewhere for now
match material{
vmt_parser::material::Material::LightMappedGeneric(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::VertexLitGeneric(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),//this just dies if there is none
vmt_parser::material::Material::VertexLitGenericDx6(mat)=>VMTContent::vtf(mat.base_texture.or(mat.decal_texture)),
vmt_parser::material::Material::UnlitGeneric(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::UnlitTwoTexture(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::Water(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::WorldVertexTransition(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::EyeRefract(mat)=>VMTContent::vtf(Some(mat.cornea_texture)),
vmt_parser::material::Material::SubRect(mat)=>VMTContent::VMT(mat.material),//recursive
vmt_parser::material::Material::Sprite(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::SpriteCard(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::Cable(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::Refract(mat)=>VMTContent::vtf(mat.base_texture),
vmt_parser::material::Material::Modulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::DecalModulate(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::Sky(mat)=>VMTContent::vtf(Some(mat.base_texture)),
vmt_parser::material::Material::Replacements(_mat)=>VMTContent::Unsupported,
vmt_parser::material::Material::Patch(mat)=>VMTContent::Patch(mat),
_=>unreachable!(),
}
}
#[derive(Debug,thiserror::Error)]
pub enum GetVMTError{
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("Utf8 error {0:?}")]
Utf8(#[from]std::str::Utf8Error),
#[error("Vdf error {0:?}")]
Vdf(#[from]vmt_parser::VdfError),
#[error("Vmt not found")]
NotFound,
}
fn get_vmt(finder:BspFinder,search_name:&str)->Result<vmt_parser::material::Material,GetVMTError>{
let vmt_data=finder.find(search_name)?.ok_or(GetVMTError::NotFound)?;
//decode vmt and then write
let vmt_str=core::str::from_utf8(&vmt_data)?;
let material=vmt_parser::from_str(vmt_str)?;
Ok(material)
}
#[derive(Debug,thiserror::Error)]
pub enum LoadVMTError{
#[error("Bsp error {0:?}")]
Bsp(#[from]vbsp::BspError),
#[error("GetVMT error {0:?}")]
GetVMT(#[from]GetVMTError),
#[error("FromUtf8 error {0:?}")]
FromUtf8(#[from]std::string::FromUtf8Error),
#[error("Vdf error {0:?}")]
Vdf(#[from]vmt_parser::VdfError),
#[error("Vmt unsupported")]
Unsupported,
#[error("Vmt unresolved")]
Unresolved,
#[error("Vmt not found")]
NotFound,
}
fn recursive_vmt_loader<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,material:vmt_parser::material::Material)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
where
'bsp:'a,
'vpk:'a,
{
match get_some_texture(material){
VMTContent::VMT(mut s)=>{
s.make_ascii_lowercase();
recursive_vmt_loader(finder,get_vmt(finder,&s)?)
},
VMTContent::VTF(s)=>{
let mut texture_file_name=PathBuf::from("materials");
texture_file_name.push(s);
texture_file_name.set_extension("vtf");
texture_file_name.as_mut_os_str().make_ascii_lowercase();
Ok(finder.find(texture_file_name.to_str().unwrap())?)
},
VMTContent::Patch(mat)=>recursive_vmt_loader(finder,
mat.resolve(|search_name|{
let name_lowercase=search_name.to_lowercase();
match finder.find(&name_lowercase)?{
Some(bytes)=>Ok(String::from_utf8(bytes.into_owned())?),
None=>Err(LoadVMTError::NotFound),
}
})?
),
VMTContent::Unsupported=>Err(LoadVMTError::Unsupported),
VMTContent::Unresolved=>Err(LoadVMTError::Unresolved),
}
}
pub fn load_texture<'bsp,'vpk,'a>(finder:BspFinder<'bsp,'vpk>,texture_name:&str)->Result<Option<Cow<'a,[u8]>>,LoadVMTError>
where
'bsp:'a,
'vpk:'a,
{
let mut texture_file_name=PathBuf::from("materials");
//lower case
texture_file_name.push(texture_name);
texture_file_name.as_mut_os_str().make_ascii_lowercase();
//remove stem and search for both vtf and vmt files
let stem=texture_file_name.file_stem().unwrap().to_owned();
texture_file_name.pop();
texture_file_name.push(stem);
if let Some(stuff)=finder.find(texture_file_name.to_str().unwrap())?{
return Ok(Some(stuff));
}
// search for both vmt,vtf
let mut texture_file_name_vmt=texture_file_name.clone();
texture_file_name_vmt.set_extension("vmt");
let get_vmt_result=get_vmt(finder,texture_file_name_vmt.to_str().unwrap());
match get_vmt_result{
Ok(material)=>{
let vmt_result=recursive_vmt_loader(finder,material);
match vmt_result{
Ok(Some(stuff))=>return Ok(Some(stuff)),
Ok(None)
|Err(LoadVMTError::NotFound)=>(),
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
Err(e)=>return Err(e),
}
}
|Err(GetVMTError::NotFound)=>(),
Err(e)=>Err(e)?,
}
// try looking for vtf
let mut texture_file_name_vtf=texture_file_name.clone();
texture_file_name_vtf.set_extension("vtf");
let get_vtf_result=get_vmt(finder,texture_file_name_vtf.to_str().unwrap());
match get_vtf_result{
Ok(material)=>{
let vtf_result=recursive_vmt_loader(finder,material);
match vtf_result{
Ok(Some(stuff))=>return Ok(Some(stuff)),
Ok(None)
|Err(LoadVMTError::NotFound)=>(),
|Err(LoadVMTError::GetVMT(GetVMTError::NotFound))=>(),
Err(e)=>return Err(e),
}
}
|Err(GetVMTError::NotFound)=>(),
Err(e)=>Err(e)?,
}
Ok(None)
}
#[derive(Debug,thiserror::Error)]
pub enum ConvertTextureError{
#[error("Vtf error {0:?}")]
Vtf(#[from]vtf::Error),
#[error("DDS encode error {0:?}")]
DDSEncode(#[from]image_dds::error::SurfaceError),
#[error("DDS create error {0:?}")]
DDS(#[from]image_dds::CreateDdsError),
#[error("DDS write error {0:?}")]
DDSWrite(#[from]image_dds::ddsfile::Error),
}
pub fn convert_texture_to_dds(vtf_data:&[u8])->Result<Vec<u8>,ConvertTextureError>{
let vtf_vec=vtf_data.to_vec();
let image=vtf::from_bytes(&vtf_vec)?.highres_image.decode(0)?.to_rgba8();
let format=if image.width()%4!=0||image.height()%4!=0{
image_dds::ImageFormat::Rgba8UnormSrgb
}else{
image_dds::ImageFormat::BC7RgbaUnormSrgb
};
let dds=image_dds::SurfaceRgba8{
width:image.width(),
height:image.height(),
depth:1,
layers:1,
mipmaps:1,
data:image.as_raw(),
}.encode(format,image_dds::Quality::Slow,image_dds::Mipmaps::GeneratedAutomatic)?.to_dds()?;
let mut buf=Vec::new();
dds.write(&mut Cursor::new(&mut buf))?;
Ok(buf)
}
pub fn read_vpks(vpk_paths:&[PathBuf])->Result<Vec<Vpk>,vpk::Error>{
vpk_paths.iter().map(|vpk_path|{
Ok(Vpk::new(vpk::VPK::read(vpk_path)?))
}).collect()
}

View File

@@ -16,21 +16,24 @@ source = ["dep:strafesnet_deferred_loader", "dep:strafesnet_bsp_loader"]
roblox = ["dep:strafesnet_deferred_loader", "dep:strafesnet_rbx_loader"]
[dependencies]
glam = "0.30.0"
glam.workspace = true
parking_lot = "0.12.1"
pollster = "0.4.0"
strafesnet_bsp_loader = { path = "../lib/bsp_loader", registry = "strafesnet", optional = true }
strafesnet_common = { path = "../lib/common", registry = "strafesnet" }
strafesnet_deferred_loader = { path = "../lib/deferred_loader", registry = "strafesnet", optional = true }
strafesnet_graphics = { path = "../engine/graphics", registry = "strafesnet" }
strafesnet_physics = { path = "../engine/physics", registry = "strafesnet" }
strafesnet_rbx_loader = { path = "../lib/rbx_loader", registry = "strafesnet", optional = true }
strafesnet_session = { path = "../engine/session", registry = "strafesnet" }
strafesnet_settings = { path = "../engine/settings", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet", optional = true }
wgpu = "26.0.1"
strafesnet_bsp_loader = { workspace = true, optional = true }
strafesnet_common.workspace = true
strafesnet_deferred_loader = { workspace = true, optional = true }
strafesnet_graphics.workspace = true
strafesnet_physics.workspace = true
strafesnet_rbx_loader = { workspace = true, optional = true }
strafesnet_session.workspace = true
strafesnet_settings.workspace = true
strafesnet_snf = { workspace = true, optional = true }
wgpu.workspace = true
winit = "0.30.7"
[profile.dev]
strip = false
opt-level = 3
[lints]
workspace = true

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +0,0 @@
# Blender MTL File: 'teslacyberv3.0.blend'
# Material Count: 6
newmtl Material
Ns 65.476285
Ka 1.000000 1.000000 1.000000
Kd 0.411568 0.411568 0.411568
Ks 0.614679 0.614679 0.614679
Ke 0.000000 0.000000 0.000000
Ni 36.750000
d 1.000000
illum 3
newmtl Материал
Ns 323.999994
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
newmtl Материал.001
Ns 900.000000
Ka 1.000000 1.000000 1.000000
Kd 0.026240 0.026240 0.026240
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 1
newmtl Материал.002
Ns 0.000000
Ka 1.000000 1.000000 1.000000
Kd 0.031837 0.032429 0.029425
Ks 0.169725 0.169725 0.169725
Ke 0.000000 0.000000 0.000000
Ni 0.000000
d 1.000000
illum 2
newmtl Материал.003
Ns 900.000000
Ka 1.000000 1.000000 1.000000
Kd 0.023585 0.083235 0.095923
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 45.049999
d 1.000000
illum 3
newmtl Материал.004
Ns 323.999994
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2

File diff suppressed because it is too large Load Diff

View File

@@ -20,8 +20,7 @@ WorkerDescription{
pub fn new(
mut graphics:graphics::GraphicsState,
mut config:wgpu::SurfaceConfiguration,
surface:wgpu::Surface<'_>,
mut surface:strafesnet_graphics::surface::Surface<'_>,
device:wgpu::Device,
queue:wgpu::Queue,
)->crate::compat_worker::INWorker<'_,Instruction>{
@@ -34,29 +33,22 @@ pub fn new(
Instruction::Resize(size,user_settings)=>{
println!("Resizing to {:?}",size);
let t0=std::time::Instant::now();
config.width=size.width.max(1);
config.height=size.height.max(1);
surface.configure(&device,&config);
graphics.resize(&device,&config,&user_settings);
let size=glam::uvec2(size.width,size.height);
surface.configure(&device,size);
let size=surface.size();
let fov=user_settings.calculate_fov(1.0,&size).as_vec2();
graphics.resize(&device,size,fov);
println!("Resize took {:?}",t0.elapsed());
}
Instruction::Render(frame_state)=>{
//this has to go deeper somehow
let frame=match surface.get_current_texture(){
Ok(frame)=>frame,
Err(_)=>{
surface.configure(&device,&config);
surface
.get_current_texture()
.expect("Failed to acquire next surface texture!")
}
};
let view=frame.texture.create_view(&wgpu::TextureViewDescriptor{
format:Some(config.view_formats[0]),
..wgpu::TextureViewDescriptor::default()
});
let frame=surface.new_frame(&device).expect("Error creating new frame");
graphics.render(&view,&device,&queue,frame_state);
let mut encoder=device.create_command_encoder(&wgpu::CommandEncoderDescriptor{label:None});
graphics.encode_commands(&mut encoder,frame.view(),graphics::view_inv(frame_state.pos(),frame_state.angles()));
queue.submit([encoder.finish()]);
frame.present();
}

View File

@@ -9,5 +9,5 @@ mod graphics_worker;
const TITLE:&'static str=concat!("Strafe Client v",env!("CARGO_PKG_VERSION"));
fn main(){
setup::setup_and_start(TITLE);
pollster::block_on(setup::setup_and_start(TITLE));
}

View File

@@ -1,198 +1,44 @@
fn optional_features()->wgpu::Features{
wgpu::Features::TEXTURE_COMPRESSION_ASTC
|wgpu::Features::TEXTURE_COMPRESSION_ETC2
}
fn required_features()->wgpu::Features{
wgpu::Features::TEXTURE_COMPRESSION_BC
}
fn required_downlevel_capabilities()->wgpu::DownlevelCapabilities{
wgpu::DownlevelCapabilities{
flags:wgpu::DownlevelFlags::empty(),
shader_model:wgpu::ShaderModel::Sm5,
..wgpu::DownlevelCapabilities::default()
}
}
use strafesnet_graphics::setup;
const LIMITS:wgpu::Limits=wgpu::Limits::defaults();
struct SetupContextPartial1{
backends:wgpu::Backends,
instance:wgpu::Instance,
}
fn create_window(title:&str,event_loop:&winit::event_loop::EventLoop<()>)->Result<winit::window::Window,winit::error::OsError>{
let mut attr=winit::window::WindowAttributes::default();
attr=attr.with_title(title);
event_loop.create_window(attr)
}
fn create_instance()->SetupContextPartial1{
let backends=wgpu::Backends::from_env().unwrap_or_default();
SetupContextPartial1{
backends,
instance:Default::default(),
}
}
impl SetupContextPartial1{
fn create_surface<'a>(self,window:&'a winit::window::Window)->Result<SetupContextPartial2<'a>,wgpu::CreateSurfaceError>{
Ok(SetupContextPartial2{
backends:self.backends,
surface:self.instance.create_surface(window)?,
instance:self.instance,
})
}
}
struct SetupContextPartial2<'a>{
backends:wgpu::Backends,
instance:wgpu::Instance,
surface:wgpu::Surface<'a>,
}
impl<'a> SetupContextPartial2<'a>{
fn pick_adapter(self)->SetupContextPartial3<'a>{
let adapter;
//TODO: prefer adapter that implements optional features
//let optional_features=optional_features();
let required_features=required_features();
//no helper function smh gotta write it myself
let adapters=self.instance.enumerate_adapters(self.backends);
let mut chosen_adapter=None;
let mut chosen_adapter_score=0;
for adapter in adapters {
if !adapter.is_surface_supported(&self.surface) {
continue;
}
let score=match adapter.get_info().device_type{
wgpu::DeviceType::IntegratedGpu=>3,
wgpu::DeviceType::DiscreteGpu=>4,
wgpu::DeviceType::VirtualGpu=>2,
wgpu::DeviceType::Other|wgpu::DeviceType::Cpu=>1,
};
let adapter_features=adapter.features();
if chosen_adapter_score<score&&adapter_features.contains(required_features) {
chosen_adapter_score=score;
chosen_adapter=Some(adapter);
}
}
if let Some(maybe_chosen_adapter)=chosen_adapter{
adapter=maybe_chosen_adapter;
}else{
panic!("No suitable GPU adapters found on the system!");
}
let adapter_info=adapter.get_info();
println!("Using {} ({:?})", adapter_info.name, adapter_info.backend);
let required_downlevel_capabilities=required_downlevel_capabilities();
let downlevel_capabilities=adapter.get_downlevel_capabilities();
assert!(
downlevel_capabilities.shader_model >= required_downlevel_capabilities.shader_model,
"Adapter does not support the minimum shader model required to run this example: {:?}",
required_downlevel_capabilities.shader_model
);
assert!(
downlevel_capabilities
.flags
.contains(required_downlevel_capabilities.flags),
"Adapter does not support the downlevel capabilities required to run this example: {:?}",
required_downlevel_capabilities.flags - downlevel_capabilities.flags
);
SetupContextPartial3{
surface:self.surface,
adapter,
}
}
}
struct SetupContextPartial3<'a>{
surface:wgpu::Surface<'a>,
adapter:wgpu::Adapter,
}
impl<'a> SetupContextPartial3<'a>{
fn request_device(self)->SetupContextPartial4<'a>{
let optional_features=optional_features();
let required_features=required_features();
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the surface.
let needed_limits=strafesnet_graphics::graphics::required_limits().using_resolution(self.adapter.limits());
let (device, queue)=pollster::block_on(self.adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: (optional_features & self.adapter.features()) | required_features,
required_limits: needed_limits,
memory_hints:wgpu::MemoryHints::Performance,
trace: wgpu::Trace::Off,
},
))
.expect("Unable to find a suitable GPU adapter!");
SetupContextPartial4{
surface:self.surface,
adapter:self.adapter,
device,
queue,
}
}
}
struct SetupContextPartial4<'a>{
surface:wgpu::Surface<'a>,
adapter:wgpu::Adapter,
device:wgpu::Device,
queue:wgpu::Queue,
}
impl<'a> SetupContextPartial4<'a>{
fn configure_surface(self,size:&'a winit::dpi::PhysicalSize<u32>)->SetupContext<'a>{
let mut config=self.surface
.get_default_config(&self.adapter, size.width, size.height)
.expect("Surface isn't supported by the adapter.");
let surface_view_format=config.format.add_srgb_suffix();
config.view_formats.push(surface_view_format);
config.present_mode=wgpu::PresentMode::AutoNoVsync;
self.surface.configure(&self.device, &config);
SetupContext{
surface:self.surface,
device:self.device,
queue:self.queue,
config,
}
}
}
pub struct SetupContext<'a>{
pub surface:wgpu::Surface<'a>,
pub device:wgpu::Device,
pub queue:wgpu::Queue,
pub config:wgpu::SurfaceConfiguration,
}
pub fn setup_and_start(title:&str){
pub async fn setup_and_start(title:&str){
let event_loop=winit::event_loop::EventLoop::new().unwrap();
println!("Initializing the surface...");
let partial_1=create_instance();
let window=create_window(title,&event_loop).unwrap();
let partial_2=partial_1.create_surface(&window).unwrap();
println!("Initializing the surface...");
let partial_3=partial_2.pick_adapter();
let desc=wgpu::InstanceDescriptor::new_with_display_handle_from_env(Box::new(event_loop.owned_display_handle()));
let instance=wgpu::Instance::new(desc);
let partial_4=partial_3.request_device();
let surface=setup::step2::create_surface(&instance,&window).unwrap();
let adapter=setup::step3::pick_adapter(&instance,&surface).await.expect("No suitable GPU adapters found on the system!");
let adapter_info=adapter.get_info();
println!("Using {} ({:?})",adapter_info.name,adapter_info.backend);
let (device,queue)=setup::step4::request_device(&adapter,LIMITS).await.unwrap();
let size=window.inner_size();
let setup_context=partial_4.configure_surface(&size);
let surface=setup::step5::configure_surface(&adapter,&device,surface,(size.width,size.height)).unwrap();
//dedicated thread to ping request redraw back and resize the window doesn't seem logical
//the thread that spawns the physics thread
let mut window_thread=crate::window::worker(
&window,
setup_context,
device,
queue,
surface,
LIMITS,
);
for arg in std::env::args().skip(1){

View File

@@ -15,6 +15,7 @@ pub enum Instruction{
struct WindowContext<'a>{
manual_mouse_lock:bool,
mouse_pos:glam::DVec2,
simulation_paused:bool,
screen_size:glam::UVec2,
window:&'a winit::window::Window,
physics_thread:crate::compat_worker::QNWorker<'a,TimedInstruction<PhysicsWorkerInstruction,SessionTime>>,
@@ -24,6 +25,35 @@ impl WindowContext<'_>{
fn get_middle_of_screen(&self)->winit::dpi::PhysicalPosition<u32>{
winit::dpi::PhysicalPosition::new(self.screen_size.x/2,self.screen_size.y/2)
}
fn free_mouse(&mut self){
self.manual_mouse_lock=false;
match self.window.set_cursor_position(self.get_middle_of_screen()){
Ok(())=>(),
Err(e)=>println!("Could not set cursor position: {:?}",e),
}
match self.window.set_cursor_grab(winit::window::CursorGrabMode::None){
Ok(())=>(),
Err(e)=>println!("Could not release cursor: {:?}",e),
}
self.window.set_cursor_visible(true);
}
fn lock_mouse(&mut self){
//if cursor is outside window don't lock but apparently there's no get pos function
//let pos=window.get_cursor_pos();
match self.window.set_cursor_grab(winit::window::CursorGrabMode::Locked){
Ok(())=>(),
Err(_)=>{
match self.window.set_cursor_grab(winit::window::CursorGrabMode::Confined){
Ok(())=>(),
Err(e)=>{
self.manual_mouse_lock=true;
println!("Could not confine cursor: {:?}",e)
},
}
}
}
self.window.set_cursor_visible(false);
}
fn window_event(&mut self,time:SessionTime,event:winit::event::WindowEvent){
match event{
winit::event::WindowEvent::DroppedFile(path)=>{
@@ -34,6 +64,10 @@ impl WindowContext<'_>{
}
},
winit::event::WindowEvent::Focused(state)=>{
// don't unpause if manually paused
if self.simulation_paused{
return;
}
//pause unpause
self.physics_thread.send(TimedInstruction{
time,
@@ -46,35 +80,8 @@ impl WindowContext<'_>{
..
}=>{
match (logical_key,state){
(winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab),winit::event::ElementState::Pressed)=>{
self.manual_mouse_lock=false;
match self.window.set_cursor_position(self.get_middle_of_screen()){
Ok(())=>(),
Err(e)=>println!("Could not set cursor position: {:?}",e),
}
match self.window.set_cursor_grab(winit::window::CursorGrabMode::None){
Ok(())=>(),
Err(e)=>println!("Could not release cursor: {:?}",e),
}
self.window.set_cursor_visible(state.is_pressed());
},
(winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab),winit::event::ElementState::Released)=>{
//if cursor is outside window don't lock but apparently there's no get pos function
//let pos=window.get_cursor_pos();
match self.window.set_cursor_grab(winit::window::CursorGrabMode::Locked){
Ok(())=>(),
Err(_)=>{
match self.window.set_cursor_grab(winit::window::CursorGrabMode::Confined){
Ok(())=>(),
Err(e)=>{
self.manual_mouse_lock=true;
println!("Could not confine cursor: {:?}",e)
},
}
}
}
self.window.set_cursor_visible(state.is_pressed());
},
(winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab),winit::event::ElementState::Pressed)=>self.free_mouse(),
(winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab),winit::event::ElementState::Released)=>self.lock_mouse(),
(winit::keyboard::Key::Named(winit::keyboard::NamedKey::F11),winit::event::ElementState::Pressed)=>{
if self.window.fullscreen().is_some(){
self.window.set_fullscreen(None);
@@ -131,8 +138,18 @@ impl WindowContext<'_>{
if let Some(session_instruction)=match keycode{
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Space)=>input_ctrl!(SetJump,s),
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Shift)=>input_ctrl!(SetSprint,s),
// TODO: bind system so playback pausing can use spacebar
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Enter)=>session_playback!(TogglePaused,s),
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Enter)=>if s{
let paused=!self.simulation_paused;
self.simulation_paused=paused;
if paused{
self.free_mouse();
}else{
self.lock_mouse();
}
Some(SessionInstructionSubset::Control(SessionControlInstruction::SetPaused(paused)))
}else{None},
winit::keyboard::Key::Named(winit::keyboard::NamedKey::ArrowUp)=>session_playback!(IncreaseTimescale,s),
winit::keyboard::Key::Named(winit::keyboard::NamedKey::ArrowDown)=>session_playback!(DecreaseTimescale,s),
winit::keyboard::Key::Named(winit::keyboard::NamedKey::ArrowLeft)=>session_playback!(SkipBack,s),
@@ -188,7 +205,7 @@ impl WindowContext<'_>{
}
}
fn device_event(&mut self,time:SessionTime,event: winit::event::DeviceEvent){
fn device_event(&mut self,time:SessionTime,event:winit::event::DeviceEvent){
match event{
winit::event::DeviceEvent::MouseMotion{
delta,
@@ -222,7 +239,10 @@ impl WindowContext<'_>{
}
pub fn worker<'a>(
window:&'a winit::window::Window,
setup_context:crate::setup::SetupContext<'a>,
device:wgpu::Device,
queue:wgpu::Queue,
surface:strafesnet_graphics::surface::Surface<'a>,
limits:wgpu::Limits,
)->crate::compat_worker::QNWorker<'a,TimedInstruction<Instruction,SessionTime>>{
// WindowContextSetup::new
#[cfg(feature="user-install")]
@@ -232,15 +252,17 @@ pub fn worker<'a>(
let user_settings=directories.settings();
let mut graphics=strafesnet_graphics::graphics::GraphicsState::new(&setup_context.device,&setup_context.queue,&setup_context.config);
graphics.load_user_settings(&user_settings);
let screen_size=surface.size();
let mut graphics=strafesnet_graphics::graphics::GraphicsState::new(&device,&queue,screen_size,surface.view_format(),limits);
//WindowContextSetup::into_context
let screen_size=glam::uvec2(setup_context.config.width,setup_context.config.height);
let graphics_thread=crate::graphics_worker::new(graphics,setup_context.config,setup_context.surface,setup_context.device,setup_context.queue);
let fov=user_settings.calculate_fov(1.0,&screen_size).as_vec2();
graphics.resize(&device,screen_size,fov);
let graphics_thread=crate::graphics_worker::new(graphics,surface,device,queue);
let mut window_context=WindowContext{
manual_mouse_lock:false,
mouse_pos:glam::DVec2::ZERO,
simulation_paused:false,
//make sure to update this!!!!!
screen_size,
window,