preface

I came across the 3D properties of CSS and had an idea for a 3D game.

Perspective, Perspective-Origin, and transform-style: Preserve-3D are the three attributes that make up the 3D world of CSS.

There is also a transform property to translate, scale, rotate and stretch 3D nodes.

Attribute values are simple and rarely used in our normal Web development.

Can you make 3D games with these CSS3D attributes?

Of course you can.

Even if it’s just a sandbox, there’s minecraft.

Today I’m going to show you a new 3D experience that you’ve never had before.

Without further ado, let’s take a look at the effect:

Here is the demo address PC side free to play

To complete the maze operation, we need to complete the following steps:

  1. Create a 3D world
  2. Write a 3D camera function
  3. Create a 3D maze
  4. Create a player who can move freely
  5. Find a shortest path prompt in the maze

So let’s start with a little bit of pre-knowledge.

The knowledge and concepts needed to make a CSS3D game

CSS3D coordinate system

In css3D, we must first define a concept, 3D coordinate system. Using the left hand coordinate system, extend our left hand, thumb and index finger in L shape, other fingers perpendicular to index finger, as shown:

The thumb is on the X axis, the index finger on the Y axis, and the other fingers on the Z axis. This is the coordinate system in CSS3D.

Perspective properties

Perspective is a perspective attribute in the CSS.

What does this property mean? We can think of our eyes as the observation point, and the distance from the eyes to the target object is the stadia, which is the perspective property here.

As you all know, “perspective” + “2D” = “3D”.

perspective: 1200px;
-webkit-perspective:  1200px;
Copy the code

3 d camera

In 3D game development, there is the concept of a camera, where what the human eye sees is what the camera sees. Moving the scene in the game is mostly about moving the camera. In a racing game, for example, the camera follows the car so that we can see the scenery along the way. In this case, we will use CSS to implement a pseudo-3D camera.

Transform properties

In CSS3D we pan, rotate, stretch and scale 3D boxes using the Transform property.

  • TranslateX translates the X-axis
  • TranslateY translates the Y axis
  • TranslateZ translates the Z axis
  • RotateX rotates the X-axis
  • RotateY rotates the Y-axis
  • RotateZ rotates the z-axis
  • Rotate3d (x,y,z,deg) rotation x,y,z axis by degrees

Note: here “translation and rotation” is not the same as “rotation and translation”. The rotation Angle is the Angle value.

There are still unclear students can refer to this article of Yu Fei [juejin.cn/post/699769…] With the demo

Matrix transformation

We will use matrix transformations as we finish the game. In js, getting the transform property of a node yields a matrix that looks like this:

var _ground = document.getElementsByClassName("ground") [0];
var bg_style = document.defaultView.getComputedStyle(_ground, null).transform;
console.log("Matrix transformation ---->>>",bg_style)
Copy the code

So how do we manipulate transform using matrices?

In linear transformations, we’re going to use matrix multiplication. CSS3D uses a 4*4 matrix for 3D transformation. I’m going to represent the following matrices as two-dimensional arrays. For example matrix3d,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,1 (1) can be expressed in a two-dimensional array:

[[1.0.0.0],
    [0.1.0.0],
    [0.0.1.0],
    [0.0.0.1]]Copy the code

Translation even if you multiply the original matrix times the following matrix, dx, dy, dz are the directions of motion x, y, and z, respectively.

[[1.0.0, dx],
    [0.1.0, dy],
    [0.0.1, dz],
    [0.0.0.1]]Copy the code

Rotate 𝞱 about the X-axis, which is multiplied by the following matrix.

[[1.0.0.0],
    [0, cos 𝞱, sin 𝞱,0],
    [0- sin 𝞱, cos 𝞱,0],
    [0.0.0.1]]Copy the code

The rotation of 𝞱 about the Y-axis is multiplied by the following matrix.

[
    [cos𝞱, 0- sin 𝞱,0],
    [0.1.0.0], [sin 𝞱,0, cos 𝞱,0],
    [0.0.0.1]]Copy the code

The rotation of 𝞱 about the z-axis is multiplied by the following matrix.

[[cos 𝞱, sin 𝞱,0.0], [- sin 𝞱, cos 𝞱,0.0],
    [0.0.1.0],
    [0.0.0.1]]Copy the code

The other knowledge of the concrete matrix is explained here, and you can learn it yourself if you are interested. We just need a very simple rotation application here.

Start creating a 3D world

Let’s create the UI first.

  • Camera div
  • The horizon div
  • The board div
  • Player div(here is a cube)

