Compare commits

..

19 Commits
main ... ramen

Author SHA1 Message Date
mars b0ecf1eb92 Paint node kind label 2022-10-21 16:32:24 -06:00
mars 1fb7f03468 Align slot labels 2022-10-21 16:09:25 -06:00
mars 9318b59ac0 Paint output slot labels 2022-10-21 15:56:25 -06:00
mars 1f2242c36c WIP node input slot label painting 2022-10-21 15:46:54 -06:00
mars 47f0288f21 Add dynamic node kinds
Move commands.rs tests to lib.rs
Add TestNodeBuilder
2022-09-20 07:08:16 -06:00
mars 247028caa8 Print command errors instead of unwrapping 2022-09-20 05:44:26 -06:00
mars 68fd63eaff Add NodeEditor command history + undo/redo buttons 2022-09-20 04:33:46 -06:00
mars 51546fc2a2 NodeEditor + interactive editing 2022-09-19 10:06:21 -06:00
mars 1803d784f9 Add MoveNode::relative 2022-09-19 09:22:52 -06:00
mars be6693b20a Add GraphLayout::pointer_target() 2022-09-19 07:42:09 -06:00
mars 36edf051ff Add ramen_egui crate 2022-09-19 07:13:47 -06:00
mars 1b3a991144 Make the ramen crate a library 2022-09-18 04:13:05 -06:00
mars 7de349e60a A little cleanup 2022-09-17 06:19:53 -06:00
mars 180cd516ea CommandHistory 2022-09-17 06:16:57 -06:00
mars bb72e6d753 Add Command unit tests 2022-09-17 05:22:18 -06:00
mars 4f3363fc0f Support optional input slots 2022-09-17 04:40:50 -06:00
mars 13142ee210 Ramen command pattern 2022-09-17 03:20:34 -06:00
mars eb5eb4f76d Add ramen to workspace 2022-09-17 03:20:27 -06:00
Skye Terran d3f9f81468 Adding ramen graph logic sketch here 2022-09-14 23:44:05 -07:00
22 changed files with 1265 additions and 68 deletions

View File

@ -1,6 +1,8 @@
[workspace]
members = [
"editor"
"editor",
"ramen",
"ramen_egui"
]
[package]
@ -23,7 +25,7 @@ slab = "^0.4"
smallmap = "^1.0"
smallvec = "^1.0"
strum = { version = "0.24", features = ["derive"] }
wgpu = "^0.13"
wgpu = "^0.12"
winit = "0.26"
[dependencies.legion]
@ -31,7 +33,7 @@ version = "^0.4"
optional = true
[dependencies.naga]
version = "0.9"
version = "0.8.5"
features = ["wgsl-in", "glsl-in", "wgsl-out", "serialize", "deserialize"]
[[bin]]

View File

