diff --git a/doc/Understanding-jme3-character-models.md b/doc/Understanding-jme3-character-models.md new file mode 100644 index 0000000..1b20f67 --- /dev/null +++ b/doc/Understanding-jme3-character-models.md @@ -0,0 +1,154 @@ +# Understanding how jME3 handles character models + +(This note is largely based on analysis of Stephen Gold's [Maud](https://github.com/stephengold/Maud)). + +## Dem Bones + +One potential way to vary character models is to adjust the length of their bones. In order to be able to do this, I need to get hold of them. This note documents how to get hold of them in the [jMonkeyEngine](https://jmonkeyengine.org/) 3 API (referenced as 'jME3'). + +Model files (including, but not limited to, `.j3o` files) are [loaded by the AssetManager](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/asset/AssetManager.html#loadModel(com.jme3.asset.ModelKey)) as instances of [Spatial](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/scene/Spatial.html). A spatial can be any sort of scene node -- a building, a tree, a rock, a container. It is not inherently a character and does not inherently have a skeleton or a rig; furthermore, the abstract class Spatial has no methods to extract a skeleton or a rig -- or even, actually, a geometry, material or texture. + +Maud loads models as instances of `maud.model.cgm.LoadedCgm`, which wraps the Spatial. + +[Instance variable declaration](https://github.com/stephengold/Maud/blob/master/src/main/java/maud/model/cgm/Cgm.java#L205): +```java + /** + * root spatial in the MVC model's copy of the C-G model + */ + protected Spatial rootSpatial = null; +``` + +[Instantiation](https://github.com/stephengold/Maud/blob/master/src/main/java/maud/model/cgm/LoadedCgm.java#L292): +```java + this.rootSpatial = Heart.deepCopy(cgmRoot); +``` + +`LoadedCgm` is a subclass of [`Cgm`](https://github.com/stephengold/Maud/blob/master/src/main/java/maud/model/cgm/Cgm.java), where 'CGM' is stated in the documentation to be an acronym for 'Computer Graphics Model'. + +The Cgm class has an instance variable `selectedSkeleton` which is instantiated at the time the Cgm instance is constructed to a new, empty, instance of [SelectedSkeleton](https://github.com/stephengold/Maud/blob/master/src/main/java/maud/model/cgm/SelectedSkeleton.java). So how does the SelectedSkeleton instance (which is instantiated before the `rootSpatial` is set) get to know about the skeleton from the Spatial, which has, inherently, no skeleton? The answer is that is calls the [`countBones()`](https://github.com/stephengold/Maud/blob/master/src/main/java/maud/model/cgm/SelectedSkeleton.java#L148) method of the selectedSkeleton: + +```java + public int countBones() { + int result = 0; + Object selected = find(); + if (selected instanceof Armature) { + result = ((Armature) selected).getJointCount(); + } else if (selected instanceof Skeleton) { + result = ((Skeleton) selected).getBoneCount(); + } + + assert result >= 0 : result; + return result; + } +``` + +Part of the complexity here is backwards compatibility. The class [`Armature`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/anim/Armature.html) is a newer replacement for the older (and now deprecated) class [`Skeleton`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/animation/Skeleton.html); this appears to be part of a major re-engineering of how jME3 handles animation. + +and `countBones()` calls [`find()`](https://github.com/stephengold/Maud/blob/master/src/main/java/maud/model/cgm/SelectedSkeleton.java#L253): + +```java + /** + * Find the selected Armature or Skeleton. + * + * @return the pre-existing instance, or null if none + */ + Object find() { + Object result = find(null); + return result; + } +``` + +which in turn calls [`find(binary[])`](https://github.com/stephengold/Maud/blob/master/src/main/java/maud/model/cgm/SelectedSkeleton.java#L181): + +```java + /** + * Find the selected Armature or Skeleton. + * + * @param storeSelectedSgcFlag if not null, set the first element to true if + * the skeleton came from the selected S-G control or its controlled + * spatial, false if it came from the C-G model root + * @return a pre-existing Armature or Skeleton, or null if none selected + */ + Object find(boolean[] storeSelectedSgcFlag) { + boolean selectedSgcFlag; + Object skeleton = null; + /* + * If the selected S-G control is an AnimControl, SkeletonControl, + * or SkinningControl, use its skeleton, if it has one. + */ + Control selectedSgc = cgm.getSgc().get(); + if (selectedSgc instanceof AnimControl) { + skeleton = ((AnimControl) selectedSgc).getSkeleton(); + } + if (skeleton == null && selectedSgc instanceof SkeletonControl) { + skeleton = ((SkeletonControl) selectedSgc).getSkeleton(); + } + if (skeleton == null && selectedSgc instanceof SkinningControl) { + skeleton = ((SkinningControl) selectedSgc).getArmature(); + } + ... +``` + +And so on. + +I'm going to confess here that coming back to object oriented programming after a decade of concentrating on functional programming, it's frustrating how complicated, messy and repetitious it is. But in this instance I can't help feeling that it would have been less messy if the abstract class `CGM` had a method `getSkeleton()` which by default returned null; and which was overridden in subclasses of `CGM` which represented things which did have skeletons to return their skeletons. + +But this only kicks the 'how to get the skeleton' one step further down the road, to [`Control`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/scene/control/Control.html). `Control` also wraps a `Spatial`, which also isn't instantiated at construction time, and also doesn't have a `getSkeleton()` method. + +To be fair I don't know what proportion of subclasses of `Control` have skeletons, but on the evidence here at least four do; and an overridable instance method on [`AbstractControl`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/scene/control/AbstractControl.html) returning `null`, declared on the [`Control`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/scene/control/Control.html) interface, would have little cost and save a lot of mess. + +As there are now a lot of branches to cover, I'm going to concentrate on the `SkinningControl` one, which *seems* to be the current state of the art. I haven't at this stage investigated how `AssetManager.loadModel(String)` determines which classes to instantiate when loading a model, but I'm going to assume that I can coerce my models to be loaded in a non-deprecated form. + +A [`SkinningControl`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/anim/SkinningControl.html) has a private instance variable `armature`: + +```java + /** + * The armature of the model. + */ + private Armature armature; +``` + +which is instantiated in the constructor: + +```java + /** + * Creates an armature control. The list of targets will be acquired + * automatically when the control is attached to a node. + * + * @param armature the armature + */ + public SkinningControl(Armature armature) { + if (armature == null) { + throw new IllegalArgumentException("armature cannot be null"); + } + this.armature = armature; + this.numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null); + this.jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null); + } +``` + +the `Armature` class has an instance method [`getJointCount()`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/anim/Armature.html#getJointCount()); it also has instance methods [`getJointList()`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/anim/Armature.html#getJointList()), [`getJoint(int)`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/anim/Armature.html#getJoint(int)), and [`getJoint(String)`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/anim/Armature.html#getJoint(java.lang.String)). + +**Note that** `Joint` *seems* to be a name used in the rewrite of the animation system to avoid confusion with the `Bone` in the earlier animation system, and *I think* represents what normal animation rig nomenclature would refer to as a bone. + +However, the `Joint` class has no `length` instance variable. What it has is a `targetGeometry` instance variable which is declared as a [`Geometry`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/scene/Geometry.html). Geometry, in turn, has no dimensions, but has an instance variable `mesh` declared as a [`Mesh`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/scene/Mesh.html). I suspect that it is the Mesh object -- which contains lines, triangles and vertices -- which provides the actual dimensioned objects. + +I don't think, however, that I either need to or should change anything in the Geometry objects themselves. Instead, the Joint object also has three instance variables bound to instances of [`Transform`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/math/Transform.html): + +* `localTransform`; +* `initialTransform`; +* `jointModelTransform`; + +Each of these is private, and has a getter but no setter. It is my hypothesis that to alter the length of the bone, one should make its `localTransform` IV a scaling transform, by calling its `setScale(float)` method. There are alternate signatures to this method, one taking three floats, one for each coordinate, and the other taking an instance of [`Vector3f`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/math/Vector3f.html); it may be that one of these would be preferable because for my purposes I'm only interested in varing the length. + +## Skin + +In order to change the overall skin colour of a character, we have to modify the [`Material`](https://javadoc.jmonkeyengine.org/v3.6.1-stable/com/jme3/material/Material.html) or the `Texture` of the skin; + +Material has a method `setColor(String, ColorRGBA)` which should do the trick; and every Geometry has a Material. The Geometry class is a subclass of Spatial, so if the Spatial returned by `AssetManager.loadNodel(String)` is an instance of Geometry, which I believe it will be, we're *probably* golden. If not, the `Cgm` class has a mechanism for getting the Texture, but not the Material, of the Spatial, and I'll have to explore that route. + +Broadly, I think that the material of the skin should be acted on by the genome (to set overall colour), while the texture may be acted on by some mechanism for handling acquired characteristics (to handle scars, amputations, tattoos, etc). + +## Eyes, hair + +I need to be able to set eye colour, eyebrow shape and colour, hair colour, and baldness from the genome (modulated by age). The hair **style** should not be set from the genome but from acquired characteristics. I'm assuming that hair dyes are not a thing in the in-game culture, otherwise hair colour would also have to be in acquired characteristics. \ No newline at end of file diff --git a/docs/codox/Understanding-jme3-character-models.html b/docs/codox/Understanding-jme3-character-models.html new file mode 100644 index 0000000..4ea4dc0 --- /dev/null +++ b/docs/codox/Understanding-jme3-character-models.html @@ -0,0 +1,116 @@ + +Understanding how jME3 handles character models

