DEV Community

Cover image for 3D character motion control via WebSocket

3D character motion control via WebSocket

jemaloqiu profile image jemaloQiu Updated on ・3 min read

Several days ago, a friend of mine contacted me and asked me the feasibility of a technical solution for a 3d human character simulation in HTML5 environment. He sent me this article which presents how to create an interactive 3d character with Three.js**. He is expecting to control the character's real-time motion (whole-body motion) via a hardware such like a joystick.

It's a very interesting work and it seems quite easy. Thus I have done a little dev work trying to make it work.

Alt Text

In file Index.html, I have defined a websocket server and an according message parser. This file then is wrapped in Electron window so it runs as a desktop software.
Core part of this Index.html is the websocket communication part as below:

<script  type="text/javascript" >
    var angle1 = 0.0;
    var angle2 = 0.0

    const qEvent = new Event('qiu');

    /* for debug */    
    function output(s)
        var out = document.getElementById("debug-area");
        out.innerText += s;

    output("Start running")

    var msg_ready = false;
    var msg_reading = false;  // True: package head 0xAA is received, but 0x7f has not yet been received
    var msg_data_buffer = [];
    var msg_lenth = 0;

    function processMsg(v)
    if (v[0] == 170) // detect the beginning byte of a message: 0xAA
        // data are sent in little endian, 
        // v.buffer is a byte-array and Int16Array(v.buffer, 8, 1) means that it parses from the 8th byte on to get ONE Int16 number

        if ( (v[1] == 0x01) && (v[2] == 0x53) ) // 01 52
            angle1 = new Int16Array(v.buffer, 8, 1)[0];
            angle2 = new Int16Array(v.buffer, 10, 1)[0];
            var temp3 = new Int16Array(v.buffer, 12, 1)[0];


    var ws = require("nodejs-websocket");
    var clients =  new Array();
    output("开始建立连接... ");
    var count = 0;
    var data = new Buffer.alloc(0);
    var server = ws.createServer(function(conn){ = count;
        count += 1;
        clients["conn"+count]  = conn;

        conn.on("text", function (str) {
            output("Received " + str + "! " )
            var typeId = str.charAt(0);         
        conn.on("close", function (code, reason) {
            output("Connection closed!")

        conn.on("binary", function (inStream) {

            inStream.on("readable", function () {
                var newData =;

                if (newData)
                    data = Buffer.concat([data, newData], data.length + newData.length);

            inStream.on("end", function () {

                var t = '', v = new Uint8Array(data);

                for (var i = 0; i < v.length; i++)
                    // packet head 0xAA reached, now start reading the data flow
                    if  ((!msg_reading ) &&(v[i] == 0xaa)){
                        msg_reading = true;


                        if (msg_data_buffer.length == 8) {
                            msg_lenth =  msg_data_buffer[5]*16 + msg_data_buffer[4]; // parsing the data length (bytes size)                            

                        // received the end of packet, and the length is correct 
                        if ((v[i] == 127 ) && (msg_data_buffer.length == (msg_lenth + 10)))  // 10 extra bytes contained in this package for : length, scope, checksum, msg-id 
                            var msg = new Uint8Array(msg_data_buffer);
                            msg_data_buffer = [];
                            msg_reading = false;
                            msg_lenth = 0;
                        } else if (msg_data_buffer.length == (msg_lenth + 10))
                            msg_data_buffer = [];
                            msg_reading = false;
                            msg_lenth = 0;
                            output("Message length error!");


            data = new Buffer.alloc(0);
            conn.sendText('Binary Received!');

        conn.on("message", function (code, reason) {
            output("message! " )
        conn.on("error", function (code, reason) {
            output("Error occurs!")
    output("Server is ready! ");
Enter fullscreen mode Exit fullscreen mode

In existing file script.js, I have defined function moveOneJoint(). It will be called each time an event 'qiu' is dispatched.

  document.addEventListener('qiu', function (e) {

    if (neck && waist) {
       moveOneJoint(neck, angle1, angle2);

  function moveOneJoint(joint, x, y) {

    joint.rotation.y = THREE.Math.degToRad(x);
    joint.rotation.x = THREE.Math.degToRad(y);

Enter fullscreen mode Exit fullscreen mode

Entire code has been pushed to github repo:



Run the following cmd:

cd Interactive3DCharacter
npm install
npm start

Control singal sending

One can write his own program to send angle via websockt. Angle data (two int16) to be sent should be written into msg_send_posture[8:9] and msg_send_posture[10:11].

Example code:

var wsUrl = "ws://localhost:9999"
websocket = new WebSocket(wsUrl)
var msg_send_posture  =  new Uint8Array([0xAA,  0x01,0x53,  0x01,  0x04,0x00,0x00,0x00,   0x01,0x00, 0x00,0x00,  0x00,0x00,   0x7F]

Original Project: Interactive 3D Character with Three.js

Demo for the tutorial on how to add an interactive 3D character to a website.

3D Character

Article on Codrops




This resource can be used freely if integrated or build upon in personal or commercial projects such as websites, web apps and web templates intended for sale. It is not allowed to take the resource "as-is" and sell it, redistribute, re-publish it, or sell "pluginized" versions of it. Free plugins built using this resource should have a visible…

I do not have a joystick so I simulate it with several range sliders in another web app (developed using MUI framework with HBuilder). By sliding the sliders, we can send the angle data via websocket to above-mentioned 3d character simulator. Data massage to be sent should be a dataarray like: [0xAA, 0x01,0x53, 0x01, 0x04,0x00,0x00,0x00, 0xMM,0xNN, 0xSS,0xTT, 0xYY,0xZZ, 0x7F] where 0xMM,0xNN and 0xSS,0xTT are angle values in Int16 and 0xYY,0xZZ can be any bytes (designed to be checksum, but I am not checking it in my code).

Below is a demo I've recorded. I am controlling the motion of the simulated 3d character's head using sliders:

In another trial, I run my device simulator app on Android platform and run Electron in full screen. Check out the demo :

Discussion (2)

Editor guide
ben profile image
Ben Halpern

Wow, this is really cool!

jemaloqiu profile image
jemaloQiu Author

Thanks 😊