本篇指南以 Amethyst 仓库中的 examples/renderable_custom/README.md 为骨架,深入剖析该示例如何加载可渲染对象(网格、材质、纹理)、配置点光源/方向光/环境光三种光照,并手写自定义RenderGraph替代默认渲染管线。读完本文,你将掌握 Prefab 场景资源加载、GraphCreator渲染图构建、按键驱动光源调试等一整套可在自己项目中复用的技术方案。
示例概览:加载可渲染对象与多种光照方案
renderable_custom是 Amethyst 官方示例中专门演示「可渲染对象加载 + 光照方案组合」的工程,其独特之处在于使用自定义RenderGraph,而非默认的RenderingBundle插件式管线(后者可参考 examples/renderable/main.rs)。
该示例的完整文件结构如下:
examples/renderable_custom/ ├── assets/ │ ├── font/square.ttf # UI 字体 │ ├── mesh/ # 场景网格(OBJ) │ │ ├── cone.obj cube.obj lid.obj rectangle.obj teapot.obj │ ├── prefab/renderable.ron # 场景 Prefab(核心资源) │ ├── texture/logo.png # 立方体表面纹理 │ └── ui/{fps.ron, loading.ron} # FPS / Loading UI ├── config/display.ron # 窗口显示配置 ├── Cargo.toml # 依赖声明 ├── main.rs # 程序入口与自定义 RenderGraph └── screenshot.png # 运行效果截图示例运行时呈现一个 3D 场景:矩形平面(地面)、红色立方体、圆锥体、贴有 logo 纹理的紫色立方体以及犹他茶壶,左上角叠加 FPS 显示,场景中一个白色点光源绕 Y 轴公转,相机缓慢自转(见 screenshot.png)。
运行方式与依赖声明
工程通过相对路径引用仓库根目录下的 amethyst 主 crate,并启用optional特性(渲染/窗口为可选模块):
[dependencies] amethyst = { path = "../../", features = ["optional"] } log = { version = "^0.4", features = ["serde"] } serde = "^1" derivative = "^2" lazy_static = "^1.4" serde-diff = "0.4" type-uuid = "0.1" glsl-layout = "0.4" ron = "^0.6"完整的依赖清单见 examples/renderable_custom/Cargo.toml。在仓库根目录执行cargo run --example无法直接运行该示例(它不是 examples 目录下的独立示例入口),需要进入工程目录:
cd examples/renderable_custom cargo run窗口标题由 config/display.ron 控制——注意该配置通过@import引用 amethyst_window/src/config.rs 中的DisplayConfig类型:
/*! @import /amethyst_window/src/config.rs#DisplayConfig DisplayConfig */ ( title: "Renderable Example - With custom Render Graph", )DisplayConfig默认分辨率等参数均可在此追加字段扩展;而对比 examples/renderable/config/display.ron(标题为 "Renderable Example"),可以看到renderable_custom通过标题明确标识了自定义渲染图这一特性。
按键对照表:实时调试光照
运行程序后,以下按键可实时切换场景中的光照状态(对应 main.rs 中handle_event的实现):
| 按键 | 作用 | 实现细节 |
|---|---|---|
r | 将点光源设为红色调 | Srgb::new(0.8, 0.2, 0.2) |
g | 将点光源设为绿色调 | Srgb::new(0.2, 0.8, 0.2) |
b | 将点光源设为蓝色调 | Srgb::new(0.2, 0.2, 0.8) |
w | 将点光源设为白色 | Srgb::new(1.0, 1.0, 1.0) |
p | 切换点光源黑(关)/白(开) | 黑(0,0,0)/ 白(1,1,1) |
a | 切换微弱白色环境光 | 开Srgba(0.01,0.01,0.01,1.0)/ 关Srgba(0,0,0,0) |
d | 切换微弱白色方向光(朝上) | 开Srgb(0.2,0.2,0.2)/ 关Srgb(0,0,0) |
按键事件处理通过StateEvent::Window匹配,再经get_key与VirtualKeyCode判定;修改状态时利用world.exec闭包同时取得多个资源的写权限,例如a键同时写入DemoState与AmbientColor,d键同时写入DemoState与WriteStorage<'_, Light>:
Some((VirtualKeyCode::A, ElementState::Pressed)) => { w.exec( |(mut state, mut color): ( Write<'_, DemoState>, Write<'_, AmbientColor>, )| { if state.ambient_light { state.ambient_light = false; color.0 = Srgba::new(0.0, 0.0, 0.0, 0.0); } else { state.ambient_light = true; color.0 = Srgba::new(0.01, 0.01, 0.01, 1.0); } }, ); }需要说明:README 中环境光写为(0.1, 0.1, 0.1)而源码实际写入(0.01, 0.01, 0.01, 1.0),本文以源码为准;Srgb/Srgba类型来自 amethyst_rendy/src/palette 相关实现 的 re-export,颜色分量取值范围均为0.0 ~ 1.0。按键Escape或窗口关闭请求则退出程序(main.rs)。
场景 Prefab:网格、材质、纹理与光照的装配
场景不是逐行代码创建的,而是通过 assets/prefab/renderable.ron 以 RON 格式声明,类型为Prefab<MyPrefabData>,其中:
type MyPrefabData = BasicScenePrefab<(Vec<Position>, Vec<Normal>, Vec<TexCoord>)>;即场景数据由「位置 + 法线 + 纹理坐标」三种顶点属性构成(Position/Normal/TexCoord来自 amethyst_rendy/src/formats/mesh.rs 所依赖的 rendy 网格模块)。RON 文件顶部通过@import声明结构类型:
#![enable(implicit_some)] /*! @import /amethyst_assets/src/prefab/mod.rs#Prefab @import ../../renderable/main.rs#MyPrefabData Prefab<MyPrefabData> */Prefab 内entities列表依次定义:环境光、五个网格对象(lid蓝、teapot绿、cube贴图、cone白、cube红)、地面rectangle、点光源、方向光与透视相机。各条目通过data携带组件,例如带纹理立方体:
( data: ( graphics: ( mesh: Asset(File("mesh/cube.obj", ("OBJ", ()))), material: ( albedo: File("texture/logo.png", ( "IMAGE", ( sampler_info: ( min_filter: Linear, mag_filter: Linear, mip_filter: Linear, wrap_mode: (Tile, Tile, Tile), lod_bias: (0), lod_range: (start: (0), end: (8000)), comparison: None, border: (0), normalized: true, anisotropic: On(8), ), ) )), ), ), transform: ( translation: (5.0, 3.0, -5.0), scale: (2.0, 2.0, 2.0), ), ), ),albedo两种来源:Generate(Srgba(...))直接生成纯色材质;File("...", ("IMAGE", ...))从文件加载纹理并配置采样器(过滤方式、环绕模式、各向异性等)。光照条目示例:
( data: ( light: ( light: Point((intensity: 50.0)), ), ), ), ( data: ( light: ( light: Directional(( direction: [-1.0, -1.0, -1.0], intensity: 0.2, color: Srgb(0.2, 0.2, 0.2), )), ), ), ),相机条目为透视投影(fovy ≈ 60°、znear 0.1、zfar 2000),并放在transform中:
( data: ( transform: Transform ( translation: (0.0, 20.0, 20.0), rotation: (-0.4, 0.0, 0.0, 0.862), ), camera: Perspective( aspect: 1.3, fovy: 1.0471975512, znear: 0.1, zfar: 2000.0, ), ), ),BasicScenePrefab由 amethyst_utils/src/lib.rs 导出,内部解析了 graphics/light/camera/transform 等场景组件;Prefab 加载系统的底层定义见 amethyst_assets/src/prefab/mod.rs。
自定义 RenderGraph:GraphCreator 全流程拆解
这是本示例的核心价值所在。渲染器在每一帧都会询问图是否需重建,并调用图构建器产出渲染图(rendy 的Graph),相关 trait 定义于 amethyst_rendy/src/system.rs:
pub trait GraphCreator<B: Backend> { /// Check if graph needs to be rebuilt. /// This function is evaluated every frame before running the graph. fn rebuild(&mut self, world: &World, resources: &Resources) -> bool; /// Retrieve configured complete graph builder. fn builder( &mut self, factory: &mut Factory<B>, world: &World, resources: &Resources, ) -> GraphBuilder<B, GraphAuxData>; }renderable_custom中ExampleGraph(main.rs)实现该 trait,包含dimensions与dirty两个状态字段。RenderingSystem内部(amethyst_rendy/src/system.rs)在rebuild返回true时会先 dispose 旧图,再调用builder构建新图;若返回false则直接复用上一帧的图继续渲染。
窗口尺寸变化检测
rebuild每帧比较当前ScreenDimensions与上次记录,若发生变化先置dirty并更新记录、返回false(等待连续两帧尺寸一致,避免窗口拖动过程中反复重建);否则返回dirty本身,由builder成功执行后清除:
fn rebuild(&mut self, world: &World) -> bool { let new_dimensions = world.try_fetch::<ScreenDimensions>(); use std::ops::Deref; if self.dimensions.as_ref() != new_dimensions.as_deref() { self.dirty = true; self.dimensions = new_dimensions.map(|d| d.deref().clone()); return false; } self.dirty }构建子图:颜色/深度缓冲与清屏
builder内先取Window资源创建渲染表面(surface),获取表面格式,并以Kind::D2(w, h, 1, 1)描述交换链尺寸,随后创建两个图像节点:
- 颜色图像:格式取
surface_format,清屏色为[0.34, 0.36, 0.52, 1.0](灰蓝色); - 深度图像:格式
Format::D32Sfloat,深度清为0.0、模板清为0:
let color = graph_builder.create_image( window_kind, 1, surface_format, Some(ClearValue { color: ClearColor { float32: [0.34, 0.36, 0.52, 1.0] }, }), ); let depth = graph_builder.create_image( window_kind, 1, Format::D32Sfloat, Some(ClearValue { depth_stencil: ClearDepthStencil { depth: 0.0, stencil: 0 }, }), );组装 Subpass 与 Present 节点
随后用SubpassBuilder装配一个包含两个渲染组的子通道:DrawShadedDesc(3D 着色渲染组)与DrawUiDesc(UI 渲染组),并绑定颜色/深度目标:
let pass = graph_builder.add_node( SubpassBuilder::new() .with_group(DrawShadedDesc::default().builder()) .with_group(DrawUiDesc::default().builder()) // Draws UI components .with_color(color) .with_depth_stencil(depth) .into_pass(), );DrawShadedDesc<B>是DrawBase3DDesc<B, ShadedPassDef>的类型别名(amethyst_rendy/src/pass/shaded.rs),ShadedPassDef指定了顶点格式Position + Normal + TexCoord、对应 SPIR-V 着色器及纹理集(TexAlbedo, TexEmission),即本示例 Prefab 顶点属性与之完全匹配;DrawUiDesc来自 amethyst_ui/src/pass.rs,负责 FPS/加载文字的绘制。
最后把子通道作为依赖接入PresentNode(present 节点负责把颜色图像呈现到 surface):
let _present = graph_builder .add_node(PresentNode::builder(factory, surface, color).with_dependency(pass)); graph_builder完整图形管线即「清屏 → 着色子通道(3D + UI)→ 呈现」,构建完成后将dirty复位为false。通过对比默认的RenderingBundle(amethyst_rendy/src/bundle.rs 中RenderToWindow与RenderShaded3D插件的组合方式,见 examples/renderable/main.rs),可以直观看出:默认插件在bundle内部同样以SubpassBuilder::new()装配DrawShadedDesc等渲染组(bundle.rs),而自定义GraphCreator则把这一过程完全交还开发者,自由决定清屏色、深度格式、渲染组顺序与呈现节点。
装载与状态机:Prefab 异步加载、UI 创建与状态切换
程序入口(main.rs)流程:启动日志 → 定位应用根目录 → 装配DispatcherBuilder→ 构建Application运行。资源加载被封装为Loading状态:
impl SimpleState for Loading { fn on_start(&mut self, data: StateData<'_, GameData>) { self.prefab = Some(data.world.exec(|loader: PrefabLoader<'_, MyPrefabData>| { loader.load("prefab/renderable.ron", RonFormat, &mut self.progress) })); data.world.exec(|mut creator: UiCreator<'_>| { creator.create("ui/fps.ron", &mut self.progress); creator.create("ui/loading.ron", &mut self.progress); }); } fn update(&mut self, data: &mut StateData<'_, GameData>) -> SimpleTrans { match self.progress.complete() { Completion::Failed => { println!("Failed loading assets: {:?}", self.progress.errors()); Trans::Quit } Completion::Complete => { println!("Assets loaded, swapping state"); // 删除 "loading" UI 实体后切换状态 Trans::Switch(Box::new(Example { scene: self.prefab.as_ref().unwrap().clone() })) } Completion::Loading => Trans::None, } } }ProgressCounter汇总多个资源(Prefab + 两个 UI 文件)的加载进度,complete()返回Loading/Complete/Failed三种状态;进入Example状态后,通过world.push((self.scene.clone(),))将整个 Prefab 场景实体写入世界(main.rs)。
两个 UI 资源均为Label(assets/ui/fps.ron 与 assets/ui/loading.ron):fps.ron锚定左上角显示 "N/A",loading.ron锚定屏幕中央显示 "Loading",字体统一使用font/square.ttf。
场景驱动系统:光照公转、相机自转与 FPS 刷新
ExampleSystem(main.rs)每帧执行四件事:
- 点光源公转:以角速度
-1.0 rad/s、轨道半径15.0、高度z = 6.0,使点光源围绕 Y 轴旋转(x = r·cos(θ),y = r·sin(θ)),并把其颜色设为DemoState.light_color(受按键控制); - 相机自转:角速度
0.1 rad/s,对每个带Camera组件的实体,用绕 Z 轴的四元数增量左乘其等距变换(delta_rot * transform.isometry())实现缓慢旋转; - FPS 刷新:通过
UiFinder找到fps_text实体,每 20 帧(time.frame_number() % 20 == 0)更新一次UiText,保留两位小数; - 相关参数常量(
light_angular_velocity = -1.0、light_orbit_radius = 15.0、light_z = 6.0、camera_angular_velocity = 0.1)都在系统开头定义,便于按需调整。
FpsCounter来自 amethyst_utils/src/fps_counter.rs,由FpsCounterBundle注入。系统在DispatcherBuilder中以with(ExampleSystem::default(), "example_system", &[])注册,TransformBundle::new().with_dep(&["example_system"])确保变换在系统之后处理(main.rs)。
渲染相关系统按注释要求放在调度器末尾并按主线程执行(main.rs):UiGlyphsSystemDesc、Processor<SpriteSheet>、VisibilitySortingSystem、MeshProcessorSystem、TextureProcessorSystem、Processor<Material>,最终用with_thread_local(RenderingSystem::<DefaultBackend, _>::new(ExampleGraph::default()))把自定义渲染图注入——thread_local保证渲染器始终在单线程连续执行。
与默认渲染管线的对比及迁移要点
| 维度 | 默认 RenderingBundle(examples/renderable/main.rs) | 自定义 RenderGraph(本示例) |
|---|---|---|
| 装配方式 | RenderingBundle::new().with_plugin(RenderToWindow).with_plugin(RenderShaded3D).with_plugin(RenderUi) | 手写GraphCreator+RenderingSystem::new(graph) |
| 清屏色 | RenderToWindow::with_clear(ClearColor {...}) | create_image的ClearValue直接指定 |
| 深度格式 | 插件内部默认 | Format::D32Sfloat由开发者决定 |
| 渲染组顺序 | 插件内部编排 | SubpassBuilder任意追加DrawShadedDesc/DrawUiDesc等 |
| 图重建策略 | 框架内部处理 | 开发者自行实现rebuild检测窗口尺寸变化 |
选择建议:若只需标准 3D + UI 渲染,优先使用RenderingBundle(配置少、不易出错);当需要定制渲染目标(多相机、离屏纹理、特殊呈现顺序、自定义清屏)时,再按本示例方式实现GraphCreator。两套方案底层共用 amethyst_rendy/src/pass/base_3d.rs 的DrawBase3DDesc机制,迁移成本可控。
小结
renderable_custom完整演示了「Prefab 场景资源 → 网格/材质/纹理装配 → 点/方向/环境光组合 → 自定义 RenderGraph → 按键实时调试」的渲染链路;- 自定义渲染图的关键是
GraphCreator的rebuild(尺寸变化检测)与builder(图像节点 + Subpass + Present 节点); - Prefab 是声明式搭建场景的推荐方式,
BasicScenePrefab封装的graphics/light/camera/transform组件可直接复用; - 更完整的示例家族(默认渲染管线的同类示例、其他光照/材质案例)可继续浏览 examples/renderable 与 examples 目录。
【免费下载链接】amethyst
Data-oriented and>项目地址:https://gitcode.com/gh_mirrors/ame/amethyst
相关推荐
FunASR 论文实现全索引:从学术论文到可运行代码的源码级导航
FunASR 论文实现全索引:从学术论文到可运行代码的源码级导航 导读 docs/reference/papers.md 是 FunASR 仓库中的一张"论文实
终极指南:掌握SkiaSharp自定义绘制与SKDrawable可绘制对象
终极指南:掌握SkiaSharp自定义绘制与SKDrawable可绘制对象 SkiaSharp作为.NET平台的跨平台2D图形API,提供了强大的自定义绘制能力
图形学图像处理跨平台Rich 可渲染协议详解:从 RichRenderable 抽象基类到自定义终端渲染对象
Rich 可渲染协议详解:从 RichRenderable 抽象基类到自定义终端渲染对象 Rich 是 Python 生态中面向终端的富文本格式化库,其核心设计