DougHamil / threeagent

ClojureScript library for building Three.js apps in a reagent-like fashion
MIT License
133 stars 10 forks source link

To be able to access the ref #30

Closed ertugrulcetin closed 2 years ago

ertugrulcetin commented 2 years ago

I'd suggest this kind of implementation (providing ref attr) for users to be able to access the THREE.js objects so they can update other attributes besides those that were given.

(defcomponent :perspective-camera [{:keys [fov aspect near far active ref]
                                    :or {fov 75.0
                                         aspect 1.0
                                         near 0.1
                                         far 2000.0
                                         active true}}]
  (let [cam (three/PerspectiveCamera. fov aspect near far)]
    (set! (.-active cam) active)
    (when ref (ref cam))
    cam))
joinr commented 2 years ago

trying to implement some layout nodes that will space objects out according to bounds. need to compute bounding boxes ahead of time, or on demand. I think this would help implementation.

joinr commented 2 years ago

It looks like the (undocumented, but used in examples) on-added and on-removed callback functions (passed via meta) can be used to get a handle to the nodes though, hmm.

ertugrulcetin commented 2 years ago

@joinr thanks for the on-added info. I think it should be obvious like, with ref or something like that. Is there any known memory leak, like after the diff some objects should be released from the global state?

joinr commented 2 years ago

Is there any known memory leak, like after the diff some objects should be released from the global state?

Not entirely clear to me yet, since I am still learning (and walking through the implementation - I already wrapped some stuff to allow lower level control over the three.js context and learned a bit).

I have this at the moment; basically a function that stores uniquely named nodes in the state and registers on-added/removed methods for a sort of lifecycle management. I haven't done a lot of dynamic addition/removal of the scene graph yet (will). I think the demo projects (particularly tetris) do a decent job of showing some of this stuff, as well as how to organize things (a lot of async loading of assets for instance, like fonts and images or SVGs in my case).

I am really using a chainsaw to cut down a sapling (intentionally) for my use case though :) I had a decent declarative experience wrapping the piccolo2d library on the JVM (for scene setup), and then working through the oop/imperative side for updates and dynamics (no react/reagent hooks). I am learning that it is useful to have the ability for components to reflect on themselves and their children to compute bounds and do things like layout or even simple collisions. The react/reagent model is not obvious to me here, but hooks like on-added/removed and the :instance component type (which is sort of a passthrough for THREE objects to be injected in to the scene) look like viable escape hatches.

DougHamil commented 2 years ago

Thanks for the discussion on this! I like the ref concept as that mirrors the behavior of Reagent and would be more familiar to users than the on-added/on-removed metadata.

@ertugrulcetin I'm afraid you would need to somehow drop the ref from your global state after a refresh, depending on what data changes. Threeagent will drop/recreate an object if any property changes that is not a transformation (scale, position, rotation), or if the component-type itself changes (ie from :box to :sphere).

Additionally, if the component is just dropped from the scene, you would need to delete any references you have to the ref to avoid a memory leak.

This is definitely a pain point I'm familiar with and very recently I've been working on support for user-provided Systems that have this protocol.

The ISystem protocol exposes the on-entity-added and on-entity-removed hooks into an entity's lifecycle.

Here's a simplified example of how I've been using it for a particle system using the Nebula particle system:

(deftype ParticleSystem [local-state]
  ISystem
  (init [_ {:keys [threejs-scene]}]
     (let [system (n/System.)
        renderer (n/MeshRenderer. threejs-scene three)]
       (.addRenderer system renderer)
       (reset! local-state {:system system})))
  (destroy [_ _]
    (.destroy (:system @local-state)))
  (on-entity-added [_ _key ^three/Object3D obj {:keys [on-dead loop?] :as cfg}]
    (let [world-pos (.getWorldPosition obj v1)
          system ^n/System (:system @local-state)
          emitter ^n/Emitter (create-emitter cfg)]
      (.setPosition emitter (.clone world-pos))
      (.addEmitter system emitter)
      (swap! local-state assoc-in [:emitters obj] emitter)
      (if loop?
        (.emit emitter)
        (.emit emitter 1 0.5))
      (when on-dead
        (.addOnEmitterDeadEventListener emitter on-dead))))
  (on-entity-removed [_ _key obj _cfg]
    (when-let [emitter (get-in @local-state [:emitters obj])]
      (.destroy emitter)))
  (tick [_ delta-time]
    (.update ^n/System (:system @local-state)
             delta-time)))

