Categories
A piece of Art as big as India

Making my first A-Frame Component

Following Kevin Ngo’s advice, I decided to make a new component to allow me to modify either the landscape, ocean or mountain component’s material’s side property to allow them to be viewed from below. I started by creating a new file, material-side-modifier.js in my /src/js folder:

AFRAME.registerComponent('material-side-modifier', {
 //dependencies: ['yourlandscapecomponent'], // Or wait on an event.

init: function () {
 // Modify material side here.
 }
});

Then I modified my main.js to require my new .js file:

require('aframe');
require('aframe-terrain-model-component');
var extras = require('aframe-extras');
// Register everything.
extras.registerAll();
require('aframe-mountain-component');
require('kframe');
//requiring my first component! https://github.com/substack/browserify-handbook told me how to do this
require('./material-side-modifier.js');

Next I created a new .pug file, terrainModelComponentWithMyComponent.pug in my /src/pug folder, to use my new component:

doctype html
html
 head
 meta(charset='utf-8')
 title A-Frame Terrain Model Component with Panoramic background
 meta(name='description', content='A-Frame Terrain Model Component with Panoramic background')
 script(src='build.js')
 link(href="style.css", rel="stylesheet", media="all")
 body
 a-scene(antialias='true')
 //Assets
 a-assets
 img(id='sky' src='firstPanorama/2016_10_18_FirstPanorama.JPG')
 //Camera
 a-entity(position='0 0 0' rotation='0 0 0')
 a-entity(camera look-controls wasd-controls)
 //Terrain
 a-entity(id='landscape' position='0 100 0' terrain-model='DEM: url(aframe-terrain-model-component/docs/Olympic-Peninsula/data/clipped-envi.bin); texture: url(aframe-terrain-model-component/docs/Olympic-Peninsula/data/olympic-texture.jpg); planeWidth: 287; planeHeight: 151; segmentsWidth: 287; segmentsHeight: 151; zPosition: 50;' material-side-modifier)
 //Sky
 a-sky(src='#sky')
 //Light
 a-entity(light='type: ambient;')

I edited material-side-modifier.js to be as follows:

AFRAME.registerComponent('material-side-modifier', {
 //dependencies: ['yourlandscapecomponent'], // or wait on an event.
 // TODO: add waiting for an event to this instead of dependencies.
 // Thanks to @pookage (http://www.beardeddevelopment.co.uk/) on the A-Frame Slack for pointing me in the correct direction:https://developer.mozilla.org/en/docs/Web/API/GlobalEventHandlers/onload
 //Thanks to @dietrich () for suggesting changing from window.onload = changeMaterialSide; to document.addEventListener(‘DOMContentLoaded’, changeMaterialSide);

// Allow material-side-modifier component default to 2
 schema: {
 type: 'int', default: 2 //just a single-property schema, parse and stringify should be inferred.
 },
 // Update the side property of the material of the component to 2 aka THREE.DoubleSide, see: https://threejs.org/docs/#Reference/Materials/Material
 init: function () {
 console.log("A-Frame and the rest of the components have loaded");
 var object3D = this.el.object3D;
 console.log("Object3D is ", object3D);
 var side = this.data; //should be 2, the default value
 console.log("The value of data (side) is ",side); //why isn't this reporting properly?

//via http://stackoverflow.com/questions/18613295/how-to-set-a-texture-on-a-object3d-child-mesh
 console.log("object3D has: ", object3D.children, " children.");
 for(var i in object3D.children) {
 //for all the children of the landscapeObject3D, change the material side property to THREE.DoubleSide aka 2
 console.log("(Method 1: The current number of sides is", object3D.children[i].material.side);
 object3D.children[i].material.side = THREE.DoubleSide;
 console.log("Method 1: The updated number of sides is", object3D.children[i].material.side);
 }

// via: http://stackoverflow.com/questions/16027131/three-js-how-to-make-double-sided-object
 object3D.traverse( function( node ) {
 console.log("Traversing...");
 if( node.material ) {
 console.log("Method 2: The current number of sides is",node.material.side);
 node.material.side = THREE.DoubleSide;
 node.material.needsUpdate = true;
 console.log("Method 2: The updated number of sides is",node.material.side);
 }
 });
}
});

