Single-object viewer POC (#14)

This commit implements a Sketchfab-style single-object viewer as a minimal
proof-of-concept.

### `@storyteller/studio-web`
This is a new package to house web components for interfacing with the
`@storyteller/studio` WASM library. It currently includes a single component,
`<sts-viewer />`, which is exported from `@storyteller/studio-web/viewer`.

### `react-frontend`
This is a new application intended as a proof-of-concept and testbed for React
interop with `@storyteller/studio-web`.

Web components are currently a little bit fiddly to use in a React application.
`react-frontend/src/app/studio-viewer.tsx` demonstrates how a React-idiomatic
component can be written as a thin wrapper around a web component, and
`react-frontend/src/app/app.tsx` demonstrates that wrapper component in use.
This commit is contained in:
Danny McGee
2023-12-07 15:46:32 -05:00
committed by GitHub
parent 155272a5ba
commit cba70a62ae
70 changed files with 71793 additions and 1157 deletions
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+172
View File
@@ -0,0 +1,172 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.27",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0
]
}
],
"nodes":[
{
"mesh":0,
"name":"Couch"
}
],
"materials":[
{
"name":"Archisketch_122724_Frame_Yellow",
"normalTexture":{
"index":0
},
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":1
},
"metallicRoughnessTexture":{
"index":2
}
}
}
],
"meshes":[
{
"name":"Couch",
"primitives":[
{
"attributes":{
"COLOR_0":0,
"POSITION":1,
"NORMAL":2,
"TEXCOORD_0":3
},
"indices":4,
"material":0
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
},
{
"sampler":0,
"source":1
},
{
"sampler":0,
"source":2
}
],
"images":[
{
"mimeType":"image/png",
"name":"archisketch_122724_frame_yellow_normal",
"uri":"../textures/archisketch_122724_frame_yellow_normal.png"
},
{
"mimeType":"image/png",
"name":"archisketch_122724_frame_yellow_basecolor",
"uri":"../textures/archisketch_122724_frame_yellow_basecolor.png"
},
{
"mimeType":"image/png",
"name":"archisketch_122724_frame_yellow_occlusionroughnessmetallic",
"uri":"../textures/archisketch_122724_frame_yellow_occlusionroughnessmetallic.png"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5123,
"count":9285,
"normalized":true,
"type":"VEC4"
},
{
"bufferView":1,
"componentType":5126,
"count":9285,
"max":[
0.5208197832107544,
0.8453625440597534,
0.4746153652667999
],
"min":[
-0.5208197832107544,
0,
-0.4746153652667999
],
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":9285,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5126,
"count":9285,
"type":"VEC2"
},
{
"bufferView":4,
"componentType":5123,
"count":37824,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":74280,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":111420,
"byteOffset":74280,
"target":34962
},
{
"buffer":0,
"byteLength":111420,
"byteOffset":185700,
"target":34962
},
{
"buffer":0,
"byteLength":74280,
"byteOffset":297120,
"target":34962
},
{
"buffer":0,
"byteLength":75648,
"byteOffset":371400,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":447048,
"uri":"couch.bin"
}
]
}
File diff suppressed because one or more lines are too long
+172
View File
@@ -0,0 +1,172 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.27",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0
]
}
],
"nodes":[
{
"mesh":0,
"name":"DeskChair"
}
],
"materials":[
{
"name":"_Chair1_101___Default",
"normalTexture":{
"index":0
},
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":1
},
"metallicRoughnessTexture":{
"index":2
}
}
}
],
"meshes":[
{
"name":"DeskChair",
"primitives":[
{
"attributes":{
"COLOR_0":0,
"POSITION":1,
"NORMAL":2,
"TEXCOORD_0":3
},
"indices":4,
"material":0
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
},
{
"sampler":0,
"source":1
},
{
"sampler":0,
"source":2
}
],
"images":[
{
"mimeType":"image/png",
"name":"archisketch_285734_frame_black_normal",
"uri":"../textures/archisketch_285734_frame_black_normal.png"
},
{
"mimeType":"image/png",
"name":"archisketch_285734_frame_black_basecolor",
"uri":"../textures/archisketch_285734_frame_black_basecolor.png"
},
{
"mimeType":"image/png",
"name":"archisketch_285734_frame_black_occlusionroughnessmetallic",
"uri":"../textures/archisketch_285734_frame_black_occlusionroughnessmetallic.png"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5123,
"count":26100,
"normalized":true,
"type":"VEC4"
},
{
"bufferView":1,
"componentType":5126,
"count":26100,
"max":[
0.30823463201522827,
1.2001636028289795,
0.33275049924850464
],
"min":[
-0.30823463201522827,
0,
-0.33275049924850464
],
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":26100,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5126,
"count":26100,
"type":"VEC2"
},
{
"bufferView":4,
"componentType":5123,
"count":127638,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":208800,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":313200,
"byteOffset":208800,
"target":34962
},
{
"buffer":0,
"byteLength":313200,
"byteOffset":522000,
"target":34962
},
{
"buffer":0,
"byteLength":208800,
"byteOffset":835200,
"target":34962
},
{
"buffer":0,
"byteLength":255276,
"byteOffset":1044000,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":1299276,
"uri":"desk-chair.bin"
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+172
View File
@@ -0,0 +1,172 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.27",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0
]
}
],
"nodes":[
{
"mesh":0,
"name":"FileCabinet"
}
],
"materials":[
{
"name":"Archisketch_13135_Frame_Silver",
"normalTexture":{
"index":0
},
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":1
},
"metallicRoughnessTexture":{
"index":2
}
}
}
],
"meshes":[
{
"name":"FileCabinet",
"primitives":[
{
"attributes":{
"COLOR_0":0,
"POSITION":1,
"NORMAL":2,
"TEXCOORD_0":3
},
"indices":4,
"material":0
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
},
{
"sampler":0,
"source":1
},
{
"sampler":0,
"source":2
}
],
"images":[
{
"mimeType":"image/png",
"name":"archisketch_13135_frame_silver_normal",
"uri":"../textures/archisketch_13135_frame_silver_normal.png"
},
{
"mimeType":"image/png",
"name":"archisketch_13135_frame_silver_basecolor",
"uri":"../textures/archisketch_13135_frame_silver_basecolor.png"
},
{
"mimeType":"image/png",
"name":"archisketch_13135_frame_silver_occlusionroughnessmetallic",
"uri":"../textures/archisketch_13135_frame_silver_occlusionroughnessmetallic.png"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5123,
"count":1381,
"normalized":true,
"type":"VEC4"
},
{
"bufferView":1,
"componentType":5126,
"count":1381,
"max":[
0.17499776184558868,
0.5099859237670898,
0.18015122413635254
],
"min":[
-0.17499776184558868,
0,
-0.18015122413635254
],
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":1381,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5126,
"count":1381,
"type":"VEC2"
},
{
"bufferView":4,
"componentType":5123,
"count":5706,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":11048,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":16572,
"byteOffset":11048,
"target":34962
},
{
"buffer":0,
"byteLength":16572,
"byteOffset":27620,
"target":34962
},
{
"buffer":0,
"byteLength":11048,
"byteOffset":44192,
"target":34962
},
{
"buffer":0,
"byteLength":11412,
"byteOffset":55240,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":66652,
"uri":"file-cabinet.bin"
}
]
}
Binary file not shown.
+560
View File
@@ -0,0 +1,560 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.27",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0
]
}
],
"nodes":[
{
"mesh":0,
"name":"PottedPlant1",
"scale":[
0.7147455811500549,
0.7147455811500549,
0.7147455811500549
]
}
],
"materials":[
{
"doubleSided":true,
"name":"05___Defaultwww1",
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":0
},
"metallicFactor":0,
"roughnessFactor":0.5
}
},
{
"doubleSided":true,
"name":"02___Defaultwe1",
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":1
},
"metallicFactor":0,
"roughnessFactor":0.5
}
},
{
"doubleSided":true,
"name":"17___Defaultweweqw1.001",
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":2
},
"metallicFactor":0,
"roughnessFactor":0.5
}
},
{
"doubleSided":true,
"name":"01___Default1",
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":3
},
"metallicFactor":0,
"roughnessFactor":0.5
}
},
{
"doubleSided":true,
"name":"16___Default1",
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":4
},
"metallicFactor":0,
"roughnessFactor":0.5
}
}
],
"meshes":[
{
"name":"PottedPlant1",
"primitives":[
{
"attributes":{
"COLOR_0":0,
"POSITION":1,
"NORMAL":2,
"TEXCOORD_0":3
},
"indices":4,
"material":0
},
{
"attributes":{
"COLOR_0":5,
"POSITION":6,
"NORMAL":7,
"TEXCOORD_0":8
},
"indices":9,
"material":1
},
{
"attributes":{
"COLOR_0":10,
"POSITION":11,
"NORMAL":12,
"TEXCOORD_0":13
},
"indices":14,
"material":2
},
{
"attributes":{
"COLOR_0":15,
"POSITION":16,
"NORMAL":17,
"TEXCOORD_0":18
},
"indices":19,
"material":3
},
{
"attributes":{
"COLOR_0":20,
"POSITION":21,
"NORMAL":22,
"TEXCOORD_0":23
},
"indices":24,
"material":4
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
},
{
"sampler":0,
"source":1
},
{
"sampler":0,
"source":2
},
{
"sampler":0,
"source":3
},
{
"sampler":0,
"source":4
}
],
"images":[
{
"mimeType":"image/png",
"name":"map__4727_mix",
"uri":"../textures/map__4727_mix.png"
},
{
"mimeType":"image/png",
"name":"map__3_gradient_ramp_0",
"uri":"../textures/map__3_gradient_ramp_0.png"
},
{
"mimeType":"image/png",
"name":"sh__014e_0",
"uri":"../textures/sh__014e_0.png"
},
{
"mimeType":"image/png",
"name":"map__2138274789_falloff",
"uri":"../textures/map__2138274789_falloff.png"
},
{
"mimeType":"image/png",
"name":"sh__049",
"uri":"../textures/sh__049.png"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5123,
"count":373,
"normalized":true,
"type":"VEC4"
},
{
"bufferView":1,
"componentType":5126,
"count":373,
"max":[
0.19935017824172974,
0.34052181243896484,
0.19498242437839508
],
"min":[
-0.2017928957939148,
0.10245688259601593,
-0.205869659781456
],
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":373,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5126,
"count":373,
"type":"VEC2"
},
{
"bufferView":4,
"componentType":5123,
"count":864,
"type":"SCALAR"
},
{
"bufferView":5,
"componentType":5123,
"count":2881,
"normalized":true,
"type":"VEC4"
},
{
"bufferView":6,
"componentType":5126,
"count":2881,
"max":[
0.20022684335708618,
0.7127547264099121,
0.3447090983390808
],
"min":[
-0.25123924016952515,
0.2595646381378174,
-0.23161326348781586
],
"type":"VEC3"
},
{
"bufferView":7,
"componentType":5126,
"count":2881,
"type":"VEC3"
},
{
"bufferView":8,
"componentType":5126,
"count":2881,
"type":"VEC2"
},
{
"bufferView":9,
"componentType":5123,
"count":5253,
"type":"SCALAR"
},
{
"bufferView":10,
"componentType":5123,
"count":953,
"normalized":true,
"type":"VEC4"
},
{
"bufferView":11,
"componentType":5126,
"count":953,
"max":[
0.19372159242630005,
0.36101800203323364,
0.17712844908237457
],
"min":[
-0.17357796430587769,
0.3098647892475128,
-0.1897440403699875
],
"type":"VEC3"
},
{
"bufferView":12,
"componentType":5126,
"count":953,
"type":"VEC3"
},
{
"bufferView":13,
"componentType":5126,
"count":953,
"type":"VEC2"
},
{
"bufferView":14,
"componentType":5123,
"count":2124,
"type":"SCALAR"
},
{
"bufferView":15,
"componentType":5123,
"count":14616,
"normalized":true,
"type":"VEC4"
},
{
"bufferView":16,
"componentType":5126,
"count":14616,
"max":[
0.23089128732681274,
0.7691824436187744,
0.37735289335250854
],
"min":[
-0.2857348322868347,
0.3321642279624939,
-0.2779516577720642
],
"type":"VEC3"
},
{
"bufferView":17,
"componentType":5126,
"count":14616,
"type":"VEC3"
},
{
"bufferView":18,
"componentType":5126,
"count":14616,
"type":"VEC2"
},
{
"bufferView":19,
"componentType":5123,
"count":35076,
"type":"SCALAR"
},
{
"bufferView":20,
"componentType":5123,
"count":465,
"normalized":true,
"type":"VEC4"
},
{
"bufferView":21,
"componentType":5126,
"count":465,
"max":[
0.1985645890235901,
0.1372201144695282,
0.19498242437839508
],
"min":[
-0.2017926573753357,
0,
-0.2054220288991928
],
"type":"VEC3"
},
{
"bufferView":22,
"componentType":5126,
"count":465,
"type":"VEC3"
},
{
"bufferView":23,
"componentType":5126,
"count":465,
"type":"VEC2"
},
{
"bufferView":24,
"componentType":5123,
"count":1896,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":2984,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":4476,
"byteOffset":2984,
"target":34962
},
{
"buffer":0,
"byteLength":4476,
"byteOffset":7460,
"target":34962
},
{
"buffer":0,
"byteLength":2984,
"byteOffset":11936,
"target":34962
},
{
"buffer":0,
"byteLength":1728,
"byteOffset":14920,
"target":34963
},
{
"buffer":0,
"byteLength":23048,
"byteOffset":16648,
"target":34962
},
{
"buffer":0,
"byteLength":34572,
"byteOffset":39696,
"target":34962
},
{
"buffer":0,
"byteLength":34572,
"byteOffset":74268,
"target":34962
},
{
"buffer":0,
"byteLength":23048,
"byteOffset":108840,
"target":34962
},
{
"buffer":0,
"byteLength":10506,
"byteOffset":131888,
"target":34963
},
{
"buffer":0,
"byteLength":7624,
"byteOffset":142396,
"target":34962
},
{
"buffer":0,
"byteLength":11436,
"byteOffset":150020,
"target":34962
},
{
"buffer":0,
"byteLength":11436,
"byteOffset":161456,
"target":34962
},
{
"buffer":0,
"byteLength":7624,
"byteOffset":172892,
"target":34962
},
{
"buffer":0,
"byteLength":4248,
"byteOffset":180516,
"target":34963
},
{
"buffer":0,
"byteLength":116928,
"byteOffset":184764,
"target":34962
},
{
"buffer":0,
"byteLength":175392,
"byteOffset":301692,
"target":34962
},
{
"buffer":0,
"byteLength":175392,
"byteOffset":477084,
"target":34962
},
{
"buffer":0,
"byteLength":116928,
"byteOffset":652476,
"target":34962
},
{
"buffer":0,
"byteLength":70152,
"byteOffset":769404,
"target":34963
},
{
"buffer":0,
"byteLength":3720,
"byteOffset":839556,
"target":34962
},
{
"buffer":0,
"byteLength":5580,
"byteOffset":843276,
"target":34962
},
{
"buffer":0,
"byteLength":5580,
"byteOffset":848856,
"target":34962
},
{
"buffer":0,
"byteLength":3720,
"byteOffset":854436,
"target":34962
},
{
"buffer":0,
"byteLength":3792,
"byteOffset":858156,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":861948,
"uri":"potted-plant-1.bin"
}
]
}
File diff suppressed because one or more lines are too long
+297
View File
@@ -0,0 +1,297 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.27",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0
]
}
],
"nodes":[
{
"mesh":0,
"name":"PottedPlant3"
}
],
"materials":[
{
"name":"230510_b",
"normalTexture":{
"index":0
},
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":1
},
"metallicRoughnessTexture":{
"index":2
}
}
},
{
"alphaCutoff":0.5,
"alphaMode":"MASK",
"doubleSided":true,
"name":"None.001",
"normalTexture":{
"index":3
},
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":4
},
"metallicRoughnessTexture":{
"index":5
}
}
}
],
"meshes":[
{
"name":"PottedPlant3",
"primitives":[
{
"attributes":{
"COLOR_0":0,
"POSITION":1,
"NORMAL":2,
"TEXCOORD_0":3
},
"indices":4,
"material":0
},
{
"attributes":{
"COLOR_0":5,
"POSITION":6,
"NORMAL":7,
"TEXCOORD_0":8
},
"indices":9,
"material":1
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
},
{
"sampler":0,
"source":1
},
{
"sampler":0,
"source":2
},
{
"sampler":0,
"source":3
},
{
"sampler":0,
"source":4
},
{
"sampler":0,
"source":5
}
],
"images":[
{
"mimeType":"image/png",
"name":"230510_b_normal",
"uri":"../textures/230510_b_normal.png"
},
{
"mimeType":"image/png",
"name":"230510_b_basecolor",
"uri":"../textures/230510_b_basecolor.png"
},
{
"mimeType":"image/png",
"name":"230510_b_occlusionroughnessmetallic",
"uri":"../textures/230510_b_occlusionroughnessmetallic.png"
},
{
"mimeType":"image/png",
"name":"none_normal",
"uri":"../textures/none_normal.png"
},
{
"mimeType":"image/png",
"name":"none_basecolor",
"uri":"../textures/none_basecolor.png"
},
{
"mimeType":"image/png",
"name":"none_occlusionroughnessmetallic",
"uri":"../textures/none_occlusionroughnessmetallic.png"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5123,
"count":27947,
"normalized":true,
"type":"VEC4"
},
{
"bufferView":1,
"componentType":5126,
"count":27947,
"max":[
0.20527732372283936,
1.1365097761154175,
0.1583448350429535
],
"min":[
-0.14585593342781067,
0,
-0.2576342821121216
],
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":27947,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5126,
"count":27947,
"type":"VEC2"
},
{
"bufferView":4,
"componentType":5123,
"count":143556,
"type":"SCALAR"
},
{
"bufferView":5,
"componentType":5123,
"count":1239,
"normalized":true,
"type":"VEC4"
},
{
"bufferView":6,
"componentType":5126,
"count":1239,
"max":[
0.30904287099838257,
1.2173328399658203,
0.35934215784072876
],
"min":[
-0.30904287099838257,
0.6203020811080933,
-0.35934215784072876
],
"type":"VEC3"
},
{
"bufferView":7,
"componentType":5126,
"count":1239,
"type":"VEC3"
},
{
"bufferView":8,
"componentType":5126,
"count":1239,
"type":"VEC2"
},
{
"bufferView":9,
"componentType":5123,
"count":5796,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":223576,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":335364,
"byteOffset":223576,
"target":34962
},
{
"buffer":0,
"byteLength":335364,
"byteOffset":558940,
"target":34962
},
{
"buffer":0,
"byteLength":223576,
"byteOffset":894304,
"target":34962
},
{
"buffer":0,
"byteLength":287112,
"byteOffset":1117880,
"target":34963
},
{
"buffer":0,
"byteLength":9912,
"byteOffset":1404992,
"target":34962
},
{
"buffer":0,
"byteLength":14868,
"byteOffset":1414904,
"target":34962
},
{
"buffer":0,
"byteLength":14868,
"byteOffset":1429772,
"target":34962
},
{
"buffer":0,
"byteLength":9912,
"byteOffset":1444640,
"target":34962
},
{
"buffer":0,
"byteLength":11592,
"byteOffset":1454552,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":1466144,
"uri":"potted-plant-3.bin"
}
]
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+177
View File
@@ -0,0 +1,177 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.27",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0
]
}
],
"nodes":[
{
"mesh":0,
"name":"Rug",
"scale":[
1.3000000715255737,
1.3000000715255737,
1.3000000715255737
]
}
],
"materials":[
{
"name":"initialShadingGroup.002",
"normalTexture":{
"index":0
},
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":1
},
"metallicRoughnessTexture":{
"index":2
}
}
}
],
"meshes":[
{
"name":"Rug",
"primitives":[
{
"attributes":{
"COLOR_0":0,
"POSITION":1,
"NORMAL":2,
"TEXCOORD_0":3
},
"indices":4,
"material":0
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
},
{
"sampler":0,
"source":1
},
{
"sampler":0,
"source":2
}
],
"images":[
{
"mimeType":"image/png",
"name":"archisketch_15261_frame_cocoa_normal",
"uri":"../textures/archisketch_15261_frame_cocoa_normal.png"
},
{
"mimeType":"image/png",
"name":"archisketch_15261_frame_cocoa_basecolor",
"uri":"../textures/archisketch_15261_frame_cocoa_basecolor.png"
},
{
"mimeType":"image/png",
"name":"archisketch_15261_frame_cocoa_occlusionroughnessmetallic",
"uri":"../textures/archisketch_15261_frame_cocoa_occlusionroughnessmetallic.png"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5123,
"count":2196,
"normalized":true,
"type":"VEC4"
},
{
"bufferView":1,
"componentType":5126,
"count":2196,
"max":[
0.7471415996551514,
0.01999974437057972,
0.7471414804458618
],
"min":[
-0.7471415996551514,
0,
-0.7471414804458618
],
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":2196,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5126,
"count":2196,
"type":"VEC2"
},
{
"bufferView":4,
"componentType":5123,
"count":12528,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":17568,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":26352,
"byteOffset":17568,
"target":34962
},
{
"buffer":0,
"byteLength":26352,
"byteOffset":43920,
"target":34962
},
{
"buffer":0,
"byteLength":17568,
"byteOffset":70272,
"target":34962
},
{
"buffer":0,
"byteLength":25056,
"byteOffset":87840,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":112896,
"uri":"rug.bin"
}
]
}
File diff suppressed because one or more lines are too long
+172
View File
@@ -0,0 +1,172 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.27",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0
]
}
],
"nodes":[
{
"mesh":0,
"name":"WickerBasket"
}
],
"materials":[
{
"name":"DefaultMaterial.002",
"normalTexture":{
"index":0
},
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":1
},
"metallicRoughnessTexture":{
"index":2
}
}
}
],
"meshes":[
{
"name":"WickerBasket",
"primitives":[
{
"attributes":{
"COLOR_0":0,
"POSITION":1,
"NORMAL":2,
"TEXCOORD_0":3
},
"indices":4,
"material":0
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
},
{
"sampler":0,
"source":1
},
{
"sampler":0,
"source":2
}
],
"images":[
{
"mimeType":"image/png",
"name":"archisketch_302398_frame-_brown__normal",
"uri":"../textures/archisketch_302398_frame-_brown__normal.png"
},
{
"mimeType":"image/png",
"name":"archisketch_302398_frame-_brown__basecolor",
"uri":"../textures/archisketch_302398_frame-_brown__basecolor.png"
},
{
"mimeType":"image/png",
"name":"archisketch_302398_frame-_brown__occlusionroughnessmetallic",
"uri":"../textures/archisketch_302398_frame-_brown__occlusionroughnessmetallic.png"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5123,
"count":4003,
"normalized":true,
"type":"VEC4"
},
{
"bufferView":1,
"componentType":5126,
"count":4003,
"max":[
0.299081951379776,
0.2872571349143982,
0.2306119054555893
],
"min":[
-0.299081951379776,
0,
-0.2306119054555893
],
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":4003,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5126,
"count":4003,
"type":"VEC2"
},
{
"bufferView":4,
"componentType":5123,
"count":19680,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":32024,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":48036,
"byteOffset":32024,
"target":34962
},
{
"buffer":0,
"byteLength":48036,
"byteOffset":80060,
"target":34962
},
{
"buffer":0,
"byteLength":32024,
"byteOffset":128096,
"target":34962
},
{
"buffer":0,
"byteLength":39360,
"byteOffset":160120,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":199480,
"uri":"wicker-basket.bin"
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+25 -1
View File
@@ -7,11 +7,18 @@ use bevy_mod_picking::debug::DebugPickingMode;
use crate::{
camera_controller::{CameraController, CameraMotionSystem},
input::{Action, InputAction, Lifecycle},
MainCamera,
};
#[derive(Resource, Deref, DerefMut)]
pub struct DebugMode(bool);
#[derive(Component, Clone, Copy, Debug)]
pub struct DebugPoint {
pub point: Vec3,
pub color: Color,
}
pub struct DebugPlugin;
impl Plugin for DebugPlugin {
fn build(&self, app: &mut App) {
@@ -31,7 +38,7 @@ impl Plugin for DebugPlugin {
toggle_debug_mode,
update_debug_picking_mode.run_if(resource_changed::<DebugMode>()),
update_gizmo_line_thickness,
draw_world_axes
(draw_world_axes, draw_debug_points)
.after(CameraMotionSystem)
.run_if(is_debug_mode),
),
@@ -84,6 +91,23 @@ fn draw_world_axes(mut gizmos: Gizmos, q: Query<&CameraController>) {
draw_axis(Vec3::Z, Color::BLUE);
}
fn draw_debug_points(
mut gizmos: Gizmos,
q_debug_points: Query<&DebugPoint>,
q_cam_xform: Query<&GlobalTransform, With<MainCamera>>,
) {
let Ok(cam_xform) = q_cam_xform.get_single() else {
return;
};
for &DebugPoint { point, color } in q_debug_points.iter() {
let dist = cam_xform.translation().distance(point);
gizmos.cuboid(
Transform::from_translation(point).with_scale(Vec3::splat(dist / 300.)),
color,
);
}
}
#[rustfmt::skip]
#[allow(dead_code)]
pub fn write_mat3(w: &mut impl fmt::Write, m: &Mat3) -> fmt::Result {
+16
View File
@@ -0,0 +1,16 @@
use bevy::prelude::*;
#[cfg(feature = "wasm")]
mod wasm;
pub struct InspectorPlugin;
impl Plugin for InspectorPlugin {
fn build(&self, app: &mut App) {
#[cfg(feature = "wasm")]
app.add_systems(
PostUpdate,
wasm::notify_entity_spawn_changes.map(bevy::utils::error),
);
}
}
+256
View File
@@ -0,0 +1,256 @@
use std::collections::VecDeque;
use bevy::{prelude::*, utils::HashSet, window::PrimaryWindow};
use serde::Serialize;
use wasm_bindgen::prelude::*;
use crate::{
scene::SceneElement,
wasm::{self, AsJson},
};
#[wasm_bindgen(typescript_custom_section)]
const TYPE_DECLARATIONS: &str = r#"
export interface SceneObject {
/** The entity ID. */
id: string;
/** A user-friendly name for displaying in the UI */
name: string;
/** The IDs of this object's children in the hierarchy */
children: string[];
/** The ID of this object's parent in the hierarchy, if it has one */
parent: string | null;
/** The local-space transform */
transform: Transform;
/** The world-space transform */
globalTransform: GlobalTransform;
}
/**
* An event indicating that a new entity has spawned.
*/
export interface EntitySpawnEvent extends CustomEvent {
type: "entity-spawn";
detail: SceneObject;
}
/**
* An event indicating that an entity has been de-spawned.
*/
export interface EntityDespawnEvent extends CustomEvent {
type: "entity-despawn";
/** The entity ID */
detail: string;
}
/**
* An event indicating that multiple entities have spawned.
*/
export interface EntityMultiSpawnEvent extends CustomEvent {
type: "entity-multi-spawn";
detail: SceneObject[];
}
declare global {
export interface HTMLElementEventMap {
"entity-spawn": EntitySpawnEvent;
"entity-despawn": EntityDespawnEvent;
"entity-multi-spawn": EntityMultiSpawnEvent;
}
}
"#;
#[derive(Serialize)]
pub(super) struct SceneObject {
id: String,
name: String,
children: Vec<String>,
parent: Option<String>,
transform: serde_json::Value,
#[serde(rename(serialize = "globalTransform"))]
global_transform: serde_json::Value,
}
struct SceneObjectBuilder<'a> {
id: Entity,
name: Option<String>,
children: Option<Vec<Entity>>,
parent: Option<Entity>,
transform: Option<&'a Transform>,
global_transform: Option<&'a GlobalTransform>,
}
impl<'a> SceneObjectBuilder<'a> {
fn new(id: Entity) -> Self {
Self {
id,
name: None,
children: None,
parent: None,
transform: None,
global_transform: None,
}
}
fn finish(self) -> SceneObject {
SceneObject {
id: format!("{:?}", self.id),
name: self.name.unwrap(),
children: self
.children
.map(|children| children.iter().map(|ent| format!("{ent:?}")).collect())
.unwrap_or_default(),
parent: self.parent.map(|ent| format!("{ent:?}")),
transform: self.transform.unwrap().as_json(),
global_transform: self.global_transform.unwrap().as_json(),
}
}
fn with_name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
fn with_children(mut self, children: Vec<Entity>) -> Self {
self.children = Some(children);
self
}
fn with_parent(mut self, parent: Entity) -> Self {
self.parent = Some(parent);
self
}
fn with_transform(mut self, transform: &'a Transform) -> Self {
self.transform = Some(transform);
self
}
fn with_global_transform(mut self, global_transform: &'a GlobalTransform) -> Self {
self.global_transform = Some(global_transform);
self
}
}
impl<'a> SceneObject {
fn build(id: Entity) -> SceneObjectBuilder<'a> {
SceneObjectBuilder::new(id)
}
}
pub(super) fn notify_entity_spawn_changes(
mut l_notified: Local<HashSet<Entity>>,
// Note: We use locals for these data structures purely to reuse
// existing allocations across multiple invocations of this system
mut l_new_objects: Local<Vec<SceneObject>>,
mut l_despawned_entities: Local<Vec<Entity>>,
mut l_missing_parents: Local<VecDeque<Entity>>,
q_window: Query<&Window, With<PrimaryWindow>>,
q_inspectables: Query<(Entity, &Transform, &GlobalTransform), With<SceneElement>>,
q_others: Query<(Entity, &Transform, &GlobalTransform), Without<SceneElement>>,
q_names: Query<&Name>,
q_parents: Query<&Children>,
q_children: Query<&Parent>,
) -> Result<(), String> {
l_missing_parents.clear();
l_new_objects.clear();
l_new_objects.extend(
q_inspectables
.iter()
.filter_map(|(ent, xform, world_xform)| {
build_scene_object(
&mut l_notified,
&mut l_missing_parents,
ent,
xform,
world_xform,
&q_inspectables,
&q_names,
&q_parents,
&q_children,
)
}),
);
while let Some(parent) = l_missing_parents.pop_front() {
let Ok((ent, xform, world_xform)) = q_others.get(parent) else {
error!("Failed to find missing parent: {parent:?}");
l_notified.insert(parent);
continue;
};
if let Some(object) = build_scene_object(
&mut l_notified,
&mut l_missing_parents,
ent,
xform,
world_xform,
&q_inspectables,
&q_names,
&q_parents,
&q_children,
) {
l_new_objects.push(object);
}
}
l_despawned_entities.clear();
l_despawned_entities.extend(
l_notified
.iter()
.filter(|ent| !q_inspectables.contains(**ent) && !q_others.contains(**ent))
.cloned(),
);
let win = q_window.single();
for ent in l_despawned_entities.iter() {
// TODO: Make this take an array like the multi-spawn event
wasm::dispatch_to_js(win, "entity-despawn", ent)?;
l_notified.remove(ent);
}
if !l_new_objects.is_empty() {
// TODO: Remove the single-spawn event
wasm::dispatch_to_js(win, "entity-multi-spawn", &*l_new_objects)?;
}
Ok(())
}
fn build_scene_object(
notified: &mut HashSet<Entity>,
missing_parents: &mut VecDeque<Entity>,
ent: Entity,
xform: &Transform,
world_xform: &GlobalTransform,
q_inspectables: &Query<(Entity, &Transform, &GlobalTransform), With<SceneElement>>,
q_names: &Query<&Name>,
q_parents: &Query<&Children>,
q_children: &Query<&Parent>,
) -> Option<SceneObject> {
if notified.contains(&ent) {
return None;
}
notified.insert(ent);
let mut result = SceneObject::build(ent)
.with_transform(xform)
.with_global_transform(world_xform);
if let Ok(children) = q_parents.get(ent) {
result = result.with_children(children.iter().cloned().collect());
}
if let Ok(parent) = q_children.get(ent) {
let parent_ent = **parent;
result = result.with_parent(parent_ent);
if !notified.contains(&parent_ent) && !q_inspectables.contains(parent_ent) {
missing_parents.push_back(parent_ent);
}
}
result = result.with_name(match q_names.get(ent) {
Ok(name) => name.into(),
Err(_) => format!("{ent:?}"),
});
Some(result.finish())
}
+28 -2
View File
@@ -2,9 +2,12 @@ use bevy::prelude::*;
use bevy_mod_outline::{
AutoGenerateOutlineNormalsPlugin, OutlineBundle, OutlinePlugin, OutlineVolume,
};
use bevy_mod_picking::{prelude::Pointer, selection::Select};
use bevy_mod_picking::{prelude::Pointer, selection::Select, PickableBundle};
use crate::input::{self, Action, InputAction, Lifecycle};
use crate::{
input::{self, Action, InputAction, Lifecycle},
scene::{SceneElement, SceneSpawnerSystem, SceneState},
};
pub struct SelectionPlugin;
impl Plugin for SelectionPlugin {
@@ -14,6 +17,12 @@ impl Plugin for SelectionPlugin {
.add_systems(
PostUpdate,
draw_selection_outline.run_if(resource_changed_or_removed::<Selection>()),
)
.add_systems(
OnEnter(SceneState::Active),
(apply_deferred, tag_scene_elements)
.chain()
.after(SceneSpawnerSystem),
);
#[cfg(feature = "wasm")]
@@ -35,6 +44,23 @@ pub struct Selected;
#[derive(Resource, Deref, DerefMut, Debug)]
pub struct Selection(Entity);
fn tag_scene_elements(
mut cmd: Commands,
q_scene_elements: Query<(Entity, &Children), (With<SceneElement>, Without<Selectable>)>,
q_primitives: Query<&Handle<Mesh>>,
) {
for (ent, children) in q_scene_elements.iter() {
cmd.entity(ent).insert(Selectable);
for &ent_child in children
.iter()
.filter(|child| q_primitives.contains(**child))
{
cmd.entity(ent_child).insert(PickableBundle::default());
}
}
}
fn update_selection(
mut cmd: Commands,
// FIXME: These events aren't aware of our input actions, so we receive a
+7 -7
View File
@@ -88,7 +88,7 @@ mod wasm {
use bevy::prelude::*;
use wasm_bindgen::prelude::*;
use crate::wasm::{self, WasmMessageBuffer};
use crate::wasm::{StaticBuffer, WasmMessageBuffer};
use super::{TransformMode, TransformSpace, TransformType};
@@ -111,22 +111,22 @@ mod wasm {
#[wasm_bindgen(js_name = setTransformType)]
pub fn set_transform_type(flags: u8) {
wasm::write_message!(TRANSFORM_TYPE_BUF, flags.into());
TRANSFORM_TYPE_BUF.write_message(flags.into());
}
#[wasm_bindgen(js_name = setTransformSpace)]
pub fn set_transform_space(space: TransformSpace) {
wasm::write_message!(TRANSFORM_SPACE_BUF, space);
TRANSFORM_SPACE_BUF.write_message(space);
}
pub(super) fn update_transform_mode(mut xform_mode: ResMut<TransformMode>) {
wasm::read_buffer!(TRANSFORM_TYPE_BUF, |ttype| {
if let Some(ttype) = TRANSFORM_TYPE_BUF.take_message() {
info!("Setting transform type: {ttype:?}");
xform_mode.ttype = ttype;
});
wasm::read_buffer!(TRANSFORM_SPACE_BUF, |space| {
}
if let Some(space) = TRANSFORM_SPACE_BUF.take_message() {
info!("Setting transform space: {space:?}");
xform_mode.space = space;
});
}
}
}
+89 -12
View File
@@ -2,27 +2,36 @@
use bevy::prelude::*;
use bevy_mod_picking::DefaultPickingPlugins;
use camera_controller::CameraControllerPlugin;
use debug::DebugPlugin;
use input::StudioInputPlugin;
use interaction::InteractionPlugin;
use math::InterpolationPlugin;
use scene::ScenePlugin;
use skybox::SkyboxPlugin;
use ui::UiLayerPlugin;
use viewport::ViewportSize;
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
mod camera_controller;
mod debug;
mod design_system;
mod input;
mod inspector;
mod interaction;
mod math;
mod scene;
mod skybox;
mod ui;
mod viewport;
mod world_grid;
use crate::{
camera_controller::CameraControllerPlugin,
debug::DebugPlugin,
input::StudioInputPlugin,
inspector::InspectorPlugin,
interaction::InteractionPlugin,
math::InterpolationPlugin,
scene::{LoadSceneEvent, ScenePlugin},
skybox::{SkyboxId, SkyboxPlugin},
ui::UiLayerPlugin,
viewport::ViewportSize,
world_grid::WorldGridPlugin,
};
#[cfg(feature = "wasm")]
mod wasm;
@@ -30,7 +39,7 @@ mod wasm;
#[derive(Component)]
pub(crate) struct MainCamera;
#[wasm_bindgen]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub fn init(viewport_size: ViewportSize) {
#[cfg(feature = "wasm")]
console_error_panic_hook::set_once();
@@ -62,12 +71,80 @@ pub fn init(viewport_size: ViewportSize) {
DefaultPickingPlugins,
DebugPlugin,
InteractionPlugin,
InspectorPlugin,
StudioInputPlugin,
InterpolationPlugin,
CameraControllerPlugin,
ScenePlugin,
SkyboxPlugin,
UiLayerPlugin,
))
.run();
WorldGridPlugin,
));
#[cfg(feature = "wasm")]
wasm::initialize();
app.run();
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = startViewer))]
/// Start the application as a read-only viewer for a single library object.
///
/// @param object_id ID of the library object to load.
/// @param skybox_id ID of the Skybox to use
/// @param canvas_selector A CSS selector for the canvas that will host the Bevy
/// application. This should be unique (i.e. `querySelectorAll` should not
/// return more than one element), so using an ID selector is a good idea.
/// @param canvas_alt `alt` text for the HTML canvas element.
/// @param viewport_size The size of the canvas in CSS `px` units.
pub fn start_viewer(
object_id: &str,
skybox_id: &str,
canvas_selector: &str,
canvas_alt: &str,
viewport_size: ViewportSize,
) {
#[cfg(feature = "wasm")]
console_error_panic_hook::set_once();
let object_id = object_id.to_string();
let skybox_id = skybox_id.to_string();
let mut app = App::new();
app.add_plugins((
#[cfg(feature = "wasm")]
viewport::wasm::ViewportPlugin,
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
resolution: viewport_size.into(),
title: canvas_alt.into(),
canvas: Some(canvas_selector.into()),
..default()
}),
..default()
}),
DefaultPickingPlugins,
DebugPlugin,
InteractionPlugin,
InspectorPlugin,
StudioInputPlugin,
InterpolationPlugin,
CameraControllerPlugin,
ScenePlugin,
SkyboxPlugin,
UiLayerPlugin,
WorldGridPlugin,
))
.add_systems(
Startup,
move |mut cmd: Commands, mut ew: EventWriter<LoadSceneEvent>| {
cmd.insert_resource(SkyboxId(skybox_id.clone()));
ew.send(LoadSceneEvent(object_id.clone()));
},
);
#[cfg(feature = "wasm")]
wasm::initialize();
app.run();
}
+50 -1
View File
@@ -1,7 +1,7 @@
use bevy::{
math::vec3,
prelude::*,
render::{mesh::Indices, render_resource::PrimitiveTopology},
render::{mesh::Indices, primitives::Aabb, render_resource::PrimitiveTopology},
};
pub struct Cone {
@@ -111,3 +111,52 @@ impl From<Plane2Sided> for Mesh {
mesh
}
}
pub trait Extrema {
type Output: Sized;
fn extrema(&self) -> Self::Output;
}
impl Extrema for Aabb {
type Output = [Vec3; 8];
fn extrema(&self) -> Self::Output {
let min = Vec3::from(self.min());
let max = Vec3::from(self.max());
[
min,
vec3(min.x, min.y, max.z),
vec3(min.x, max.y, min.z),
vec3(max.x, min.y, min.z),
vec3(min.x, max.y, max.z),
vec3(max.x, min.y, max.z),
vec3(max.x, max.y, min.z),
max,
]
}
}
pub trait Combinable {
type IterItem<'a>;
type Output;
fn combine<'a>(elements: impl Iterator<Item = Self::IterItem<'a>>) -> Self::Output;
}
impl Combinable for Aabb {
type IterItem<'a> = (&'a Self, &'a GlobalTransform);
type Output = Option<Self>;
fn combine<'a>(elements: impl Iterator<Item = Self::IterItem<'a>>) -> Self::Output {
let points = elements.flat_map(|(aabb, world_xform)| {
let min = world_xform.transform_point(aabb.min().into());
let max = world_xform.transform_point(aabb.max().into());
[min, max]
});
Aabb::enclosing(points)
}
}
+352 -493
View File
@@ -4,80 +4,114 @@ use bevy::{
fxaa::Fxaa,
tonemapping::Tonemapping,
},
gltf::{self, Gltf},
math::vec3,
pbr::{NotShadowCaster, ShadowFilteringMethod},
gltf::Gltf,
math::{vec2, vec3},
pbr::ShadowFilteringMethod,
prelude::*,
reflect::TypePath,
render::render_resource::{AsBindGroup, ShaderRef},
utils::HashSet,
render::primitives::Aabb,
scene::InstanceId,
};
use bevy_mod_picking::PickableBundle;
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::wasm_bindgen;
use crate::{
camera_controller::{CameraController, CameraControllerBundle, CameraMotionSystem},
interaction::Selectable,
skybox::SkyboxId,
camera_controller::{CameraController, CameraControllerBundle},
debug::DebugPoint,
math::{Combinable, Extrema},
MainCamera,
};
pub struct ScenePlugin;
#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct SceneSpawnerSystem;
impl Plugin for ScenePlugin {
fn build(&self, app: &mut App) {
app.insert_resource(ClearColor(Color::hsl(0.0, 0.0, 0.25)))
.insert_resource(Msaa::Off)
.add_plugins(MaterialPlugin::<ProceduralGridMaterial>::default())
.add_state::<SceneState>()
.add_event::<LoadSceneEvent>()
.add_systems(Startup, initialize)
.add_systems(
PreUpdate,
spawn_new_gltf_scene.run_if(
resource_exists::<NewGltf>()
.and_then(new_gltf_loaded)
.and_then(run_once()),
),
Update,
(
init_scene_loading,
tick_scene_loading.run_if(in_state(SceneState::Loading)),
tick_scene_spawning.run_if(in_state(SceneState::Spawning)),
)
.in_set(SceneSpawnerSystem),
)
.add_systems(
Update,
mark_scene_objects.run_if(
resource_exists::<NewGltf>()
.and_then(new_gltf_loaded)
.and_then(gltf_scene_spawned),
),
OnEnter(SceneState::Loading),
despawn_previous_scene.in_set(SceneSpawnerSystem),
)
.add_systems(PostUpdate, move_plane_with_camera.after(CameraMotionSystem));
.add_systems(
OnEnter(SceneState::Spawning),
spawn_next_scene.in_set(SceneSpawnerSystem),
)
.add_systems(
OnEnter(SceneState::Active),
(
tag_scene_elements,
spawn_main_scene_light,
frame_scene_in_view,
)
.in_set(SceneSpawnerSystem),
);
#[cfg(feature = "wasm")]
app.add_systems(
PostUpdate,
wasm::notify_entity_spawn_changes.map(bevy::utils::error),
Update,
(
wasm::dispatch_scene_request,
wasm::notify_state_changes
.map(bevy::utils::error)
.run_if(resource_changed::<State<SceneState>>()),
),
);
}
}
#[derive(Component)]
struct WorldGridPlane;
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(States, Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
pub enum SceneState {
#[default]
None,
Loading,
Spawning,
Active,
}
#[derive(Resource, Deref, DerefMut)]
struct NewGltf(pub Handle<Gltf>);
struct PendingScene(pub Handle<Gltf>);
#[derive(Resource, Deref)]
pub struct ActiveScene {
#[deref]
pub instance: InstanceId,
pub source: Handle<Gltf>,
}
#[derive(Event, Deref)]
pub struct LoadSceneEvent(pub String);
// TODOL This could be converted into an enum that indicates what "kind" of
// element this is (e.g. StaticMesh, SkinnedMesh, Bone, Light, Camera).
// We don't have an immediate need for that just yet, but likely will
// soon.
/// A tag that identifies its entity as a user-facing scene element like a prop,
/// a light, or a camera.
#[derive(Component)]
pub struct SceneElement;
#[derive(Component)]
struct Inspectable;
fn initialize(
mut cmd: Commands,
assets: Res<AssetServer>,
mut ra_meshes: ResMut<Assets<Mesh>>,
mut ra_mat_proc_grid: ResMut<Assets<ProceduralGridMaterial>>,
) {
// Skybox
cmd.insert_resource(SkyboxId("test_scene".into()));
// glTF scene
cmd.insert_resource(NewGltf(assets.load("gltf/sample-room.gltf")));
pub struct SceneRoot;
fn initialize(mut cmd: Commands) {
// Camera, graphics settings
let focus = vec3(0.35011, 0.5, -0.99576);
let focus = Vec3::ZERO;
let camera_xform = Transform::from_xyz(-2.5, 4.5, 5.0).looking_at(focus, Vec3::Y);
cmd.spawn((
Name::new("Main Camera"),
@@ -104,495 +138,320 @@ fn initialize(
},
Fxaa::default(),
));
// Main light
// TODO: get Blender to export enough metadata to automate light placements
cmd.spawn((
Name::new("Ceiling Light"),
Selectable,
Inspectable,
PointLightBundle {
point_light: PointLight {
color: Color::hex("FFE5B4").unwrap(),
intensity: 200.,
radius: 0.1,
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(0.35263, 2.4348, 0.19601),
..default()
},
));
// Monitor glow
cmd.spawn((
Name::new("Monitor Glow"),
Selectable,
Inspectable,
PointLightBundle {
point_light: PointLight {
color: Color::WHITE,
intensity: 6.,
radius: 0.25,
shadows_enabled: false,
..default()
},
transform: Transform::from_xyz(0.42615, 1.0706, -2.0705),
..default()
},
));
// Plane
let plane = ra_meshes.add(Mesh::from(shape::Plane {
size: 200.0,
subdivisions: 0,
}));
let proc_grid = ra_mat_proc_grid.add(ProceduralGridMaterial {});
cmd.spawn((
Name::new("World Grid"),
SpatialBundle::from_transform(Transform::IDENTITY),
WorldGridPlane,
))
.with_children(|parent| {
parent.spawn((
MaterialMeshBundle {
mesh: plane.clone(),
material: proc_grid.clone(),
transform: Transform::IDENTITY,
..default()
},
NotShadowCaster,
));
parent.spawn((
MaterialMeshBundle {
mesh: plane,
material: proc_grid,
transform: Transform::IDENTITY.looking_to(Vec3::Z, Vec3::NEG_Y),
..default()
},
NotShadowCaster,
));
});
}
#[derive(Default)]
struct LoadState(Handle<Gltf>, bool);
fn init_scene_loading(
mut cmd: Commands,
assets: Res<AssetServer>,
mut er: EventReader<LoadSceneEvent>,
mut st_next: ResMut<NextState<SceneState>>,
) {
if let Some(id) = er.read().last() {
let name = &**id;
cmd.insert_resource(PendingScene(assets.load(format!("gltf/{name}.gltf"))));
st_next.set(SceneState::Loading);
}
}
fn new_gltf_loaded(
r_gltf: Res<NewGltf>,
fn despawn_previous_scene(mut cmd: Commands, q_scene_root: Query<Entity, With<SceneRoot>>) {
if let Ok(scene_root) = q_scene_root.get_single() {
cmd.entity(scene_root).despawn_recursive();
}
}
fn tick_scene_loading(
r_pending_scene: Res<PendingScene>,
ra_gltfs: Res<Assets<Gltf>>,
mut er_scene_events: EventReader<AssetEvent<Scene>>,
mut l_loaded: Local<LoadState>,
) -> bool {
if l_loaded.0.path() != r_gltf.path() {
*l_loaded = LoadState(r_gltf.clone(), false);
} else if l_loaded.1 {
return true;
}
l_loaded.1 = er_scene_events.read().any(|event| match event {
AssetEvent::LoadedWithDependencies { id } => {
let gltf = ra_gltfs.get(&**r_gltf).unwrap();
let Some(default_scene) = &gltf.default_scene else {
return false;
};
*id == default_scene.id()
}
_ => false,
});
l_loaded.1
}
fn spawn_new_gltf_scene(mut cmd: Commands, ra_gltfs: Res<Assets<Gltf>>, r_gltf: Res<NewGltf>) {
let gltf = ra_gltfs.get(&**r_gltf).unwrap();
let Some(scene) = &gltf.default_scene else {
return;
};
cmd.spawn((
Name::new("Scene"),
Inspectable,
SceneBundle {
scene: scene.clone(),
..default()
},
));
}
fn gltf_scene_spawned(
r_gltf: Res<NewGltf>,
ra_gltfs: Res<Assets<Gltf>>,
q_scenes: Query<&Handle<Scene>, (With<Name>, With<Children>)>,
) -> bool {
let gltf = ra_gltfs.get(&**r_gltf).unwrap();
let Some(gltf_scene_path) = gltf.default_scene.as_ref().and_then(|handle| handle.path()) else {
warn!("No default scene found for glTF: {:#?}", gltf);
return false;
};
q_scenes.iter().any(|handle| {
let Some(path) = handle.path() else {
return false;
};
path == gltf_scene_path
})
}
fn mark_scene_objects(
mut cmd: Commands,
r_gltf: Res<NewGltf>,
ra_gltfs: Res<Assets<Gltf>>,
ra_gltf_nodes: Res<Assets<gltf::GltfNode>>,
ra_gltf_meshes: Res<Assets<gltf::GltfMesh>>,
q_parents: Query<(Entity, &Name, &Children), Without<Selectable>>,
q_primitives: Query<&Handle<Mesh>>,
mut st_next: ResMut<NextState<SceneState>>,
) {
info!("mark_scene_objects");
let gltf = ra_gltfs.get(&**r_gltf).unwrap();
for (ent_parent, name, children) in q_parents.iter() {
if gltf.named_nodes.contains_key(&**name) {
cmd.entity(ent_parent).insert(Inspectable);
}
// See if this name matches any of the meshes or nodes in our glTF scene
let Some(gltf_mesh) = gltf
.named_meshes
.get(&**name)
.cloned()
.or_else(|| {
gltf.named_nodes.get(&**name).and_then(|node_handle| {
ra_gltf_nodes
.get(node_handle)
.and_then(|node| node.mesh.clone())
})
})
.and_then(|handle| ra_gltf_meshes.get(&handle))
else {
continue;
};
// FIXME: These hashsets don't actually contain anything. Ever. -_-
// This has "worked" this whole time because {} == {}
// Collect all of the mesh handles in the glTF mesh
let prims_from_gltf = gltf_mesh
.primitives
.iter()
.filter_map(|prim| prim.mesh.path())
.collect::<HashSet<_>>();
// Collect all of the mesh handles in this entity's children
let prims_from_ecs = children
.iter()
.filter_map(|ent_child| {
q_primitives
.get(*ent_child)
.ok()
.and_then(|handle| handle.path())
})
.collect::<HashSet<_>>();
// If the sets match, this is the entity that represents the glTF mesh
if prims_from_gltf == prims_from_ecs {
cmd.entity(ent_parent).insert(Selectable);
for &ent_child in children.iter() {
if q_primitives.contains(ent_child) {
cmd.entity(ent_child).insert(PickableBundle::default());
}
for event in er_scene_events.read() {
if let AssetEvent::LoadedWithDependencies { id } = event {
let gltf = ra_gltfs.get(&**r_pending_scene).unwrap();
if gltf.default_scene.is_some() && gltf.default_scene.as_ref().unwrap().id() == *id {
st_next.set(SceneState::Spawning);
break;
}
}
}
cmd.remove_resource::<NewGltf>();
}
fn move_plane_with_camera(
mut plane: Query<(&WorldGridPlane, &mut Transform)>,
camera: Query<&CameraController, Changed<CameraController>>,
fn spawn_next_scene(
mut cmd: Commands,
ra_gltfs: Res<Assets<Gltf>>,
r_pending_scene: Res<PendingScene>,
mut r_scene_spawner: ResMut<SceneSpawner>,
) {
if let Ok(&CameraController { focus, .. }) = camera.get_single() {
let (_, mut xform) = plane.single_mut();
let gltf = ra_gltfs.get(&**r_pending_scene).unwrap();
let scene = gltf
.default_scene
.as_ref()
.expect("Expected glTF to include a default scene");
xform.translation.x = focus.x;
xform.translation.z = focus.z;
let parent = cmd
.spawn((
Name::new("Scene"),
SceneRoot,
SceneElement,
SpatialBundle::default(),
))
.id();
let instance = r_scene_spawner.spawn_as_child(scene.clone(), parent);
cmd.remove_resource::<PendingScene>();
cmd.insert_resource(ActiveScene {
instance,
source: r_pending_scene.clone(),
});
}
fn tick_scene_spawning(
r_scene_spawner: Res<SceneSpawner>,
r_active_scene: Res<ActiveScene>,
mut st_next: ResMut<NextState<SceneState>>,
) {
// TODO: The instance is considered "ready" even if shaders still need to be
// compiled, so this event isn't really sufficient for controlling a
// loading screen.
if r_scene_spawner.instance_is_ready(**r_active_scene) {
st_next.set(SceneState::Active);
}
}
#[derive(Asset, AsBindGroup, TypePath, Clone)]
struct ProceduralGridMaterial {}
fn tag_scene_elements(
mut cmd: Commands,
r_scene_spawner: Res<SceneSpawner>,
r_active_scene: Res<ActiveScene>,
ra_gltfs: Res<Assets<Gltf>>,
// A "glTF mesh" is actually a container for one or more "glTF primitives",
// which is basically a bundle of vertices paired with the material used to
// shade them. It's those "sub-meshes" that carry the actual mesh handles,
// but we don't necessarily want to expose those to users a la carte,
// because they often consist of stuff like a single eyeball, or just the
// leafy parts of a plant. Here we only want to tag the elements that a user
// might want to individually address when arranging or animating a scene.
q_named_non_primitives: Query<&Name, Without<Handle<Mesh>>>,
) {
let gltf = ra_gltfs.get(&r_active_scene.source).unwrap();
impl Material for ProceduralGridMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/procedural_grid.wgsl".into()
}
for ent in r_scene_spawner.iter_instance_entities(**r_active_scene) {
let Ok(name) = q_named_non_primitives.get(ent) else {
continue;
};
fn alpha_mode(&self) -> AlphaMode {
AlphaMode::Blend
if gltf.named_nodes.contains_key(&**name) {
cmd.entity(ent).insert(SceneElement);
}
}
}
fn spawn_main_scene_light(
mut cmd: Commands,
r_scene_spawner: Res<SceneSpawner>,
r_active_scene: Res<ActiveScene>,
q_aabbs: Query<(&Aabb, &GlobalTransform)>,
q_scene_root: Query<Entity, With<SceneRoot>>,
) {
let Ok(scene_root) = q_scene_root.get_single() else {
return;
};
let Some(aabb) = Aabb::combine(
r_scene_spawner
.iter_instance_entities(**r_active_scene)
.filter_map(|ent| q_aabbs.get(ent).ok()),
) else {
return;
};
let light_offset_base = vec3(0.5, 1., 1.).normalize();
let size = f32::max(
f32::max(aabb.half_extents.x, aabb.half_extents.y),
aabb.half_extents.z,
) * 2.;
let light_pos = Vec3::from(aabb.center) + light_offset_base * size * 2.;
cmd.spawn(DebugPoint {
point: light_pos,
color: Color::WHITE,
});
cmd.entity(scene_root).with_children(|cmd| {
cmd.spawn((
Name::new("Light"),
SceneElement,
SpotLightBundle {
spot_light: SpotLight {
color: Color::WHITE,
// TODO: This doesn't scale super well to very large objects
intensity: 200. * size,
radius: 0.1,
shadows_enabled: true,
shadow_normal_bias: 0.2 * size,
outer_angle: f32::to_radians(150.),
inner_angle: f32::to_radians(45.),
..default()
},
transform: Transform::from_translation(light_pos)
.looking_at(aabb.center.into(), Vec3::Y),
..default()
},
));
});
}
fn frame_scene_in_view(
mut cmd: Commands,
r_scene_spawner: Res<SceneSpawner>,
r_active_scene: Res<ActiveScene>,
q_aabbs: Query<(&Aabb, &GlobalTransform)>,
mut q_camera: Query<(&mut CameraController, &mut Transform, &Projection)>,
) {
let Ok((mut cam, mut cam_xform, cam_proj)) = q_camera.get_single_mut() else {
return;
};
let Some(aabb) = Aabb::combine(
r_scene_spawner
.iter_instance_entities(**r_active_scene)
.filter_map(|ent| q_aabbs.get(ent).ok()),
) else {
return;
};
let world2cam = cam_xform.compute_matrix().inverse();
let plane_origin = Vec3::from(aabb.center);
let plane_normal = cam_xform.forward();
let Some((_depth, bounding_rect)) = aabb
.extrema()
.iter()
.copied()
.map(|mut p| {
cmd.spawn(DebugPoint {
point: p,
color: Color::CYAN,
});
// Inflate the combined AABB to give the model some breathing room
p *= 1.5;
// Project each corner onto the camera plane
let v = p - plane_origin;
let proj = p - v.project_onto_normalized(plane_normal);
cmd.spawn(DebugPoint {
point: proj,
color: Color::FUCHSIA,
});
// Transform into the camera's local space
world2cam.transform_point(proj)
})
// Fold the camera-space plane-projected points into a bounding rect and view depth
.fold(None, |accum: Option<(f32, Rect)>, p: Vec3| match accum {
Some((depth, rect)) => Some((depth, rect.union_point(vec2(p.x, p.y)))),
None => {
let depth = p.z.abs();
let p = vec2(p.x, p.y);
Some((depth, Rect::from_corners(p, p)))
}
})
else {
return;
};
let pproj = match cam_proj {
Projection::Perspective(p) => p,
_ => panic!("Expected a perspective projection"),
};
// Formula for the target camera distance
// (c/o https://stackoverflow.com/questions/2866350/move-camera-to-fit-3d-scene):
//
// d = (s/2) / tan(a/2)
// where:
// d = distance
// s = size of the largest camera-aligned axis in world coords
// a = field-of-view angle for the same axis
let Vec2 {
x: width,
y: height,
} = bounding_rect.size();
let s = f32::max(width, height);
let a = if s == height {
pproj.fov
} else {
f32::atan(pproj.aspect_ratio * f32::tan(pproj.fov / 2.))
};
// TODO:
// According to "I eat babies" at the Ars Technica forums
// (https://arstechnica.com/civis/threads/if-you-have-aspect-ratio-and-vertical-fov-can-you-calc-horizontal-fov.37447/):
//
// tan(fov_v / 2) = y / z
// tan(fov_h / 2) = x / z
//
// Theoretically, this would let us skip some costly tan/atan
// computations, but I must be missing something, because just
// substituting (s / depth) for the divisor below would make the
// whole equation independent of the FOV, which is obviously
// wrong.
//
// Unfortunately I'm embarrassingly bad at algebra, but there
// must be some room for simplification here.
let radius = (s / 2.) / f32::tan(a / 2.);
cam.focus = aabb.center.into();
cam.radius = radius;
cam_xform.translation = cam.focus + cam_xform.back() * radius;
}
#[cfg(feature = "wasm")]
mod wasm {
use std::collections::VecDeque;
use bevy::{prelude::*, utils::HashSet, window::PrimaryWindow};
use serde::Serialize;
use bevy::{prelude::*, window::PrimaryWindow};
use wasm_bindgen::prelude::*;
use crate::wasm::{self, AsJson};
use super::{LoadSceneEvent, SceneState};
use crate::wasm::{self, StaticBuffer, WasmMessageBuffer};
use super::Inspectable;
static OBJECT_LOAD_REQUEST: WasmMessageBuffer<String> = WasmMessageBuffer::new();
#[wasm_bindgen(js_name = loadObject)]
pub fn load_object(id: &str) {
OBJECT_LOAD_REQUEST.write_message(id.into());
}
// FIXME: Is it an object or a scene? Inconsistent terminology is
// unnecessarily confusing
pub(super) fn dispatch_scene_request(mut ew_load_scene: EventWriter<LoadSceneEvent>) {
if let Some(id) = OBJECT_LOAD_REQUEST.take_message() {
ew_load_scene.send(LoadSceneEvent(id));
}
}
#[wasm_bindgen(typescript_custom_section)]
const TYPE_DECLARATIONS: &str = r#"
export interface SceneObject {
/** The entity ID. */
id: string;
/** A user-friendly name for displaying in the UI */
name: string;
/** The IDs of this object's children in the hierarchy */
children: string[];
/** The ID of this object's parent in the hierarchy, if it has one */
parent: string | null;
/** The local-space transform */
transform: Transform;
/** The world-space transform */
globalTransform: GlobalTransform;
}
/**
* An event indicating that a new entity has spawned.
*/
export interface EntitySpawnEvent extends CustomEvent {
type: "entity-spawn";
detail: SceneObject;
}
/**
* An event indicating that an entity has been de-spawned.
*/
export interface EntityDespawnEvent extends CustomEvent {
type: "entity-despawn";
/** The entity ID */
detail: string;
}
/**
* An event indicating that multiple entities have spawned.
*/
export interface EntityMultiSpawnEvent extends CustomEvent {
type: "entity-multi-spawn";
detail: SceneObject[];
const SCENE_STATE_EVENT: &str = r#"
export interface SceneStateEvent extends CustomEvent {
type: "scene-state";
detail: SceneState;
}
declare global {
export interface HTMLElementEventMap {
"entity-spawn": EntitySpawnEvent;
"entity-despawn": EntityDespawnEvent;
"entity-multi-spawn": EntityMultiSpawnEvent;
"scene-state": SceneStateEvent;
}
}
"#;
#[derive(Serialize)]
pub(super) struct SceneObject {
id: String,
name: String,
children: Vec<String>,
parent: Option<String>,
transform: serde_json::Value,
#[serde(rename(serialize = "globalTransform"))]
global_transform: serde_json::Value,
}
struct SceneObjectBuilder<'a> {
id: Entity,
name: Option<String>,
children: Option<Vec<Entity>>,
parent: Option<Entity>,
transform: Option<&'a Transform>,
global_transform: Option<&'a GlobalTransform>,
}
impl<'a> SceneObjectBuilder<'a> {
fn new(id: Entity) -> Self {
Self {
id,
name: None,
children: None,
parent: None,
transform: None,
global_transform: None,
}
}
fn finish(self) -> SceneObject {
SceneObject {
id: format!("{:?}", self.id),
name: self.name.unwrap(),
children: self
.children
.map(|children| children.iter().map(|ent| format!("{ent:?}")).collect())
.unwrap_or_default(),
parent: self.parent.map(|ent| format!("{ent:?}")),
transform: self.transform.unwrap().as_json(),
global_transform: self.global_transform.unwrap().as_json(),
}
}
fn with_name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
fn with_children(mut self, children: Vec<Entity>) -> Self {
self.children = Some(children);
self
}
fn with_parent(mut self, parent: Entity) -> Self {
self.parent = Some(parent);
self
}
fn with_transform(mut self, transform: &'a Transform) -> Self {
self.transform = Some(transform);
self
}
fn with_global_transform(mut self, global_transform: &'a GlobalTransform) -> Self {
self.global_transform = Some(global_transform);
self
}
}
impl<'a> SceneObject {
fn build(id: Entity) -> SceneObjectBuilder<'a> {
SceneObjectBuilder::new(id)
}
}
pub(super) fn notify_entity_spawn_changes(
mut l_notified: Local<HashSet<Entity>>,
// Note: We use locals for these data structures purely to reuse
// existing allocations across multiple invocations of this system
mut l_new_objects: Local<Vec<SceneObject>>,
mut l_despawned_entities: Local<Vec<Entity>>,
mut l_missing_parents: Local<VecDeque<Entity>>,
pub(super) fn notify_state_changes(
r_state: Res<State<SceneState>>,
q_window: Query<&Window, With<PrimaryWindow>>,
q_inspectables: Query<(Entity, &Transform, &GlobalTransform), With<Inspectable>>,
q_others: Query<(Entity, &Transform, &GlobalTransform), Without<Inspectable>>,
q_names: Query<&Name>,
q_parents: Query<&Children>,
q_children: Query<&Parent>,
) -> Result<(), String> {
l_missing_parents.clear();
l_new_objects.clear();
l_new_objects.extend(
q_inspectables
.iter()
.filter_map(|(ent, xform, world_xform)| {
build_scene_object(
&mut l_notified,
&mut l_missing_parents,
ent,
xform,
world_xform,
&q_inspectables,
&q_names,
&q_parents,
&q_children,
)
}),
);
while let Some(parent) = l_missing_parents.pop_front() {
let Ok((ent, xform, world_xform)) = q_others.get(parent) else {
error!("Failed to find missing parent: {parent:?}");
l_notified.insert(parent);
continue;
};
if let Some(object) = build_scene_object(
&mut l_notified,
&mut l_missing_parents,
ent,
xform,
world_xform,
&q_inspectables,
&q_names,
&q_parents,
&q_children,
) {
l_new_objects.push(object);
}
if let Ok(window) = q_window.get_single() {
wasm::dispatch_to_js(window, "scene-state", &(**r_state as u32))?;
}
l_despawned_entities.clear();
l_despawned_entities.extend(
l_notified
.iter()
.filter(|ent| !q_inspectables.contains(**ent) && !q_others.contains(**ent))
.cloned(),
);
let win = q_window.single();
for ent in l_despawned_entities.iter() {
// TODO: Make this take an array like the multi-spawn event
wasm::dispatch_to_js(win, "entity-despawn", ent)?;
l_notified.remove(ent);
}
if !l_new_objects.is_empty() {
// TODO: Remove the single-spawn event
wasm::dispatch_to_js(win, "entity-multi-spawn", &*l_new_objects)?;
}
Ok(())
}
fn build_scene_object(
notified: &mut HashSet<Entity>,
missing_parents: &mut VecDeque<Entity>,
ent: Entity,
xform: &Transform,
world_xform: &GlobalTransform,
q_inspectables: &Query<(Entity, &Transform, &GlobalTransform), With<Inspectable>>,
q_names: &Query<&Name>,
q_parents: &Query<&Children>,
q_children: &Query<&Parent>,
) -> Option<SceneObject> {
if notified.contains(&ent) {
return None;
}
notified.insert(ent);
let mut result = SceneObject::build(ent)
.with_transform(xform)
.with_global_transform(world_xform);
if let Ok(children) = q_parents.get(ent) {
result = result.with_children(children.iter().cloned().collect());
}
if let Ok(parent) = q_children.get(ent) {
let parent_ent = **parent;
result = result.with_parent(parent_ent);
if !notified.contains(&parent_ent) && !q_inspectables.contains(parent_ent) {
missing_parents.push_back(parent_ent);
}
}
result = result.with_name(match q_names.get(ent) {
Ok(name) => name.into(),
Err(_) => format!("{ent:?}"),
});
Some(result.finish())
}
}
+26
View File
@@ -22,6 +22,9 @@ impl Plugin for SkyboxPlugin {
.run_if(skybox_assets_loaded),
),
);
#[cfg(feature = "wasm")]
app.add_systems(Update, wasm::set_skybox_from_frontend);
}
}
@@ -112,3 +115,26 @@ impl SkyboxPaths {
}
}
}
#[cfg(feature = "wasm")]
mod wasm {
use bevy::prelude::*;
use wasm_bindgen::prelude::*;
use crate::wasm::{StaticBuffer, WasmMessageBuffer};
use super::SkyboxId;
static SKYBOX_REQUEST: WasmMessageBuffer<String> = WasmMessageBuffer::new();
#[wasm_bindgen(js_name = setSkybox)]
pub fn set_skybox(id: &str) {
SKYBOX_REQUEST.write_message(id.to_string());
}
pub(super) fn set_skybox_from_frontend(mut cmd: Commands) {
if let Some(id) = SKYBOX_REQUEST.take_message() {
cmd.insert_resource(SkyboxId(id));
}
}
}
+4 -4
View File
@@ -50,7 +50,7 @@ pub mod wasm {
use bevy::prelude::*;
use wasm_bindgen::prelude::*;
use crate::wasm::{self, WasmMessageBuffer};
use crate::wasm::{StaticBuffer, WasmMessageBuffer};
use super::ViewportSize;
@@ -58,7 +58,7 @@ pub mod wasm {
#[wasm_bindgen]
pub fn resize(viewport_size: ViewportSize) {
wasm::write_message!(RESIZE_BUF, viewport_size);
RESIZE_BUF.write_message(viewport_size);
}
pub struct ViewportPlugin;
@@ -85,8 +85,8 @@ pub mod wasm {
}
fn dispatch_resize_events(mut events: EventWriter<ViewportResizeEvent>) {
wasm::read_buffer!(RESIZE_BUF, |resize| {
if let Some(resize) = RESIZE_BUF.take_message() {
events.send(ViewportResizeEvent(resize));
});
}
}
}
+6
View File
@@ -132,3 +132,9 @@ where
}
}
}
impl AsJsValue for u32 {
fn as_js(&self) -> Result<JsValue, String> {
Ok(JsValue::from_f64(*self as f64))
}
}
+18 -77
View File
@@ -1,90 +1,31 @@
use std::{cell::RefCell, sync::OnceLock};
use bevy::window::Window;
use parking_lot::ReentrantMutex;
use web_sys::{CustomEvent, CustomEventInit, Element};
use wasm_bindgen::prelude::wasm_bindgen;
mod conv;
mod util;
pub use conv::*;
pub(crate) use util::*;
pub(crate) type WasmMessageBuffer<T> = OnceLock<ReentrantMutex<RefCell<Option<T>>>>;
static INITIALIZED: OnceLock<ReentrantMutex<RefCell<bool>>> = OnceLock::new();
pub(crate) fn dispatch_to_js(
window: &Window,
event_type: &str,
// NOTE: We use dynamic dispatch here to keep the wasm binary small(-ish)
value: &dyn AsJsValue,
) -> Result<(), String> {
let canvas = try_get_canvas(window)?;
/// The Studio application must only be initialized once (via `init` or
/// `startViewer`). Multiple calls to either of those functions will cause the
/// application to panic. This function can be used to query whether the app has
/// already been initialized.
#[wasm_bindgen(js_name = isInitialized)]
pub fn is_initialized() -> bool {
let wrapper = INITIALIZED.get_or_init(|| ReentrantMutex::new(RefCell::new(false)));
let guard = wrapper.lock();
let result = *guard.borrow();
let detail = value.as_js()?;
let event = CustomEvent::new_with_event_init_dict(
event_type,
CustomEventInit::new()
.bubbles(true)
.cancelable(true)
.detail(&detail),
)
.map_err(|err| format!("{err:?}"))?;
canvas
.dispatch_event(&event)
.map_err(|err| format!("{err:?}"))?;
Ok(())
result
}
fn try_get_canvas(window: &Window) -> Result<Element, String> {
let canvas_selector = window
.canvas
.as_ref()
.ok_or("Canvas selector not set on Bevy Window".to_string())?;
let window = web_sys::window().ok_or("web_sys::window() returned None".to_string())?;
let document = window
.document()
.ok_or("window.document() returned None".to_string())?;
match document.query_selector(canvas_selector) {
Ok(Some(canvas)) => Ok(canvas),
Ok(None) => Err(format!(
"Failed to find element with selector '{canvas_selector}'"
)),
Err(err) => Err(format!("JavaScript threw an Error: {err:?}")),
}
pub(crate) fn initialize() {
let wrapper = INITIALIZED.get_or_init(|| ReentrantMutex::new(RefCell::new(false)));
let guard = wrapper.lock();
*guard.borrow_mut() = true;
}
macro_rules! get_buffer {
($buffer:ident) => {
$buffer.get_or_init(|| {
::parking_lot::ReentrantMutex::new(::std::cell::RefCell::new(
::core::option::Option::None,
))
})
};
}
macro_rules! write_message {
($buffer:ident, $expr:expr) => {{
let buffer = crate::wasm::get_buffer!($buffer);
if let Some(lock) = buffer.try_lock() {
*lock.borrow_mut() = Some($expr);
}
}};
}
macro_rules! read_buffer {
($buffer:ident, |$msg:ident| $stmt:stmt) => {{
let buffer = crate::wasm::get_buffer!($buffer);
if let Some(lock) = buffer.try_lock() {
if let Some($msg) = lock.borrow_mut().take() {
$stmt
}
}
}};
}
pub(crate) use get_buffer;
pub(crate) use read_buffer;
pub(crate) use write_message;
+85
View File
@@ -0,0 +1,85 @@
use std::{cell::RefCell, sync::OnceLock};
use bevy::window::Window;
use parking_lot::ReentrantMutex;
use web_sys::{CustomEvent, CustomEventInit, Element};
use super::AsJsValue;
pub(crate) type WasmMessageBuffer<T> = OnceLock<ReentrantMutex<RefCell<Option<T>>>>;
pub(crate) trait StaticBuffer<T> {
type Inner;
fn inner(&self) -> &Self::Inner;
fn write_message(&self, value: T);
fn take_message(&self) -> Option<T>;
}
impl<T> StaticBuffer<T> for WasmMessageBuffer<T> {
type Inner = ReentrantMutex<RefCell<Option<T>>>;
#[inline]
fn inner(&self) -> &Self::Inner {
self.get_or_init(|| ReentrantMutex::new(RefCell::new(None)))
}
#[inline]
fn write_message(&self, value: T) {
let mutex = self.inner();
if let Some(guard) = mutex.try_lock() {
*guard.borrow_mut() = Some(value);
}
}
#[inline]
fn take_message(&self) -> Option<T> {
let mutex = self.inner();
mutex.try_lock().and_then(|guard| guard.borrow_mut().take())
}
}
pub(crate) fn dispatch_to_js(
window: &Window,
event_type: &str,
// NOTE: We use dynamic dispatch here to keep the wasm binary small(-ish)
value: &dyn AsJsValue,
) -> Result<(), String> {
let canvas = try_get_canvas(window)?;
let detail = value.as_js()?;
let event = CustomEvent::new_with_event_init_dict(
event_type,
CustomEventInit::new()
.bubbles(true)
.cancelable(true)
.detail(&detail),
)
.map_err(|err| format!("{err:?}"))?;
canvas
.dispatch_event(&event)
.map_err(|err| format!("{err:?}"))?;
Ok(())
}
fn try_get_canvas(window: &Window) -> Result<Element, String> {
let canvas_selector = window
.canvas
.as_ref()
.ok_or("Canvas selector not set on Bevy Window".to_string())?;
let window = web_sys::window().ok_or("web_sys::window() returned None".to_string())?;
let document = window
.document()
.ok_or("window.document() returned None".to_string())?;
match document.query_selector(canvas_selector) {
Ok(Some(canvas)) => Ok(canvas),
Ok(None) => Err(format!(
"Failed to find element with selector '{canvas_selector}'"
)),
Err(err) => Err(format!("JavaScript threw an Error: {err:?}")),
}
}
+83
View File
@@ -0,0 +1,83 @@
use bevy::{
pbr::NotShadowCaster,
prelude::*,
render::render_resource::{AsBindGroup, ShaderRef},
};
use crate::camera_controller::{CameraController, CameraMotionSystem};
pub struct WorldGridPlugin;
impl Plugin for WorldGridPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(MaterialPlugin::<ProceduralGridMaterial>::default())
.add_systems(Startup, spawn_world_grid)
.add_systems(PostUpdate, move_plane_with_camera.after(CameraMotionSystem));
}
}
#[derive(Component)]
struct WorldGridPlane;
#[derive(Asset, AsBindGroup, TypePath, Clone)]
struct ProceduralGridMaterial {}
impl Material for ProceduralGridMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/procedural_grid.wgsl".into()
}
fn alpha_mode(&self) -> AlphaMode {
AlphaMode::Blend
}
}
fn spawn_world_grid(
mut cmd: Commands,
mut ra_meshes: ResMut<Assets<Mesh>>,
mut ra_mat_proc_grid: ResMut<Assets<ProceduralGridMaterial>>,
) {
let plane = ra_meshes.add(Mesh::from(shape::Plane {
size: 200.0,
subdivisions: 0,
}));
let proc_grid = ra_mat_proc_grid.add(ProceduralGridMaterial {});
cmd.spawn((
Name::new("World Grid"),
SpatialBundle::from_transform(Transform::IDENTITY),
WorldGridPlane,
))
.with_children(|parent| {
parent.spawn((
MaterialMeshBundle {
mesh: plane.clone(),
material: proc_grid.clone(),
transform: Transform::IDENTITY,
..default()
},
NotShadowCaster,
));
parent.spawn((
MaterialMeshBundle {
mesh: plane,
material: proc_grid,
transform: Transform::IDENTITY.looking_to(Vec3::Z, Vec3::NEG_Y),
..default()
},
NotShadowCaster,
));
});
}
fn move_plane_with_camera(
mut plane: Query<(&WorldGridPlane, &mut Transform)>,
camera: Query<&CameraController, Changed<CameraController>>,
) {
if let Ok(&CameraController { focus, .. }) = camera.get_single() {
let (_, mut xform) = plane.single_mut();
xform.translation.x = focus.x;
xform.translation.z = focus.z;
}
}