Simulated Muscles

aurellem

Written by:

Robert McIntyre

1 Muscles

Surprisingly enough, terrestrial creatures only move by using torque applied about their joints. There's not a single straight line of force in the human body at all! (A straight line of force would correspond to some sort of jet or rocket propulsion.)

In humans, muscles are composed of muscle fibers which can contract to exert force. The muscle fibers which compose a muscle are partitioned into discrete groups which are each controlled by a single alpha motor neuron. A single alpha motor neuron might control as little as three or as many as one thousand muscle fibers. When the alpha motor neuron is engaged by the spinal cord, it activates all of the muscle fibers to which it is attached. The spinal cord generally engages the alpha motor neurons which control few muscle fibers before the motor neurons which control many muscle fibers. This recruitment strategy allows for precise movements at low strength. The collection of all motor neurons that control a muscle is called the motor pool. The brain essentially says "activate 30% of the motor pool" and the spinal cord recruits motor neurons until 30% are activated. Since the distribution of power among motor neurons is unequal and recruitment goes from weakest to strongest, the first 30% of the motor pool might be 5% of the strength of the muscle.

My simulated muscles follow a similar design: Each muscle is defined by a 1-D array of numbers (the "motor pool"). Each entry in the array represents a motor neuron which controls a number of muscle fibers equal to the value of the entry. Each muscle has a scalar strength factor which determines the total force the muscle can exert when all motor neurons are activated. The effector function for a muscle takes a number to index into the motor pool, and then "activates" all the motor neurons whose index is lower or equal to the number. Each motor-neuron will apply force in proportion to its value in the array. Lower values cause less force. The lower values can be put at the "beginning" of the 1-D array to simulate the layout of actual human muscles, which are capable of more precise movements when exerting less force. Or, the motor pool can simulate more exotic recruitment strategies which do not correspond to human muscles.

This 1D array is defined in an image file for ease of creation/visualization. Here is an example muscle profile image.

basic-muscle.png

Figure 1: 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.

2 Blender Meta-data

In blender, each muscle is an empty node whose top level parent is named "muscles", just like eyes, ears, and joints.

These functions define the expected meta-data for a muscle node.