Understanding how jME3 handles character models

+

(This note is largely based on analysis of Stephen Gold’s Maud).

+

Dem Bones

+

One potential way to vary character models is to adjust the length of their bones. In order to be able to do this, I need to get hold of them. This note documents how to get hold of them in the jMonkeyEngine 3 API (referenced as ‘jME3’).

+

Model files (including, but not limited to, .j3o files) are loaded by the AssetManager as instances of Spatial. A spatial can be any sort of scene node – a building, a tree, a rock, a container. It is not inherently a character and does not inherently have a skeleton or a rig; furthermore, the abstract class Spatial has no methods to extract a skeleton or a rig – or even, actually, a geometry, material or texture.

+

Maud loads models as instances of maud.model.cgm.LoadedCgm, which wraps the Spatial.

+

Instance variable declaration:

+
    /**
+     * root spatial in the MVC model's copy of the C-G model
+     */
+    protected Spatial rootSpatial = null;
+
+

Instantiation:

+
        this.rootSpatial = Heart.deepCopy(cgmRoot);
+
+

LoadedCgm is a subclass of Cgm, where ‘CGM’ is stated in the documentation to be an acronym for ‘Computer Graphics Model’.

+

The Cgm class has an instance variable selectedSkeleton which is instantiated at the time the Cgm instance is constructed to a new, empty, instance of SelectedSkeleton. So how does the SelectedSkeleton instance (which is instantiated before the rootSpatial is set) get to know about the skeleton from the Spatial, which has, inherently, no skeleton? The answer is that is calls the countBones() method of the selectedSkeleton:

+
    public int countBones() {
+        int result = 0;
+        Object selected = find();
+        if (selected instanceof Armature) {
+            result = ((Armature) selected).getJointCount();
+        } else if (selected instanceof Skeleton) {
+            result = ((Skeleton) selected).getBoneCount();
+        }
+
+        assert result >= 0 : result;
+        return result;
+    }
+
+

Part of the complexity here is backwards compatibility. The class Armature is a newer replacement for the older (and now deprecated) class Skeleton; this appears to be part of a major re-engineering of how jME3 handles animation.

+

and countBones() calls find():

+
    /**
+     * Find the selected Armature or Skeleton.
+     *
+     * @return the pre-existing instance, or null if none
+     */
+    Object find() {
+        Object result = find(null);
+        return result;
+    }
+
+

which in turn calls find(binary[]):

+
    /**
+     * Find the selected Armature or Skeleton.
+     *
+     * @param storeSelectedSgcFlag if not null, set the first element to true if
+     * the skeleton came from the selected S-G control or its controlled
+     * spatial, false if it came from the C-G model root
+     * @return a pre-existing Armature or Skeleton, or null if none selected
+     */
+    Object find(boolean[] storeSelectedSgcFlag) {
+        boolean selectedSgcFlag;
+        Object skeleton = null;
+        /*
+         * If the selected S-G control is an AnimControl, SkeletonControl,
+         * or SkinningControl, use its skeleton, if it has one.
+         */
+        Control selectedSgc = cgm.getSgc().get();
+        if (selectedSgc instanceof AnimControl) {
+            skeleton = ((AnimControl) selectedSgc).getSkeleton();
+        }
+        if (skeleton == null && selectedSgc instanceof SkeletonControl) {
+            skeleton = ((SkeletonControl) selectedSgc).getSkeleton();
+        }
+        if (skeleton == null && selectedSgc instanceof SkinningControl) {
+            skeleton = ((SkinningControl) selectedSgc).getArmature();
+        }
+        ...
+
+

And so on.

+

I’m going to confess here that coming back to object oriented programming after a decade of concentrating on functional programming, it’s frustrating how complicated, messy and repetitious it is. But in this instance I can’t help feeling that it would have been less messy if the abstract class CGM had a method getSkeleton() which by default returned null; and which was overridden in subclasses of CGM which represented things which did have skeletons to return their skeletons.

+

But this only kicks the ‘how to get the skeleton’ one step further down the road, to Control. Control also wraps a Spatial, which also isn’t instantiated at construction time, and also doesn’t have a getSkeleton() method.

+

To be fair I don’t know what proportion of subclasses of Control have skeletons, but on the evidence here at least four do; and an overridable instance method on AbstractControl returning null, declared on the Control interface, would have little cost and save a lot of mess.

+

As there are now a lot of branches to cover, I’m going to concentrate on the SkinningControl one, which seems to be the current state of the art. I haven’t at this stage investigated how AssetManager.loadModel(String) determines which classes to instantiate when loading a model, but I’m going to assume that I can coerce my models to be loaded in a non-deprecated form.

+

A SkinningControl has a private instance variable armature:

+
    /**
+     * The armature of the model.
+     */
+    private Armature armature;
+
+

which is instantiated in the constructor:

+
    /**
+     * Creates an armature control. The list of targets will be acquired
+     * automatically when the control is attached to a node.
+     *
+     * @param armature the armature
+     */
+    public SkinningControl(Armature armature) {
+        if (armature == null) {
+            throw new IllegalArgumentException("armature cannot be null");
+        }
+        this.armature = armature;
+        this.numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
+        this.jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
+    }
+
+

the Armature class has an instance method getJointCount(); it also has instance methods getJointList(), getJoint(int), and getJoint(String).

+

Note that Joint seems to be a name used in the rewrite of the animation system to avoid confusion with the Bone in the earlier animation system, and I think represents what normal animation rig nomenclature would refer to as a bone.

+

However, the Joint class has no length instance variable. What it has is a targetGeometry instance variable which is declared as a Geometry. Geometry, in turn, has no dimensions, but has an instance variable mesh declared as a Mesh. I suspect that it is the Mesh object – which contains lines, triangles and vertices – which provides the actual dimensioned objects.

+

I don’t think, however, that I either need to or should change anything in the Geometry objects themselves. Instead, the Joint object also has three instance variables bound to instances of Transform:

+
    +
  • localTransform;
  • +
  • initialTransform;
  • +
  • jointModelTransform;
  • +
+

Each of these is private, and has a getter but no setter. It is my hypothesis that to alter the length of the bone, one should make its localTransform IV a scaling transform, by calling its setScale(float) method. There are alternate signatures to this method, one taking three floats, one for each coordinate, and the other taking an instance of Vector3f; it may be that one of these would be preferable because for my purposes I’m only interested in varing the length.

+

Skin

+

In order to change the overall skin colour of a character, we have to modify the Material or the Texture of the skin;

+

Material has a method setColor(String, ColorRGBA) which should do the trick; and every Geometry has a Material. The Geometry class is a subclass of Spatial, so if the Spatial returned by AssetManager.loadNodel(String) is an instance of Geometry, which I believe it will be, we’re probably golden. If not, the Cgm class has a mechanism for getting the Texture, but not the Material, of the Spatial, and I’ll have to explore that route.

+

Broadly, I think that the material of the skin should be acted on by the genome (to set overall colour), while the texture may be acted on by some mechanism for handling acquired characteristics (to handle scars, amputations, tattoos, etc).

+

Eyes, hair

+

I need to be able to set eye colour, eyebrow shape and colour, hair colour, and baldness from the genome (modulated by age). The hair style should not be set from the genome but from acquired characteristics. I’m assuming that hair dyes are not a thing in the in-game culture, otherwise hair colour would also have to be in acquired characteristics.

+
\ No newline at end of file diff --git a/docs/codox/cc.journeyman.simulated-genetics.genome.html b/docs/codox/cc.journeyman.simulated-genetics.genome.html index 85107f6..2f3b571 100644 --- a/docs/codox/cc.journeyman.simulated-genetics.genome.html +++ b/docs/codox/cc.journeyman.simulated-genetics.genome.html @@ -1,6 +1,6 @@ -cc.journeyman.simulated-genetics.genome documentation

cc.journeyman.simulated-genetics.genome

lightweight simulation of a genome.

+cc.journeyman.simulated-genetics.genome documentation

cc.journeyman.simulated-genetics.genome

lightweight simulation of a genome.

create-genome

(create-genome)(create-genome father mother)

Create a new genome; if father and mother are passed, the result will comprise bits taken randomly from those genomes.

ethnically-biased-feature-index

macro

(ethnically-biased-feature-index genome start end)

Some feature values are associated with particular ethnicities.

expand-genome

(expand-genome genome)

TODO: write docs

diff --git a/docs/codox/cc.journeyman.simulated-genetics.launcher.html b/docs/codox/cc.journeyman.simulated-genetics.launcher.html index 919b2c3..c7206a0 100644 --- a/docs/codox/cc.journeyman.simulated-genetics.launcher.html +++ b/docs/codox/cc.journeyman.simulated-genetics.launcher.html @@ -1,6 +1,6 @@ -cc.journeyman.simulated-genetics.launcher documentation

cc.journeyman.simulated-genetics.launcher

TODO: write docs

+cc.journeyman.simulated-genetics.launcher documentation

cc.journeyman.simulated-genetics.launcher

TODO: write docs

-main

(-main & args)

Start an app into which generated characters can ultimately be rendered.

app

TODO: write docs

cli-options

I haven’t yet thought out what command line arguments (if any) I need. This is a placeholder.

diff --git a/docs/codox/cc.journeyman.simulated-genetics.makehuman-bridge.html b/docs/codox/cc.journeyman.simulated-genetics.makehuman-bridge.html index 3fcdce6..c59386e 100644 --- a/docs/codox/cc.journeyman.simulated-genetics.makehuman-bridge.html +++ b/docs/codox/cc.journeyman.simulated-genetics.makehuman-bridge.html @@ -1,6 +1,6 @@ -cc.journeyman.simulated-genetics.makehuman-bridge documentation

cc.journeyman.simulated-genetics.makehuman-bridge

Bridge to MakeHuman, in an attempt to use it to generate character models.

+cc.journeyman.simulated-genetics.makehuman-bridge documentation

cc.journeyman.simulated-genetics.makehuman-bridge

Bridge to MakeHuman, in an attempt to use it to generate character models.

NOTE: Currently not under active development. I’ve failed to get this to work, but, even if I succeeded, it would be a very complex and fragile solution.

initialise-makehuman!

(initialise-makehuman! mh-path)

Initialise the local instance of MakeHuman. mh-path should be a valid path to the directory in which MakeHuman is installed, i.e. the directory which contains makehuman.py.

initialize-makehuman!

macro

(initialize-makehuman! mh-path)

For those who don’t know how to spell…

diff --git a/docs/codox/cc.journeyman.simulated-genetics.utils.html b/docs/codox/cc.journeyman.simulated-genetics.utils.html index 223a739..4bb747d 100644 --- a/docs/codox/cc.journeyman.simulated-genetics.utils.html +++ b/docs/codox/cc.journeyman.simulated-genetics.utils.html @@ -1,6 +1,6 @@ -cc.journeyman.simulated-genetics.utils documentation

cc.journeyman.simulated-genetics.utils

TODO: write docs

+cc.journeyman.simulated-genetics.utils documentation

cc.journeyman.simulated-genetics.utils

TODO: write docs

bits-in-genome

Number of bits we’re actually using. NOTE THAT as this implementation is based on Java longs, this number must not be more than 63 or else we’ve a lot of rewriting to do.

create-binary-string-mask

(create-binary-string-mask start end)

Mainly for testing, create a binary string mask with those bits indexed from start (inclusive) to end (exclusive) set, and all others cleared.

create-mask

(create-mask start end)

Create a with those bits indexed from start (inclusive) to end (exclusive) set, and all others cleared.

diff --git a/docs/codox/index.html b/docs/codox/index.html index ff558db..726f400 100644 --- a/docs/codox/index.html +++ b/docs/codox/index.html @@ -1,6 +1,6 @@ -Simulated-genetics 0.1.0-SNAPSHOT

Simulated-genetics 0.1.0-SNAPSHOT

Released under the GNU General Public License,version 2.0 or (at your option) any later version

A lightweight simulation of genetics, intended for use in games only.

Installation

To install, add the following dependency to your project or build file:

[simulated-genetics "0.1.0-SNAPSHOT"]

Topics

Namespaces

cc.journeyman.simulated-genetics.genome

lightweight simulation of a genome.

+Simulated-genetics 0.1.0-SNAPSHOT

Simulated-genetics 0.1.0-SNAPSHOT

Released under the GNU General Public License,version 2.0 or (at your option) any later version

A lightweight simulation of genetics, intended for use in games only.

Installation

To install, add the following dependency to your project or build file:

[simulated-genetics "0.1.0-SNAPSHOT"]

Topics

Namespaces

cc.journeyman.simulated-genetics.launcher

TODO: write docs

Public variables and functions:

cc.journeyman.simulated-genetics.makehuman-bridge

Bridge to MakeHuman, in an attempt to use it to generate character models.

Public variables and functions:

cc.journeyman.simulated-genetics.utils

TODO: write docs

diff --git a/docs/codox/intro.html b/docs/codox/intro.html index 993d8e1..87eeea3 100644 --- a/docs/codox/intro.html +++ b/docs/codox/intro.html @@ -1,6 +1,6 @@ -Introduction to simulated-genetics

Introduction to simulated-genetics

+Introduction to simulated-genetics

Introduction to simulated-genetics

simulated-genetics

A clojure library (OK, at this moment it’s an app, but that’s during development only) to generate character models for games, such that characters who are represented as related to one another will have systematically similar appearance, as creatures of natural species (including humans) do. This is specifically NOT simulating genetics on any deep or quasi-scientific level, just experimenting to see how adequate a solution can be achieved with simple code and limited data.

Part of The Great Game project.