<Reality>
概述
<Reality> 跟同为 3D 内容容器元素的 <Model> 一样具备空间化 HTML 元素的能力,作为空间中悬浮的 2D 面片使用,参与 HTML/CSS 布局,跟<Model> 的区别是,<Reality> 是动态 3D 内容容器,它的 3D 内容不是用预先制作好的、静态的 3D 模型文件来实现,而是在 2D 面片前方的局部空间中可以用支持统一渲染的 3D Engine API 动态渲染任意 3D 内容。
这些 3D 引擎 API 被 WebSpatial SDK 作为 React 组件提供:
import {
Reality,
Material,
ModelAsset,
AttachmentAsset,
World,
Entity,
Box,
Sphere,
Plane,
Cone,
Cylinder,
ModelEntity,
AttachmentEntity,
} from "@webspatial/react-sdk";
<Reality style={{ width: "500px", height: "500px", "--xr-depth": 100 }}>
<Material type="unlit" id="red" color="#ff0000" />
<ModelAsset id="teapot" src="https://example.com/model.usdz" />
<World>
<Box materials={["red"]} width={0.2} height={0.2} depth={0.2} />
</World>
</Reality>
场景图
这些 3D 引擎 API 包含两类:
第一类是 3D Entity,这种 React 组件只能在 <World>(也可写作 <SceneGraph>)里使用,<World> 是动态 3D 容器内所有 3D 内容的根节点。
第二类是 3D 资产的声明,比如材质(<Material>),这种 React 组件只能作为 <Reality> 中的顶层子节点、在 <World> 外面使用,需要被 3D Entity 引用才能实际影响渲染结果。
3D 资产
在 <Reality> 的顶层子节点中,可以包含以下 3D 资产声明:
- 可以引用预先声明的材质。
<Reality>
<Material type="unlit" id="solid" color="#00ff00" />
<Material type="unlit" id="glass" color="#0000ff" transparent opacity={0.5} />
<World>
- 可以声明 3D 模型文件。
<Reality>
<ModelAsset
id="ship-blueprint"
src="https://example.com/fighter.usdz"
onLoad={() => {}}
onError={() => {}} />
<World>
- 可以声明要附着在 Entity 上的 2D HTML/CSS 内容。
<Reality>
<AttachmentAsset name="info">
<div style={{ width: '100%' }}>
<p>Some text</p>
</div>
</AttachmentAsset>
<World>
3D 实体
3D Entity 组件不参与 HTML 布局,只在 <Reality> 容器内按照 3D 引擎体系来渲染,它们不支持 CSS 样式,而是采用 3D 引擎体系里的「Transform 属性」:
<Entity
// Position: x (left/right), y (down/up), z (away/toward)
position={{ x: 0.1, y: -0.2, z: 0.3 }}
// Rotation: radians (Math.PI = 180°)
rotation={{ x: 0, y: Math.PI / 2, z: 0 }} // 90° on Y-axis
// Scale: 1 = normal, 2 = double, 0.5 = half
scale={{ x: 1, y: 2, z: 1 }} // stretched vertically
>
Transform 属性默认使用 <Reality> 对应的 2D 面片前方局部 3D 空间的坐标系,原点是这个空间的中心点。采用右手坐标系,Y 轴朝上,Z 轴朝向用户,长度单位默认用面向现实世界物体的物理单位(m)。
这个空间的深度可以用 depth 设置,可以用 clientDepth 查询当前的深度。
对于 <World> 顶层的 Entity 节点,Transform 属性中 position 的值是相对于坐标系原点的,对于其他作为子节点的 Entity,Transform 属性中 position 的值是相对于父 Entity 的 position。
WebSpatial SDK 目前提供的开箱即用的 Entity 有以下几类:
基础实体
<Entity> 不可见,作为其他 Entity 的父组件和 group container 使用,可以用它把多个 Entity 包含在一个 group 里。
<Reality>
<World>
<Entity position={{ x: 1, y: 0, z: 0 }}>
<Box />
<Box />
</Entity>
<Box position={{ x: 2, y: 0, z: 0 }} />
</World>
几何实体
几何实体(primitive)包括以下几何形状,它们各自有不同的额外属性:
<Box>- 属性:
width,height,depth,cornerRadius
- 属性:
<Plane>- 属性:
width,height,cornerRadius
- 属性:
<Sphere>- 属性:
radius
- 属性:
<Cone>- 属性:
height,radius
- 属性:
<Cylinder>- 属性:
height,radius
- 属性:
示例:
<Box
width={0.2}
height={0.2}
depth={0.2} // meters (0.1 = 10cm)
cornerRadius={0.01} // rounded edges
/>
这些几何实体都支持 materials 属性,可以引用预先声明的材质。
<Reality>
<Material type="unlit" id="solid" color="#00ff00" />
<Material type="unlit" id="glass" color="#0000ff" transparent opacity={0.5} />
<World>
<Box
width={0.2}
height={0.2}
depth={0.2} // meters (0.1 = 10cm)
materials={["glass"]}
cornerRadius={0.01} // rounded edges
/>
</World>
</Reality>
模型实体
<ModelEntity> 是用预制好的 3D 模型文件渲染内容的 Entity。
以「飞船舰队」为例:将飞船模型下载和加载到内存中一次,然后引用它生成 3 个独立的 Model Entity,渲染出 3 艘飞船
import { Reality, World, ModelAsset, ModelEntity } from "@webspatial/react-sdk";
function SpaceshipFleet() {
return (
<Reality style={{ width: "100%", height: "500px" }}>
{/* --- 1. THE RESOURCE --- */}
{/* This downloads the file once. It is INVISIBLE right now. */}
<ModelAsset
id="ship-blueprint"
src="https://example.com/fighter-jet.usdz"
/>
{/* --- 2. THE SCENE --- */}
<World>
{/* Leader Ship: Center, Normal Size */}
<ModelEntity
model="ship-blueprint" // Points to the ID above
position={{ x: 0, y: 0, z: 0 }}
scale={{ x: 1, y: 1, z: 1 }}
/>
{/* Left Wingman: Moved left, slightly smaller */}
<ModelEntity
model="ship-blueprint" // Reuses the same loaded file!
position={{ x: -0.5, y: -0.2, z: 0.3 }}
scale={{ x: 0.8, y: 0.8, z: 0.8 }}
/>
{/* Right Wingman: Moved right, slightly smaller */}
<ModelEntity
model="ship-blueprint" // Reuses the same loaded file
position={{ x: 0.5, y: -0.2, z: 0.3 }}
scale={{ x: 0.8, y: 0.8, z: 0.8 }}
/>
</World>
</Reality>
);
}
对于动画需求,可以用 JS 轮询修改 Transform 属性来实现。 示例:
const [rotation, setRotation] = useState({ x: 0, y: 0, z: 0 });
useEffect(() => {
let id;
function animate() {
setRotation(prev => ({ ...prev, y: prev.y + 0.02 }));
id = requestAnimationFrame(animate);
}
animate();
return () => cancelAnimationFrame(id);
}, []);
<Box rotation={rotation} />;
附着实体
<AttachmentEntity> 是一个类似 <Plane> 的 Entity,可以引用预先声明好的 2D HTML/CSS 内容,让它附着在自己表面上。
WebSpatial SDK 后续版本会让 <AttachmentEntity> 像 <Plane> 一样支持 width 和 height(当前版本暂不支持)和完整 Transform 属性(当前版本只支持 position),需要临时用 size 属性设置大小(单位是跟 2D 内容一样的 px)。
<Reality>
<AttachmentAsset name="info">
<div style={{ width: "100%" }}>
<p>Some text</p>
</div>
</AttachmentAsset>
<World>
<Box position={{ x: 0.5, y: -0.2, z: 0.3 }} />
<Entity position={{ x: -0.5, y: -0.2, z: 0.3 }}>
<AttachmentEntity
attachment="info"
position={{ x: 0, y: 0, z: 0 }}
size={{ width: 100, height: 100 }}
/>
</Entity>
</World>
</Reality>