
Graphite 的 2D 内容创作愿景以节点图驱动的非破坏性图形引擎与工作流解析【免费下载链接】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 图形内容创作应用同时提供桌面端与 Web 端覆盖矢量艺术、位图处理、数字绘画、插画、数据可视化与合成等场景。本篇技术文章以项目官方博客《Graphite: a vision for the future of 2D content creation》为核心骨架结合当前仓库中的 节点图文档、文档图源码 与 节点注册表实现深入讲解 Graphite 的 100% 非破坏性工作流、Layers 面板与节点图双视图协作机制以及从文档节点到可执行代码的完整编译管线帮助你理解并复现节点开发的全过程。基于节点图程序化生成的 Mandelbrot 分形作品体现了无限缩放的解析度无关特性一、Graphite 的产品定位与技术愿景根据官方博客website/content/blog/2022-03-12-graphite-a-vision-for-the-future-of-2d-content-creation.md的定位Graphite 是一个开源的、跨平台的数字内容创作应用用于 2D 图形编辑、照片处理、矢量艺术、数字绘画、插画、数据可视化和合成compositing等场景。它受开源领域 Blender 在 3D 方向成功经验的启发目标是把 2D 内容创作推向新的高度其交互体验参考了 Photoshop/Gimp 与 Illustrator/Inkscape 的高效流程底层则采用 Houdini、Nuke、Blender 等专业工具验证过的节点式、非破坏性方法。从当前仓库的工程结构可以印证这一桌面 Web双端战略frontend/基于 Svelte TypeScript 的前端包含 Graph.svelte节点图视图、Layers.svelte图层面板、Properties.svelte属性面板等核心 UI 组件以及frontend/wrapper/中把 Rust 核心编译为 Wasm 的胶水层desktop/面向 Linux、macOS、Windows 的桌面端外壳其中desktop/顶层的bundle/、platform/、ui/基于 CEF 的浏览器进程封装与wrapper/共同构成跨平台发布与原生集成能力node-graph/节点图引擎的核心 crate 群包括graph-craft文档图与 proto 图定义、interpreted-executor解释执行器与节点注册表、graphene-cli命令行导出工具以及nodes/下按类别组织的节点实现gcore、gstd、raster、brush、blending、transform等。仓库根目录的 README.md 进一步将 Graphite 概括为你的 2D 内容创作程序化工具箱一个把图层合成layer-based compositing与节点式生成设计node-based generative design结合起来的完全非破坏性编辑工作流其编辑器工具包裹着节点图核心为矢量、位图以及更多领域提供友好的工作流。二、100% 非破坏性解析度无关的创作范式博客文章阐述了 Graphite 最核心的设计原则100% 非破坏性nondestructive且解析度无关resolution-independent的工作流。这意味着传统的栅格式图像编辑可以无限放大、并在之后的任意时间缩放到任意解析度——因为编辑过程不是直接改写像素而是以参数化方式记录画笔笔触、矢量形状和其他操作。博客给出了一个极具代表性的使用场景用户可以在小型笔记本电脑屏幕上用绘画工具作画放大到局部区域补充细节完成作品之后可以尝试事后修改模拟画笔风格例如从硬头铅笔改为柔软的丙烯画笔最后以超高解析度导出完整画作用于大幅海报打印。这一场景在技术层面是如何成立的关键就在于参数化记录画布上的每一笔、每一个图形节点在 文档图 中都对应一个携带类型化输入TaggedValue的DocumentNode而任何修改改画笔风格、调不透明度都只是改动节点图的参数与连线原始数据从未被破坏。仓库中的示例作品 demo-artwork/changing-seasons.graphite、demo-artwork/marbled-mandelbrot.graphite 等即是以.graphite文档格式保存的节点网络其中marbled-mandelbrot正对应 README 中展示的填充了噪声图案、程序化生成且无限可缩放的分形示例是解析度无关特性的直接体现。值得说明的是项目还在推进更先进的文档格式设计node-graph/rfcs/document-format.md 提出用扁平节点注册表 基于操作operation-based的 CRDT 增量来取代旧格式以统一支撑历史记录、撤销/重做、多用户并发编辑、迁移与增量编译——这从格式层面进一步巩固了非破坏性、可回溯的编辑模型。三、双视图协作Layers 面板与节点图的无缝切换博客强调 Graphite 的用户体验居于核心地位提供精心设计的 UI 以支撑直观、高效的艺术创作流程。其工作方式是双视图协作传统交互式视口WYSIWYG用户像使用常规 2D 编辑器一样直接在画布上绘制与编辑配合Layers 面板对应 Layers.svelte组织内容层级节点图Node Graph用户可随时切入或跳出节点图回改此前的操作构建强大的程序化图像生成器procedural image generators并实时与交互式视口同步。前端视图层中Graph.svelte 负责渲染节点图网格缩放、节点/连线/导入导出口的可视化通过/src/stores/node-graph状态仓库与编辑器核心通信Properties.svelte 则负责渲染选中节点的属性控件。两者共享同一份文档状态这正是画布所见与节点图所编辑的始终是同一模型的工程基础——视口渲染结果由节点图执行器驱动修改节点参数会即时反映到画布而画布上的传统绘制操作也会被记录为节点网络中的新节点。四、文档图Document Graph一切操作的底层数据结构在 Graphite 中呈现给用户的图被称为文档图document graph其数据模型定义于 node-graph/graph-craft/src/document.rs 的NodeNetwork结构体。每个被放入图中的节点都是一个DocumentNode实例见 document.rspub struct DocumentNode { pub inputs: VecNodeInput, pub call_argument: Type, pub implementation: DocumentNodeImplementation, pub skip_deduplication: bool, pub visible: bool, pub original_location: OriginalLocation, }各字段的含义如下依据源码注释与 节点图文档字段说明inputs节点的输入列表具体形态由NodeInput枚举描述见下call_argument该节点可以被求值所携带的参数类型即节点的主输入类型implementation节点的实现方式嵌套的文档子网络Network、指向 proto 节点的标识符ProtoNode或用于元编程/GPU 提取的Extract标记skip_deduplication是否跳过去重当两个 proto 节点哈希一致时例如两个内容相同的值节点编译期默认会合并重复项但某些需要从图外访问的节点如MonitorNode必须跳过visible对应节点图 UI 中的眼睛图标隐藏节点在 flatten 阶段会被替换为直通passthrough节点original_location节点在文档中的原始路径、输入来源映射与依赖关系用于类型推导与错误定位NodeInput枚举document.rs描述了输入的几种可能形态Node { node_id, output_index }引用同网络中另一个节点的输出Value { tagged_value, exposed }一个硬编码常量值编译成图时会被转换为值节点exposed标记该输入默认是否在节点图 UI 中显示为圆点Import { import_type, import_index }来自父网络的输入嵌套网络场景下在 flatten 阶段解析Scope(key)从节点所在的外层作用域中提取输入Reflection(...)编译时替换为关于该节点位置的元数据如节点路径Inline(InlineRust)一段内联 Rust 源码字符串主要用于 GPU 编译。值得注意的工程细节NodeNetwork提供is_acyclic()document.rs来检测图是否含环并提供remove_dead_nodes()document.rs在编译前清理未被任何导出引用的死节点保证执行效率。五、节点定义与属性面板以 Opacity 节点为例一个文档节点归属于某一特定节点类型例如 Opacity 不透明度类型。文档图文档node-graph/README.md给出了一个 Opacity 节点定义示例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 节点实现路径完全一致通常是节点实现的路径如graphene_core::raster::OpacityNode。文档特别提醒graphene_core中定义的节点由graphene_std重新导出但如果字符串类型名不完全匹配就会报错。5.1 输入暴露exposed与属性面板节点定义的inputs中每个输入由TaggedValue支持 serde 序列化的动态类型包装加一个exposed布尔值构成。exposed决定该输入默认是否在节点图 UI 中显示为圆点例如 Opacity 节点中 Image 输入默认暴露Factor 输入默认隐藏以减少图面杂乱。properties字段是一个返回LayoutGroup列表的函数用于生成选中该节点时属性面板中显示的控件。文档展示了multiply_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 }] }即为索引 1 的 Factor 输入生成一个取值范围0..100、单位为%的数字控件。5.2 从旧实现到新宏体系需要注意的是上述示例摘自 2022 年的文档与早期代码当前仓库中的不透明度/混合节点已全面迁移到node_macro宏体系。例如 node-graph/nodes/blending/src/lib.rs 中的opacity节点以声明式函数定义并利用#[implementations(Graphic, Vector, RasterCPU, RasterGPU, Color, Gradient, String)]自动生成对多种内容类型的实现#[default(100.)]与#[widget(ParsedWidgetOverride::Custom optional_percentage)]控制默认值与属性控件。这种演进恰好展示了项目简化节点书写的持续努力文档结论部分也明确提及。六、Graphene 运行时Node trait 与逐像素求值节点真正的实现位于 graphene 核心库gcore/即 node-graph/nodes/gcore/src与 graphene 标准库gstd/即 node-graph/nodes/gstd/src。实现一个节点的基本方式是定义一个实现了Nodetrait 的 structtrait 要求实现名为eval的方法该方法接受一个泛型输入。文档给出的作用于单个颜色的 OpacityNode 实现如下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只能接收一个输入。要支持多个输入节点 struct 可以存储对其他节点的引用。这里opacity_multiplier字段是泛型的约束为Nodei, (), Output f64即一个输入为()无需输入即可求值、输出为f64的节点计算 OpacityNode 时调用self.opacity_multiplier.eval(())求得不透明度乘数——每次运行该节点时都会触发文档还给出了对应的单元测试把opacity_multiplier设为crate::value::CopiedNode(10_f64)即 10% 不透明度断言白色经过节点后 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)); }由于 OpacityNode 一次只处理一个像素实际运行时需要通过MapImageNode把它映射到整幅图像的每个像素上见第八节。七、用node宏快速创建节点手动实现带复杂泛型的Nodetrait 较为繁琐。为此项目提供#[node_macro::node]过程宏可直接施加于普通函数自动生成节点 struct、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) }7.1 支持的宏属性宏调用可以扩展额外属性。从 node-graph/node-macro/src/parsing.rs 的NodeFnAttributes与参数解析代码可以确认当前支持的属性包括属性作用name覆盖节点的显示名称path覆盖节点的注册路径/标识符skip_impl跳过自动生成具体类型实现此时不生成register_node只生成元数据注册见 codegen.rscategory节点在节点目录中的分类如category(Blending)implementations(...)当函数使用泛型时自动为列出的具体类型生成节点注册表条目例如#[implementations(Graphic, Vector, RasterCPU, ...)]default设置参数的默认值如#[default(100.)]expose控制输入默认是否在图上暴露range将数字输入渲染为可拖拽滑块而非默认的步进数字框soft(a..b)设置滑块的建议范围soft extenthard(a..b)设置强制钳制范围hard clamp关于soft与hard的语义文档和 parsing.rs 的注释一致说明#[soft(a..b)]与#[hard(a..b)]分别设置滑块的建议范围与强制钳制任一端点都可省略如0..或..100两端都是闭区间因此不存在..形式用户在输入框中键入的值可以超出 soft 范围但会被钳制到 hard 边界所以#[soft]只有与#[range]一起使用才有意义。7.2 真实节点示例基础画笔当前仓库中一个很好的宏使用范例是基础画笔节点 node-graph/nodes/brush/src/basic_brush/mod.rs#[node_macro::node(category(Raster: Brush))] pub async fn basic_brusha: n( ctx: impl Ctx ExtractFootprint, strokes: ListGraphic, #[widget(ParsedWidgetOverride::Hidden)] cache: ItemBrushCache, #[scope(basic_brush_pipeline::IDENTIFIER)] pipeline: ItemWgpuPipelineCache, ) - ListRasterGPU { // ...遍历 stroke 列表提取颜色/直径/硬度/流量属性 // 通过 WgpuExecutor 运行 GPU 管线生成栅格纹理 }它展示了宏体系的几个进阶用法异步函数、impl Ctx上下文注入、#[widget(...)]覆盖属性控件、#[scope(...)]从作用域获取共享的 GPU 管线缓存——最终节点图在 GPU 上以管线方式渲染画笔笔触与博客中模拟画笔风格的愿景直接呼应。八、执行管线从文档图到可运行代码当文档图NodeNetwork被执行时会经历一套清晰的编译与执行步骤。文档node-graph/README.md与源码共同描述了这条管线Flatten 扁平化通过NodeNetwork::flattendocument.rs移除所有DocumentNodeImplementation::Network嵌套文档子网络把内层节点全部提升到单一节点图中同时隐藏节点被替换为直通节点PassthroughNode常量值输入被替换为独立的值节点ClonedNode转换为 proto 图每个节点的输入被整理为ProtoNode中ConstructionArgs结构体的节点 ID 列表通过NodeNetwork::into_proto_networksdocument.rs为每个输出生成一个ProtoNetworkConstructionArgs有三种形态proto.rsValue已知类型值、Nodes作为输入的其他节点 ID 列表、InlineGPU 计算使用的内联 Rust 源码映射为构造函数新生成的ProtoNode按照 node-graph/interpreted-executor/src/node_registry.rs 中的映射转换为对应的构造函数构造器以(ProtoNodeIdentifier, 构造函数闭包, NodeIOTypes)三元组注册节点图运行时通过BorrowTree::push_node完成执行构造函数构造函数以ConstructionArgs为参数运行通常会先求值输入节点例如把Pi节点作为Add节点的第二个输入时Add的构造器会先求值Pi可在其实现中放置日志语句验证存入 BorrowTree解析后的函数被存储在一个BorrowTree中允许后续节点引用前序 proto 节点作为输入并保证被引用节点不会被移除。8.1 类型擦除与逐像素映射文档详细展示了如何为对图像每个像素应用不透明度变换的节点注册构造函数( // 匹配文档节点中定义的字符串 ProtoNodeIdentifier::new(graphene_core::raster::OpacityNode), // 将 ProtoNode 转换为目标 struct 时运行的函数 |args| { Box::pin(async move { // 创建定义节点的 struct 实例 let node construct_node!(args, graphene_core::raster::OpacityNode_, [f64]).await; // 创建 map image 节点对每个像素调用 node let map_node graphene_std::raster::MapImageNode::new(graphene_core::value::ValueNode::new(node)); // 包装为类型擦除的 Future使其支持异步 let map_node graphene_std::any::FutureWrapperNode::new(map_node); // DynAnyNode 把 Boxdyn DynAny 动态类型输入下转换为期望的静态类型 // 运行包装节点后再把结果转回 Boxdyn DynAny let any: DynAnyNodeImageColor, _, _ graphene_std::any::DynAnyNode::new(graphene_core::value::ValueNode::new(map_node)); // 节点以类型擦除形式存储Boxdyn NodeIo Node // 通过动态分发创建动态图无需在编译期预知所有节点组合 any.into_type_erased() }) }, // 定义调用参数、返回值与输入 NodeIOTypes::new(concrete!(ImageColor), concrete!(ImageColor), vec![fn_type!((), f64)]), ),理解这段代码的关键概念Borrow stack 中的节点输入输出都是Boxdyn DynAny以支持任意类型要使用具体类型必须把传入值下转换downcastOpacityNode一次只处理一个像素所以先用MapImageNode把它逐像素应用到整幅图像最后调用into_type_erased()把结果放入 borrow stack。该方法的定义位于 node-graph/nodes/gstd/src/any.rs为满足NodeIO约束的节点实现将其装箱为Boxdyn NodeIo Node形式的TypeErasedBox异步支持通过FutureWrapperNode实现把类型擦除的Future包装为普通节点接口。8.2 注册宏raster_node!与register_node!手写上述构造函数模板仍然繁琐为此项目提供两个宏简化注册文档亦给出定义raster_node!专用于逐像素运行的栅格节点一行即可完成注册raster_node!(graphene_core::raster::OpacityNode_, params: [f64]),register_node!适用于不需要逐像素运行的一般节点register_node!(graphene_core::transform_nodes::SetTransformNode_, input: Vector, params: [DAffine2]),当前版本中node宏已经能够自动生成注册代码在原生平台通过#[ctor]自动调用register_node()在 Wasm 平台则生成extern C函数供 JS 调用见 codegen.rs最终所有注册条目在 node_registry.rs 中合并进全局NODE_REGISTRY以ProtoNodeIdentifier与NodeIOTypes为键的构造函数查找表。8.3 调试手段在节点内部进行调试可以直接使用log::debug!()宏例如log::debug!(The opacity is {opacity_multiplier});文档同时指出项目未来需要补充的能力一个用于分步查看图处理过程的工具以及一个能透明观察哪些构造函数在运行、哪些节点在求值、求值顺序如何的可视化手段——这部分在文档撰写时仍属待办事项引用时应保持这一前提。九、通往更广阔的未来GPU 执行与格式演进博客的愿景不止于解释器执行。从源码可以看到 Graphite 的节点引擎正在向两个方向深化GPU 执行DocumentNodeImplementation::Extract变体document.rs支持把单个文档节点提取为其源码表示并作为值传递给GpuCompiler节点在运行时编译为计算着色器compute shader。这与ConstructionArgs::Inline、#[gpu_image]以及gstd中的大量.wgsl着色器如 render_background.wgsl相配合构成面向 GPU 的渲染与合成管线文档格式演进node-graph/rfcs/document-format.md规划的.gdd格式将文档抽象为扁平节点注册表 CRDT 增量树让同一份增量数据同时承担历史步骤、CRDT 操作、迁移单元与编译失效信号四种职责为非破坏性编辑、多人协作与增量编译奠定数据基础。node-graph/目录下的graphene-climain.rs与 export.rs 提供了脱离编辑器、直接从节点网络导出渲染结果的命令行能力对应博客中以超高解析度导出用于打印的场景interpreted-executor的 benchmarks 则提供run_once、update_executor等基准测试用于度量节点图执行与增量更新的性能。结语Graphite 的愿景文章描述了一幅清晰的蓝图在熟悉的 2D 创作界面上以节点图引擎驱动程序化图形处理让艺术家既能像使用传统绘图软件那样作画又能随时进入节点图调整任何一步参数并且始终保持 100% 非破坏、解析度无关的编辑体验。而当前仓库的实现表明这一愿景已经落地为完整的技术栈graph-craft定义文档图模型node_macro与注册宏把节点书写简化为普通 Rust 函数interpreted-executor负责类型擦除与动态分发的执行frontend与desktop提供 Web/桌面双端体验rfcs/document-format.md则为多用户与 GPU 化的未来铺路。对于希望参与或扩展该项目的开发者而言从 node-graph/README.md 出发沿着本文所述的定义文档节点 → 用宏实现节点 → 注册并执行路径即可开始构建自己的第一个节点。【免费下载链接】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),仅供参考