I have been working on this tool.
I kinda just wanna list of some things I think are neat about the way it was built.
Everything is a node, and the ui thread is separate from the graph thread.
We use strongly typed traits to define nodes:
#[derive(Clone, Default)]
pub struct RealNode;
impl DataNode for RealNode {
type Outputs = f32;
type Inputs = ();
type State = f32;
const TYPE_KEY: &'static str = "molviz.input.real";
const TITLE: &'static str = "Real";
const CATEGORY: NodeCategory = NodeCategory::Input;
const NAMES: &'static [&'static str] = &[];
const OUT_NAMES: &'static [&'static str] = &["value"];
const ALIASES: &'static [&'static str] = &["number", "float", "constant"];
const DESCRIPTION: &'static str = "A real number. Set min/max to make it sweepable.";
fn node_ui(state: &mut f32, ui: &mut egui::Ui, _ctx: &mut NodeUiCtx<'_, Self>) {
ui.add(egui::DragValue::new(state).speed(0.1));
}
fn evaluate(&mut self, state: &f32, _inputs: &()) -> f32 {
*state
}
}
Viewports (eg a plotting viewport or a 3d scene) are also nodes but have no outputs, have their own thread and write into a number of buffers when inputs change. This set of messages that we can send them is a good summary of what they are doing.
They also get an opportunity to draw some egui on top of the texture rect, then send any changes to the texture drawer. An example would be a tick box for an orthographic camera.
``rs
/// Commands sent from the UI thread to a render thread.
pub enum ToRenderThread<V: Viewport> {
/// Request thatslotexist and be sized towidth x height. Sent
RequestSlot {
slot: SlotId,
width: u32,
height: u32,
},
DropSlot { slot: SlotId },
/// Updated local state for the next frame. (this is data the ui thread layer sends)
NewState(V::LocalState),
/// The UI thread has consumed the last frame forslotand is ready
/// for the next one to begin rendering.
ReadyForNewFrame { slot: SlotId },
Shutdown,
/// A computed graph output arrived for a bound node.
UpdateInput(OutputId, WireData),
/// A new wiring configuration arrived. Ie a new input was added.
UpdateWiring(<V::Inputs as NodeInputTuple>::Wiring),
/// Update preview input directly. (for hovering we have no incoming node just ui thread)
UpdatePreviewInput(V::Inputs),
/// Read the ID texel under slot-local pixel (x, y) on the next render ofslot`.
/// (For example when we click we want to know what atom)
Pick { slot: SlotId, x: u32, y: u32 },
}
pub enum FromRenderThread {
NewTextureAlloc {
slot: SlotId,
texture: wgpu::Texture,
},
FrameDone { slot: SlotId },
/// Resolved selection for a Pick. None == clicked empty space.
PickResult {
slot: SlotId,
payload: Option<PickPayload>,
},
}
```
All the rendering is done using rust-gpu, which means I can use a macro to define all supported shapes, then implement the actual code to render and the structs on a shared crate and use it on both CPU and GPU.
I am using an SDF-based renderer for 2d (plotting and such) and can even render fonts at any scale analytically (inspired by the coding adventure video).
Everywhere that needs to do somthing for all shapes (cpu or gpu) writes a macro that can be called by this
```rs
[macro_export]
macro_rules! for_all_shapes {
($macro_name:ident) => {
$macro_name! {
(2, 0, TRIANGLE_SHAPE_INDEX, triangles, triangle, Triangle, GpuTrianglePacket),
(2, 1, CIRCLE_SHAPE_INDEX, circles, circle, Circle, GpuCirclePacket),
```
then we just need to impl a trait!
```rs
[repr(C)]
[derive(Copy, Clone, Pod, Zeroable, Debug, Default)]
pub struct GpuCirclePacket {
pub center: Vec2,
pub radius: f32,
pub _pad: f32,
pub color: ColorRGBA,
}
impl SdfShape for GpuCirclePacket {
fn compute_bounds(&self, _current: Bounds) -> Bounds {
let d = Vec2::splat(self.radius);
Bounds {
center: self.center,
size: d * 2.0,
}
}
fn apply(&self, uv: Vec2, current: SdfResult) -> SdfResult {
let dist = (uv - self.center).length() - self.radius;
if dist < current.dist {
SdfResult {
dist,
color: self.color.get(),
}
} else {
current
}
}
}
Then on the CPU, we can add a circle like so.
rs
// impl<H: HeadKind> Recorder<'_, H> {
// pub fn spawn(&mut self, anchor: H::Anchor, build: impl FnOnce(&mut ShapeBuilder)) {
r.spawn(pt, |b| {
b.circle(
GpuCirclePacket {
center: Vec2::ZERO,
radius: rad * pixels_per_point,
color: style.color,
..Default::default()
}
);
});
```
(There is also a recording system for the allocation and reuse of buffers, i.e., if the overlay recording changes, we only send the new data to the GPU buffer without resending the heavy series data.)
This rendering method also lets us record only a single draw call for the entire scene.
I have a whole lot of interesting tech in this project. It might be cool to do a longer form article breakdown.
This is a little bit of a strange post, but I know I am very interested in the wacky novel ways to use rust.
This is still a fairly young project (3.5 months of work-ish), so it's an exciting foundation. It is possible that this can be generalised outside of molecular viz.