preface

The beginning of heaven and earth, the beginning of chaos. Our solar system was born with the Big Bang. Today, with the author’s thoughts to explore the mysteries of the solar system ~. Hey hey, let’s start from the most simple, study the motion of each planet in the solar system!

We will first try to simulate the workings of the solar system using HTML + CSS, and then go one step further and dive into the mysteries of their movement.

Simulated solar system

We can simulate such a solar system using HTML + CSS. This for everyone must be very simple??

We can easily write an HTML skeleton:

<body>
        <div class="solar-system">
            <! -- <div class="sun-orbit"> </div> -->
            <div class="sun"></div>
            <div class="mercury-orbit">
                <div class="mercury"></div>
            </div>
            <div class="venus-orbit">
                <div class="venus"></div>
            </div>
            <div class="earth-orbit">
                <div class="earth">
                    <div class="moon-orbit">
                        <div class="moon"></div>
                    </div>
                </div>
            </div>
        </div>
    </body>
Copy the code

Add some simple CSS animations to get the look we want!! (In order to make it easier to change parameters, CSS uses a large number of variables.)

:root {
    --sun-size: 100px;
    --mercury-orbit-size: 200px;
    --mercury-size: 20px;
    --venus-orbit-size: 300px;
    --venus-size: 30px;
    --earth-orbit-size: 450px;
    --earth-size: 40px;
    --moon-orbit-size: 80px;
    --moon-size: 10px;

}

body {
    width: 100vw;
    height: 100vh;
    transform: translate3d(0);
}

.solar-system {
    position: relative;
    transform: translate(
        calc(100vw / 2 - var(--sun-size) / 2),
        calc(100vh / 2 - var(--sun-size) / 2)); }.solar-system * {
    position: absolute;
}