Notice that the cube rotates first and then translates. This should be the easiest way. A plane rotated 180 degrees, plus or minus 90 degrees, about the X and Y axes requires only a translation of the Z axis. And you’ll see if you tried it.

Let’s start with the HTML section:

    <div class="camera">
        <! - the ground -- -- >
        <div class="ground">
            <div class="box">
                <div class="box-con">
                    <div class="wall">z</div>
                    <div class="wall">z</div>
                    <div class="wall">y</div>
                    <div class="wall">y</div>
                    <div class="wall">x</div>
                    <div class="wall">x</div>
                    <div class="linex"></div>
                    <div class="liney"></div>
                    <div class="linez"></div>
                </div>
                <! -- -- -- > board
                <div class="pan"></div>
            </div>
        </div>
    </div>
Copy the code

Very simple layout, where LINex, Liney and Linez are the auxiliary axes I drew. The red line is the X axis, the green line is the Y axis, and the blue line is the Z axis. Next, let’s look at the cube’s main CSS code.

..box-con{
      width: 50px;
      height: 50px;
      transform-style: preserve-3d;
      transform-origin: 50% 50%;
      transform: translateZ(25px);transition: all 2s cubic-bezier(0.075.0.82.0.165.1);
  }
  .wall{
      width: 100%;
      height: 100%;
      border: 1px solid #fdd894;
      background-color: #fb7922;
    
  }
  .wall:nth-child(1) {
      transform: translateZ(25px);
  }
  .wall:nth-child(2) {
      transform: rotateX(180deg) translateZ(25px);
  }
  .wall:nth-child(3) {
      transform: rotateX(90deg) translateZ(25px);
  }
  .wall:nth-child(4) {
      transform: rotateX(-90deg) translateZ(25px);
  }
  .wall:nth-child(5) {
      transform: rotateY(90deg) translateZ(25px);
  }
  .wall:nth-child(6) {
      transform: rotateY(-90deg) translateZ(25px);
  }
Copy the code

Pasting a bunch of CSS code looks stupid. Other CSS will not be pasted here, interested students can directly download the source code to view. The interface is completed as shown in the figure:

Now comes the big part, where we write JS code to finish the game.

Complete a 3D camera function

Cameras are essential for 3D development. Not only can you view 3D models of the world, but you can also achieve many cool functions in real time.

What functions does a 3D camera need?

The simplest, up, down, left and right can be 360 degrees without dead Angle view of the map. You also need to zoom in and zoom out.

Interaction with the mouse

Mouse around movement can be rotated to view the map; Mouse up and down movement can observe up and down map; The mouse wheel can zoom in and out of sight.

✅ 1. Listen for mouse events

First, we need to record the mouse position by listening to mouse events to determine the camera to look up, down, left, and right.

    /** Last mouse position */
    var lastX = 0, lastY = 0;
      /** Controls a slide */
    var isDown = false;
      /** Listen for mouse press */
    document.addEventListener("mousedown".(e) = > {
        lastX = e.clientX;
        lastY = e.clientY;
        isDown = true;
    });
        /** Monitor mouse movement */
    document.addEventListener("mousemove".(e) = > {
        if(! isDown)return;
        let _offsetX = e.clientX - lastX;
        let _offsetY = e.clientY - lastY;
        lastX = e.clientX;
        lastY = e.clientY;
        // Determine the direction
        var dirH = 1, dirV = 1;
        if (_offsetX < 0) {
            dirH = -1;
        }
        if (_offsetY > 0) {
            dirV = -1; }});document.addEventListener("mouseup".(e) = > {
        isDown = false;
    });
Copy the code

✅ 2. Check whether the camera is up, down, or left

Use perspective-Origin to set the camera’s up and down line of sight. Use transform to rotate the Z axis to see 360 degrees left and right.

/** Monitor mouse movement */
    document.addEventListener("mousemove".(e) = > {
        if(! isDown)return;
        let _offsetX = e.clientX - lastX;
        let _offsetY = e.clientY - lastY;
        lastX = e.clientX;
        lastY = e.clientY;
        var bg_style = document.defaultView.getComputedStyle(_ground, null).transform;
        var camera_style = document.defaultView.getComputedStyle(_camera, null).perspectiveOrigin;
        var matrix4 = new Matrix4();
        var _cy = +camera_style.split(' ') [1].split('px') [0];
        var str = bg_style.split("matrix3d(") [1].split(")") [0].split(",");
        var oldMartrix4 = str.map((item) = > +item);
        var dirH = 1, dirV = 1;
        if (_offsetX < 0) {
            dirH = -1;
        }
        if (_offsetY > 0) {
            dirV = -1;
        }
        // Rotate the Angle of each move
        var angleZ = 2 * dirH;
        var newMartri4 = matrix4.set(Math.cos(angleZ * Math.PI / 180), -Math.sin(angleZ * Math.PI / 180), 0.0.Math.sin(angleZ * Math.PI / 180), Math.cos(angleZ * Math.PI / 180), 0.0.0.0.1.0.0.0.0.1);
        var new_mar = null;
        if (Math.abs(_offsetX) > Math.abs(_offsetY)) {
            new_mar = matrix4.multiplyMatrices(oldMartrix4, newMartri4);
        } else {
            _camera.style.perspectiveOrigin = `500px ${_cy + 10 * dirV}px`;
        }
        new_mar && (_ground.style.transform = `matrix3d(${new_mar.join(', ')}) `);
    });
Copy the code

Here using the method of matrix to rotate the Z axis, matrix class Matrix4 is my temporary write a class method, two methods, a set of two-dimensional array Matrix4. Set, a matrix multiplication Matrix4. MultiplyMatrices. At the end of the source address, here is no longer described.

✅ 3. Monitor the scroll wheel and pull it for a long distance

This is the setting of the horizon according to the perspective.

// Listen to the scroll wheel
document.addEventListener('mousewheel'.(e) = > {
    var per = document.defaultView.getComputedStyle(_camera, null).perspective;
    let newper = (+per.split("px") [0] + Math.floor(e.deltaY / 10)) + "px";
    _camera.style.perspective = newper
}, false);
Copy the code

Note: The perspective-Origin attribute has only X and Y values and cannot be used as a camera like u3D. I’ve used a clever rotation of the horizon to achieve the same effect. The zoom and zoom of the scroll wheel is a bit awkward, and the 3D engine is still very different.

After finishing, you can see the following scene, which is ready to observe our map at any time.

In this way, a 3D camera is completed. If you are interested, you can write about it yourself. It is still very interesting.

Draw a maze board

The easiest way to draw a grid map is to use a 15 by 15 array, with “0” representing the paths that can be crossed and “1” representing the obstacles.

var grid = [
    0.0.0.1.0.0.0.0.0.1.0.0.0.1.0.0.1.0.1.0.0.1.0.1.0.1.1.0.1.0.1.0.0.0.0.1.0.0.0.0.0.0.0.0.1.0.0.0.1.0.0.0.0.0.1.0.0.1.0.1.0.0.1.0.1.0.0.1.0.1.0.1.0.0.0.0.0.0.0.1.1.0.0.0.0.0.1.0.0.0.0.0.1.0.0.0.1.0.0.1.0.0.0.1.0.1.1.0.0.1.0.0.1.0.0.0.1.0.0.0.1.0.1.0.0.1.0.1.0.1.0.0.0.1.0.0.0.0.0.1.1.0.0.0.0.0.0.0.0.0.1.0.1.0.0.1.0.1.0.0.0.1.0.0.1.0.1.0.0.1.0.0.0.0.0.0.0.0.1.0.1.1.0.0.1.0.0.1.0.1.0.1.0.0.0.1.0.1.0.0.1.0.0.0.0.0.0.1.0.0.0.0.1.0.1.0.1.0.0.1.1.0.0.0.0
];
Copy the code

And then we’re going to go through the array and get the map. Write a method to create a map grid that returns both a grid array and a node array. The block here is a prefab created in HTML, which is a cube. Then add them to the board by cloning the nodes.

/ * * * / board
function pan() {
    const con = document.getElementsByClassName("pan") [0];
    const block = document.getElementsByClassName("block") [0];
    let elArr = [];
    grid.forEach((item, index) = > {
        let r = Math.floor(index / 15);
        let c = index % 15;
        const gezi = document.createElement("div");
        gezi.classList = "pan-item"
        // gezi.innerHTML = `${r},${c}`
        con.appendChild(gezi);
        var newBlock = block.cloneNode(true);
        / / obstacles
        if (item == 1) {
            gezi.appendChild(newBlock);
            blockArr.push(c + "-" + r);
        }
        elArr.push(gezi);
    });
    const panArr = arrTrans(15, grid);
    return { elArr, panArr };
}
const panData = pan();
Copy the code

As you can see, our interface has become something like this.

Next, we need to control player movement.

Controlling player movement

Through up and down around w S A D keys to control the player movement. Use transform to move and rotate the player box.

✅ Listens for keyboard events

Check the key value by listening for the keyboard event onKeyDown.

document.onkeydown = function (e) {
    /** Move object */
    move(e.key);
}
Copy the code

✅ to carry out displacement

In the shift, translate is used to translate, the Z axis is always facing our camera, so we only need to move the X and Y axes. Declare a variable to record the current location. We also need to record the value of the transform that we transformed last time, so we won’t continue the matrix transformation here.

/** The current position */
var position = { x: 0.y: 0 };
/** Record the last change */
var lastTransform = {
    translateX: '0px'.translateY: '0px'.translateZ: '25px'.rotateX: '0deg'.rotateY: '0deg'.rotateZ: '0deg'
};
Copy the code

Each cell can be viewed as a subscript of a two-dimensional array, and each time we move a cell, we move a cell.

 switch (key) {
    case 'w':
        position.y++;
        lastTransform.translateY = position.y * 50 + 'px';
        break;
    case 's':
        position.y--;
        lastTransform.translateY = position.y * 50 + 'px';
        break;
    case 'a':
        position.x++;
        lastTransform.translateX = position.x * 50 + 'px';
        break;
    case 'd':
        position.x--;
        lastTransform.translateX = position.x * 50 + 'px';
        break;
}
// Assign style
for (let item in lastTransform) {
    strTransfrom += item + '(' + lastTransform[item] + ') ';
}
target.style.transform = strTransfrom;
Copy the code

At this point, our player box is ready to move.

Note that translation in css3D can be viewed as world coordinates. So we only care about the X and Y axes. I don’t have to move the z-axis. Even if we rotate it.

✅ rotates as it moves

In CSS3D, 3D rotation is different from other 3D engines, such as u3D and Threejs, which recalibrate the world coordinates after each rotation, making it relatively easy to calculate how many degrees to rotate around any axis.

However, I also underestimated the rotation of CSS3D. I thought it was easy to roll a cube up, down, left, and right. That’s not the case.

The rotation of CSS3D involves quaternions and universal locks.

Let’s say we rotate our player box. As shown in the figure:

First, the first cell (0,0) is rotated up 90 degrees about the X-axis to reach (1.0). Rotate 90 degrees to the left about the Y-axis to get to (0,1); Then we can get the rule as follows:

As you can see, it’s fine to just rotate up and down, left and right, but to rotate to the red cell, two different ways, once you get to the red cell there are two possible rotations. This leads to a rotation error.

And it’s hard to find, but it can be written down, and most importantly, it’s not right to rotate a box in CSS3D

So someone just said, like, that bullshit?

Through the author’s experiment, it was found a few rules. So we’re going to continue to follow this pattern.

  • When you rotate the X-axis, look at the degree of the current z-axis, which is an odd multiple of 90 degrees, rotate the Y-axis, otherwise rotate the X-axis.
  • When you rotate the Y-axis, look at the degree of the current z-axis, which is an odd multiple of 90 degrees, and rotate the X-axis, otherwise rotate the Z-axis.
  • As you rotate the z-axis, you keep rotating the Z-axis

And then we have the direction of rotation.

if (nextRotateDir[0] = ="X") {
    if (Math.floor(Math.abs(lastRotate.lastRotateZ) / 90) % 2= =1) {
        lastTransform[`rotateY`] = (lastRotate[`lastRotateY`] + 90 * dir) + 'deg';
    } else {
        lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] - 90 * dir) + 'deg'; }}if (nextRotateDir[0] = ="Y") {
    if (Math.floor(Math.abs(Math.abs(lastRotate.lastRotateZ)) / 90) % 2= =1) {
        lastTransform[`rotateX`] = (lastRotate[`lastRotateX`] + 90 * dir) + 'deg';
    } else {
        lastTransform[`rotateZ`] = (lastRotate[`lastRotateZ`] + 90 * dir) + 'deg'; }}if (nextRotateDir[0] = ="Z") {
    lastTransform[`rotate${nextRotateDir[0]}`] = (lastRotate[`lastRotate${nextRotateDir[0]}`] - 90 * dir) + 'deg';
}
Copy the code

However, this is not the end, there is a pit in this way of rotation, is I do not know whether to rotate 90 degrees or -90 degrees. It’s not just going up, down, left, or right.

It’s rotating in the right direction. I don’t know how to calculate the rotation Angle.

Specific code can be viewed source code.

Eggs in time

⚠️⚠️⚠️ At the same time, there will be a “universal lock”, that is, the Z axis and X axis rejoin. Ha ha ha ~ ⚠️⚠️⚠️ here the author has not solved, also hope the omnipotent net friend can say for help ~ ⚠️ college the author solved later will update. Hahaha, big pit.

Well, the problem here doesn’t affect our project. Let’s move on to how to find shortest paths and give hints.

Shortest path calculation

How do you calculate the shortest path from one point to another in a maze? Here the author uses the breadth-first traversal (BFS) algorithm to calculate the shortest path.

Let’s think about it:

  1. Find the shortest path in a two-dimensional array
  2. The shortest path in each space has only four adjacent Spaces
  3. So just recursively look for the shortest distance in each grid until you find the end point

Here we need to use the “queue” fifO feature.

Let’s start with a picture:

It’s pretty clear that you can get the shortest path.

Notice the use of two arrays of length 4 to represent the subscript offsets that need to be added for the upper, lower, left, and right adjacent cells. You need to judge whether you are already in the team before you join the team. Each time you leave the team, you need to determine whether it is the end. You need to record the parent node of the current joining target to obtain the shortest path.

Let’s look at the code:

// Early spring path
var stack = [];
/** * BFS implements pathfinding *@param {*} grid 
 * @param {*} start {x: 0,y: 0}
 * @param {*} end {x: 3,y: 3}
 */
function getShortPath(grid, start, end, a) {
    let maxL_x = grid.length;
    let maxL_y = grid[0].length;
    let queue = new Queue();
    // Minimum number of steps
    let step = 0;
    // Top left, bottom right
    let dx = [1.0, -1.0];
    let dy = [0.1.0, -1];
    // Add the first element
    queue.enqueue(start);
    // Store an identical one to check whether it has been traversed
    let mem = new Array(maxL_x);
    for (let n = 0; n < maxL_x; n++) {
        mem[n] = new Array(maxL_y);
        mem[n].fill(100);
    }
    while(! queue.isEmpty()) {let p = [];
        for (let i = queue.size(); i > 0; i--) {
            let preTraget = queue.dequeue();
            p.push(preTraget);
            // Find the target
            if (preTraget.x == end.x && preTraget.y == end.y) {
                stack.push(p);
                return step;
            }
            // Iterate over four adjacent cells
            for (let j = 0; j < 4; j++) {
                let nextX = preTraget.x + dx[j];
                let nextY = preTraget.y + dy[j];

                if (nextX < maxL_x && nextX >= 0 && nextY < maxL_y && nextY >= 0) {
                    let nextTraget = { x: nextX, y: nextY };
                    if(grid[nextX][nextY] == a && a < mem[nextX][nextY]) { queue.enqueue({ ... nextTraget,f: { x: preTraget.x, y: preTraget.y } }); mem[nextX][nextY] = a; } } } } stack.push(p); step++; }}/* Find the shortest path **/
function recall(end) {
    let path = [];
    let front = { x: end.x, y: end.y };
    while (stack.length) {
        let item = stack.pop();
        for (let i = 0; i < item.length; i++) {
            if(! item[i].f)break;
            if (item[i].x == front.x && item[i].y == front.y) {
                path.push({ x: item[i].x, y: item[i].y });
                front.x = item[i].f.x;
                front.y = item[i].f.y;
                break; }}}return path;
}

Copy the code

This way we can find a shortest path and get the shortest number of steps. Then we continue to traverse our original array. Click on the prompt to light the path.

var step = getShortPath(panArr, { x: 0.y: 0 }, { x: 14.y: 14 }, 0);
console.log("Minimum distance ----", step);
_perstep.innerHTML = ` please < span >${step}</span> walk to the end of the line;
var path = recall({ x: 14.y: 14 });
console.log("Path -", path);
/ * * hint * /
var tipCount = 0;
_tip.addEventListener("click".() = > {
    console.log("9999", tipCount)
    elArr.forEach((item, index) = > {
        let r = Math.floor(index / 15);
        let c = index % 15;
        path.forEach((_item, i) = > {
            if (_item.x == r && _item.y == c) {
                // console.log("ooo",_item)
                if (tipCount % 2= =0)
                    item.classList = "pan-item pan-path";
                else
                    item.classList = "pan-item"; }})}); tipCount++; });Copy the code

In this way, we can get a hint like the following:

Done. Hey, isn’t it amazing feeling ~

The end of the

Of course, there’s still room for improvement in my little game:

  • Can increase props, pick can reduce the number of steps
  • Configuration levels can be added
  • You can also add a jump function
  • .

Original so, CSS3D can do a lot of things, how to use all to see how rich their imagination.

Ha ha ha, really want to use CSS3D write a “My world” play, I’m afraid the performance problem will be a little big.

The examples in this article are all well experienced on PC.

Demo address

The source address

Welcome to pat brick correction, the author’s skill is shallow, if there is improper place please be corrected!

The article is shallow, hope you don’t hesitate to your comments and praise ~

Note: this article is the author’s painstaking work, reprint must be declared