☰
Amethyst 自定义 RenderGraph 实战:用 renderable_custom 示例掌握可渲染对象加载与光照控制
2026/9/27 7:26:34 网站建设 项目流程

本篇指南以 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. 点光源公转:以角速度-1.0 rad/s、轨道半径15.0、高度z = 6.0,使点光源围绕 Y 轴旋转(x = r·cos(θ),y = r·sin(θ)),并把其颜色设为DemoState.light_color(受按键控制);
  2. 相机自转:角速度0.1 rad/s,对每个带Camera组件的实体,用绕 Z 轴的四元数增量左乘其等距变换(delta_rot * transform.isometry())实现缓慢旋转;
  3. FPS 刷新:通过UiFinder找到fps_text实体,每 20 帧(time.frame_number() % 20 == 0)更新一次UiText,保留两位小数;
  4. 相关参数常量(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

点击查看免费下载
上一篇:openwisp-controller高级技巧:子网划分与IP自动分配策略
下一篇:CTF Wiki 协作贡献实战:从「编辑此页」到 Pull Request 合并的完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询