(in-ns 'cortex.movement)

(def 
  ^{:doc "Return the children of the creature's \"muscles\" node."
    :arglists '([creature])}
  muscles
  (sense-nodes "muscles"))


(defn muscle-profile-image
  "Get the muscle-profile image from the node's blender meta-data."
  [#^Node muscle]
  (if-let [image (meta-data muscle "muscle")]
    (load-image image)))

(defn muscle-strength
  "Return the strength of this muscle, or 1 if it is not defined."
  [#^Node muscle]
  (if-let [strength (meta-data muscle "strength")]
    strength 1))

(defn motor-pool
  "Return a vector where each entry is the strength of the \"motor
   neuron\" at that part in the muscle."
  [#^Node muscle]
  (let [profile (muscle-profile-image muscle)]
    (vec
     (let [width (.getWidth profile)]
       (for [x (range width)]
       (- 255
          (bit-and
           0x0000FF
           (.getRGB profile x 0))))))))

Of note here is motor-pool which interprets the muscle-profile image in a way that allows me to use gradients between white and red, instead of shades of gray as I've been using for all the other senses. This is purely an aesthetic touch.

3 Creating Muscles

(in-ns 'cortex.movement)

(defn movement-kernel
  "Returns a function which when called with a integer value inside a
   running simulation will cause movement in the creature according
   to the muscle's position and strength profile. Each function
   returns the amount of force applied / max force."
  [#^Node creature #^Node muscle]
  (let [target (closest-node creature muscle)
        axis
        (.mult (.getWorldRotation muscle) Vector3f/UNIT_Y)
        strength (muscle-strength muscle)
        
        pool (motor-pool muscle)
        pool-integral (reductions + pool)
        forces
        (vec (map  #(float (* strength (/ % (last pool-integral))))
              pool-integral))
        control (.getControl target RigidBodyControl)]
    ;;(println-repl (.getName target) axis)
    (fn [n]
      (let [pool-index (max 0 (min n (dec (count pool))))
            force (forces pool-index)]
        (.applyTorque control (.mult axis force))
        (float (/ force strength))))))

(defn movement!
  "Endow the creature with the power of movement. Returns a sequence
   of functions, each of which accept an integer value and will
   activate their corresponding muscle."
  [#^Node creature]
    (for [muscle (muscles creature)]
      (movement-kernel creature muscle)))

movement-kernel creates a function that will move the nearest physical object to the muscle node. The muscle exerts a rotational force dependent on it's orientation to the object in the blender file. The function returned by movement-kernel is also a sense function: it returns the percent of the total muscle strength that is currently being employed. This is analogous to muscle tension in humans and completes the sense of proprioception begun in the last post.

4 Visualizing Muscle Tension

Muscle exertion is a percent of a total, so the visualization is just a simple percent bar.

(defn movement-display-kernel
  "Display muscle exertion data as a bar filling up with red."
  [exertion]
     (let [height 20
           width 300
           image (BufferedImage. width height
                                 BufferedImage/TYPE_INT_RGB)
           fill (min (int (* width exertion)) width)]
       (dorun
        (for [x (range fill)
              y (range height)]
          (.setRGB image x y 0xFF0000)))
       image))

(defn view-movement
  "Creates a function which accepts a list of muscle-exertion data and
  displays each element of the list to the screen."
  []
  (view-sense movement-display-kernel))

5 Adding Muscles to the Worm

To the worm, I add two new nodes which describe a single muscle.

worm-with-muscle.png

Figure 2: 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.

(in-ns 'cortex.test.movement)

(defn test-worm-movement
  "Testing movement:
   You should see the worm suspended in mid air and a display on the
   right which shows the current relative power being exerted by the
   muscle. As you increase muscle strength, the bar should fill with
   red, and the worm's upper segment should move.

   Keys:
     h  : increase muscle exertion
     n  : decrease muscle exertion"
  ([] (test-worm-movement false))
  ([record?]
     (let [creature (doto (worm) (body!))

           muscle-exertion (atom 0)
           muscles (movement! creature)
           muscle-display (view-movement)]
       (.setMass
        (.getControl (.getChild creature "worm-11") RigidBodyControl)
        (float 0))
       (world
        (nodify [creature (floor)])
        (merge standard-debug-controls
               {"key-h"
                (fn [_ value]
                  (if value
                    (swap!  muscle-exertion (partial + 20))))
                "key-n"
                (fn [_ value]
                  (if value
                    (swap! muscle-exertion (fn [v] (- v 20)))))})
        (fn [world]

          (let [timer (RatchetTimer. 60)]
            (.setTimer world timer)
            (display-dilated-time world timer))
          (if record?
            (Capture/captureVideo
             world
             (File. "/home/r/proj/cortex/render/worm-muscles/main-view")))
          (light-up-everything world)
          (enable-debug world)
          (set-gravity world (Vector3f. 0 0 0))
          (.setLocation (.getCamera world)
                        (Vector3f. -4.912815, 2.004171, 0.15710819))
          (.setRotation (.getCamera world)
                        (Quaternion. 0.13828252, 0.65516764, 
                                     -0.12370994, 0.7323449)))
        (fn [world tpf]
          (muscle-display
           (map #(% @muscle-exertion) muscles)
           (if record?
             (File. "/home/r/proj/cortex/render/worm-muscles/muscles"))))))))

6 Video Demonstration


YouTube

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

6.1 Making the Worm Muscles Video

(ns cortex.video.magick7
  (:import java.io.File)
  (:use clojure.java.shell))

(defn images [path]
  (sort (rest (file-seq (File. path)))))

(def base "/home/r/proj/cortex/render/worm-muscles/")

(defn pics [file]
  (images (str base file)))

(defn combine-images []
  (let [main-view (pics "main-view")
        muscles (pics "muscles/0")
        targets (map
                 #(File. (str base "out/" (format "%07d.png" %)))
                 (range (count main-view)))]
    (dorun
     (pmap
      (comp
       (fn [[ main-view muscles target]]
         (println target)
         (sh "convert"
             main-view 
             muscles "-geometry" "+320+440" "-composite"
             target))
       (fn [& args] (map #(.getCanonicalPath %) args)))
       main-view muscles targets))))
cd ~/proj/cortex/render/worm-muscles
ffmpeg -r 60 -i out/%07d.png -b:v 9000k -c:v libtheora worm-muscles.ogg

7 Headers

(ns cortex.movement
  "Give simulated creatures defined in special blender files the power
   to move around in a simulated environment."
  {:author "Robert McIntyre"}
  (:use (cortex world util sense body))
  (:import java.awt.image.BufferedImage)
  (:import com.jme3.scene.Node)
  (:import com.jme3.math.Vector3f)
  (:import com.jme3.bullet.control.RigidBodyControl))
(ns cortex.test.movement
  (:use (cortex world util sense body movement))
  (:use cortex.test.body)
  (:import java.io.File)
  (:import java.awt.image.BufferedImage)
  (:import com.jme3.scene.Node)
  (:import (com.jme3.math Quaternion Vector3f))
  (:import (com.aurellem.capture Capture RatchetTimer IsoTimer))
  (:import com.jme3.bullet.control.RigidBodyControl))

8 Source Listing

Author: Robert McIntyre

Created: 2015-04-19 Sun 07:04

Emacs 24.4.1 (Org mode 8.3beta)

Validate