Accessing the new webpage I just created resulted in the following output:

build.js:70503 A-Frame Version: 0.3.2
build.js:70504 three Version: ^0.76.1
build.js:70505 WebVR Polyfill Version: 0.9.15
build.js:88899 A-Frame and the rest of the components have loaded
build.js:88901 Object3D is THREE.Group
build.js:88903 The value of data (side) is NaN
build.js:88906 object3D has: Array[1] children.
build.js:88916 Traversing...
build.js:38629 THREE.WebGLRenderer 76

Which suggested to me that neither of my loops to change the material properties are functioning as desired. Should I be using update instead of init? Or should I be waiting for a completely different event? Or do I need to specify custom dependencies? I’d really rather not do the latter as I want this component to be able to change the side property of any component that has a material.

After chatting on the A-Frame Slack, I found and filed a nasty bug for a Single Property Schema, and changed my component to use the Multi Property Schema syntax. I also found a silly mistake in my gulpfile.js, which I corrected, so that it watched for all Javascript file changes, rather than just main.js.

I simplified my component to the following:

AFRAME.registerComponent('material-side-modifier', {
 // This component can be used multiple times
 multiple: true,
 // Allow material-side-modifier component a single property schema, of type int, defaulting to 2, aka THREE.DoubleSide, see https://threejs.org/docs/#Reference/Materials/Material.side
 schema: {
 side: {
 type:'int',
 default: 2
 }
 },
 update: function () {
 console.log("Inside of update() of material-side-modifier");
 var side = this.data.side; //should be 2, the default value, all I want to be able to do is material.side = side; - change the side property of the material to
 var object3D = this.el.object3D;

 console.log("Starting traverse of object3D");
 object3D.traverse( function( child ){
 console.log("Traversing...")
 console.log("The current child object is: ", child);
 console.log("The type of the child is", typeof child);
 if ( child instanceof THREE.Group ) {
 console.log("Found a THREE.Group!")
 child.traverse(function(childOfChild) {
 console.log("Traversing the traversing...")
 console.log("The current child of child object is: ", childOfChild);
 console.log("The type of the child is", typeof childOfChild); //how is the mesh object always one away from me?
 if ( childOfChild instanceof THREE.Mesh ) {
 console.log("Found a THREE.Mesh!")
 }
 }
 );
 }
 }
 );
 console.log("Finished traverse of object3D");
 }
});

Then I found this issue on the Three.js Github where the author of three.js (MrDoob) states:

Geometries and Materials are not children.

Which means that the traverse() method of Object3D in three.js doesn’t iterate through the Materials and Geometries of Object3D’s. So I’m never going to get to my material by the above method. What method should I use?

After further discussions on the A-Frame Slack on the #Learning channel, I altered my new component to be as follows:

AFRAME.registerComponent('material-side-modifier', {
 // This component can be used multiple times
 multiple: true,
 // Allow material-side-modifier component a single property schema, of type int, defaulting to 2, aka THREE.DoubleSide, see https://threejs.org/docs/#Reference/Materials/Material.side
 schema: {
 side: {
 type:'int',
 default: 2
 }
 },
 tick: function () {
 var side = this.data.side; //should be 2, the default value, all I want to be able to do is material.side = side; - change the side property of the material to
 var object3D = this.el.object3D;
 if(this.el.getObject3D('terrain')){
 var terrain = this.el.getObject3D('terrain');
 terrain.material.side = THREE.DoubleSide;
 }
 }
});

As can be seen on this demo page, the component finally works, albeit in a way that is custom to the component it alters, the Terrain Model component.