You then provide the system in the :systems map when calling render:

(th/render root dom-el {:systems {:particles (->ParticleSystem (atom {}))}})

And use the :particles keyword in any component to use it:

(defn some-threeagent-fn []
  [:box {:particles {:type :metal-sparks :on-dead #(println "dead")}}])

I've been building a more full-featured game to test out this protocol, and so far I have found it extremely helpful. I plan on open-sourcing and documenting the game itself to better demonstrate how to use it. To give you an idea, here's the system map I'm using for the game right now:

(defn create []
  (let [event-bus (messaging/create-event-bus)]
    {:input (input/create event-bus)
     :camera (camera/create)
     :canvas (canvas/create)
     :particle (particle/create event-bus)
     :combat (combat/create event-bus)
     :animation (animation/create event-bus)
     :attachment (attachment/create event-bus)
     :follow (follow/create event-bus)
     :player (player/create event-bus)
     :weapon (weapon/create event-bus)
     :spawner (spawner/create event-bus)
     :physics (physics/create event-bus)
     :audio (audio/create event-bus)
     :zombie (zombie/create event-bus)
     :entity (entity/create event-bus)}))

For simpler cases, maybe just exposing the on-added/on-removed callbacks as top-level parameters (and actually documenting them :smile: ) would be good enough:

(defn comp-fn []
  [:box {:on-added #(js/console.log "Added" %)
           :on-removed #(js/console.log "Removed" %)}])
joinr commented 2 years ago

Interesting. I rolled an ECS (or CES...haha) years back https://github.com/joinr/spork/blob/master/src/spork/entitysystem/store.clj and have used it for discrete event simulation (and in the case of the aforementioned piccolo2d project, and maybe now threeagent, stored visual components for entities - references to scene nodes). I think the ECS approach is really nice and compatible (look at Zach Oakes https://github.com/oakes/odoyle-rules for another approach to having a reactive rules system to manage state, updates, components, etc...might pair well with threeagent).

By the by, I think better interop with native three is useful (although I am learning three concurrently). I noticed three nodes have properties for Name and userData; this reminds of piccolo's scenegraph, and seems amenable to unifying with extending IMeta to threenodes and storing information in userData.

@DougHamil how are you handling node layout? I think you have something in your physics example where you maintain a parallel system of node info in the form of rigid bodies for the ammo library (this is where I picked up on the add/remove meta). I am trying to implement something like the very simple 2d planar GUI stuff (could be extended to 3d), like declarative notions of over beside shelf column etc. Basically defining a custom node that can apply translations to its children to affect some kind of layout. It is unclear how to do this under the reagent paradigm (although I am hacking through it with the add/remove stuff to at least get access to bounding boxes and the like). My use case is pretty simple for now (align some sprites in a "container" in the scene, based on the container's size and its children's bounds).

DougHamil commented 2 years ago

Interesting. I rolled an ECS (or CES...haha) years back https://github.com/joinr/spork/blob/master/src/spork/entitysystem/store.clj and have used it for discrete event simulation (and in the case of the aforementioned piccolo2d project, and maybe now threeagent, stored visual components for entities - references to scene nodes). I think the ECS approach is really nice and compatible (look at Zach Oakes https://github.com/oakes/odoyle-rules for another approach to having a reactive rules system to manage state, updates, components, etc...might pair well with threeagent).

Yeah, I've definitely found the ECS approach to work pretty well so far, the terminology I've used in Threeagent so far is misaligned with ECS because what should be called "entities" are called "components" (ie defcomponent). This is due to trying to mirror Reagent/React where you deal with "UI Components" rather than "UI Entities". So I'm afraid to outright call it ECS :smile: (also there's certainly no data-orientation happening under the hood).

I'll have to try out O'Doyle and see how it integrates, there are so many cool clj libraries out there!

By the by, I think better interop with native three is useful (although I am learning three concurrently). I noticed three nodes have properties for Name and userData; this reminds of piccolo's scenegraph, and seems amenable to unifying with extending IMeta to threenodes and storing information in userData.

That's an interesting idea, it could help with some of the book-keeping that is so common once you start tracking the ThreeJS objects directly.

@DougHamil how are you handling node layout? I think you have something in your physics example where you maintain a parallel system of node info in the form of rigid bodies for the ammo library (this is where I picked up on the add/remove meta). I am trying to implement something like the very simple 2d planar GUI stuff (could be extended to 3d), like declarative notions of over beside shelf column etc. Basically defining a custom node that can apply translations to its children to affect some kind of layout. It is unclear how to do this under the reagent paradigm (although I am hacking through it with the add/remove stuff to at least get access to bounding boxes and the like). My use case is pretty simple for now (align some sprites in a "container" in the scene, based on the container's size and its children's bounds).

Good question, for physics engine integration it's usually a matter of updating the transformations of the threejs objects so they match the latest physics state after the physics engine tick. The most performant way is to directly update the threejs objects and don't go through Threeagent for these transformations. With this approach your Threeagent scene reflects the initial position/rotation/scale of the object, which is then used to initialize the simulated body in the physics state. Then the physics engine "takes over" the object's transformations. This can be done if you're careful to not have frequent reactive updates to these particular entities in your threeagent scenegraph (otherwise you'll be generating/destroying physics bodies frequently).

The other way, which is less performant due to the garbage being generated, is to take the position/rotation/scale from the physics state and write it to your reactive atom, and then reference those transformations in your threeagent components. Threeagent will update those transformations in-place (it won't destroy and recreate the threejs object every frame), but just be aware that you might be generating lots of temporary data each frame which will need to be collected. This is why I usually recommend using Chrome for my games because it has a concurrent garbage collector which will help with hitching. The benefit of this approach is that your entity transformations are persistent across hot-reloads.

For your particular lay-out use-case, I would recommend going with the latter approach, as I imagine the updates aren't nearly as frequent and you won't be dealing with too many entities. I would therefore run your layout algorithm however you need to, then update your reactive state atom with the new transformations and let threeagent re-position everything. If you need to access the underlying threejs objects for bounding boxes during layout, then go with the on-added/on-removed approach and store a reference to each threejs object created by threeagent.

joinr commented 2 years ago

@DougHamil I actually just successfully (and probably inefficiently, but meh) got declarative layouts working.

The invocation is this:

(defn beside
  "Places r beside l in the plane, affects no vertical transfom."
  [l r]
  (let [lt            (as-three l)
        lbounds       (world-bounds lt)
        lext          (extents lbounds)
        rt            (as-three r)
        rbounds       (world-bounds rt)
        rext          (extents rbounds)]
    [:group
     [:instance {:object lt}] ;;just inject the objects for now.
     [translate [(- (:left rext) (:right lext)) 0 0]
      [:instance {:object rt}]]]))

used in a scene thusly:

    [translate [0 -3 -5]
      [beside
       [:box {:position [-1 -3 0] :dims [2 2 2] :material {:color "red"}}]
       [:box {:position [1  -3 0] :dims [2 2 2] :material {:color "blue"}}]]]

I wrote some helpers like as-three and wrappers around bounding box methods/functions to implement this. The idea is that the node defined in the threeagent subtree basically compiles itself, coercing the children to their three.js objects, then operating on them as needed. Afterward we don't recompute the instances, we just inject them into the computed subtree (this time with translations applied to layout children) as direct :instance components. Seems to work pretty well so far.

joinr commented 2 years ago

hellothree

The blue and red boxes on the bottom are laid out in the plane. I think I need to go to an orthographic projection for this particular experiment though.

DougHamil commented 2 years ago

I have updated the docs with information about accessing the created ThreeJS object instances. Also, in v1.0 there is now a :ref property you can use which is an alias for the :on-added property.

Thank you!