Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Model/View relevance when drawing interactions between parts of the model #244

Closed
MacTuitui opened this issue Jan 12, 2019 · 9 comments
Closed

Comments

@MacTuitui
Copy link
Contributor

The title might not be very clear, but I don't know how to do this (efficiently) in nannou.

Let's say I have a particle system stored in my model (as a vec for example) and I am doing a standard "particles interact when closer than a threshold", so I need to compute the distance between two different elements of the vector. (Such kind of systems might be found for example is Casey Reas' Process series.). So far so good, I just loop once from 0 to n and then from i+1 to n over the vec and use split_at_mut to tell the borrow checker that I'll play nice and not try to modify two elements of the same vector. I'm not sure that is the best way to do this, but that's the only one I found right now...

As far as I've understood the architecture that nannou applies, the modification of the particles is happening in the event function (on a update event) because that's the only way to get a mutable reference to the model.

The problem I have is that I want to display that the interaction is happening: in the draw function, I then need to do the same distance computation. And by doing so, I'm suffering a pretty heavy penalty that makes any thing I do look painfully slow compared to the same thing coded in Processing for example, where I would update and draw the particles in the same loop.

I don't think that memoizing which pairs of particles must be drawn is an pretty solution (and I'm not doing it) but I'm sure there is a smarter way to do this.

There is the obvious (at least for me) solution of allowing ViewFn to take a mutable reference to the model, but this might go against the design guidelines of the project.

What would be the preferred approach to deal with such system?

(For reference, I've been doing generative animations every day for two years and I switched to nannou for 2019. I'm still struggling with the language and the framework, but I'm starting to get it!)

@freesig
Copy link
Collaborator

freesig commented Jan 13, 2019

Hey it's a little hard to tell without a code example but I think what you are looking for is a way to write to a draw buffer in the update function. This is something we a currently working on and will be apart of the v0.9 release. That way you could just add the particles into a buffer during your double loop and it will be displayed when the frame is drawn.
@mitchmindtree can probably add to this but as far as I'm aware this is a limitation in v0.8

@JoshuaBatty
Copy link
Member

I see what you are saying. In Nannou we conceptually treat the update where you model is updated and the view function where you display the current state of the model. Although it might be nice, and easier and quicker sometimes to just have a mutable reference to model in the view function... This can quickly create confusion as to where state is being modified in large programs. The idea behind our approach is that no matter how large the program gets you always know that the state is being updated inside the event functions and that the view function is simply where everything is drawn.

@freesig is correct however if you wanted to draw into a framebuffer, you could do this in the update function and then draw that framebuffer to the screen in the view function.. although this isn't currently supported yet until 0.9 is released.

@MacTuitui would you be able to share a striped back Nannou example file demonstrating the technique your mentioning. This way we can suggest the most idiomatic way of approaching this in nannou 👍

@MacTuitui
Copy link
Contributor Author

Thanks for the comments! I'm glad you guys also seem to be in the same time zone (JST for me)!

Sorry for not attaching this before but here is a simple version of what I am doing now: Gist

There are three points I'm sure there are smarter ways to tackle:

  • how to split the vec to mutate two elements at once
  • how to compute only once the distance between the pairs
  • how to combine all the draw.line calls into one big draw call because it's also very slow

(disclosure: I'm still a complete rust beginner, so there might be simple ways to use iterators there)

On my machine I can't really get to simulate a thousand of points, so obviously I'm doing it wrong :)

@freesig
Copy link
Collaborator

freesig commented Jan 13, 2019

how to split the vec to mutate two elements at once.

I would use just indices and not references. Like this

@mitchmindtree
Copy link
Member

mitchmindtree commented Jan 15, 2019

Here are some tips that might be handy re your specific points:

how to split the vec to mutate two elements at once

Indices is one approach, but in some cases it might also be useful to divide up the Vec itself using std::slice::split_at_mut. Edit: Agh I just noticed you're already aware of this, my bad!

how to compute only once the distance between the pairs

I think the nested loop that you currently have is a fine approach. You shouldn't run into any ownership issues here as you don't need mutable access to the slice in order to read the positions from it and calculate the distances. Some other related functions you might find useful are std::slice::windows for sliding a "window" iterator of arbitrary length over the slice and std::slice::chunks and std::slice::chunks_mut for iterating over consecutive chunks of arbitrary length one at a time.

how to combine all the draw.line calls into one big draw call because it's also very slow

For this you might be able to use draw.polyline (you can see the simple_polyline.rs example as a demo). In order to separate each individual line, you might need to draw a transparent (0 alpha) line between the vertices that you don't actually want joined by a line. One problem you might run into though is that polyline currently only supports mitered line joints so this might make the line ends look a little strange. I agree it would be useful to have a draw.lines alternative for drawing many individual lines a little easier/efficient when drawing 1000+ of them 👍

On my machine I can't really get to simulate a thousand of points

Out of curiosity, are you using draw.eclipse to draw each point? If so, it might be useful to know that the eclipse resolution (the number of lines used to draw the circumference) defaults to 50, so if you have say 10,000 points, you end up submitting 500,000 vertices to the GPU. Try using the .resolution(...) builder method to specify a lower resolution for the eclipses - this might let you get away with a lot more.

One thing I'd like to add to assist with huge numbers of points is an easy to use "mesh" type. The Draw API actually uses one big mesh under the hood, but it also uses a hash map for mapping properties of the vertices together, a Theme for retrieving styling defaults and a nannou::geom::Graph for describing the relative positioning of sets of vertices. A simpler mesh type would be useful for bypassing the overhead of these extra unnecessary steps when you just want to draw loads of some simple shapes. Actually, now that I think of it, I already did this via the draw.mesh() API! See the simple_mesh.rs example to get an idea of how draw.mesh works. The basic idea is that you feed it a giant list of triangles. Each of the nannou::geom::* modules (like eclipse, quad, etc) should provide functions for easily converting shapes into the individual triangles that make them up. You should be able to use these functions for producing the triangles that you need for draw.mesh().tris(triangles_of_all_of_my_shapes). I'm making a mental note that we need some more nice docs and examples of how to use draw.mesh!

W.r.t performance of the draw API in general, we haven't actually done any hardcore profiling just yet as the draw API is still under way. That said, while implementing the Nature of Code examples we noticed many run significantly faster in nannou without even trying than they do in Processing, but maybe that's not so surprising considering the differences in the languages :)

