Compare commits

..

5 Commits

Author SHA1 Message Date
267aa89469 union decode test 2025-01-23 16:47:31 -08:00
c6cba01dcc fix size 2025-01-23 12:34:07 -08:00
3e7d64b514 fix it 2025-01-23 11:49:53 -08:00
ca5d19e800 wip 2025-01-23 11:39:29 -08:00
e9bb9e9a9f minimize lints 2025-01-23 11:39:21 -08:00
8 changed files with 259 additions and 199 deletions

7
Cargo.lock generated
View File

@@ -969,8 +969,11 @@ dependencies = [
name = "integration-testing"
version = "0.1.0"
dependencies = [
"rbx_dom_weak",
"rbx_mesh",
"strafesnet_common",
"strafesnet_physics",
"strafesnet_rbx_loader",
"strafesnet_snf",
]
@@ -1955,9 +1958,9 @@ dependencies = [
[[package]]
name = "rbx_mesh"
version = "0.2.0"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1205fdae1f9a8bfd5d8fbe6036066673d530ee392f7840d6f8a24e763559c7fd"
checksum = "864ead0e98afce28c960f653d6203483834890d07f87b60e2f01415530a2fe9d"
dependencies = [
"binrw",
"lazy-regex",

View File

@@ -4,6 +4,9 @@ version = "0.1.0"
edition = "2021"
[dependencies]
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_mesh = { version = "0.1.2", path = "../../rbx_mesh" }
strafesnet_common = { version = "0.5.2", path = "../lib/common", registry = "strafesnet" }
strafesnet_physics = { version = "0.1.0", path = "../engine/physics", registry = "strafesnet" }
strafesnet_rbx_loader = { version = "0.5.2", path = "../lib/rbx_loader", registry = "strafesnet" }
strafesnet_snf = { path = "../lib/snf", registry = "strafesnet" }

View File

@@ -4,7 +4,7 @@ use std::{io::{Cursor,Read},path::Path};
use strafesnet_physics::physics::{PhysicsData,PhysicsState,PhysicsContext};
fn main(){
test_determinism().unwrap();
decode_all_unions().unwrap();
}
#[allow(unused)]
@@ -219,3 +219,125 @@ fn test_determinism()->Result<(),ReplayError>{
Ok(())
}
#[allow(unused)]
#[derive(Debug)]
enum UnionError{
IO(std::io::Error),
Read(strafesnet_rbx_loader::ReadError),
Union(rbx_mesh::physics_data::Error),
}
impl From<std::io::Error> for UnionError{
fn from(value:std::io::Error)->Self{
Self::IO(value)
}
}
impl From<strafesnet_rbx_loader::ReadError> for UnionError{
fn from(value:strafesnet_rbx_loader::ReadError)->Self{
Self::Read(value)
}
}
impl From<rbx_mesh::physics_data::Error> for UnionError{
fn from(value:rbx_mesh::physics_data::Error)->Self{
Self::Union(value)
}
}
fn test_unions_in_map(path:impl AsRef<Path>)->Result<u32,UnionError>{
let mut count=0;
let file=read_entire_file(path)?;
let model=strafesnet_rbx_loader::read(file)?;
let dom=model.as_ref();
for instance in dom.descendants(){
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=instance.properties.get("PhysicsData"){
count+=1;
let physics_data:&[u8]=data.as_ref();
if let b""=physics_data{
continue;
}else{
let mut cursor=std::io::Cursor::new(physics_data);
let physics_data:rbx_mesh::physics_data::PhysicsData=rbx_mesh::read_physics_data(&mut cursor)?;
assert_eq!(cursor.position(),cursor.into_inner().len() as u64);
match physics_data.collision_data{
rbx_mesh::physics_data::CollisionData::Block=>(),
rbx_mesh::physics_data::CollisionData::Meshes(_)=>(),
rbx_mesh::physics_data::CollisionData::PhysicsInfoMesh(_)=>{
println!("pim!");
},
}
}
}
}
Ok(count)
}
#[derive(Debug)]
struct UnionResult{
path:std::path::PathBuf,
result:Result<u32,UnionError>,
}
fn do_union_thread(path:std::path::PathBuf,send:std::sync::mpsc::Sender<UnionResult>){
std::thread::spawn(move ||{
let result=test_unions_in_map(path.as_path());
send.send(UnionResult{
path,
result,
}).unwrap();
});
}
fn decode_all_unions()->Result<(),std::io::Error>{
let thread_limit=std::thread::available_parallelism()?.get();
let (send,recv)=std::sync::mpsc::channel();
let mut read_dir=std::fs::read_dir("/run/media/quat/Files/Documents/map-files/verify-scripts/maps/bhop_all")?;
let mut union_count=0;
let mut success_count=0;
let mut fail_count=0;
let mut f=|thing:UnionResult|{
println!("file={:?} result={:?}",thing.path.file_stem(),thing.result);
match thing.result{
Ok(count)=>{
union_count+=count;
success_count+=1;
},
Err(_)=>fail_count+=1,
}
};
// spawn threads
println!("spawning up to {thread_limit} threads...");
let mut active_thread_count=0;
while active_thread_count<thread_limit{
if let Some(dir_entry_result)=read_dir.next(){
if let Some(file_path)=get_file_path(dir_entry_result?)?{
active_thread_count+=1;
do_union_thread(file_path,send.clone());
}
}else{
break;
}
}
// spawn another thread every time a message is received from the channel
println!("riding parallelism wave...");
while let Some(dir_entry_result)=read_dir.next(){
if let Some(file_path)=get_file_path(dir_entry_result?)?{
// wait for a thread to complete
f(recv.recv().unwrap());
do_union_thread(file_path,send.clone());
}
}
// wait for remaining threads to complete
println!("waiting for all threads to complete...");
for _ in 0..active_thread_count{
f(recv.recv().unwrap());
}
println!("===RESULTS===\nunion_count={union_count}\nsuccess={success_count}\nfail={fail_count}");
Ok(())
}

View File

@@ -15,7 +15,7 @@ glam = "0.29.0"
lazy-regex = "3.1.0"
rbx_binary = { version = "0.7.4", registry = "strafesnet" }
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" }
rbx_mesh = "0.2.0"
rbx_mesh = "0.1.2"
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" }
rbx_xml = { version = "0.13.3", registry = "strafesnet" }
roblox_emulator = { path = "../roblox_emulator", registry = "strafesnet" }

View File

@@ -1,6 +0,0 @@
// TODO: make a directories structure like strafe client
struct Directories{
textures:PathBuf,
meshes:PathBuf,
unions:PathBuf,
}

View File

@@ -1,4 +0,0 @@
// TODO: move code from deferred_loader to here
// use generics to specify a hashable type for the acquire_X function signature
// use impls/traits instead of passing around functions
// part of the goob remains in deferred loader, the common bits between both

View File

@@ -403,94 +403,14 @@ enum RobloxBasePartDescription{
Wedge(RobloxWedgeDescription),
CornerWedge(RobloxCornerWedgeDescription),
}
fn get_texture_description<AcquireRenderConfigId>(
temp_objects:&mut Vec<rbx_dom_weak::types::Ref>,
acquire_render_config_id:&mut AcquireRenderConfigId,
dom:&rbx_dom_weak::WeakDom,
object:&rbx_dom_weak::Instance,
size:&rbx_dom_weak::types::Vector3,
)->RobloxPartDescription
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
{
//use the biggest one and cut it down later...
let mut part_texture_description:RobloxPartDescription=[None,None,None,None,None,None];
temp_objects.clear();
recursive_collect_superclass(temp_objects,&dom,object,"Decal");
for &mut decal_ref in temp_objects{
if let Some(decal)=dom.get_by_ref(decal_ref){
if let (
Some(rbx_dom_weak::types::Variant::Content(content)),
Some(rbx_dom_weak::types::Variant::Enum(normalid)),
Some(rbx_dom_weak::types::Variant::Color3(decal_color3)),
Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)),
) = (
decal.properties.get("Texture"),
decal.properties.get("Face"),
decal.properties.get("Color3"),
decal.properties.get("Transparency"),
) {
let render_id=acquire_render_config_id(Some(content.as_ref()));
let normal_id=normalid.to_u32();
if normal_id<6{
let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{
//generate tranform
if let (
Some(rbx_dom_weak::types::Variant::Float32(ox)),
Some(rbx_dom_weak::types::Variant::Float32(oy)),
Some(rbx_dom_weak::types::Variant::Float32(sx)),
Some(rbx_dom_weak::types::Variant::Float32(sy)),
) = (
decal.properties.get("OffsetStudsU"),
decal.properties.get("OffsetStudsV"),
decal.properties.get("StudsPerTileU"),
decal.properties.get("StudsPerTileV"),
)
{
let (size_u,size_v)=match normal_id{
0=>(size.z,size.y),//right
1=>(size.x,size.z),//top
2=>(size.x,size.y),//back
3=>(size.z,size.y),//left
4=>(size.x,size.z),//bottom
5=>(size.x,size.y),//front
_=>unreachable!(),
};
(
glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency),
RobloxTextureTransform{
offset_u:*ox/(*sx),offset_v:*oy/(*sy),
scale_u:size_u/(*sx),scale_v:size_v/(*sy),
}
)
}else{
(glam::Vec4::ONE,RobloxTextureTransform::default())
}
}else{
(glam::Vec4::ONE,RobloxTextureTransform::default())
};
part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{
render:render_id,
color:roblox_texture_color,
transform:roblox_texture_transform,
});
}else{
println!("NormalId={} is invalid",normal_id);
}
}
}
}
part_texture_description
}
enum Shape{
Primitive(primitives::Primitives),
MeshPart,
PhysicsData,
}
enum MeshAvailability<'a>{
enum MeshAvailability{
Immediate,
DeferredMesh(RenderConfigId),
DeferredUnion(RobloxPartDescription,UnionDeferredAttributes<'a>),
Deferred(RenderConfigId),
}
struct DeferredModelDeferredAttributes{
render:RenderConfigId,
@@ -502,17 +422,6 @@ struct ModelDeferredAttributes{
color:model::Color4,//transparency is in here
transform:Planar64Affine3,
}
struct DeferredUnionDeferredAttributes<'a>{
render:RobloxPartDescription,
model:ModelDeferredAttributes,
union:UnionDeferredAttributes<'a>,
}
#[derive(Hash)]
struct UnionDeferredAttributes<'a>{
asset_id:Option<&'a str>,
mesh_data:Option<&'a [u8]>,
physics_data:Option<&'a [u8]>,
}
struct ModelOwnedAttributes{
mesh:model::MeshId,
attributes:attr::CollisionAttributes,
@@ -524,21 +433,21 @@ struct GetAttributesArgs{
can_collide:bool,
velocity:Planar64Vec3,
}
pub fn convert<'a,AcquireRenderConfigId,AcquireMeshId>(
dom:&'a rbx_dom_weak::WeakDom,
pub fn convert<AcquireRenderConfigId,AcquireMeshId>(
dom:&rbx_dom_weak::WeakDom,
mut acquire_render_config_id:AcquireRenderConfigId,
mut acquire_mesh_id:AcquireMeshId,
)->PartialMap1<'a>
)->PartialMap1
where
AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
AcquireMeshId:FnMut(&str)->model::MeshId,
{
let mut deferred_unions_deferred_attributes=Vec::new();
let mut deferred_models_deferred_attributes=Vec::new();
let mut primitive_models_deferred_attributes=Vec::new();
let mut primitive_meshes=Vec::new();
let mut mesh_id_from_description=HashMap::new();
let mut mesh_id_from_physics_data=HashMap::<&[u8],_>::new();
//just going to leave it like this for now instead of reworking the data structures for this whole thing
let textureless_render_group=acquire_render_config_id(None);
@@ -605,8 +514,74 @@ where
let (availability,mesh_id)=match shape{
Shape::Primitive(primitive_shape)=>{
//TODO: TAB TAB
let part_texture_description=get_texture_description(&mut temp_objects,&mut acquire_render_config_id,dom,object,size);
//TODO: TAB TAB
//use the biggest one and cut it down later...
let mut part_texture_description:RobloxPartDescription=[None,None,None,None,None,None];
temp_objects.clear();
recursive_collect_superclass(&mut temp_objects, &dom, object,"Decal");
for &decal_ref in &temp_objects{
if let Some(decal)=dom.get_by_ref(decal_ref){
if let (
Some(rbx_dom_weak::types::Variant::Content(content)),
Some(rbx_dom_weak::types::Variant::Enum(normalid)),
Some(rbx_dom_weak::types::Variant::Color3(decal_color3)),
Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)),
) = (
decal.properties.get("Texture"),
decal.properties.get("Face"),
decal.properties.get("Color3"),
decal.properties.get("Transparency"),
) {
let render_id=acquire_render_config_id(Some(content.as_ref()));
let normal_id=normalid.to_u32();
if normal_id<6{
let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{
//generate tranform
if let (
Some(rbx_dom_weak::types::Variant::Float32(ox)),
Some(rbx_dom_weak::types::Variant::Float32(oy)),
Some(rbx_dom_weak::types::Variant::Float32(sx)),
Some(rbx_dom_weak::types::Variant::Float32(sy)),
) = (
decal.properties.get("OffsetStudsU"),
decal.properties.get("OffsetStudsV"),
decal.properties.get("StudsPerTileU"),
decal.properties.get("StudsPerTileV"),
)
{
let (size_u,size_v)=match normal_id{
0=>(size.z,size.y),//right
1=>(size.x,size.z),//top
2=>(size.x,size.y),//back
3=>(size.z,size.y),//left
4=>(size.x,size.z),//bottom
5=>(size.x,size.y),//front
_=>unreachable!(),
};
(
glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency),
RobloxTextureTransform{
offset_u:*ox/(*sx),offset_v:*oy/(*sy),
scale_u:size_u/(*sx),scale_v:size_v/(*sy),
}
)
}else{
(glam::Vec4::ONE,RobloxTextureTransform::default())
}
}else{
(glam::Vec4::ONE,RobloxTextureTransform::default())
};
part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{
render:render_id,
color:roblox_texture_color,
transform:roblox_texture_transform,
});
}else{
println!("NormalId={} unsupported for shape={:?}",normal_id,primitive_shape);
}
}
}
}
//obscure rust syntax "slice pattern"
let [
f0,//Cube::Right
@@ -719,7 +694,7 @@ where
object.properties.get("TextureID"),
){
(
MeshAvailability::DeferredMesh(acquire_render_config_id(Some(texture_asset_id.as_ref()))),
MeshAvailability::Deferred(acquire_render_config_id(Some(texture_asset_id.as_ref()))),
acquire_mesh_id(mesh_asset_id.as_ref()),
)
}else{
@@ -728,35 +703,34 @@ where
Shape::PhysicsData=>{
//The union mesh is sized already
model_transform=planar64_affine3_from_roblox(cf,&rbx_dom_weak::types::Vector3{x:2.0,y:2.0,z:2.0});
let mut asset_id=None;
let mut mesh_data=None;
let mut physics_data=None;
if let Some(rbx_dom_weak::types::Variant::Content(content))=object.properties.get("AssetId"){
let value:&str=content.as_ref();
if !value.is_empty(){
asset_id=Some(value);
}
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get("MeshData"){
let value:&[u8]=data.as_ref();
if !value.is_empty(){
mesh_data=Some(value);
}
}
if let Some(rbx_dom_weak::types::Variant::BinaryString(data))=object.properties.get("PhysicsData"){
let value:&[u8]=data.as_ref();
if !value.is_empty(){
physics_data=Some(value);
}
let physics_data=data.as_ref();
let mesh_id=if let Some(&mesh_id)=mesh_id_from_physics_data.get(physics_data){
mesh_id
}else{
match crate::union::convert(physics_data){
Ok(mesh)=>{
let mesh_id=model::MeshId::new(primitive_meshes.len() as u32);
primitive_meshes.push(mesh);
mesh_id_from_physics_data.insert(physics_data,mesh_id);
mesh_id
},
Err(e)=>{
println!("Union mesh decode error {e:?}");
*mesh_id_from_description.entry(RobloxBasePartDescription::Part(RobloxPartDescription::default()))
.or_insert_with(||{
let mesh_id=model::MeshId::new(primitive_meshes.len() as u32);
let mesh=primitives::unit_cube(textureless_render_group);
primitive_meshes.push(mesh);
mesh_id
})
},
}
};
(MeshAvailability::Immediate,mesh_id)
}else{
panic!("Mesh has no Mesh or Texture");
}
let part_texture_description=get_texture_description(&mut temp_objects,&mut acquire_render_config_id,dom,object,size);
let union_deferred_attributes=UnionDeferredAttributes{
asset_id,
mesh_data,
physics_data,
};
(MeshAvailability::DeferredUnion(part_texture_description,union_deferred_attributes),mesh_id)
},
};
let model_deferred_attributes=ModelDeferredAttributes{
@@ -771,15 +745,10 @@ where
};
match availability{
MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes),
MeshAvailability::DeferredMesh(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
MeshAvailability::Deferred(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{
render,
model:model_deferred_attributes
}),
MeshAvailability::DeferredUnion(part_texture_description,union_deferred_attributes)=>deferred_unions_deferred_attributes.push(DeferredUnionDeferredAttributes{
render:part_texture_description,
model:model_deferred_attributes,
union:union_deferred_attributes,
}),
}
}
}
@@ -794,11 +763,10 @@ struct MeshWithAabb{
mesh:model::Mesh,
aabb:strafesnet_common::aabb::Aabb,
}
pub struct PartialMap1<'a>{
pub struct PartialMap1{
primitive_meshes:Vec<model::Mesh>,
primitive_models_deferred_attributes:Vec<ModelDeferredAttributes>,
deferred_models_deferred_attributes:Vec<DeferredModelDeferredAttributes>,
deferred_union_deferred_attributes:Vec<DeferredModelDeferredAttributes>,
}
impl PartialMap1{
pub fn add_meshpart_meshes_and_calculate_attributes(

View File

@@ -7,11 +7,9 @@ use strafesnet_common::integer::vec3;
#[derive(Debug)]
pub enum Error{
Block,
NotSupposedToHappen,
MissingVertexId(u32),
Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError),
RobloxPhysicsData(rbx_mesh::physics_data::Error),
RobloxMeshData(rbx_mesh::mesh_data::Error),
}
impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
@@ -20,38 +18,14 @@ impl std::fmt::Display for Error{
}
impl std::error::Error for Error{}
pub fn convert(roblox_physics_data:&[u8],roblox_mesh_data:&[u8])->Result<model::Mesh,Error>{
match (roblox_physics_data,roblox_mesh_data){
(b"",b"")=>return Err(Error::Block),
(b"",_)
|(_,b"")=>return Err(Error::NotSupposedToHappen),
_=>(),
}
// graphical
let mesh_data=rbx_mesh::read_mesh_data_versioned(
std::io::Cursor::new(roblox_mesh_data)
).map_err(Error::RobloxMeshData)?;
let graphics_mesh=match mesh_data{
rbx_mesh::mesh_data::CSGPHS::CSGK(csgk)=>return Err(Error::NotSupposedToHappen),
rbx_mesh::mesh_data::CSGPHS::CSGPHS2(mesh_data2)=>mesh_data2.mesh,
rbx_mesh::mesh_data::CSGPHS::CSGPHS4(mesh_data4)=>mesh_data4.mesh,
};
// physical
let physics_data=rbx_mesh::read_physics_data(
std::io::Cursor::new(roblox_physics_data)
).map_err(Error::RobloxPhysicsData)?;
let physics_convex_meshes=match physics_data{
rbx_mesh::physics_data::PhysicsData::CSGK(_)
// have not seen this format in practice
|rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Block)
=>return Err(Error::NotSupposedToHappen),
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Meshes3(meshes))
|rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::Meshes5(meshes))
=>meshes.meshes,
rbx_mesh::physics_data::PhysicsData::CSGPHS(rbx_mesh::physics_data::CSGPHS::PhysicsInfoMesh(pim))
=>vec![pim.mesh],
pub fn convert(roblox_physics_data:&[u8])->Result<model::Mesh,Error>{
let mut cursor=std::io::Cursor::new(roblox_physics_data);
let physics_data:rbx_mesh::physics_data::PhysicsData=rbx_mesh::read_physics_data(&mut cursor).map_err(Error::RobloxPhysicsData)?;
assert_eq!(cursor.position(),cursor.into_inner().len() as u64);
let meshes=match physics_data.collision_data{
rbx_mesh::physics_data::CollisionData::Block=>return Err(Error::Block),
rbx_mesh::physics_data::CollisionData::Meshes(meshes)=>meshes.meshes,
rbx_mesh::physics_data::CollisionData::PhysicsInfoMesh(pim)=>vec![pim.mesh],
};
let mut unique_pos=Vec::new();
let mut pos_id_from=HashMap::new();
@@ -65,46 +39,46 @@ pub fn convert(roblox_physics_data:&[u8],roblox_mesh_data:&[u8])->Result<model::
let mut vertex_id_from=HashMap::new();
let mut acquire_pos_id=|pos|{
let p=vec3::try_from_f32_array(pos).map_err(Error::Planar64Vec3)?;
Ok(*pos_id_from.entry(p).or_insert_with(||{
let pos_id=PositionId::new(unique_pos.len() as u32);
Ok(PositionId::new(*pos_id_from.entry(p).or_insert_with(||{
let pos_id=unique_pos.len();
unique_pos.push(p);
pos_id
}))
}) as u32))
};
let mut acquire_tex_id=|tex|{
let h=bytemuck::cast::<[f32;2],[u32;2]>(tex);
*tex_id_from.entry(h).or_insert_with(||{
let tex_id=TextureCoordinateId::new(unique_tex.len() as u32);
TextureCoordinateId::new(*tex_id_from.entry(h).or_insert_with(||{
let tex_id=unique_tex.len();
unique_tex.push(glam::Vec2::from_array(tex));
tex_id
})
}) as u32)
};
let mut acquire_normal_id=|normal|{
let n=vec3::try_from_f32_array(normal).map_err(Error::Planar64Vec3)?;
Ok(*normal_id_from.entry(n).or_insert_with(||{
let normal_id=NormalId::new(unique_normal.len() as u32);
Ok(NormalId::new(*normal_id_from.entry(n).or_insert_with(||{
let normal_id=unique_normal.len();
unique_normal.push(n);
normal_id
}))
}) as u32))
};
let mut acquire_color_id=|color|{
let h=bytemuck::cast::<[f32;4],[u32;4]>(color);
*color_id_from.entry(h).or_insert_with(||{
let color_id=ColorId::new(unique_color.len() as u32);
ColorId::new(*color_id_from.entry(h).or_insert_with(||{
let color_id=unique_color.len();
unique_color.push(glam::Vec4::from_array(color));
color_id
})
}) as u32)
};
let mut acquire_vertex_id=|vertex:IndexedVertex|{
*vertex_id_from.entry(vertex.clone()).or_insert_with(||{
let vertex_id=VertexId::new(unique_vertices.len() as u32);
VertexId::new(*vertex_id_from.entry(vertex.clone()).or_insert_with(||{
let vertex_id=unique_vertices.len();
unique_vertices.push(vertex);
vertex_id
})
}) as u32)
};
let color=acquire_color_id([1.0f32;4]);
let tex=acquire_tex_id([0.0f32;2]);
let polygon_groups:Vec<PolygonGroup>=physics_convex_meshes.into_iter().map(|mesh|{
let polygon_groups:Vec<PolygonGroup>=meshes.into_iter().map(|mesh|{
Ok(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|[vertex_id0,vertex_id1,vertex_id2]|{
let v0=mesh.vertices.get(vertex_id0.0 as usize).ok_or(Error::MissingVertexId(vertex_id0.0))?;
let v1=mesh.vertices.get(vertex_id1.0 as usize).ok_or(Error::MissingVertexId(vertex_id1.0))?;