.sun {
    width: 100px;
    height: 100px;
    background-image: url(./assets//images/sun.jpeg);
    background-size: 100% 100%;
    border-radius: 50%;
}

.mercury-orbit {
    width: var(--mercury-orbit-size);
    height: var(--mercury-orbit-size);
    left: -50px;
    top: -50px;
    border: 1px solid #ccc;
    border-radius: 50%;
    animation: 2.38 s linear 0s infinite reverse both running volvo;
}

.mercury-orbit .mercury {
    width: var(--mercury-size);
    height: var(--mercury-size);
    top: calc(var(--mercury-orbit-size) / 2 - var(--mercury-size) / 2);
    left: calc(var(--mercury-orbit-size) / 2 - var(--mercury-size) / 2);
    border-radius: 50%;
    background-image: url("./assets/images/mercury.png");
    background-size: 100% 100%;
    transform: translate(calc(var(--mercury-orbit-size) / 2));
    animation: 58s linear 0s infinite reverse both running mercury-self-volvo;
}

.venus-orbit {
    width: var(--venus-orbit-size);
    height: var(--venus-orbit-size);
    left: calc((var(--venus-orbit-size) - var(--sun-size)) / -2);
    top: calc((var(--venus-orbit-size) - var(--sun-size)) / -2);
    border: 1px solid #ccc;
    border-radius: 50%;
    animation: 6.14 s linear 0s infinite reverse both running volvo;
}

.venus-orbit .venus {
    width: var(--venus-size);
    height: var(--venus-size);
    top: calc(var(--venus-orbit-size) / 2 - var(--venus-size) / 2);
    left: calc(var(--venus-orbit-size) / 2 - var(--venus-size) / 2);
    border-radius: 50%;
    background-image: url("./assets/images/venus.png");
    background-size: 100% 100%;
    transform: translate(calc(var(--venus-orbit-size) / 2));
    animation: 243s linear 0s infinite reverse both running venus-self-volvo;
}
.earth-orbit {
    width: var(--earth-orbit-size);
    height: var(--earth-orbit-size);
    left: calc((var(--earth-orbit-size) - var(--sun-size)) / -2);
    top: calc((var(--earth-orbit-size) - var(--sun-size)) / -2);
    border: 1px solid #ccc;
    border-radius: 50%;
    animation: 10s linear 0s infinite reverse both running volvo;
}

.earth-orbit .earth {
    width: var(--earth-size);
    height: var(--earth-size);
    top: calc(var(--earth-orbit-size) / 2 - var(--earth-size) / 2);
    left: calc(var(--earth-orbit-size) / 2 - var(--earth-size) / 2);
    border-radius: 50%;
    background-image: url("./assets/images/earth.png");
    background-size: 100% 100%;
    transform: translate(calc(var(--earth-orbit-size) / 2));
    animation: 1s linear 0s infinite reverse both running earth-self-volvo;
}

.moon-orbit {
    width: var(--moon-orbit-size);
    height: var(--moon-orbit-size);
    left: calc((var(--moon-orbit-size) - var(--earth-size)) / -2);
    top: calc((var(--moon-orbit-size) - var(--earth-size)) / -2);
    border: 1px solid #ccc;
    border-radius: 50%;
    animation: 10s linear 0s infinite reverse both running volvo;
}

.moon-orbit .moon {
    width: var(--moon-size);
    height: var(--moon-size);
    top: calc(var(--moon-orbit-size) / 2 - var(--moon-size) / 2);
    left: calc(var(--moon-orbit-size) / 2 - var(--moon-size) / 2);
    border-radius: 50%;
    background-image: url("./assets/images/moon.jpeg");
    background-size: 100% 100%;
    transform: translate(calc(var(--moon-orbit-size) / 2));
    /* animation: 1s linear 0s infinite reverse both running earth-self-volvo; * /
}
@keyframes volvo {
    from {
        transform: rotate(0);
    }
    to {
        transform: rotate(360deg); }}@keyframes mercury-self-volvo {
    from {
        transform: translate(calc(var(--mercury-orbit-size) / 2)) rotate(0);
    }
    to {
        transform: translate(calc(var(--mercury-orbit-size) / 2)) rotate(360deg); }}@keyframes venus-self-volvo {
    from {
        transform: translate(calc(var(--venus-orbit-size) / 2)) rotate(0);
    }
    to {
        transform: translate(calc(var(--venus-orbit-size) / 2)) rotate(-360deg); }}@keyframes earth-self-volvo {
    from {
        transform: translate(calc(var(--earth-orbit-size) / 2)) rotate(0);
    }
    to {
        transform: translate(calc(var(--earth-orbit-size) / 2)) rotate(-360deg); }}Copy the code

Now we’re going to get to the big part, where we’re going to explore the mysteries of our solar system

I’m going to give our solar system (and later even the Milky Way, the universe, and all that crap) a unified name: “Scene maps.”

In fact, most game engines have such a concept, and so does the scene in ThreeJS. Scene diagrams are hierarchical (you can understand them in contrast to HTML parent-child relationships, which are essentially a tree)

Scene graph

meaning

Its most important function is to provide a parent-child relationship between nodes.

In our solar system, for example: when the moon moves around the Earth, if the earth moves, the moon moves with the Earth!

So when we describe the moon’s path, there are two ways to think about it:

  1. We only care about the motion of the moon around the earth, and then calculate the motion of the moon to the sun according to the motion of the earth
  2. Calculate the motion of the moon directly against the sun.

It is not hard to imagine that if there is no scene diagram for us to provide the parent-child relationship between nodes, then we directly calculate the motion relationship between two objects will become very complicated! (As you can see below, if we calculate the motion between the moon and the sun directly, this spiral is extremely difficult to calculate!)

So how do we calculate from the scene diagram?

Scene diagram calculation

First, we need to clarify some basic concepts:

Node, local coordinate system, world coordinate system

We specify some basic properties for Node:

  • Position: indicates the position of the node (x, y)
  • Rotation: Indicates the rotation Angle of the node
  • Scale: indicates the scale of a node
  • Anchor: indicates the location of the local coordinate system in the node. Changes of anchor will affect the local coordinate system as well as the world coordinate system.

The figure shows a gray square with sides of length 1. Its position in the world coordinate system is (x, y). Its anchor is right in the center, so for its four vertices:

In the local coordinate system of coordinates, respectively: (0.5, 0.5), (0.5, 0.5), (0.5, 0.5), (0.5, 0.5)

So the coordinates in the world coordinate system need to add the information of their position in the world coordinate system (x-0.5, y + 0.5)…

Now, we use a matrix to represent the position, rotation, and scale of a Node. If you are not familiar with how a matrix represents the position of an object, please refer to the following article:

WebGL practical article (4) – affine transformation – the nuggets (juejin. Cn) matrix () – the CSS (cascading style sheets (CSS) | MDN (mozilla.org)

We first give the calculation method of node position in the scene diagram, and then we further understand it by example code:

worldMatrix = parentWorldMatrix * localMatrix;
Copy the code

Building a Scene Diagram

Before building the scene diagram, we need to implement our drawing framework first. We use Canvas2D to draw our nodes. Let’s start by defining our Node class


export class SceneNode {
    public x: number = 0;
    public y: number = 0;
    public width: number = 0;
    public height: number = 0;
    public rotation: number = 0;
    public scaleX: number = 1;
    public scaleY: number = 1;
    public color: Color = new Color(0.0.0);
    public anchorX: number = 0.5;
    public anchorY: number = 0.5;
    public renderComponent: (ctx: CanvasRenderingContext2D) = > void = null;
    private children: SceneNode[] = [];
    private parent: SceneNode = null;

    private localMatrix: mat3 = mat3.create();
    private worldMatrix: mat3 = mat3.create();
    constructor(public name: string = "", public renderNode = false) {}

    addChild(child: SceneNode): void {
        child.remove();
        const index = this.children.indexOf(child);
        if (index < 0) {
            this.children.push(child);
            child.parent = this;
        }
    }

    setParent(parent: SceneNode): void {
        parent.addChild(this);
    }

    removeChild(child: SceneNode) {
        child.parent = null;
        const index = this.children.indexOf(child);
        if (index >= 0) {
            this.children.splice(index, 1);
        }
    }

    remove(): void {
        if (this.parent) {
            this.parent.removeChild(this);
        }
    }

    getWorldMatrix(): mat3 {
        if (!this.parent) {
            return this.getLocalMatrix();
        }
        const out = mat3.create();
        mat3.multiply(out, this.parent.getWorldMatrix(), this.getLocalMatrix());
        return out;
    }

    getLocalMatrix(): mat3 {
        const out = mat3.create();
        mat3.translate(out, out, vec2.fromValues(this.x, this.y));
        mat3.rotate(out, out, angle2Rad(this.rotation));
        mat3.scale(out, out, vec2.fromValues(this.scaleX, this.scaleY));
        return out;
    }

    setAttribute(key: any, value: any): void{(this as any)[key] = value;
    }

    visit(iterator: (node: SceneNode) = > void) :void {
        iterator(this);
        for (let i = 0; i < this.children.length; i++) {
            this.children[i].visit(iterator);
        }
    }

    private conditionVisit(
        condition: (node: SceneNode) = > boolean
    ): SceneNode | null {
        const result = condition(this);
        if (result) {
            return this;
        }
        for (let i = 0; i < this.children.length; i++) {
            const result = this.children[i].conditionVisit(condition);
            if (result) {
                returnresult; }}return null;
    }
    getColor(): string | CanvasGradient | CanvasPattern {
        return `rgba(The ${this.color.r}.The ${this.color.g}.The ${this.color.b}.The ${this.color.a}) `;
    }

    findNodeByName(name: string): SceneNode | null {
        return this.conditionVisit((node) = >node.name === name); }}Copy the code

The core code

The core of the above code is the getWorldMatrix method, which is a recursive function that continuously looks up the parent element, returns its own local matrix if there is no parent element, and multiplies the world matrix of the parent element if there is one!

After obtaining the world matrix, we can multiply the (0, 0)[local coordinate] coordinate by the world matrix of the current node to get the world coordinate of the current node!

After completing our SceneNode, we can start building our scene. For simplicity, we will only build the sun – Earth – moon relationship.

We can create SceneNode one by one and append it to the corresponding node. Like this:


const solarSystem = new SceneNode('root');
const sun = new SceneNode('sun');
const earth = new SceneNode('earth'); solarSystem.addChild(sun); .Copy the code

But this is too cumbersome, and here the author borrows from JSX, which has the advantage of being able to build the UI “declaratively.” As follows:

function EarthOrbit() :SceneNode {
    const earthOrbitRadius = 150;
    const moonOrbitRadius = 50;
    // renderNode=true indicates that the node needs to be drawn.
    // renderComponent specifies the program to draw
    return (
        <earth-orbit
            rotation={30}
            width={earthOrbitRadius}
            renderComponent={OrbitRenderer}
            color={new Color(100.100.255)}
        >
            <earth
                x={earthOrbitRadius}
                renderNode={true}
                width={20}
                color={new Color(10.128.255)}
            >
                <moon-orbit
                    width={moonOrbitRadius}
                    rotation={30}
                    renderComponent={OrbitRenderer}
                    color={new Color(100.100.100)}
                >
                    <moon
                        renderNode={true}
                        x={moonOrbitRadius}
                        width={10}
                        color={new Color(128.128.128)} / >
                </moon-orbit>
            </earth>
        </earth-orbit>
    );
}


export function generateScene() :SceneNode {
    return (
        <root x={320} y={240}>
            <sun renderNode={true} width={50} color={new Color(255.20.0)} / >
            <EarthOrbit />
        </root>
    );
}
Copy the code

How is this done? @babel/plugin-transform-react-jsx replaces the react. createElement API with a custom API. For details, see @babel/plugin-transform-react-jsx.

The scene is defined, now all we need is a rendering program, let’s perfect a simple rendering program:


export class Canvas2DRenderer {
    constructor(public ctx: CanvasRenderingContext2D) {}

    render(scene: SceneNode) {
        const ctx = this.ctx;
        ctx.clearRect(0.0, ctx.canvas.width, ctx.canvas.height);
        const renderNodes: SceneNode[] = [];
        // Collect render nodes
        scene.visit((node) = > {
            if(node.renderNode || node.renderComponent) { renderNodes.push(node); }});for (let i = 0; i < renderNodes.length; i++) {
            ctx.beginPath();
            
            const node = renderNodes[i];
            if (node.renderComponent) {
                // If there is a custom rendering program, go to the custom rendering process.
                node.renderComponent(ctx);
            } else {
                const m = node.getWorldMatrix();
                const pos = vec2.fromValues(0.0);
                vec2.transformMat3(pos, pos, m);
                ctx.moveTo(pos[0], pos[1]);
                ctx.arc(pos[0], pos[1], node.width, 0.Math.PI * 2); ctx.fillStyle = node.getColor(); ctx.fill(); }}}}Copy the code

Finally, we use a main loop to get our solar system moving!! ~ ~ ~

function main() {
    earthOrbit.rotation += 0.5;
    moonOrbit.rotation += 1;
    renderer.render(scene);
    requestAnimationFrame(main);
}
Copy the code

The effect is as follows:

summary

This article briefly explains the concept of scene graph, and how the position relationship between the parent and child is affected, especially you need to have a clear understanding of the local coordinates and world coordinates of nodes. In the course of explaining scene diagrams, I interspersed some knowledge of using JSX syntax. This piece needs to download the source code to comb. If you do not know the place, welcome to leave a message below the article for discussion.

Click here to view the source code: Solar-system-Example: Scene Diagram & JSX syntax declaration scene example