@ -7,7 +7,7 @@
> A repulsive reaper of all of human consciousness -- but mostly SIGGRAPH presentations.
> H.R Giger's irreconcilable reality... if H.R. Giger was a computer programmer.
Cyborg is an experimental, GPU-driven rendering engine using Rust and wgpu.
Cyborg is a free-form rendering engine that we mash whatever we feel like into.
It's a test bed for all sorts of modern rendering technology. Our goal is to take
techniques and features from modern game engines and reimplement them on our own, for
the sake of education, performance, and reusability. We wanna *make shit work.*
@ -16,15 +16,16 @@ We also want to give artists a playground for generating all sorts of unique
3D visuals -- custom shaders, procedurally generated meshes and textures,
and a focus on *interesting* visuals.
Realism is a non-goal!
Make shrooms obsolete.
Go wild.
Modularity and reusability are important. We want to be able to unplug certain
Modularity and reusability are important. We wanna be able to unplug certain
parts of the rendering pipeline, upgrade them, fix them, document them, commit
them, tag them, etc., then stick them back in in a different place to end up
with a different resulting image.
Cyborg is licensed under the GPL-3.0. Yes, this does mean that any software using
Cyborg will be required to make its source code public. But it also means that
Cyborg is licensed under the GPL-3.0. Yes, this does mean that Bevy games using
Cyborg will be required to make their source code public. But it also means that
new visuals added to Cyborg will become available to all other developers and
artists to use. Hopefully, this will expand the collective knowledge of how different
rendering effects work, and decrease the pressure on graphics programmers to
@ -36,3 +37,10 @@ gathered from countless programmers and individual projects.
> Resistance is futile.
>
> -- The Borg
Cyborg is based on [Bevy](https://bevyengine.org/), a "refreshingly simple"
game engine written in Rust. Bevy provides a lot of useful utilities like
ECS-based data parallelization and a modular plugin system. Cyborg is integrated
into the rest of the Bevy ecosystem, and can also work as a replacement Bevy's
provided rendering engine, giving Bevy games GPU-powered, procedurally-generated
content and access to modern rendering optimizations.

View File

@ -7,16 +7,16 @@ edition = "2021"
bytemuck = "^1.0"
crossbeam-channel = "^0.5"
cyborg = { path = "../", features = ["legion"] }
egui = "0.18"
egui-winit = "0.18.0"
egui_wgpu_backend = "0.18.0"
egui = "0.17.0"
egui-winit = "0.17.0"
egui_wgpu_backend = "0.17.0"
glam = { version = "0.20", features = ["serde"] }
gltf = { version = "1.0", features = ["utils"] }
legion = "^0.4"
parking_lot = "^0.11"
pollster = "0.2"
puffin = "^0.13"
puffin_egui = "0.16.0"
puffin_egui = "0.14.0"
rfd = "^0.8"
stl = "0.2.1"

View File

@ -60,10 +60,10 @@ impl Application {
.unwrap();
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: *surface.get_supported_formats(&adapter).first().unwrap(),
format: surface.get_preferred_format(&adapter).unwrap(),
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Fifo,
present_mode: wgpu::PresentMode::Mailbox,
};
surface.configure(&device, &config);

8
ramen/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "ramen"
version = "0.1.0"
edition = "2021"
[dependencies]
glam = { version = "0.20", features = ["serde"] }
slab = "^0.4"

239
ramen/src/command.rs Normal file
View File

@ -0,0 +1,239 @@
use super::{Graph, GraphError, GraphResult};
use crate::node::{Edge, Node, SlotIndex};
use std::fmt::Debug;
pub trait Command: Debug {
fn undo(&self, graph: &Graph) -> GraphResult<DynCommand>;
fn apply(&self, graph: &mut Graph) -> GraphResult<()>;
}
pub type DynCommand = Box<dyn Command>;
pub struct CommandHistory {
pub(crate) commands: Vec<(DynCommand, DynCommand)>,
pub(crate) cursor: usize,
}
impl CommandHistory {
pub fn new() -> Self {
Self {
commands: Vec::new(),
cursor: 0,
}
}
pub fn push(&mut self, graph: &mut Graph, cmd: DynCommand) -> GraphResult<()> {
let undo = cmd.undo(graph)?;
cmd.apply(graph)?;
// Discard redo history on new push
if self.cursor < self.commands.len() {
self.commands.truncate(self.cursor);
}
self.commands.push((cmd, undo));
self.cursor += 1;
Ok(())
}
pub fn undo(&mut self, graph: &mut Graph) -> GraphResult<bool> {
if self.cursor > 0 {
self.cursor -= 1;
self.commands[self.cursor].1.apply(graph)?;
Ok(true)
} else {
Ok(false)
}
}
pub fn redo(&mut self, graph: &mut Graph) -> GraphResult<bool> {
if self.cursor < self.commands.len() {
self.commands[self.cursor].0.apply(graph)?;
self.cursor += 1;
Ok(true)
} else {
Ok(false)
}
}
}
#[derive(Clone, Debug)]
pub struct CreateNode {
pub kind: usize,
pub pos: glam::Vec2,
pub inputs: Vec<Option<SlotIndex>>,
}
impl Command for CreateNode {
fn undo(&self, graph: &Graph) -> GraphResult<DynCommand> {
let target = graph.nodes.vacant_key();
let undo = DeleteNode { target };
Ok(Box::new(undo))
}
fn apply(&self, graph: &mut Graph) -> GraphResult<()> {
let node = Node {
kind: self.kind,
pos: self.pos,
inputs: self.inputs.clone(),
};
graph.nodes.insert(node);
Ok(())
}
}
impl From<Node> for CreateNode {
fn from(node: Node) -> Self {
Self {
kind: node.kind,
pos: node.pos,
inputs: node.inputs,
}
}
}
#[derive(Clone, Debug)]
pub struct DeleteNode {
pub target: usize,
}
impl Command for DeleteNode {
fn undo(&self, graph: &Graph) -> GraphResult<DynCommand> {
let target = graph.get_node(self.target)?;
let undo = CreateNode {
kind: target.kind,
pos: target.pos,
inputs: target.inputs.clone(),
};
Ok(Box::new(undo))
}
fn apply(&self, graph: &mut Graph) -> GraphResult<()> {
graph
.nodes
.try_remove(self.target)
.ok_or(GraphError::InvalidReference)
.map(|_| ())
}
}
#[derive(Clone, Debug)]
pub struct MoveNode {
pub target: usize,
pub to: glam::Vec2,
pub relative: bool,
}
impl Command for MoveNode {
fn undo(&self, graph: &Graph) -> GraphResult<DynCommand> {
let target = graph.get_node(self.target)?;
let undo = Self {
target: self.target,
to: target.pos,
relative: false,
};
Ok(Box::new(undo))
}
fn apply(&self, graph: &mut Graph) -> GraphResult<()> {
let target = graph.get_node_mut(self.target)?;
if self.relative {
target.pos += self.to;
} else {
target.pos = self.to;
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct SetEdge {
pub edge: Edge,
}
impl Command for SetEdge {
fn undo(&self, graph: &Graph) -> GraphResult<DynCommand> {
let target = graph.get_node(self.edge.output.node)?;
let old_input = target
.inputs
.get(self.edge.output.slot)
.ok_or(GraphError::InvalidReference)?;
Ok(match old_input {
Some(old_input) => Box::new(SetEdge {
edge: Edge {
input: *old_input,
output: self.edge.output,
},
}),
None => Box::new(DeleteEdge {
input: self.edge.output,
}),
})
}
fn apply(&self, graph: &mut Graph) -> GraphResult<()> {
let target = graph.get_node_mut(self.edge.output.node)?;
let input = target
.inputs
.get_mut(self.edge.output.slot)
.ok_or(GraphError::InvalidReference)?;
*input = Some(self.edge.input);
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct DeleteEdge {
pub input: SlotIndex,
}
impl Command for DeleteEdge {
fn undo(&self, graph: &Graph) -> GraphResult<DynCommand> {
let target = graph.get_node(self.input.node)?;
let old_input = target
.inputs
.get(self.input.slot)
.ok_or(GraphError::InvalidReference)?
.ok_or(GraphError::MissingEdge)?;
let undo = SetEdge {
edge: Edge {
input: old_input,
output: self.input,
},
};
Ok(Box::new(undo))
}
fn apply(&self, graph: &mut Graph) -> GraphResult<()> {
let target = graph.get_node_mut(self.input.node)?;
let input = target
.inputs
.get_mut(self.input.slot)
.ok_or(GraphError::InvalidReference)?;
if input.is_none() {
Err(GraphError::MissingEdge)
} else {
*input = None;
Ok(())
}
}
}

324
ramen/src/lib.rs Normal file
View File

@ -0,0 +1,324 @@
use slab::Slab;
use std::sync::Arc;
pub mod command;
pub mod node;
pub mod node_kind;
use command::*;
use node::{Edge, Node, SlotIndex};
use node_kind::*;
#[derive(Debug)]
pub enum GraphError {
InvalidReference,
MissingEdge,
Cyclic,
}
pub type GraphResult<T> = Result<T, GraphError>;
#[derive(Debug)]
pub struct Graph {
pub node_kinds: Arc<NodeKindStore>,
pub nodes: Slab<Node>,
}
impl Graph {
pub fn new(node_kinds: Arc<NodeKindStore>) -> Self {
Self {
node_kinds,
nodes: Slab::new(),
}
}
pub fn get_node(&self, index: usize) -> GraphResult<&Node> {
self.nodes.get(index).ok_or(GraphError::InvalidReference)
}
pub fn get_node_mut(&mut self, index: usize) -> GraphResult<&mut Node> {
self.nodes
.get_mut(index)
.ok_or(GraphError::InvalidReference)
}
// Return the edges that can be derived from the graph
pub fn edges(&self) -> Result<Vec<Edge>, GraphError> {
let topo_order = self.topological_order()?;
let mut edges = Vec::new();
for (node_idx, node) in topo_order.iter().enumerate() {
for (slot_idx, input) in self.nodes[*node].inputs.iter().enumerate() {
if let Some(input) = input {
let output = SlotIndex {
node: node_idx,
slot: slot_idx,
};
edges.push(Edge {
input: *input,
output,
});
}
}
}
Ok(edges)
}
pub fn topological_order(&self) -> Result<Vec<usize>, GraphError> {
// The number of nodes
let v = self.nodes.len();
// Forward-directed adjacency lists
let mut adjacency_lists: Vec<Vec<usize>> = vec![Vec::new(); v];
for (i, node) in self.nodes.iter() {
for j in node.inputs.iter().flatten() {
adjacency_lists[j.node].push(i);
}
}
// Create list of indegrees
let mut indegrees: Vec<usize> = vec![0; v];
for (i, node) in self.nodes.iter() {
indegrees[i] = node.inputs.len();
}
// Create a queue and initialize it with nodes with no indegrees
let mut queue: Vec<usize> = Vec::new();
for (i, count) in indegrees.iter().enumerate() {
if *count == 0 {
queue.push(i);
}
}
let mut sort_count = 0;
let mut topo_order: Vec<usize> = Vec::new();
while !queue.is_empty() {
let u = queue.pop().unwrap();
topo_order.push(u);
for i in &adjacency_lists[u] {
indegrees[*i] -= 1;
if indegrees[*i] == 0 {
queue.push(*i);
}
}
sort_count += 1;
}
// Check if there was a cycle
if sort_count != v {
Err(GraphError::Cyclic)
} else {
Ok(topo_order)
}
}
}
pub enum TestOpKind {
Add,
Sub,
Mul,
Div,
}
pub struct TestGraphBuilder {
pub graph: Graph,
pub literal_kind: usize,
pub add_kind: usize,
pub sub_kind: usize,
pub mul_kind: usize,
pub div_kind: usize,
}
impl TestGraphBuilder {
pub fn new() -> Self {
let mut kinds = NodeKindStore { kinds: vec![] };
let literal_kind = kinds.push(NodeKind {
title: "Literal".into(),
inputs: vec![],
outputs: vec![NodeKindOutput { label: "".into() }],
});
let mut make_arith_kind = |title: &str| -> usize {
kinds.push(NodeKind {
title: title.to_string(),
inputs: vec![
NodeKindInput {
label: "A".to_string(),
},
NodeKindInput {
label: "B".to_string(),
},
],
outputs: vec![NodeKindOutput {
label: "Result".to_string(),
}],
})
};
let add_kind = make_arith_kind("Add");
let sub_kind = make_arith_kind("Subtract");
let mul_kind = make_arith_kind("Multiply");
let div_kind = make_arith_kind("Divide");
let graph = Graph::new(Arc::new(kinds));
Self {
graph,
literal_kind,
add_kind,
sub_kind,
mul_kind,
div_kind,
}
}
pub fn add(&mut self, node: Node) -> usize {
self.graph.nodes.insert(node)
}
pub fn add_literal(&mut self, value: f32) -> usize {
self.graph.nodes.insert(self.make_literal(value))
}
pub fn make_literal(&self, _value: f32) -> Node {
Node {
kind: self.literal_kind,
pos: glam::Vec2::ZERO,
inputs: vec![],
}
}
pub fn add_op(&mut self, kind: TestOpKind, a: usize, b: usize) -> usize {
self.graph.nodes.insert(self.make_op(kind, a, b))
}
pub fn make_op(&self, kind: TestOpKind, a: usize, b: usize) -> Node {
let kind = match kind {
TestOpKind::Add => self.add_kind,
TestOpKind::Sub => self.sub_kind,
TestOpKind::Mul => self.mul_kind,
TestOpKind::Div => self.div_kind,
};
Node {
kind,
pos: glam::Vec2::ZERO,
inputs: vec![
Some(SlotIndex { node: a, slot: 0 }),
Some(SlotIndex { node: b, slot: 0 }),
],
}
}
pub fn three_node_add_cmds(&self) -> Vec<DynCommand> {
vec![
Box::new(CreateNode::from(self.make_literal(1.0))),
Box::new(CreateNode::from(self.make_literal(2.0))),
Box::new(CreateNode::from(Node {
kind: self.add_kind,
pos: glam::Vec2::ZERO,
inputs: vec![None; 2],
})),
Box::new(SetEdge {
edge: Edge {
input: SlotIndex { node: 0, slot: 0 },
output: SlotIndex { node: 2, slot: 0 },
},
}),
Box::new(SetEdge {
edge: Edge {
input: SlotIndex { node: 1, slot: 0 },
output: SlotIndex { node: 2, slot: 1 },
},
}),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
impl CommandHistory {
pub fn push_multi(
&mut self,
graph: &mut Graph,
cmds: impl IntoIterator<Item = DynCommand>,
) -> GraphResult<()> {
for cmd in cmds {
println!("{:#?}", cmd);
self.push(graph, cmd)?;
println!("{:#?}", graph);
}
Ok(())
}
pub fn rewind(&mut self, graph: &mut Graph) -> GraphResult<()> {
while self.cursor > 0 {
self.cursor -= 1;
let undo = &self.commands[self.cursor].1;
println!("{:#?}", undo);
undo.apply(graph)?;
println!("{:#?}", graph);
}
Ok(())
}
}
#[test]
fn create_node() -> GraphResult<()> {
let mut builder = TestGraphBuilder::new();
let cmd = CreateNode {
kind: builder.literal_kind,
pos: glam::Vec2::ZERO,
inputs: vec![],
};
let undo = cmd.undo(&builder.graph)?;
cmd.apply(&mut builder.graph)?;
assert_eq!(builder.graph.nodes.len(), 1);
undo.apply(&mut builder.graph)?;
assert_eq!(builder.graph.nodes.len(), 0);
Ok(())
}
#[test]
fn three_node_add() -> GraphResult<()> {
let mut builder = TestGraphBuilder::new();
let cmds = builder.three_node_add_cmds();
for cmd in cmds.into_iter() {
println!("{:#?}", cmd);
cmd.apply(&mut builder.graph)?;
println!("{:#?}", builder.graph);
}
builder.graph.topological_order()?;
Ok(())
}
#[test]
fn three_node_add_rewind() -> GraphResult<()> {
let mut builder = TestGraphBuilder::new();
let mut history = CommandHistory::new();
let cmds = builder.three_node_add_cmds();
history.push_multi(&mut builder.graph, cmds)?;
builder.graph.topological_order()?;
history.rewind(&mut builder.graph)?;
assert_eq!(builder.graph.nodes.len(), 0);
Ok(())
}
}

43
ramen/src/node.rs Normal file
View File

@ -0,0 +1,43 @@
use crate::{GraphError, GraphResult};
#[derive(Debug, Clone)]
pub struct Node {
pub kind: usize,
pub pos: glam::Vec2,
pub inputs: Vec<Option<SlotIndex>>,
}
impl Node {
pub fn unwrap_inputs(&self) -> GraphResult<Vec<SlotIndex>> {
let mut inputs = Vec::with_capacity(self.inputs.len());
for input in self.inputs.iter() {
if let Some(input) = input {
inputs.push(*input);
} else {
return Err(GraphError::MissingEdge);
}
}
Ok(inputs)
}
pub fn at(self, pos: glam::Vec2) -> Self {
Self {
pos,
..self
}
}
}
#[derive(Copy, Debug, Clone, Hash, PartialEq, Eq)]
pub struct SlotIndex {
pub node: usize,
pub slot: usize,
}
#[derive(Copy, Debug, Clone, Hash, PartialEq, Eq)]
pub struct Edge {
pub input: SlotIndex,
pub output: SlotIndex,
}

29
ramen/src/node_kind.rs Normal file
View File

@ -0,0 +1,29 @@
#[derive(Debug)]
pub struct NodeKindStore {
pub kinds: Vec<NodeKind>,
}
impl NodeKindStore {
pub fn push(&mut self, kind: NodeKind) -> usize {
let idx = self.kinds.len();
self.kinds.push(kind);
idx
}
}
#[derive(Debug)]
pub struct NodeKind {
pub title: String,
pub inputs: Vec<NodeKindInput>,
pub outputs: Vec<NodeKindOutput>,
}
#[derive(Debug)]
pub struct NodeKindInput {
pub label: String,
}
#[derive(Debug)]
pub struct NodeKindOutput {
pub label: String,
}

12
ramen_egui/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "ramen_egui"
version = "0.1.0"
edition = "2021"
[dependencies]
egui = "0.18"
glam = "0.20"
ramen = { path = "../ramen" }
[dev-dependencies]
eframe = "0.18"

View File

@ -0,0 +1,61 @@
use eframe::egui;
use ramen::{TestGraphBuilder, TestOpKind};
use ramen_egui::NodeEditor;
fn main() {
let native_options = eframe::NativeOptions::default();
eframe::run_native(
"Ramen example",
native_options,
Box::new(|cc| Box::new(Application::new(cc))),
);
}
struct Application {
editor: NodeEditor,
}
impl Application {
fn new(cc: &eframe::CreationContext<'_>) -> Self {
cc.egui_ctx.set_visuals(egui::Visuals::dark());
let scale = 250.0;
let pos = |x, y| glam::Vec2::new(x, y) * scale;
let mut builder = TestGraphBuilder::new();
let n0 = builder.add(builder.make_literal(1.0).at(pos(1.0, 1.0)));
let n1 = builder.add(builder.make_literal(2.0).at(pos(1.0, 2.0)));
let n2 = builder.add(builder.make_op(TestOpKind::Add, n0, n1).at(pos(2.0, 1.5)));
let n3 = builder.add(builder.make_literal(4.0).at(pos(2.0, 2.5)));
let n4 = builder.add(builder.make_op(TestOpKind::Div, n2, n3).at(pos(3.0, 2.0)));
let n5 = builder.add(builder.make_op(TestOpKind::Mul, n4, n4).at(pos(4.0, 2.0)));
let _n6 = builder.add(builder.make_op(TestOpKind::Add, n5, n0).at(pos(3.0, 1.0)));
Self {
editor: NodeEditor::from_graph(builder.graph),
}
}
}
impl eframe::App for Application {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::TopBottomPanel::top("menu_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
egui::widgets::global_dark_light_mode_switch(ui);
if ui.button("Undo").clicked() {
self.editor.undo();
}
if ui.button("Redo").clicked() {
self.editor.redo();
}
});
});
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Hello world!");
self.editor.show(ui);
});
}
}

472
ramen_egui/src/lib.rs Normal file
View File

@ -0,0 +1,472 @@
use egui::{Color32, Frame, Stroke};
use glam::Vec2;
use ramen::command::CommandHistory;
use ramen::node::{Edge, SlotIndex};
use ramen::Graph;
use std::collections::HashMap;
use std::sync::Arc;
pub struct NodeEditor {
graph: Graph,
cmd_history: CommandHistory,
pointer_state: PointerState,
}
impl NodeEditor {
pub fn from_graph(graph: Graph) -> Self {
Self {
graph,
cmd_history: CommandHistory::new(),
pointer_state: PointerState::new(),
}
}
pub fn undo(&mut self) {
self.cmd_history.undo(&mut self.graph).unwrap();
}
pub fn redo(&mut self) {
self.cmd_history.redo(&mut self.graph).unwrap();
}
pub fn show(&mut self, ui: &mut egui::Ui) {
let style = ui.style();
let style = GraphStyle::from_ui_style(style);
let layout = GraphLayout::layout(ui, &style, &self.pointer_state, &self.graph);
let rect = ui.available_rect_before_wrap();
let sense = egui::Sense::click_and_drag();
let response = ui.allocate_rect(rect, sense);
let pointer_pos = response.interact_pointer_pos();
let pointer_target = pointer_pos.map(|pos| layout.pointer_target(pos));
let painter = ui.painter_at(rect);
layout.paint(rect, &painter);
if let Some(pointer_pos) = pointer_pos {
self.pointer_state.position = pointer_pos;
self.pointer_state.delta = response.drag_delta();
if let PointerAction::Idle(idle) = &mut self.pointer_state.action {
*idle = pointer_target.clone();
}
}
if response.drag_started() && response.dragged_by(egui::PointerButton::Primary) {
let pointer_target = pointer_target.expect("Drag started but no target");
self.pointer_state.start = self.pointer_state.position;
let next_state = match pointer_target {
PointerTarget::Grid => None, // TODO grid navigation
PointerTarget::Node(idx) => Some(PointerAction::MovingNode(idx)),
PointerTarget::Input(idx) => Some(PointerAction::SettingInput(idx)),
PointerTarget::Output(idx) => Some(PointerAction::SettingOutput(idx)),
};
if let Some(next_state) = next_state {
self.pointer_state.action = next_state;
}
} else if response.drag_released() {
use ramen::command::*;
let cmd: Option<DynCommand> = match self.pointer_state.action {
PointerAction::Idle(_) => None,
PointerAction::MovingNode(idx) => {
let delta = self.pointer_state.position - self.pointer_state.start;
Some(Box::new(MoveNode {
target: idx,
to: Vec2::new(delta.x, delta.y),
relative: true,
}))
}
PointerAction::SettingInput(output) => {
if let Some(PointerTarget::Output(input)) = pointer_target {
Some(Box::new(SetEdge {
edge: Edge { input, output },
}))
} else if self.graph.nodes[output.node].inputs[output.slot].is_some() {
Some(Box::new(DeleteEdge { input: output }))
} else {
None
}
}
PointerAction::SettingOutput(input) => {
if let Some(PointerTarget::Input(output)) = pointer_target {
Some(Box::new(SetEdge {
edge: Edge { input, output },
}))
} else {
None
}
}
};
if let Some(cmd) = cmd {
let cmd_debug = format!("{:#?}", cmd);
match self.cmd_history.push(&mut self.graph, cmd) {
Ok(_) => {}
Err(e) => eprintln!("{}:\n {:#?}", cmd_debug, e),
}
}
self.pointer_state.action = PointerAction::Idle(None); // TODO idle pointer targeting
}
}
}
pub struct PointerState {
pub start: egui::Pos2,
pub position: egui::Pos2,
pub delta: egui::Vec2,
pub action: PointerAction,
}
impl PointerState {
pub fn new() -> Self {
Self {
start: egui::Pos2::ZERO,
position: egui::Pos2::ZERO,
delta: egui::Vec2::ZERO,
action: PointerAction::Idle(None),
}
}
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum PointerAction {
Idle(Option<PointerTarget>),
MovingNode(usize),
SettingInput(SlotIndex),
SettingOutput(SlotIndex),
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum PointerTarget {
Grid,
Node(usize),
Input(SlotIndex),
Output(SlotIndex),
}
#[derive(Debug)]
pub struct EdgeLayout {
pub points: [Vec2; 4],
}
#[derive(Clone, Debug)]
pub struct GraphStyle {
pub grid_spacing: f32,
pub grid_stroke: Stroke,
pub node_frame: Frame,
pub edge_stroke: Stroke,
pub edge_control_offset: f32,
pub slot_radius: f32,
pub slot_spacing: f32,
pub slot_label_margin: f32,
pub slot_fill: Color32,
pub slot_stroke: Stroke,
}
impl GraphStyle {
pub fn from_ui_style(style: &egui::Style) -> Self {
let node_alpha = 192;
let node_bg = if style.visuals.dark_mode {
Color32::from_black_alpha(node_alpha)
} else {
Color32::from_white_alpha(node_alpha)
};
Self {
grid_spacing: 25.0,
grid_stroke: style.visuals.widgets.noninteractive.bg_stroke,
node_frame: Frame::window(style).fill(node_bg),
edge_stroke: style.visuals.widgets.active.fg_stroke,
edge_control_offset: 200.0,
slot_radius: 7.0,
slot_spacing: 40.0,
slot_label_margin: 5.0,
slot_fill: style.visuals.widgets.noninteractive.bg_fill,
slot_stroke: style.visuals.widgets.noninteractive.bg_stroke,
}
}
}
#[derive(Debug)]
pub struct TextLayout {
pub pos: egui::Pos2,
pub galley: Arc<egui::Galley>,
}
#[derive(Debug)]
pub struct GraphLayout {
pub style: GraphStyle,
pub node_rects: HashMap<usize, egui::Rect>,
pub input_positions: HashMap<SlotIndex, Vec2>,
pub output_positions: HashMap<SlotIndex, Vec2>,
pub edges: HashMap<Edge, EdgeLayout>,
pub active_edge: Option<(Vec2, Vec2)>,
pub texts: Vec<TextLayout>,
}
impl GraphLayout {
pub fn layout(
ui: &egui::Ui,
style: &GraphStyle,
pointer: &PointerState,
graph: &Graph,
) -> Self {
let mut layout = Self {
style: style.clone(),
node_rects: Default::default(),
input_positions: Default::default(),
output_positions: Default::default(),
edges: Default::default(),
active_edge: None,
texts: Default::default(),
};
let mut edges = Vec::new();
let kind_font = egui::FontSelection::Style(egui::TextStyle::Heading);
let kind_font = kind_font.resolve(ui.style());
let kind_spacing = ui.style().spacing.item_spacing.y;
let kind_color = ui.visuals().text_color();
let label_font = egui::FontSelection::Style(egui::TextStyle::Body);
let label_font = label_font.resolve(ui.style());
let label_color = ui.visuals().text_color();
for (node_index, node) in graph.nodes.iter() {
let node_kind = &graph.node_kinds.kinds[node.kind];
let mut node_pos = egui::pos2(node.pos.x, node.pos.y);
if pointer.action == PointerAction::MovingNode(node_index) {
node_pos += pointer.position - pointer.start;
}
let node_size = egui::vec2(150., 150.);
let node_rect = egui::Rect::from_min_size(node_pos, node_size);
layout.node_rects.insert(node_index, node_rect);
let text = node_kind.title.to_string();
let kind_font = kind_font.clone();
let wrap_width = f32::MAX;
let galley = ui.fonts().layout(text, kind_font, kind_color, wrap_width);
let kind_height = galley.rect.height() + kind_spacing * 2.0;
layout.texts.push(TextLayout {
pos: egui::pos2(
node_rect.center().x - galley.rect.center().x,
node_pos.y + kind_height - galley.rect.height(),
),
galley,
});
let slot_top = node_pos.y + kind_height + style.slot_spacing;
let slot_left = node_rect.left();
let slot_right = node_rect.right();
for (slot_index, input) in node.inputs.iter().enumerate() {
let slot_y = slot_index as f32 * style.slot_spacing + slot_top;
let position = glam::Vec2::new(slot_left, slot_y);
let slot_index = SlotIndex {
node: node_index,
slot: slot_index,
};
layout.input_positions.insert(slot_index, position);
let text = node_kind.inputs[slot_index.slot].label.to_owned();
let label_font = label_font.clone();
let wrap_width = f32::MAX;
let galley = ui.fonts().layout(text, label_font, label_color, wrap_width);
layout.texts.push(TextLayout {
pos: egui::pos2(
position.x - galley.rect.left()
+ style.slot_radius
+ style.slot_label_margin,
position.y - galley.rect.bottom() + style.slot_radius,
),
galley,
});
if pointer.action == PointerAction::SettingInput(slot_index) {
let pointer = Vec2::new(pointer.position.x, pointer.position.y);
layout.active_edge = Some((position, pointer));
} else if let Some(input) = input {
edges.push(Edge {
input: *input,
output: slot_index,
});
}
}
for (slot_index, output) in node_kind.outputs.iter().enumerate() {
let slot_y = slot_index as f32 * style.slot_spacing + slot_top;
let position = glam::Vec2::new(slot_right, slot_y);
let slot_index = SlotIndex {
node: node_index,
slot: slot_index,
};
layout.output_positions.insert(slot_index, position);
let text = output.label.to_owned();
let label_font = label_font.clone();
let wrap_width = f32::MAX;
let galley = ui.fonts().layout(text, label_font, label_color, wrap_width);
layout.texts.push(TextLayout {
pos: egui::pos2(
position.x
- galley.rect.right()
- style.slot_radius
- style.slot_label_margin,
position.y - galley.rect.bottom() + style.slot_radius,
),
galley,
});
if pointer.action == PointerAction::SettingOutput(slot_index) {
let pointer = Vec2::new(pointer.position.x, pointer.position.y);
layout.active_edge = Some((pointer, position));
}
}
}
for edge in edges.into_iter() {
let input = *layout.input_positions.get(&edge.output).unwrap();
let output = *layout.output_positions.get(&edge.input).unwrap();
let edge_layout = layout.layout_edge(input, output);
layout.edges.insert(edge, edge_layout);
}
layout
}
pub fn layout_edge(&self, input: Vec2, output: Vec2) -> EdgeLayout {
let control_offset = Vec2::new(self.style.edge_control_offset, 0.0);
let mut input_control = input - control_offset;
let mut output_control = output + control_offset;
if output_control.x > input_control.x && output.x < input.x {
let mid = (output_control.x + input_control.x) / 2.0;
output_control.x = mid;
input_control.x = mid;
}
EdgeLayout {
points: [input, input_control, output_control, output],
}
}
pub fn pointer_target(&self, pointer: egui::Pos2) -> PointerTarget {
let test_slots = |positions: &HashMap<SlotIndex, Vec2>| -> Option<SlotIndex> {
for (index, position) in positions.iter() {
let dx = pointer.x - position.x;
let dy = pointer.y - position.y;
let d2 = (dx * dx) + (dy * dy);
let slot_radius2 = self.style.slot_radius.powi(2);
if d2 < slot_radius2 {
return Some(*index);
}
}
None
};
if let Some(index) = test_slots(&self.input_positions) {
return PointerTarget::Input(index);
}
if let Some(index) = test_slots(&self.output_positions) {
return PointerTarget::Output(index);
}
for (node_index, node_rect) in self.node_rects.iter() {
if node_rect.contains(pointer) {
return PointerTarget::Node(*node_index);
}
}
PointerTarget::Grid
}
pub fn paint(&self, rect: egui::Rect, painter: &egui::Painter) {
let style = &self.style;
let grid_spacing = style.grid_spacing;
let grid_pos = rect.left_top().to_vec2() / egui::vec2(grid_spacing, grid_spacing);
let mut grid_pos = grid_pos.floor() * grid_spacing;
while grid_pos.x < rect.right() {
grid_pos.x += grid_spacing;
let start = egui::pos2(grid_pos.x, rect.top());
let end = egui::pos2(grid_pos.x, rect.bottom());
painter.line_segment([start, end], style.grid_stroke);
}
while grid_pos.y < rect.bottom() {
grid_pos.y += grid_spacing;
let start = egui::pos2(rect.left(), grid_pos.y);
let end = egui::pos2(rect.right(), grid_pos.y);
painter.line_segment([start, end], style.grid_stroke);
}
let paint_edge = |layout: &EdgeLayout| {
let points = layout.points.map(|p| p.to_array().into());
let shape = egui::epaint::CubicBezierShape {
points,
closed: false,
fill: egui::Color32::TRANSPARENT,
stroke: style.edge_stroke,
};
painter.add(shape);
};
for (_edge, edge_layout) in self.edges.iter() {
paint_edge(edge_layout);
}
if let Some(active_edge) = self.active_edge {
let layout = self.layout_edge(active_edge.0, active_edge.1);
paint_edge(&layout);
}
for (_node_index, node_rect) in self.node_rects.iter() {
let shape = style.node_frame.paint(*node_rect);
painter.add(shape);
}
let draw_slots = |positions: &HashMap<SlotIndex, Vec2>| {
for (_index, position) in positions.iter() {
let shape = egui::epaint::CircleShape {
center: egui::pos2(position.x, position.y),
radius: style.slot_radius,
fill: style.slot_fill,
stroke: style.slot_stroke,
};
painter.add(shape);
}
};
draw_slots(&self.input_positions);
draw_slots(&self.output_positions);
for text in self.texts.iter() {
painter.add(egui::epaint::TextShape {
pos: text.pos,
galley: text.galley.to_owned(),
underline: Stroke::default(),
override_text_color: None,
angle: 0.0,
});
}
}
}

View File

@ -1,30 +1,30 @@
struct CameraUniform {
eye: vec4<f32>,
vp: mat4x4<f32>,
eye: vec4<f32>;
vp: mat4x4<f32>;
};
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) tan_frame: u32,
[[location(0)]] position: vec3<f32>;
[[location(1)]] tan_frame: u32;
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) position: vec3<f32>,
@location(1) color: vec3<f32>,
[[builtin(position)]] clip_position: vec4<f32>;
[[location(0)]] position: vec3<f32>;
[[location(1)]] color: vec3<f32>;
};
@group(0) @binding(0)
[[group(0), binding(0)]]
var<uniform> camera: CameraUniform;
fn random(seed: u32, salt: f32) -> f32 {
return abs(sin((f32(seed & u32(0x11111)) * 0.7071 + salt) * 78.233));
}
@vertex
[[stage(vertex)]]
fn vs_main(
@builtin(instance_index) mesh_idx: u32,
@builtin(vertex_index) vertex_idx: u32,
[[builtin(instance_index)]] mesh_idx: u32,
[[builtin(vertex_index)]] vertex_idx: u32,
vertex: VertexInput,
) -> VertexOutput {
let world_pos = vertex.position;
@ -38,9 +38,9 @@ fn vs_main(
return out;
}
@fragment
[[stage(fragment)]]
fn fs_main(
frag: VertexOutput,
) -> @location(0) vec4<f32> {
) -> [[location(0)]] vec4<f32> {
return vec4<f32>(frag.color, 1.0);
}

View File

@ -2,33 +2,32 @@
#include skin.wgsl
struct SkinningUniform {
transform: mat4x4<f32>,
src_offset: u32,
dst_offset: u32,
count: u32,
transform: mat4x4<f32>;
src_offset: u32;
dst_offset: u32;
count: u32;
};
@group(0) @binding(0)
[[group(0), binding(0)]]
var<storage,read> skinning_ubo: SkinningUniform;
@group(0) @binding(1)
[[group(0), binding(1)]]
var<storage,write> dst_vertices: SkinnedVertexArray;
@group(0) @binding(2)
[[group(0), binding(2)]]
var<storage,read> src_vertices: SkinnedVertexArray;
@compute
@workgroup_size(64)
[[stage(compute), workgroup_size(64)]]
fn cs_main(
@builtin(global_invocation_id) global_invocation_id: vec3<u32>,
[[builtin(global_invocation_id)]] global_invocation_id: vec3<u32>,
) {
let vertex_index = global_invocation_id.x;
if (vertex_index >= skinning_ubo.count) {
return;
}
let src_index = skinning_ubo.src_offset + vertex_index;
let dst_index = skinning_ubo.dst_offset + vertex_index;
let src_index = skinning_ubo.src_offset + vertex_index;
let dst_index = skinning_ubo.dst_offset + vertex_index;
let ptf = src_vertices.data[src_index].ptf;
let position = ptf.xyz;

View File

@ -1,6 +1,6 @@
struct TangentFrame {
normal: vec3<f32>,
tangent: vec3<f32>
normal: vec3<f32>;
tangent: vec3<f32>;
};
// https://knarkowicz.wordpress.com/2014/04/16/octahedron-normal-vector-encoding/?like_comment=12

View File

@ -1,7 +1,7 @@
struct SkinnedVertex {
ptf: vec4<f32>
ptf: vec4<f32>;
};
struct SkinnedVertexArray {
data: array<SkinnedVertex>
data: array<SkinnedVertex>;
};

View File

@ -268,14 +268,14 @@ impl Renderer {
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
color_attachments: &[wgpu::RenderPassColorAttachment {
view: target_views.output,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: true,
},
})],
}],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: target_views.depth,
depth_ops: Some(wgpu::Operations {

View File

@ -24,7 +24,7 @@ impl DebugPass {
target_info: ViewportInfo,
) -> Self {
// TODO hook into ShaderStore system
let shader = device.create_shader_module(wgpu::include_wgsl!("debug_shader.wgsl"));
let shader = device.create_shader_module(&wgpu::include_wgsl!("debug_shader.wgsl"));
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("DebugPass Pipeline Layout"),
@ -43,11 +43,11 @@ impl DebugPass {
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
targets: &[wgpu::ColorTargetState {
format: target_info.output_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
}],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::LineList,
@ -130,7 +130,7 @@ impl RenderPass for DebugPass {
self.device
.create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
label: Some("DebugPass Render Bundle"),
color_formats: &[Some(self.target_info.output_format)],
color_formats: &[self.target_info.output_format],
depth_stencil: Some(wgpu::RenderBundleDepthStencil {
format: self.target_info.depth_format,
depth_read_only: false, // TODO optimize?

View File

@ -1,26 +1,26 @@
struct CameraUniform {
eye: vec4<f32>,
vp: mat4x4<f32>,
eye: vec4<f32>;
vp: mat4x4<f32>;
};
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) color: vec3<f32>
[[location(0)]] position: vec3<f32>;
[[location(1)]] color: vec3<f32>;
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) position: vec3<f32>,
@location(1) color: vec3<f32>
[[builtin(position)]] clip_position: vec4<f32>;
[[location(0)]] position: vec3<f32>;
[[location(1)]] color: vec3<f32>;
};
@group(0) @binding(0)
[[group(0), binding(0)]]
var<uniform> camera: CameraUniform;
@vertex
[[stage(vertex)]]
fn vs_main(
@builtin(instance_index) mesh_idx: u32,
@builtin(vertex_index) vertex_idx: u32,
[[builtin(instance_index)]] mesh_idx: u32,
[[builtin(vertex_index)]] vertex_idx: u32,
vertex: VertexInput,
) -> VertexOutput {
let world_pos = vertex.position;
@ -32,9 +32,9 @@ fn vs_main(
return out;
}
@fragment
[[stage(fragment)]]
fn fs_main(
frag: VertexOutput,
) -> @location(0) vec4<f32> {
) -> [[location(0)]] vec4<f32> {
return vec4<f32>(frag.color, 1.0);
}

View File

@ -142,11 +142,11 @@ impl MeshPass {
let shader = shader_info.store.get(&shader_info.forward).unwrap();
let targets = &[Some(wgpu::ColorTargetState {
let targets = &[wgpu::ColorTargetState {
format: target_info.output_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})];
}];
let mut pipeline_desc = wgpu::RenderPipelineDescriptor {
label: Some("Opaque MeshPass Pipeline"),
@ -449,7 +449,7 @@ impl RenderPass for MeshPass {
mesh.vertex_count / 64 + 1
};
cmds.dispatch_workgroups(workgroup_num as u32, 1, 1);
cmds.dispatch(workgroup_num as u32, 1, 1);
}
}
}
@ -465,7 +465,7 @@ impl RenderPass for MeshPass {
self.device
.create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
label: Some("Opaque Pass Render Bundle"),
color_formats: &[Some(self.target_info.output_format)],
color_formats: &[self.target_info.output_format],
depth_stencil: Some(wgpu::RenderBundleDepthStencil {
format: self.target_info.depth_format,
depth_read_only: false, // TODO optimize?

View File

@ -83,7 +83,7 @@ impl ShaderStore {
fn load_wgsl(&self, wgsl_source: String) -> Result<wgpu::ShaderModule, ShaderError> {
let shader = self
.device
.create_shader_module(wgpu::ShaderModuleDescriptor {
.create_shader_module(&wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(wgsl_source)),
});

View File

@ -58,10 +58,10 @@ impl WinitViewport {
.unwrap();
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: *surface.get_supported_formats(&adapter).first().unwrap(),
format: surface.get_preferred_format(&adapter).unwrap(),
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Fifo,
present_mode: wgpu::PresentMode::Mailbox,
};
surface.configure(&device, &config);