@mitchmindtree
Copy link
Member

Also, like @freesig mentioned you might be interested in checking out the v0.9 branch as there are many changes and API tweaks waiting to land including more fine-grained control over the way you register functions for handling updates, window events, key presses etc.

@mitchmindtree
Copy link
Member

What would be the preferred approach to deal with such system?

Just to add to what @JoshuaBatty and @freesig have mentioned, nannou tries to make a clear distinction between the updating of application state and the presentation of application state in order to encourage a more modular coding approach. That said, like you have mentioned there are some cases in graphical/drawing pipelines where you really want to cache a bit of draw state to avoid redoing some work, and updating that cache might involve mutating a collection of some sort, even though it doesn't really directly relate to your actual application state. In these cases, you might find RefCell useful, or maybe Mutex in the case that the state might be accessed by multiple threads. These types allow for interior mutability (even behind a & immutable reference) by moving rust's ownership guarantee checks from compile time (via compile errors) to runtime (via a panic! with a relevant message).

As a side note (if you're not already aware of the technique!) I'd recommend checking out quadtrees as a potentially big performance boost for the particle distance calculations - Dan Shiffman has a really nice video series on them solving exactly this problem.

@MacTuitui
Copy link
Contributor Author

Thanks a lot for the thorough responses!

I threw away a pretty heavy collection of helpers/custom libraries (that did include quadtrees) to start afresh in nannou/rust, so given my framework of doing a new animation per day, I'm slowly building up things again, but until I understand how things work, I'll be fighting a lot against the borrow checker (at least that's my main issue now). I've been so much spoiled by the forgiving nature of processing/java that it's actually far more rewarding now when I actually get it working with no complains from the compiler. Thanks again for putting this framework to the level where it can deal with users like me and taking the time to answer.

Regarding the decision to not have a mutable reference to the model in the draw function while I agree with your approach I would argue that it might be a choice that could be offered to the user, like you have right now the choice between a sketch (with no model) and an app (with a model), you could also have a "processing-like" app with a draw function (with a mutable model) where I could not need to compute the same distances (actually since the particles move the distances are different but that's another issue) in both event and draw functions. But again I'm not pushing for this (especially if I can eventually go around the restriction if/when I know what I'm doing).

As for the performance aspect of multiple draw calls, I'll do what @mitchmindtree is proposing. On ellipses I've already played with the resolution but there was another issue at play because it did not really change things. I'll try to get a simple example for profiling if I think I'm doing it right (that might be a recurring theme for me this year). I'm not sure however that I'll manage to get the 0.9 branch running giving my lack of knowledge of the rust ecosystem...

While I'm not at the technical level to contribute to the core of nannou, I'm more than willing to provide examples if you find them useful for newcomers to understand the framework and also work on the documentation as I go along the beginner route. For example I think a "use nannou-new to create a empty app/sketch" in the getting started part of the docs might be useful: I did not find obvious for someone not used to cargo that building nannou (and running the examples) is not related at all to building your app/sketch. Or you could have the empty app/sketch directly as a github repo that you would clone then let cargo do its magic.

The plan would be to continue to get familiar with the language and the framework, and hopefully be ready to contribute soon!

@mitchmindtree
Copy link
Member

@MacTuitui, @JoshuaBatty and I were just looking at some of your sketches on your twitter feed - awesome stuff! Are you using nannou for any of these?

We'd love to have a proper chat with you about nannou sometime if you're interested? It would be great to get your thoughts on things like our short-term goals (speeding up compilation with sccache, a render graph API on top of vulkan, etc), long-term goals (travelling with workshops, courses at universities, gantz and the GUI editor), what you'd like to get out of nannou, the things you miss coming from Processing, how your experience has been in general, any rust Qs you might have, etc! If you're interested, feel free to send us an email at contact at mindbuffer.net or join us on slack and we can work out a time to chat :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants