Third-Person Planet Exploration with Three.js

I should share my experiments more often on this blog, and maybe not only the 3D javascript game development that I’m just having fun with. But anyway, here is the last one in date : a third person planet exloration thing.

It’s not exactly “work in progress” since I don’t really plan on improving any of it from here, so just consider this work bare and unfinished. You can try it out here : battle-royale.webmaestro.fr.

3D models of trees

The 3D models used, such as the trees and stones, are from Poly by Google.

The planet is generated when page loads. Noise is applied to the shere vertices length to create the terrain, and “biomes” (materials and 3D models) are set according to the elevation and latitude.

3D Planet from a Noise Sphere

Controls are the classic W, A, S, D and mouse. I had to adapt the “third-person” logic to rotate rather than translate over space.

There is a day and night cycle that depends on where the player is positionned on the globe. The sun and the moon are casting light on opposite sides while turning around.

Day and Night Cycle

The water is… just ugly. There is no collision detection. And the character is a simple cone.

Oh, and there is no server to make it a multi-player shooter, even though that was the ispiration. The idea came when a friend showed me the very entertaining Fortnite. We thought it would be fun to turn this “Battle Royale” island into a planet. Instead of a “storm” shrinking toward the gathered players, we could simply reduce the radius of the spherical terrain… That was the concept.

Maybe I could post details about the code if whoever is interested. In the meantime I have other things to focus on !

3D Noise Sphere Geometry with Three.js

This extended Three.js geometry applies noise elevation over a sphere.

class NoiseSphereGeometry extends THREE.SphereGeometry {
    constructor(radius, widthSegments, heightSegments, {seed, noiseWidth, noiseHeight}) {
        super(radius, widthSegments, heightSegments);
        const getNoise = (vertice) => ImprovedNoise.noise(
                seed + vertice.x / noiseWidth,
                seed + vertice.y / noiseWidth,
                seed + vertice.z / noiseWidth
            ),
            noiseMap = this
                .vertices
                .map(getNoise),
            noiseMax = Math.max(...noiseMap),
            noiseMin = -Math.min(...noiseMap);
        for (const v in this.vertices) {
            if (noiseMap[v] > 0) {
                this
                    .vertices[v]
                    .elevation = noiseMap[v] / noiseMax;
            } else {
                this
                    .vertices[v]
                    .elevation = noiseMap[v] / noiseMin;
            }
            this
                .vertices[v]
                .multiplyScalar(1 + this.vertices[v].elevation * noiseHeight / radius);
        }
    }
}

Make sure to import the ImprovedNoise function from the Three.js examples.

<script src="three/examples/js/ImprovedNoise.js"></script>

Sass Compiler for WordPress

It uses the scssphp Compiler.

Installation

  1. Download the latest release
  2. Unzip it into your wp-content/plugins directory
  3. Activate the plugin in WordPress

Compiler

This first page allows you to write and compile Sass. The resulting stylesheet is automatically enqueued.

Compiler Page

Variables

This second page lists all registered variables, and allows you to edit their values upon compiling.

Variables Page after configuration

PHP Configuration

add_filter( 'sass_configuration', 'my_sass_config' );
function my_sass_config( $defaults ) {
  return array(
    'variables' => array( 'sass/_variables.scss' ),
    'imports'   => array( 'sass/bootstrap.scss', 'sass/_theme.scss' )
  );
}

Configuration of the plugin is optional, but you should at least register your variables if you are using a CSS framework.

Paths to Sass files are relative to the theme directory.

Use the filter 'sass_configuration' to return your configurations array.

  • variables(array)
    In order to list and edit your Sass variables on the plugin dedicated page, it is necessary to register their “definition” file(s). It is assumed that those files’ only role is to declare variables and their values.
  • imports(array)
    It will prepend the code to compile with an @import directive for each listed file. Useful to compile dependencies, and to “hide” them from the Compiler page. It is necessary to hit the “Compile” button once for these imports to be compiled.
  • cache(string)
    Allows you to define the path to your cache directory. This directory has to be writable (0755). The default cache directory path is wp-content/cache.
  • search(boolean)
    Wether or not to display the “Search Variables” filter box. It can come handy if you have a lot of variables. Default is true.

Once registered, you can access any variables with the function sass_get( 'my-variable' );, or override its value (upon what has been set on the Variables page) with sass_set( 'my-variable', 'my-value' ).

It is also possible to use @import directives, as well as any Sass language features, straight from the Compiler page.

Enqueuing external Sass

Out of the main stylesheet, simply use the WordPress wp_enqueue_style function to enqueue separated Sass files.

add_action( 'wp_enqueue_scripts', 'my_other_sass_enqueue' );
function my_other_sass_enqueue() {
  wp_enqueue_style( 'my-other-handle', get_template_directory_uri() . '/my-other-file.scss', array( 'wm-sass' ) );
}

It will be compared to the cached version, compiled if changes occurred, and the resulting stylesheet will be enqueued.
Don’t forget to set the main Sass stylesheet handle 'wm-sass' as a dependency (… if it is one).

LESS Compiler for WordPress

It uses the Less.php Compiler.

Installation

  1. Download the latest release
  2. Unzip it into your wp-content/plugins directory
  3. Activate the plugin in WordPress

Compiler

This first page allows you to write and compile LESS. The resulting stylesheet is automatically enqueued.

Compiler page

Variables

This second page lists all registered variables, and allows you to edit their values upon compiling.

Variables Page after configuration

PHP Configuration

add_filter( 'less_configuration', 'my_less_config' );
function my_less_config( $config ) {
  $my_variables = array( 'less/variables.less' );
  $my_imports = array(
    'less/bootstrap.less',
    'less/theme.less'
  );
  return array_merge_recursive( $config, array(
    'variables' => $my_variables,
    'imports'   => $my_imports
  ) );
}

Configuration of the plugin is optional, but you should at least register your variables if you are using a CSS framework.

Paths to LESS files are relative to the theme directory.

Use the filter 'less_configuration' to return your configurations array.

  • variables(array)
    In order to list and edit your LESS variables on the plugin dedicated page, it is necessary to register their “definition” file(s). It is assumed that those files’ only role is to declare variables and their values.
  • imports(array)
    It will prepend the code to compile with an @import directive for each listed file. Useful to compile dependencies, and to “hide” them from the Compiler page. It is necessary to hit the “Compile” button once for these imports to be compiled.
  • cache(string)
    Allows you to define the path to your cache directory. This directory has to be writable (0755). The default cache directory path is wp-content/cache.
  • search(boolean)
    Wether or not to display the “Search Variables” filter box. It can come handy if you have a lot of variables. Default is true.

Once registered, you can access any variables with the function less_get( 'my-variable' );, or override its value (upon what has been set on the Variables page) with less_set( 'my-variable', 'my-value' ).

It is also possible to use @import directives, as well as any LESS language features, straight from the Compiler page.

Enqueuing external LESS

Out of the main stylesheet, simply use the WordPress wp_enqueue_style function to enqueue separated LESS files.

add_action( 'wp_enqueue_scripts', 'my_other_less_enqueue' );
function my_other_less_enqueue() {
  wp_enqueue_style( 'my-other-handle', get_template_directory_uri() . '/my-other-file.less', array( 'wm-less' ) );
}

It will be compared to the cached version, compiled if changes occurred, and the resulting stylesheet will be enqueued.
Don’t forget to set the main LESS stylesheet handle 'wm-less' as a dependency (… if it is one).

Floating Labels for Bootstrap 3

As I’m working quite a lot with Bootstrap these days, I wrote a jQuery plugin to apply a “floating” effect on the framework’s horizontal forms labels.

Plugin

The jQuery plugin is very simple. Here is the full code.

(function ($) {
  'use strict';
  $.fn.bsPeekabooLabel = function () {
    var input = $(this),
      control = input.closest('[class*="col-"]'),
      label = control.siblings('.control-label'),
      show = false,
      place = function (d) {
        var m;
        if ($(control).css('float') === 'left') {
          m = '0 0 0 ' + label.outerWidth() + 'px';
          label.animate({ margin: show ? 0 : m, opacity: show ? 1 : 0 }, d);
          control.animate({ margin: m }, d);
        } else {
          label.animate({ margin: 0, opacity: 1 }, d);
          control.animate({ margin: show ? label.outerHeight() + 'px 0 0' : 0 }, d);
        }
      };
    label.css({ position: 'absolute' });
    place(0);
    input.keyup(function () {
      if (show === !input.val()) {
        show = !show;
        place(400);
      }
    });
    $(window).resize(function () { place(0); });
  };
}(jQuery));

How To Use

First, make sure your horizontal form respects Bootstrap’s HTML structure, and that your inputs have a placeholder attribute. Then, target each input you want to apply the effect to.

$('.form-control[placeholder]', '.form-horizontal').each(function () {
  $(this).bsPeekabooLabel();
});

WordPress Settings Framework for Options Pages

Settings are really useful to provide an easy configuration of themes and plugins to our users within their administration panel. But the creation of options pages often ends up in a messy and repetitive use of the great WordPress Settings API.

Considering generic form fields, I wrote a class to clean and simplify the process. It’s something light that shall be used on the admin side.

https://github.com/WebMaestroFr/wm-settings

Example

// Define the page
$my_page = create_settings_page(
  'my_page_id',
  __( 'My Page' ),
  array(
    'title' => __( 'My Menu' )
  ),
  array(
    'my_setting_id' => array(
      'title'       => __( 'My Setting' ),
      'description' => __( 'This is my section description.' ),
      'fields'      => array(
        'my_option_name' => array(
          'label'        => __( 'My Option' ),
          'description'  => __( 'This is my field description.' )
        )
      )
    )
  )
);
// Access the values
$my_value = get_setting( 'my_setting_id', 'my_option_name' );
The option page generated with our really basic example.

Installation

  1. Download the last release
  2. Unzip it into your theme or plugin
  3. require_once( 'path/to/wm-settings/wm-settings.php' );

Documentation

Create an Options Page and its Menu

$page = create_settings_page( $page_id, $page_title, $menu, $fields, $args );

  1. $page_id(string) A unique identifier
  2. $page_title : (string) A title for your page
  3. $menu : (array) (Optional) An array of menu parameters. Set to false if you don’t want to display any page.
    • parent‘ : (string) The slug name for the parent menu. Use false to create a top level menu item.
      Default : ‘themes.php’
    • title‘ : (string) The text to be used for the menu.
      Default : value of $page_title
    • capability‘ : (string) The capability required for this menu to be displayed to the user.
      Default : ‘manage_options’
    • icon_url‘ : (string) (for top level menu item) The icon for this menu.
      Default : ‘dashicons-admin-generic’
    • position‘ : (integer) (for top level menu item) The position in the menu order this menu should appear.
      Default : bottom of menu structure
  4. $fields : (array) (Optional) An array of sections and fields (See Settings Sections and Options Fields).
  5. $args : (array) (Optional) An array of miscellaneous arguments.
    • tabs‘ : (boolean) Whether to display the different sections as “tabs” or not. There must be several sections, and they must have a title.
      Default : false
    • submit‘ : (string) Text of the submit button.
      Default : ‘Save Settings’
    • reset‘ : (string) Text of the reset button. Set to false to disable the reset button.
      Default : ‘Reset Settings’
    • description‘ : (string) Page description.
    • updated‘ : (string) Message of the success notice. Set to false to disable the notice.
      Default : ‘Settings saved.’
// A top level page
$my_top_page = create_settings_page(
  'my_top_level_page',
  __( 'My Top Level Page' ),
  array(
    'parent'   => false,
    'title'    => __( 'Top Level Menu' ),
    'icon_url' => 'dashicons-admin-generic',
    'position' => '63.3'
  ),
  array(
    'my_standard_section' => array(
      'title'  => __( 'Standard' ),
      'description' => __( 'My section description.' ),
      'fields' => array(
        'my_input'    => array(
          'label' => __( 'Input example' )
        ),
        'my_checkbox' => array(
          'type'  => 'checkbox',
          'label' => __( 'Checkbox example' )
        ),
        'my_textarea' => array(
          'type'  => 'textarea',
          'label' => __( 'Textarea example' )
        )
      )
    )
  ),
  array(
    'tabs'        => true,
    'submit'      => __( 'My Submit' ),
    'reset'       => __( 'My reset' ),
    'description' => __( 'My page description.' ),
    'updated'     => __( 'My success message !')
  )
);
// And a sub-page
$my_sub_page = create_settings_page(
  'my_sub_page',
  __( 'My Sub Page' ),
  array(
    'parent' => 'my_top_level_page',
    'title'  => __( 'Sub Level Menu' )
  )
);
Top level page after blank submit

Apply Settings Sections and Options Fields

$page->apply_settings( $settings );

Append sections and fields to a form, with an associative array where each setting is a section, and each option is a field. Each key of $settings defines a new setting id.

  • title‘ : (string) (Optional) Section title
  • description‘ : (string) (Optional) Section description
  • fields‘ : (array) (Optional) An array of options field declarations.
    Each key of this new array defines a new option name.

    • type‘ : (string) (Optional) Field type (checkbox, textarea, radio, select, multi, media, action, color or any valid HTML5 input type attribute).
      Default : ‘text’
    • label‘ : (string) (Optional) Field label. Use false to hide the label column on this field.
    • description‘ : (string) (Optional) Field description.
    • default‘ : (string) (Optional) Option’s default value.
    • sanitize‘ : (callback) (Optional) A function to apply in place of the default sanitation of this field’s type. Receive the input value and the option name as parameters, and is expected to return a properly sanitised value.
    • attributes‘ : (array) (Optional) An array( 'my_attribute' => 'my_value', ... ) of HTML attributes. Useful for placeholder or pattern for instance.
    • options‘ : (array) (Optional) Only for radio, select and multi field types, an array( 'my_value' => __( 'My Label' ), ... ) of predefined values.
    • action‘ : (callback) (Optional) Only for action field type. Expect a response sent with either wp_send_json_success (success) or wp_send_json_error (failure), where $data (Optional) is either the message to display (string), or array( 'reload' => true ) if the page needs to reload on action’s success.
$my_top_page->apply_settings( array(
  'my_formatted_section' => array(
    'title'  => __( 'Formatted' ),
    'fields' => array(
      'my_email'  => array(
        'type'  => 'email',
        'label' => __( 'Email example' )
      ),
      'my_url'    => array(
        'type'  => 'url',
        'label' => __( 'URL example' )
      ),
      'my_number' => array(
        'type'  => 'number',
        'label' => __( 'Number example' )
      )
    )
  ),
  'my_multi_section'    => array(
    'title'  => __( 'Multiple Options' ),
    'fields' => array(
      'my_radio'  => array(
        'type'    => 'radio',
        'label'   => __( 'Radio example' ),
        'options' => array(
          'one'   => __( 'First option'),
          'two'   => __( 'Second option'),
          'three' => __( 'Third option')
        )
      ),
      'my_select' => array(
        'type'    => 'select',
        'label'   => __( 'Select example' ),
        'options' => array(
          'one'   => __( 'First option'),
          'two'   => __( 'Second option'),
          'three' => __( 'Third option')
        )
      ),
      'my_multi'  => array(
        'type'    => 'multi',
        'label'   => __( '"Multi" example' ),
        'options' => array(
          'one'   => __( 'First option'),
          'two'   => __( 'Second option'),
          'three' => __( 'Third option')
        )
      )
    )
  )
) );
Formatted and multiple fields, sections displayed as tabs

Apply settings anywhere, but in order to sanitise the data properly and to run the eventual callbacks, do it before ‘admin_init‘ is hooked.

$my_sub_page->apply_settings( array(
  'my_advanced_section' => array(
    'title'  => __( 'Advanced' ),
    'fields' => array(
      'my_media'  => array(
        'type'  => 'media',
        'label' => __( 'Media Example' )
      ),
      'my_color'  => array(
        'type'  => 'color',
        'label' => __( 'Color Example' )
      ),
      'my_action' => array(
        'type'        => 'action',
        'label'       => __( 'Action Example' ),
        'description' => __( 'Will call the PHP function "do_my_action" with AJAX.'),
        'action'      => 'do_my_action'
      )
    )
  ),
  'my_custom_section'   => array(
    'title'  => __( 'Custom' ),
    'fields' => array(
      'my_custom'   => array(
        'label'       => false,
        'description' => __( 'Will be sanitized through the PHP function "do_my_sanitation".'),
        'default'     => __( 'DEFAULT' ),
        'attributes'  => array(
          'style'   => 'font-family: "Comic Sans MS";',
          'pattern' => '[A-Z]*'
        ),
        'sanitize'    => 'do_my_sanitation'
      )
    )
  )
) );

function do_my_action() {
  // If error
  wp_send_json_error( __( 'Error !' ) );
  // If success
  wp_send_json_success( __( 'Success !' ) );
  // If the page needs to reload
  wp_send_json_success( array(
    'reload'  => true,
    'message' => __( 'This message is only displayed if "reload" => false.' )
  ) );
}

function do_my_sanitation( $input, $name ) {
  return sanitize_text_field( strtoupper( $input ) );
}
Advanced and Custom Fields

Get the values

Every setting is recorded as an array of options values. An easier way to get the data back, from anywhere in the code, is to use the function get_setting( $setting_id, $option_name );.

$my_attachment_id = get_setting( 'my_advanced_section', 'my_media' );
// ... is the same than :
$my_setting = get_option( 'my_advanced_section' );
$my_attachment_id = $my_setting['my_media'];

Callback after settings are updated

The class hooks actions tagged ‘(page_id)_settings_updated‘ when settings are updated.

add_action( 'my_top_level_page_settings_updated', 'do_my_page_callback' );
function do_my_page_callback() {
  // All settings of my_top_level_page have been updated.
}

You can also use the ‘update_option_(setting_id)WordPress action that will apply on every section update.

add_action( 'update_option_custom_fields_section', 'do_my_section_callback' );
function do_my_section_callback() {
  $my_custom_options = get_setting( 'my_custom_section' );
  // All options of custom_fields_section have been updated.
}

Custom notices

You can display custom notices on the options pages.

$my_page->add_notice( __( 'My info message.') );
$my_page->add_notice( __( 'My updated message.'), 'updated' );
$my_page->add_notice( __( 'My warning message.'), 'warning' );
$my_page->add_notice( __( 'My error message.'), 'error' );

Allowed types are info (default), updated, warning and error.

Custom notices

What do you think ?

I’ve been working on and using this with fun. Feel free to contact me for questions, feedback or suggestions. And you can find all of it, fork and contribute on GitHub. Peace out.

A jQuery Plugin to Fade In and Out with a Vertical Slide

JQuery’s .show() and .hide() effects are useful, but I’m not cool with their width resizing.
This plugin allows you to show and hide elements with a vertical slide and a fading effect.

You can use the .showDown(), .hideUp() and .verticalFade() functions the same way you would use jQuery’s .fadeIn(), .fadeOut() or .fadeToggle() effects.

$('#trigger-example').click(function () {
  $('#example').verticalFade({ duration: 'slow' });
});

https://gist.github.com/WebMaestroFr/9316374

Plugin

The jQuery plugin is very simple. Here is the full code.

(function ($) {
  var getUnqueuedOpts = function (opts) {
    return {
      queue: false,
      duration: opts.duration,
      easing: opts.easing
    };
  };
  $.fn.showDown = function (opts) {
    opts = opts || {};
    $(this).hide().slideDown(opts).animate({ opacity: 1 }, getUnqueuedOpts(opts));
  };
  $.fn.hideUp = function (opts) {
    opts = opts || {};
    $(this).show().slideUp(opts).animate({ opacity: 0 }, getUnqueuedOpts(opts));
  };
  $.fn.verticalFade = function (opts) {
    opts = opts || {};
    if ($(this).is(':visible')) {
      $(this).hideUp(opts);
    } else {
      $(this).showDown(opts);
    }
  };
}(jQuery));

Basic Collisions Detection, RayCasting with Three.Js

I thought I would have to use a physics engine (like Cannon.Js or Ammo.Js), but Three.Js on its own is enough to sort us out with collisions, thanks to its Raycaster’s .intersectObjects() method.

This post follows a previous one, about setting up a scene and basic character controls with Three.Js, that maybe you should read if you need to understand a bit more those Character and World classes I’m playing with here.

Using the RayCaster

Even after a little read about raycasting, I’m not quite sure I can define its concept properly… But let’s put it like this : from one origin (the very center position of our character’s Object3D), we’re going to spread vectors in every direction we’re able to move. For each one of those “rays“, we’ll be able to test if it intersects with any given mesh, and if so, to disable any move in that direction.

Collecting the obstacles

So first of all, we need to collect every obstacle in an array : all the meshes that we’re not supposed to cross.

var World = Class.extend({
  /* ... */
  getObstacles: function () {
    'use strict';
    return this.obstacles.concat(this.walls);
  }
});

Testing and prevent collisions

Our little character motions are based on its .direction vector. Now by testing every possible direction (with the rays), we’ll be able to update that vector for it not to drive us into an obstacle.

var Character = Class.extend({
  // Class constructor
  init: function (args) {
    /* ... */
    // Set the character modelisation object
    this.mesh = new THREE.Object3D();
    /* ... */
    // Set the rays : one vector for every potential direction
    this.rays = [
      new THREE.Vector3(0, 0, 1),
      new THREE.Vector3(1, 0, 1),
      new THREE.Vector3(1, 0, 0),
      new THREE.Vector3(1, 0, -1),
      new THREE.Vector3(0, 0, -1),
      new THREE.Vector3(-1, 0, -1),
      new THREE.Vector3(-1, 0, 0),
      new THREE.Vector3(-1, 0, 1)
    ];
    // And the "RayCaster", able to test for intersections
    this.caster = new THREE.Raycaster();
  },
  // Test and avoid collisions
  collision: function () {
    'use strict';
    var collisions, i,
      // Maximum distance from the origin before we consider collision
      distance = 32,
      // Get the obstacles array from our world
      obstacles = basicScene.world.getObstacles();
    // For each ray
    for (i = 0; i < this.rays.length; i += 1) {
      // We reset the raycaster to this direction
      this.caster.set(this.mesh.position, this.rays[i]);
      // Test if we intersect with any obstacle mesh
      collisions = this.caster.intersectObjects(obstacles);
      // And disable that direction if we do
      if (collisions.length > 0 && collisions[0].distance <= distance) {
        // Yep, this.rays[i] gives us : 0 => up, 1 => up-left, 2 => left, ...
        if ((i === 0 || i === 1 || i === 7) && this.direction.z === 1) {
          this.direction.setZ(0);
        } else if ((i === 3 || i === 4 || i === 5) && this.direction.z === -1) {
          this.direction.setZ(0);
        }
        if ((i === 1 || i === 2 || i === 3) && this.direction.x === 1) {
          this.direction.setX(0);
        } else if ((i === 5 || i === 6 || i === 7) && this.direction.x === -1) {
          this.direction.setX(0);
        }
      }
    }
  },
  // Process the character motions
  motion: function () {
    'use strict';
    // Update the directions if we intersect with an obstacle
    this.collision();
    // If we're not static
    if (this.direction.x !== 0 || this.direction.z !== 0) {
      // Rotate the character
      this.rotate();
      // Move the character
      this.move();
      return true;
    }
  }
  /* ... */
});

And that’s it. So of course this isn’t perfect : some meshes are not taken into account (the hands and feet for instance), and it’s possible for some obstacles to get through our rays (try to walk just a little bit too close beside the cube on the demo). One solution would be to add some more rays. But still, this is meant to stay basic, so I’ll keep it like this so far.

See ya folks !

Basic Character Controls with Three.Js

I wanted to try Three.Js out since a little while, and I finally found some time for it. I’ll detail here the different classes I wrote for this to work.

Notice that the collisions are not handled yet. But this will be the topic of an upcoming post, as soon as I figure it out !

I used John Resig’s Simple JavaScript Inheritance in order to get rid of those ugly prototype declarations, to get a cleaner code, and to simplify any further implementation that would require classes inherithance.

Oh, that, and Paul Irish’s requestAnimationFrame polyfill.

That’s it for the tools. You don’t have to use them, but I will in the following explanations. In case you don’t, just replace the classes declarations by their prototype equivalences.

Now the first thing to say about what I experimented : so much trigonometry fun ! Let’s go.

Set the scene

I’m rendering within a <figure> tag.

<figure id="basic-scene"></figure>

Now we got to create the scene. We need a camera, to define from which view point we’re looking, some light or we won’t see a thing, and a “renderer”, which is actually the canvas viewport we render with.

This main class here will also set some event listeners for us to handle the user’s interactions, and a “frame” method that we’ll call at every animation frame request.

var basicScene;
var BasicScene = Class.extend({
    // Class constructor
    init: function () {
        'use strict';
        // Create a scene, a camera, a light and a WebGL renderer with Three.JS
        this.scene = new THREE.Scene();
        this.camera = new THREE.PerspectiveCamera(45, 1, 0.1, 10000);
        this.scene.add(this.camera);
        this.light = new THREE.PointLight();
        this.light.position.set(-256, 256, -256);
        this.scene.add(this.light);
        this.renderer = new THREE.WebGLRenderer();
        // Define the container for the renderer
        this.container = jQuery('#basic-scene');
        // Create the user's character
        this.user = new Character({
            color: 0x7A43B6
        });
        this.scene.add(this.user.mesh);
        // Create the "world" : a 3D representation of the place we'll be putting our character in
        this.world = new World({
            color: 0xF5F5F5
        });
        this.scene.add(this.world.mesh);
        // Define the size of the renderer
        this.setAspect();
        // Insert the renderer in the container
        this.container.prepend(this.renderer.domElement);
        // Set the camera to look at our user's character
        this.setFocus(this.user.mesh);
        // Start the events handlers
        this.setControls();
    },
    // Event handlers
    setControls: function () {
        'use strict';
        // Within jQuery's methods, we won't be able to access "this"
        var user = this.user,
            // State of the different controls
            controls = {
                left: false,
                up: false,
                right: false,
                down: false
            };
        // When the user presses a key 
        jQuery(document).keydown(function (e) {
            var prevent = true;
            // Update the state of the attached control to "true"
            switch (e.keyCode) {
                case 37:
                    controls.left = true;
                    break;
                case 38:
                    controls.up = true;
                    break;
                case 39:
                    controls.right = true;
                    break;
                case 40:
                    controls.down = true;
                    break;
                default:
                    prevent = false;
            }
            // Avoid the browser to react unexpectedly
            if (prevent) {
                e.preventDefault();
            } else {
                return;
            }
            // Update the character's direction
            user.setDirection(controls);
        });
        // When the user releases a key
        jQuery(document).keyup(function (e) {
            var prevent = true;
            // Update the state of the attached control to "false"
            switch (e.keyCode) {
                case 37:
                    controls.left = false;
                    break;
                case 38:
                    controls.up = false;
                    break;
                case 39:
                    controls.right = false;
                    break;
                case 40:
                    controls.down = false;
                    break;
                default:
                    prevent = false;
            }
            // Avoid the browser to react unexpectedly
            if (prevent) {
                e.preventDefault();
            } else {
                return;
            }
            // Update the character's direction
            user.setDirection(controls);
        });
        // On resize
        jQuery(window).resize(function () {
            // Redefine the size of the renderer
            basicScene.setAspect();
        });
    },
    // Defining the renderer's size
    setAspect: function () {
        'use strict';
        // Fit the container's full width
        var w = this.container.width(),
            // Fit the initial visible area's height
            h = jQuery(window).height() - this.container.offset().top - 20;
        // Update the renderer and the camera
        this.renderer.setSize(w, h);
        this.camera.aspect = w / h;
        this.camera.updateProjectionMatrix();
    },
    // Updating the camera to follow and look at a given Object3D / Mesh
    setFocus: function (object) {
        'use strict';
        this.camera.position.set(object.position.x, object.position.y + 128, object.position.z - 256);
        this.camera.lookAt(object.position);
    },
    // Update and draw the scene
    frame: function () {
        'use strict';
        // Run a new step of the user's motions
        this.user.motion();
        // Set the camera to look at our user's character
        this.setFocus(this.user.mesh);
        // And draw !
        this.renderer.render(this.scene, this.camera);
    }
});

Hello World

Or so… Because of obvious performance reasons, we won’t recreate an exact 3D representation of planet earth. We’ll create one ground and four walls instead. But I’ll call it world anyway.

Using a Object3D instead of a simple Mesh allows us to update independently the geometries within this very group of meshes.

var World = Class.extend({
    // Class constructor
    init: function (args) {
        'use strict';
        // Set the different geometries composing the room
        var ground = new THREE.PlaneGeometry(512, 1024),
            height = 128,
            walls = [
                    new THREE.PlaneGeometry(ground.height, height),
                    new THREE.PlaneGeometry(ground.width, height),
                    new THREE.PlaneGeometry(ground.height, height),
                    new THREE.PlaneGeometry(ground.width, height)
            ],
            obstacles = [
                    new THREE.CubeGeometry(64, 64, 64)
            ],
            // Set the material, the "skin"
            material = new THREE.MeshLambertMaterial(args),
            i;
        // Set the "world" modelisation object
        this.mesh = new THREE.Object3D();
        // Set and add the ground
        this.ground = new THREE.Mesh(ground, material);
        this.ground.rotation.x = -Math.PI / 2;
        this.mesh.add(this.ground);
        // Set and add the walls
        this.walls = [];
        for (i = 0; i < walls.length; i += 1) {
            this.walls[i] = new THREE.Mesh(walls[i], material);
            this.walls[i].position.y = height / 2;
            this.mesh.add(this.walls[i]);
        }
        this.walls[0].rotation.y = -Math.PI / 2;
        this.walls[0].position.x = ground.width / 2;
        this.walls[1].rotation.y = Math.PI;
        this.walls[1].position.z = ground.height / 2;
        this.walls[2].rotation.y = Math.PI / 2;
        this.walls[2].position.x = -ground.width / 2;
        this.walls[3].position.z = -ground.height / 2;
        // Set and add the obstacles
        this.obstacles = [];
        for (i = 0; i < obstacles.length; i += 1) {
            this.obstacles[i] = new THREE.Mesh(obstacles[i], material);
            this.mesh.add(this.obstacles[i]);
        }
        this.obstacles[0].position.set(0, 32, 128);
    }
});

And now the funny part…

Live little 3D character, live !

Here as well, we use an Object3D in order to group our meshes.

Our character has two different types of motion to update : its position and its rotation. Further on, we could easily make it jump, dive, or dance. But let’s start with a simple “moving around” action only.

A direction vector will represent the motion that our user is calling through the controls.

The step property will record the progression of the character’s position motion. We will use it to animate its feet and hands.

The feet of our simple character are half-spheres. The trick is simply to properly set the phiStart, phiLength, thetaStart and thetaLength parameters of the SphereGeometry method.

var Character = Class.extend({
    // Class constructor
    init: function (args) {
        'use strict';
        // Set the different geometries composing the humanoid
        var head = new THREE.SphereGeometry(32, 8, 8),
            hand = new THREE.SphereGeometry(8, 4, 4),
            foot = new THREE.SphereGeometry(16, 4, 4, 0, Math.PI * 2, 0, Math.PI / 2),
            nose = new THREE.SphereGeometry(4, 4, 4),
            // Set the material, the "skin"
            material = new THREE.MeshLambertMaterial(args);
        // Set the character modelisation object
        this.mesh = new THREE.Object3D();
        this.mesh.position.y = 48;
        // Set and add its head
        this.head = new THREE.Mesh(head, material);
        this.head.position.y = 0;
        this.mesh.add(this.head);
        // Set and add its hands
        this.hands = {
            left: new THREE.Mesh(hand, material),
            right: new THREE.Mesh(hand, material)
        };
        this.hands.left.position.x = -40;
        this.hands.left.position.y = -8;
        this.hands.right.position.x = 40;
        this.hands.right.position.y = -8;
        this.mesh.add(this.hands.left);
        this.mesh.add(this.hands.right);
        // Set and add its feet
        this.feet = {
            left: new THREE.Mesh(foot, material),
            right: new THREE.Mesh(foot, material)
        };
        this.feet.left.position.x = -20;
        this.feet.left.position.y = -48;
        this.feet.left.rotation.y = Math.PI / 4;
        this.feet.right.position.x = 20;
        this.feet.right.position.y = -48;
        this.feet.right.rotation.y = Math.PI / 4;
        this.mesh.add(this.feet.left);
        this.mesh.add(this.feet.right);
        // Set and add its nose
        this.nose = new THREE.Mesh(nose, material);
        this.nose.position.y = 0;
        this.nose.position.z = 32;
        this.mesh.add(this.nose);
        // Set the vector of the current motion
        this.direction = new THREE.Vector3(0, 0, 0);
        // Set the current animation step
        this.step = 0;
    },
    // Update the direction of the current motion
    setDirection: function (controls) {
        'use strict';
        // Either left or right, and either up or down (no jump or dive (on the Y axis), so far ...)
        var x = controls.left ? 1 : controls.right ? -1 : 0,
            y = 0,
            z = controls.up ? 1 : controls.down ? -1 : 0;
        this.direction.set(x, y, z);
    },
    // Process the character motions
    motion: function () {
        'use strict';
        // (if any)
        if (this.direction.x !== 0 || this.direction.z !== 0) {
            // Rotate the character
            this.rotate();
            // And, only if we're not colliding with an obstacle or a wall ...
            if (this.collide()) {
                return false;
            }
            // ... we move the character
            this.move();
            return true;
        }
    },
    // Rotate the character
    rotate: function () {
        'use strict';
        // Set the direction's angle, and the difference between it and our Object3D's current rotation
        var angle = Math.atan2(this.direction.x, this.direction.z),
            difference = angle - this.mesh.rotation.y;
        // If we're doing more than a 180°
        if (Math.abs(difference) > Math.PI) {
            // We proceed to a direct 360° rotation in the opposite way
            if (difference > 0) {
                this.mesh.rotation.y += 2 * Math.PI;
            } else {
                this.mesh.rotation.y -= 2 * Math.PI;
            }
            // And we set a new smarter (because shorter) difference
            difference = angle - this.mesh.rotation.y;
            // In short : we make sure not to turn "left" to go "right"
        }
        // Now if we haven't reached our target angle
        if (difference !== 0) {
            // We slightly get closer to it
            this.mesh.rotation.y += difference / 4;
        }
    },
    move: function () {
        'use strict';
        // We update our Object3D's position from our "direction"
        this.mesh.position.x += this.direction.x * ((this.direction.z === 0) ? 4 : Math.sqrt(8));
        this.mesh.position.z += this.direction.z * ((this.direction.x === 0) ? 4 : Math.sqrt(8));
        // Now let's use Sine and Cosine curves, using our "step" property ...
        this.step += 1 / 4;
        // ... to slightly move our feet and hands
        this.feet.left.position.setZ(Math.sin(this.step) * 16);
        this.feet.right.position.setZ(Math.cos(this.step + (Math.PI / 2)) * 16);
        this.hands.left.position.setZ(Math.cos(this.step + (Math.PI / 2)) * 8);
        this.hands.right.position.setZ(Math.sin(this.step) * 8);
    },
    collide: function () {
        'use strict';
        // INSERT SOME MAGIC HERE
        return false;
    }
});

I started to look at Cannon.JS to deal with the collisions, but it will have to wait for another post.

Now let’s call our main object and start the animated rendering.

basicScene = new BasicScene();
function animate () {
    requestAnimationFrame(animate);
    basicScene.frame();
}
animate();

And here we are !

Conclusion

Isn’t it surprising how easy it became to set up in-browser 3D ? Thanks Mr.doob for Three.JS !

This is only an experiment on a basic set up, but I would like to upgrade it every now and then. The next step will be the collisions detection.

Have fun fellows, I hope I’ll see you around again !