rlm@260: #+title: Simulated Muscles rlm@158: #+author: Robert McIntyre rlm@158: #+email: rlm@mit.edu rlm@158: #+description: muscles for a simulated creature rlm@158: #+keywords: simulation, jMonkeyEngine3, clojure rlm@158: #+SETUPFILE: ../../aurellem/org/setup.org rlm@158: #+INCLUDE: ../../aurellem/org/level-0.org rlm@158: rlm@180: rlm@260: * Muscles rlm@260: rlm@306: Surprisingly enough, terrestrial creatures only move by using torque rlm@180: applied about their joints. There's not a single straight line of rlm@180: force in the human body at all! (A straight line of force would rlm@260: correspond to some sort of jet or rocket propulsion.) rlm@180: rlm@278: In humans, muscles are composed of muscle fibers which can contract to rlm@278: exert force. The muscle fibers which compose a muscle are partitioned rlm@278: into discrete groups which are each controlled by a single alpha motor rlm@306: neuron. A single alpha motor neuron might control as little as three rlm@278: or as many as one thousand muscle fibers. When the alpha motor neuron rlm@278: is engaged by the spinal cord, it activates all of the muscle fibers rlm@278: to which it is attached. The spinal cord generally engages the alpha rlm@278: motor neurons which control few muscle fibers before the motor neurons rlm@306: which control many muscle fibers. This recruitment strategy allows rlm@306: for precise movements at low strength. The collection of all motor rlm@278: neurons that control a muscle is called the motor pool. The brain rlm@278: essentially says "activate 30% of the motor pool" and the spinal cord rlm@306: recruits motor neurons until 30% are activated. Since the rlm@278: distribution of power among motor neurons is unequal and recruitment rlm@278: goes from weakest to strongest, the first 30% of the motor pool might rlm@278: be 5% of the strength of the muscle. rlm@260: rlm@306: My simulated muscles follow a similar design: Each muscle is defined rlm@267: by a 1-D array of numbers (the "motor pool"). Each entry in the array rlm@306: represents a motor neuron which controls a number of muscle fibers rlm@278: equal to the value of the entry. Each muscle has a scalar strength rlm@278: factor which determines the total force the muscle can exert when all rlm@278: motor neurons are activated. The effector function for a muscle takes rlm@278: a number to index into the motor pool, and then "activates" all the rlm@278: motor neurons whose index is lower or equal to the number. Each rlm@267: motor-neuron will apply force in proportion to its value in the array. rlm@267: Lower values cause less force. The lower values can be put at the rlm@267: "beginning" of the 1-D array to simulate the layout of actual human rlm@306: muscles, which are capable of more precise movements when exerting rlm@306: less force. Or, the motor pool can simulate more exotic recruitment rlm@306: strategies which do not correspond to human muscles. rlm@260: rlm@260: This 1D array is defined in an image file for ease of rlm@260: creation/visualization. Here is an example muscle profile image. rlm@260: rlm@260: #+caption: A muscle profile image that describes the strengths of each motor neuron in a muscle. White is weakest and dark red is strongest. This particular pattern has weaker motor neurons at the beginning, just like human muscle. rlm@260: [[../images/basic-muscle.png]] rlm@260: rlm@260: * Blender Meta-data rlm@260: rlm@260: In blender, each muscle is an empty node whose top level parent is rlm@260: named "muscles", just like eyes, ears, and joints. rlm@260: rlm@260: These functions define the expected meta-data for a muscle node. rlm@180: rlm@261: #+name: muscle-meta-data rlm@158: #+begin_src clojure rlm@260: (in-ns 'cortex.movement) rlm@158: rlm@317: (def rlm@317: ^{:doc "Return the children of the creature's \"muscles\" node." rlm@317: :arglists '([creature])} rlm@180: muscles rlm@317: (sense-nodes "muscles")) rlm@317: rlm@158: rlm@260: (defn muscle-profile-image rlm@260: "Get the muscle-profile image from the node's blender meta-data." rlm@260: [#^Node muscle] rlm@260: (if-let [image (meta-data muscle "muscle")] rlm@260: (load-image image))) rlm@260: rlm@260: (defn muscle-strength rlm@260: "Return the strength of this muscle, or 1 if it is not defined." rlm@260: [#^Node muscle] rlm@260: (if-let [strength (meta-data muscle "strength")] rlm@260: strength 1)) rlm@260: rlm@260: (defn motor-pool rlm@260: "Return a vector where each entry is the strength of the \"motor rlm@260: neuron\" at that part in the muscle." rlm@260: [#^Node muscle] rlm@260: (let [profile (muscle-profile-image muscle)] rlm@260: (vec rlm@260: (let [width (.getWidth profile)] rlm@260: (for [x (range width)] rlm@260: (- 255 rlm@260: (bit-and rlm@260: 0x0000FF rlm@260: (.getRGB profile x 0)))))))) rlm@260: #+end_src rlm@260: rlm@273: Of note here is =motor-pool= which interprets the muscle-profile rlm@260: image in a way that allows me to use gradients between white and red, rlm@260: instead of shades of gray as I've been using for all the other rlm@260: senses. This is purely an aesthetic touch. rlm@260: rlm@260: * Creating Muscles rlm@261: #+name: muscle-kernel rlm@260: #+begin_src clojure rlm@261: (in-ns 'cortex.movement) rlm@261: rlm@260: (defn movement-kernel rlm@180: "Returns a function which when called with a integer value inside a rlm@191: running simulation will cause movement in the creature according rlm@191: to the muscle's position and strength profile. Each function rlm@191: returns the amount of force applied / max force." rlm@260: [#^Node creature #^Node muscle] rlm@260: (let [target (closest-node creature muscle) rlm@158: axis rlm@158: (.mult (.getWorldRotation muscle) Vector3f/UNIT_Y) rlm@260: strength (muscle-strength muscle) rlm@260: rlm@260: pool (motor-pool muscle) rlm@260: pool-integral (reductions + pool) rlm@296: forces rlm@260: (vec (map #(float (* strength (/ % (last pool-integral)))) rlm@260: pool-integral)) rlm@158: control (.getControl target RigidBodyControl)] rlm@321: ;;(println-repl (.getName target) axis) rlm@158: (fn [n] rlm@260: (let [pool-index (max 0 (min n (dec (count pool)))) rlm@296: force (forces pool-index)] rlm@191: (.applyTorque control (.mult axis force)) rlm@191: (float (/ force strength)))))) rlm@158: rlm@180: (defn movement! rlm@180: "Endow the creature with the power of movement. Returns a sequence rlm@180: of functions, each of which accept an integer value and will rlm@180: activate their corresponding muscle." rlm@158: [#^Node creature] rlm@180: (for [muscle (muscles creature)] rlm@260: (movement-kernel creature muscle))) rlm@260: #+end_src rlm@158: rlm@273: =movement-kernel= creates a function that will move the nearest rlm@260: physical object to the muscle node. The muscle exerts a rotational rlm@306: force dependent on it's orientation to the object in the blender rlm@273: file. The function returned by =movement-kernel= is also a sense rlm@260: function: it returns the percent of the total muscle strength that is rlm@260: currently being employed. This is analogous to muscle tension in rlm@260: humans and completes the sense of proprioception begun in the last rlm@260: post. rlm@260: rlm@260: * Visualizing Muscle Tension rlm@306: Muscle exertion is a percent of a total, so the visualization is just a rlm@260: simple percent bar. rlm@260: rlm@261: #+name: visualization rlm@260: #+begin_src clojure rlm@191: (defn movement-display-kernel rlm@191: "Display muscle exertion data as a bar filling up with red." rlm@191: [exertion] rlm@191: (let [height 20 rlm@191: width 300 rlm@191: image (BufferedImage. width height rlm@191: BufferedImage/TYPE_INT_RGB) rlm@191: fill (min (int (* width exertion)) width)] rlm@191: (dorun rlm@191: (for [x (range fill) rlm@191: y (range height)] rlm@191: (.setRGB image x y 0xFF0000))) rlm@191: image)) rlm@191: rlm@191: (defn view-movement rlm@191: "Creates a function which accepts a list of muscle-exertion data and rlm@191: displays each element of the list to the screen." rlm@191: [] rlm@191: (view-sense movement-display-kernel)) rlm@158: #+end_src rlm@158: rlm@386: * Adding Muscles to the Worm rlm@158: rlm@278: To the worm, I add two new nodes which describe a single muscle. rlm@277: rlm@277: #+attr_html: width=755 rlm@277: #+caption: The node highlighted in orange is the parent node of all muscles in the worm. The arrow highlighted in yellow represents the creature's single muscle, which moves the top segment. The other nodes which are not highlighted are joints, eyes, and ears. rlm@277: [[../images/worm-with-muscle.png]] rlm@277: rlm@283: #+name: test-movement rlm@261: #+begin_src clojure rlm@340: (in-ns 'cortex.test.movement) rlm@340: rlm@283: (defn test-worm-movement rlm@321: "Testing movement: rlm@321: You should see the worm suspended in mid air and a display on the rlm@321: right which shows the current relative power being exerted by the rlm@321: muscle. As you increase muscle strength, the bar should fill with rlm@321: red, and the worm's upper segment should move. rlm@321: rlm@321: Keys: rlm@321: h : increase muscle exertion rlm@321: n : decrease muscle exertion" rlm@283: ([] (test-worm-movement false)) rlm@261: ([record?] rlm@261: (let [creature (doto (worm) (body!)) rlm@261: rlm@261: muscle-exertion (atom 0) rlm@261: muscles (movement! creature) rlm@261: muscle-display (view-movement)] rlm@261: (.setMass rlm@261: (.getControl (.getChild creature "worm-11") RigidBodyControl) rlm@261: (float 0)) rlm@261: (world rlm@261: (nodify [creature (floor)]) rlm@261: (merge standard-debug-controls rlm@261: {"key-h" rlm@261: (fn [_ value] rlm@261: (if value rlm@261: (swap! muscle-exertion (partial + 20)))) rlm@261: "key-n" rlm@261: (fn [_ value] rlm@261: (if value rlm@261: (swap! muscle-exertion (fn [v] (- v 20)))))}) rlm@261: (fn [world] rlm@340: rlm@340: (let [timer (RatchetTimer. 60)] rlm@340: (.setTimer world timer) rlm@340: (display-dilated-time world timer)) rlm@261: (if record? rlm@261: (Capture/captureVideo rlm@261: world rlm@261: (File. "/home/r/proj/cortex/render/worm-muscles/main-view"))) rlm@261: (light-up-everything world) rlm@261: (enable-debug world) rlm@261: (set-gravity world (Vector3f. 0 0 0)) rlm@261: (.setLocation (.getCamera world) rlm@261: (Vector3f. -4.912815, 2.004171, 0.15710819)) rlm@261: (.setRotation (.getCamera world) rlm@261: (Quaternion. 0.13828252, 0.65516764, rlm@278: -0.12370994, 0.7323449))) rlm@261: (fn [world tpf] rlm@261: (muscle-display rlm@261: (map #(% @muscle-exertion) muscles) rlm@261: (if record? rlm@261: (File. "/home/r/proj/cortex/render/worm-muscles/muscles")))))))) rlm@261: #+end_src rlm@261: rlm@340: #+results: test-movement rlm@340: : #'cortex.test.movement/test-worm-movement rlm@340: rlm@261: * Video Demonstration rlm@261: rlm@261: #+begin_html rlm@261:
rlm@261:
rlm@261: rlm@309:
YouTube rlm@261:
rlm@261:

