资讯详情

在 Graphite 中创建节点:从文档图到 Graphene 原型节点执行器的完整指南

📅 2026/9/10 16:36:11 | 华诺云谱 👁 阅读
在 Graphite 中创建节点:从文档图到 Graphene 原型节点执行器的完整指南
在 Graphite 中创建节点从文档图到 Graphene 原型节点执行器的完整指南【免费下载链接】GraphiteCommunity-built comprehensive 2D content creation appplication for graphic design, digital art, and interactive real-time motion graphics powered by a node-based procedural graphics engine项目地址: https://gitcode.com/GitHub_Trending/gr/GraphiteGraphite 是一款以节点化编辑为核心工作流的 2D 内容创作应用所有图层操作都可以在节点图中以可视化的方式连接、修改与回放。本文以官方节点开发指南node-graph/README.md为主线结合仓库源码系统讲解从定义DocumentNode、编写属性面板控件到实现 Graphene 原型节点、注册节点构造函数、最终执行整张文档图的完整链路帮助你掌握为 Graphite 添加自定义节点的全部技术要素。节点的用途Purpose of NodesGraphite 是一个以节点化编辑工作流为核心的图像编辑器所有操作在图中以可视化方式相互连接。这种设计非常灵活因为它允许在任意时刻查看或修改所有操作而不会丢失原始数据——例如对图像施加的滤镜、混合、变换都以节点与连线的方式保留在文档中随时可以回到任意一步调整参数。节点系统在设计上追求尽可能通用所有数据类型都可表示并且为各种使用场景规划了广泛的内置节点集合。节点不只是滤镜它既是文档编辑的基础单元也是 Graphite 底层 Graphene 程序化渲染引擎node-based procedural graphics engine的计算原语。文档图The Document Graph编辑器呈现给用户的图称为文档图document graph它在NodeNetwork结构体中定义。每一个被放入图中的节点DocumentNode具有以下属性该结构体的真实定义位于 node-graph/graph-craft/src/document.rs 的DocumentNode定义中pub struct DocumentNode { pub inputs: VecNodeInput, pub call_argument: Type, pub implementation: DocumentNodeImplementation, pub skip_deduplication: bool, pub visible: bool, pub original_location: OriginalLocation, }对照源码node-graph/graph-craft/src/document.rs 第 36-66 行实际结构还额外包含一个context_features: ContextDependencies字段用于记录节点的 Context 抽取/注入注解。各字段的语义如下inputs节点的输入列表。每个输入要么是图中其他节点的输出NodeInput::Node保存node_id与output_index要么是常量值NodeInput::Value由TaggedValue承载要么是NodeInput::Import——表示该输入来自图外部在嵌套网络的 flatten扁平化阶段解析此外还有Scope、Reflection、Inline内联 Rust 源码用于 GPU 编译等变体。call_argument该节点可被求值的参数类型。implementation节点实现可以是嵌套的文档网络DocumentNodeImplementation::Network或一个原型节点标识符DocumentNodeImplementation::ProtoNode也可以是Extract用于元编程/GPU 源码提取。visible对应图中节点的眼睛图标。隐藏的节点在 flatten 阶段会被替换为一个直通passthrough节点。skip_deduplication当两个不同原型节点哈希到相同值时例如两个内容相同的值节点编译期默认会去重但某些节点如需要在图外访问的MonitorNode不希望被去重可置为true。original_location节点在文档网络中的路径用于推导类型与错误信息。定义一种新的文档节点类型每个DocumentNode都有特定类型例如 Opacity不透明度节点。你可以在编辑器的节点图消息处理器中定义自己的文档节点类型。原文档指向的document_node_types.rs在当前仓库中已演化为 editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs并配套宏生成器 document_node_derive.rs。一个不透明度节点的类型定义示例如下DocumentNodeDefinition { name: Opacity, category: Image Adjustments, implementation: DocumentNodeImplementation::proto(graphene_core::raster::OpacityNode), inputs: vec![ DocumentInputType::value(Image, TaggedValue::ImageFrame(ImageFrame::empty()), true), DocumentInputType::value(Factor, TaggedValue::F32(100.), false), ], outputs: vec![DocumentOutputType::new(Image, FrontendGraphDataType::Raster)], properties: node_properties::multiply_opacity, ..Default::default() },这里的标识符必须与即将讨论的原型节点proto-node的标识符保持一致通常是节点实现的路径。[!NOTE] 定义在graphene_core中的节点由graphene_std重新导出。但如果类型名的字符串与实现不完全匹配你将会遇到错误。属性面板Properties panel节点的输入名称会在输入被**暴露expose**时显示在图中在属性面板中以一个圆点呈现。默认输入值在节点首次创建或连线断开时被使用。一个输入由TaggedValue允许通过 serde 序列化动态类型外加一个exposed布尔值构成exposed决定该输入是否默认在节点图 UI 中显示为一个圆点。在 Opacity 节点中Image 输入默认显示而 Factor 输入默认隐藏从而使图面更清爽。需要指出的是NodeInput枚举node-graph/graph-craft/src/document.rs 第 203-228 行还提供了NodeInput::value(tagged_value, exposed)等构造函数且is_exposed()方法第 285-294 行表明节点连线输入永远视为暴露Value输入取决于exposed标记Scope/Inline/Reflection输入则不暴露。properties字段是一个函数用于定义数字输入控件——在图中选中 Opacity 节点即可看到。其代码如下pub fn multiply_opacity(document_node: DocumentNode, node_id: NodeId, _context: mut NodePropertiesContext) - VecLayoutGroup { let factor number_widget(document_node, node_id, 1, Factor, NumberInput::default().min(0.).max(100.).unit(%), true); vec![LayoutGroup::Row { widgets: factor }] }这里number_widget的NumberInput通过min(0.).max(100.).unit(%)配置了取值范围 0–100 与百分比单位true表示该控件默认暴露。Graphene原型节点执行器Graphene crate位于 node-graph/nodes/gcore与 Graphene 标准库位于 node-graph/nodes/gstd是节点实际实现代码所在的位置。实现一个节点就是定义一个实现了Nodetrait 的struct。Nodetrait 位于 node-graph/libraries/core-types/src/lib.rs 第 49-64 行其核心是一个接收一个泛型输入的eval函数/// The node trait allows for defining any node. Nodes can only take one call argument input, however they can store references to other nodes inside the struct. /// See node-graph/README.md for information on how to define a new node. pub trait Nodei, Input { type Output: i; /// Evaluates the node with the single specified input. fn eval(i self, input: Input) - Self::Output; /// Resets the node, e.g. the LetNodes cache is set to None. fn reset(self) {} /// Returns the name of the node for diagnostic purposes. fn node_name(self) - static str { std::any::type_name::Self() } /// Serialize the node which is used for the introspect function which can retrieve values from monitor nodes. fn serialize(self) - Optionstd::sync::Arcdyn std::any::Any Send Sync { log::warn!(Node::serialize not implemented for {}, std::any::type_name::Self()); None } }一个作用于颜色的不透明度节点实现示例use crate::{Color, Node}; #[derive(Debug, Clone, Copy)] pub struct OpacityNodeOpacityMultiplierInput { opacity_multiplier: OpacityMultiplierInput, } impli, OpacityMultiplierInput: Nodei, (), Output f64 i Nodei, Color for OpacityNodeOpacityMultiplierInput { type Output Color; fn eval(i self, color: Color) - Color { let opacity_multiplier self.opacity_multiplier.eval(()) as f32 / 100.; Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), color.a() * opacity_multiplier) } }eval函数只能接收一个输入。为了支持多个输入节点结构体可以存储对其他节点的引用。这里opacity_multiplier字段是泛型的被约束为 traitNodei, (), Output f64——这意味着它是一个输入为()计算不透明度无需输入、输出为f64的节点。在执行OpacityNode时需要调用self.opacity_multiplier.eval(())来求值提供opacity_multiplier输入的节点这发生在每次运行该节点时。对应的单元测试求值Color::WHITE后 alpha 通道应变为 0.1#[test] fn test_opacity_node() { let opacity_node OpacityNode { opacity_multiplier: crate::value::CopiedNode(10_f64), // set opacity to 10% }; assert_eq!(opacity_node.eval(Color::WHITE), Color::from_rgbaf32_unchecked(1., 1., 1., 0.1)); }graphene_core::value::CopiedNode是一个求值时复制10_f64并返回的节点。其实现位于 node-graph/libraries/core-types/src/value.rs 第 158-170 行pub struct CopiedNodeT: Copy(pub T)且为任意输入类型I实现了Nodei, I忽略输入、返回持有的值——这正是它可被用作()输入节点、输出固定常量的原因。此外NodeIOtraitnode-graph/libraries/core-types/src/lib.rs 第 69-104 行为节点提供了运行时类型信息input_type/output_type/input_type_name/output_type_name并可将节点转为NodeIOTypes调用参数、返回值与输入列表这是节点注册与类型推断的基础。使用node宏创建新节点无需手动用复杂泛型实现Nodetrait可以使用node宏将其应用于像opacity这样的函数。该宏会自动生成结构体、trait 实现、节点注册表node_registry条目、文档节点定义以及属性面板条目#[node_macro::node(category(Raster: Adjustments))] fn opacity(_input: (), #[default(424242)] color: Color, #[range] #[soft(0..100)] opacity_multiplier: f64) - Color { let opacity_multiplier opacity_multiplier as f32 / 100.; Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), color.a() * opacity_multiplier) }从宏的实现源码node-graph/node-macro/src/codegen.rs可以确认其生成流程fn_name/struct_name/mod_name由函数名派生结构体名会追加Node后缀category属性是必需的缺失会在解析期报错每个普通参数对应生成一个泛型字段与输入下划线开头的参数会被隐藏不生成输入并在eval中逐一求值#[soft/hard]边界会通过Clampable::clamp_hard_min/clamp_hard_maxnode-graph/node-macro/src/codegen.rs 第 306-316 行在执行前对参数做强制钳制。宏的附加选项Additional Macro Options宏调用可以通过附加属性进行扩展。当前支持的属性有name、path、skip_impl、category。使用泛型时#[implementations()]属性可以自动为你填充节点注册表。此外还可以使用default、expose、soft、hard和range属性来影响属性的生成方式。各参数详细说明结合 node-graph/node-macro/src/parsing.rs 第 271-329 行的解析实现#[default(value)]指定参数的默认值如示例中的#[default(424242)]。注意节点的调用参数call argument不允许设置默认值解析器会直接报错parsing.rs 第 649-650 行。#[expose]默认暴露该输入在属性面板/图中显示圆点。#[range]将该数字输入渲染为可拖动的滑块而不是默认的步进输入框parsing.rs 中number_mode_range: bool字段。#[soft(a..b)]与#[hard(a..b)]分别设置滑块的建议范围suggested extent与强制钳制范围enforced clamp。任一端点都可以省略例如0..或..100两个端点都是闭区间包含边界因此不存在..形式。输入框内键入的值可以超出 soft 范围但会被钳制到 hard 边界内——所以#[soft]只有与#[range]组合使用时才有意义。解析器接受整数或浮点字面量统一按f64处理parsing.rs 第 217-235 行且要求至少指定一个边界第 310 行。#[name(...)]覆盖节点显示名否则按结构体名转换为 Title Case。#[path(...)]显式指定节点的注册路径/标识符。#[skip_impl]跳过自动生成的register_node实现用于需要手写注册逻辑的场景codegen.rs 第 1065 行显示此时只会生成register_metadata调用。#[implementations(...)]列出该泛型参数的多个具体实现类型自动生成多条注册表行codegen.rs 中通过implementations字段逐行生成结构化条目。执行一个文档NodeNetwork当文档图被执行时会发生以下步骤对照源码可进一步确认各环节的真实实现扁平化NodeNetwork通过NodeNetwork::flatten扁平化。这一步会移除所有DocumentNodeImplementation::Network它允许嵌套的文档节点网络把所有内部节点移动到单一节点图中。源码实现在 node-graph/graph-craft/src/document.rs 第 909-1000 行扁平化时隐藏节点会被替换为直通节点passthrough值输入会被替换为独立的值节点嵌套网络内部节点 ID 通过merge_ids对父子 ID 哈希得到稳定新 ID重映射后并入父网络Import输入则按索引与父节点输入一一对接。转换为原型图proto-graphNodeNetwork被转换为原型图。每个节点的输入以节点 ID 列表的形式存储在ProtoNode的ConstructionArgs结构体中。文档图到原型图的转换由NodeNetwork::into_proto_networks完成同样位于 node-graph/graph-craft/src/document.rs并配合 node-graph/graph-craft/src/graphene_compiler.rs 的编译流水线。解析为构造函数新创建的ProtoNode通过 node-graph/interpreted-executor/src/node_registry.rs 中定义的映射转换为对应的构造函数这一步由BorrowTree::push_node完成。node_registry.rs 第 641 行声明了静态注册表NODE_REGISTRY其键为ProtoNodeIdentifier值为HashMapNodeIOTypes, NodeConstructor——同一标识符可按不同的输入/输出类型组合NodeIOTypes注册多个构造函数重载。执行构造函数构造函数以ConstructionArgs枚举运行。构造函数通常会对这些输入进行求值例如一个Pi节点作为Add节点的第二个输入时Add节点的构造函数会求值Pi节点——如果你在Pi节点实现里放置一条 log 语句就能观察到这一点。存入借用树BorrowTree解析后的函数存放在BorrowTree中它允许后续节点引用先前的原型节点作为输入并确保节点在被其他节点引用期间不会被移除。BorrowTree与DynamicExecutor的实现在 node-graph/interpreted-executor/src/dynamic_executor.rsDynamicExecutor持有tree: BorrowTree与typing_context: TypingContextTypingContext::new(node_registry::NODE_REGISTRY)负责类型推断update方法在图形变更时增量重建借用树尽量复用未变化的节点orphaned_nodes记录跨帧存留的孤立节点以支持 introspection。节点构造函数定义对图像的每个像素应用不透明度变换的节点其构造函数定义如下( // Matches against the string defined in the document node. ProtoNodeIdentifier::new(graphene_core::raster::OpacityNode), // This function is run when converting the ProtoNode struct into the desired struct. |args| { Box::pin(async move { // Creates an instance of the struct that defines the node. let node construct_node!(args, graphene_core::raster::OpacityNode_, [f64]).await; // Create a new map image node, that calls the node for each pixel. let map_node graphene_std::raster::MapImageNode::new(graphene_core::value::ValueNode::new(node)); // Wraps this in a type erased future BoxPindyn core::future::FutureOutput T n - this allows it to work with async. let map_node graphene_std::any::FutureWrapperNode::new(map_node); // The DynAnyNode downcasts its input from a Boxdyn DynAny i.e. dynamically typed, to the desired statically typed input value. It then runs the wrapped node and converts the result back into a dynamically typed Boxdyn DynAny. let any: DynAnyNodeImageColor, _, _ graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(map_node)); // Nodes are stored as type erased, which means they are Boxdyn NodeIo Node. This allows us to create dynamic graphs, using dynamic dispatch so we do not have to know all node combinations at compile time. any.into_type_erased() }) }, // Defines the call argument, return value, and inputs. NodeIOTypes::new(concrete!(ImageColor), concrete!(ImageColor), vec![fn_type!((), f64)]), ),借用栈中的节点以Boxdyn DynAny作为输入并输出另一个Boxdyn DynAny以支持任意类型。要使用具体类型必须对传入的值进行向下转型downcast。由于OpacityNode一次只处理一个像素我们首先插入一个MapImageNode对图像中的每个像素调用OpacityNode。最后对结果调用.into_type_erased()将其插入借用栈。对照 node-graph/interpreted-executor/src/node_registry.rs 第 643-677 行的async_node!宏可以看到更现代的注册写法宏为每个fn_params类型依次执行downcast_node、用DynAnyNode包裹、最后Box::new(any) as TypeErasedBox并同时生成NodeIOTypes含call_argument、return_value与参数列表。例如注册表顶部的 Monitor 节点族第 39-84 行就为Context Item.../Context List...的每一种类型组合生成了独立注册行。为了简化光栅节点的注册还有一个raster_node!宏它可以把不透明度节点的定义简化为raster_node!(graphene_core::raster::OpacityNode_, params: [f64]),对于不需要逐像素运行的节点还有更通用的register_node!register_node!(graphene_core::transform_nodes::SetTransformNode_, input: Vector, params: [DAffine2]),类型适配与注册表结构值得补充的是node_registry 除了业务节点外还通过一组宏注册了大量类型适配节点node-graph/interpreted-executor/src/node_registry.rs 第 155-638 行input_adapter_node!为每个元素类型注册ItemT/ListT直通与Into转换item_to_list_node!/bundle_node!/unbundle_node!处理单例提升与列表打包/解包convert_adapter_wildcard!注册数值类型之间的强制转换如f64 f32/u32/.../DVec2/String还有ranked_value_types!统一驱动值类型i32、BlendMode、Stroke、Font等的秩提升适配与 memoize/monitor/context 缓存链。所有这些适配器与业务节点一起在node_registry()末尾合并进同一张NODE_REGISTRY哈希表第 607-637 行并统一做秩归一化normalize_rank与泛型名清理去掉stringify!产生的换行、剥离generics后缀仅保留适配器节点的元素后缀。调试Debugging在节点内部可以使用log::debug!()宏进行调试例如log::debug!(The opacity is {opacity_multiplier});官方指南同时指出还需要一个工具来方便地查看图在应用各个步骤时的状态也需要一种透明的方式看到哪些构造函数正在运行、哪些节点正在被求值、以及它们的执行顺序——这也是 Graphite 后续持续改进的方向。从源码看Nodetrait 提供的node_name()返回类型名与serialize()配合 Monitor 节点的introspect能力取回运行值node-graph/libraries/core-types/src/lib.rs 第 56-63 行正是为这类诊断场景预留的基础设施。结论虽然通过宏可以隐藏部分细节来简化节点的编写但创建节点仍然涉及众多文件和概念文档图侧的DocumentNode/DocumentNodeDefinition定义与属性面板控件editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs、Graphene 侧的Nodetrait 实现node-graph/libraries/core-types/src/lib.rs、node/raster_node!/register_node!等宏node-graph/node-macro/src、以及执行端的节点注册表与借用树node-graph/interpreted-executor/src/node_registry.rs、node-graph/interpreted-executor/src/dynamic_executor.rs。Graphite 团队正在持续让这套系统更易用社区贡献者如有疑问可以在 Graphite 的 Discord 中寻求帮助。【免费下载链接】GraphiteCommunity-built comprehensive 2D content creation appplication for graphic design, digital art, and interactive real-time motion graphics powered by a node-based procedural graphics engine项目地址: https://gitcode.com/GitHub_Trending/gr/Graphite创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。