The worm is now able to move. The bar in the lower right displays rlm@261: the power output of the muscle . Each jump causes 20 more motor neurons to rlm@261: be recruited. Notice that the power output increases non-linearly rlm@306: with motor neuron recruitment, similar to a human muscle.

rlm@261:
rlm@261: #+end_html rlm@261: rlm@261: ** Making the Worm Muscles Video rlm@261: #+name: magick7 rlm@261: #+begin_src clojure rlm@261: (ns cortex.video.magick7 rlm@261: (:import java.io.File) rlm@316: (:use clojure.java.shell)) rlm@261: rlm@261: (defn images [path] rlm@261: (sort (rest (file-seq (File. path))))) rlm@261: rlm@261: (def base "/home/r/proj/cortex/render/worm-muscles/") rlm@261: rlm@261: (defn pics [file] rlm@261: (images (str base file))) rlm@261: rlm@261: (defn combine-images [] rlm@261: (let [main-view (pics "main-view") rlm@261: muscles (pics "muscles/0") rlm@261: targets (map rlm@261: #(File. (str base "out/" (format "%07d.png" %))) rlm@261: (range 0 (count main-view)))] rlm@261: (dorun rlm@261: (pmap rlm@261: (comp rlm@261: (fn [[ main-view muscles target]] rlm@261: (println target) rlm@261: (sh "convert" rlm@261: main-view rlm@261: muscles "-geometry" "+320+440" "-composite" rlm@261: target)) rlm@261: (fn [& args] (map #(.getCanonicalPath %) args))) rlm@261: main-view muscles targets)))) rlm@261: #+end_src rlm@261: rlm@261: #+begin_src sh :results silent rlm@261: cd ~/proj/cortex/render/worm-muscles rlm@261: ffmpeg -r 60 -i out/%07d.png -b:v 9000k -c:v libtheora worm-muscles.ogg rlm@261: #+end_src rlm@158: rlm@260: * Headers rlm@260: #+name: muscle-header rlm@260: #+begin_src clojure rlm@260: (ns cortex.movement rlm@260: "Give simulated creatures defined in special blender files the power rlm@260: to move around in a simulated environment." rlm@260: {:author "Robert McIntyre"} rlm@260: (:use (cortex world util sense body)) rlm@260: (:import java.awt.image.BufferedImage) rlm@260: (:import com.jme3.scene.Node) rlm@260: (:import com.jme3.math.Vector3f) rlm@260: (:import com.jme3.bullet.control.RigidBodyControl)) rlm@260: #+end_src rlm@260: rlm@261: #+name: test-header rlm@261: #+begin_src clojure rlm@261: (ns cortex.test.movement rlm@261: (:use (cortex world util sense body movement)) rlm@261: (:use cortex.test.body) rlm@261: (:import java.io.File) rlm@261: (:import java.awt.image.BufferedImage) rlm@261: (:import com.jme3.scene.Node) rlm@283: (:import (com.jme3.math Quaternion Vector3f)) rlm@341: (:import (com.aurellem.capture Capture RatchetTimer IsoTimer)) rlm@261: (:import com.jme3.bullet.control.RigidBodyControl)) rlm@261: #+end_src rlm@261: rlm@341: #+results: test-header rlm@341: : com.jme3.bullet.control.RigidBodyControl rlm@341: rlm@261: * Source Listing rlm@261: - [[../src/cortex/movement.clj][cortex.movement]] rlm@261: - [[../src/cortex/test/movement.clj][cortex.test.movement]] rlm@261: - [[../src/cortex/video/magick7.clj][cortex.video.magick7]] rlm@261: #+html: rlm@261: - [[http://hg.bortreb.com ][source-repository]] rlm@158: rlm@158: * COMMENT code generation rlm@158: #+begin_src clojure :tangle ../src/cortex/movement.clj rlm@261: <> rlm@261: <> rlm@261: <> rlm@261: <> rlm@158: #+end_src rlm@261: rlm@261: #+begin_src clojure :tangle ../src/cortex/test/movement.clj rlm@261: <> rlm@261: <> rlm@261: #+end_src rlm@261: rlm@261: #+begin_src clojure :tangle ../src/cortex/video/magick7.clj rlm@261: <> rlm@261: #+end_src