I am participating in the Mid-Autumn Festival Creative Submission contest, please see: Mid-Autumn Festival Creative Submission Contest for details

Make a mooncake generator with three.js

preface

I have never touched threejs library before, but I have heard that it is very powerful. I just happened to catch up with the Mid-Autumn Festival activity, so I used Threejs to make a moon cake generator to practice my hand and experience how to make 3D effect on the Web side. I started from the basic graphics and got familiar with the basic construction process.

Results the overview

The first edition was a little rough and ready, and it’s still a work in progress…

Technology stack

The main use of mainstream Vite + VUe3 operation page data, ThreejS as the core 3D construction

Design ideas

Decided to use object-oriented thought design, easy to understand and management, the main function is divided into the following several parts

  • Design three shapes of mooncakes, including standard shape, polygon shape and round shape
  • Making a mooncake maker, using this maker to generate different mooncakes according to different parameters, is a simple factory method
  • Design a moon cake box class, used to install the moon cake, a moon cake a small square box, the number of restrictions, can not be added endlessly

Draw the implementation

Defining the basic interface

Define this interface to include general methods that the mooncake class needs to include, such as create and destroy methods, as well as methods for changing colors and patterns. Identify some basic capabilities to be implemented as follows:

export interface IMooncake {
  group: Group;
  changeColor(color: number) :void;
  changeTexture(url: string) :void;
  name: string;
  destroy(): void;
  create(): Mesh;
}
Copy the code

Define a base abstract class

Such main interface implementation moon cakes, and extract some general methods so that a subclass inherits, such as destruction and change texture design method, because the subclass basic are all the same, change the way the texture so drawn to the base class, if there are different in the subclass coverage, at the same time, the base class also defines a common attributes, such as height and radius, color names, etc., Instead of redefining the code in each subclass as follows:

abstract class BaseMoonCake implements IMooncake {
  name: string;
  group: Group;
  height: number;
  r: number;
  color: number;
  texture: string | undefined;
  moonCake: Mesh | undefined;
  constructor(params: {
    name: string;
    height: number;
    r: number;
    color: number; texture? :string;
  }) {
    const { name, height, r, color, texture } = params;
    this.group = new Group();
    this.name = name;
    this.texture = texture;
    this.height = height;
    this.r = r;
    this.color = color;
  }
  abstract changeColor(color: number) :void;
  changeTexture(url: string) :void {
    const { color, moonCake } = this;
    // Add texture
    const loader = new TextureLoader();
    loader.load(
      url,
      (texture) = > {
        const material = new MeshLambertMaterial({
          color: new Color(color),
          map: texture, // Set the texture map
        });

        texture.needsUpdate = true;
        if (moonCake) {
          console.log('render');
          moonCake.material = material;
        }
        render();
      },
      undefined.function (err) {
        console.error('Error occurred'); }); } destroy():void {
    // throw new Error('Method not implemented.');
    scene.remove(this.group);
    render();
  }
  abstract create(): Mesh<BufferGeometry, Material | Material[]>;
}

export default BaseMoonCake;
Copy the code

Define the scenario

Because I only need one scene, and multiple graphics need to be drawn into the same scene at the same time, I defined a hooks function to make it easier for other objects to reuse scene objects. And a renderer method. In order to better view the position, I added an auxiliary coordinate system, which was created by three. AxesHelper. The parameters passed in were the boundary of the coordinate system, namely the maximum length of x(red), Y (green), and Z (blue) axes.

export function useThree() :{
  scene: Scene;
  camera: Camera;
  renderer: WebGLRenderer;
} {
  if(! init) { scene = createScene(); camera = createCamera(); renderer = createRenderer();// addPlane();
    addAxis();
    render();

    init = true;
  }

  return {
    scene,
    camera,
    renderer,
  };
}


function createScene() :Scene {
  console.log('Create a Scene');
  const scene = new THREE.Scene(); // Create a scene
  scene.background = new THREE.Color(0xcce0ff);
  return scene;
}
function createCamera() {
  const camera = new THREE.PerspectiveCamera(
    75.window.innerWidth / window.innerHeight,
    0.1.500
  ); / / the camera
  camera.position.z = 20;
  return camera;
}

function addAxis() {
  / / coordinate system
  const axishelper = new THREE.AxesHelper(100);
  scene.add(axishelper);
}
export function render() {
  renderer.render(scene, camera);
}
Copy the code

Add the dom object in the renderer to the div of the page to display the renderer.

const { renderer } = useThree();
const threeContainer = ref();
onMounted(() = > {
  threeContainer.value.appendChild(renderer.domElement);
});
Copy the code

To make it easier to rotate objects manually, we need to introduce a controller, OrbitControls, that introduces methods not in the main class but in the Examples folder

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
Copy the code

The code is as follows:

export function controls() {
  const controls = new OrbitControls(camera, renderer.domElement); // Drag and rotate
  controls.addEventListener('change', render);
  controls.enableDamping = true;
  // The dynamic damping coefficient is the mouse drag rotation sensitivity
  controls.dampingFactor = 0.5;
  // Whether it can be scaled
  controls.enableZoom = true;
  // Whether to rotate automatically
  controls.autoRotate = false;
  // Set the maximum distance of the camera from the origin
  controls.minDistance = 10;
  // Set the maximum distance of the camera from the origin
  controls.maxDistance = 50;
  // Whether to enable right-click drag
  controls.enablePan = true;
}

Copy the code

Note: This method needs to pass in a DOM object, so it is initialized in onMounted

Standard moon cake implementation

Brief description

In the traditional sense, the standard moon cake is composed of a polygon in the middle and several semicircles around it, as shown below

implementation

It is mainly divided into the following steps

  • Create a definition calledMooncakeStandClass inherits the base classBaseMooncake
  • rewritecreateMethod, draw the mooncake shape (middle part + sides)

Draw the middle part of the moon cake:

The general steps are divided into the following three steps:

  1. Draw the shape

The four parameters are used respectively, the radius of the top surface, the radius of the bottom surface, and the height of the CylinderGeometry, and the total number of sides. If the number of sides is sufficient, the CylinderGeometry is approximate to a cylinder. In fact, there is no real surface, only enough polygons are cut into enough circles. It looks like a surface.

 const mooncakeCenterGeo = new CylinderGeometry(r, r, height, totalSides);
Copy the code
  1. Drawing material

The second step is to create the material element, which is to add color and texture to the drawing surface. If you set the wireFrame, draw with border lines instead of colors. This is useful for adjusting the shape position in the early stage, so that you can see the boundary clearly

   const defaultMaterial = new MeshBasicMaterial({
      color,
      // Wireframe: true, // set the border line
    });
Copy the code
  1. Combine the above two elements to create the polygon of the middle part of the moon cake
const moonCake = new Mesh(mooncakeCenterGeo, defaultMaterial); // Grid model object
Copy the code

Draw the edges and align them:

Analysis: Because of the standard round frame of the moon cake, first to construct a cylinder, in fact, a relatively more number of sides of the cylinder, and then draw half to form a semi-cylinder, and then find a side of the central cylinder, adjust the corresponding Angle and position, the other sides and so on.

  • First, calculate the position of the border semicircle:

Give a schematic diagram:

The yellow line represents the x axis, the y axis, and the gray line represents the coordinate line. As can be seen from the figure, to find the position of the semicircle is to find the x and y coordinates of the semicircle.

  1. Find the radius of a semicircle

This relatively simple, as long as know the size of the Angle 1, semicircle of the radius is the radius of the circle in the middle of the cosine values So how to evaluate Angle 1 This will need to use the knowledge of the junior high school, the angles of polygon angles, polygon angles and formula: (number of edges – 2) each interior Angle Angle = * 180 degrees polygon angles and/total number of edges in the code is:

const perAngel = ((totalSides - 2) * Math.PI) / totalSides;
Copy the code

Math.pi stands for 180 degrees, and Angle 1 is perAngel/2, so the radius of the semicircle is:

const cos = Math.cos(perAngel / 2);
const sideR = cos * r;
Copy the code
  1. X, y coordinates
    • Strives for the center distance

The y value of the center of a semicircle is the distance between the center of a semicircle and the center of a semicircle times the value of sine of Angle 2, so we need to figure out the value of the center of a semicircle, and we already got the radius of the small circle from the previous step, so we can just multiply sine of Angle 1 times the radius of the large circle r, or we can figure out the Pythagorean theorem.

const sin = Math.sin(perAngel / 2);
const shortR = sin * r; 
Copy the code
    • Find the central Angle:

Angle 2 is half the central Angle of each side, so let’s take the central Angle first. The coordinate system is 360 degrees, so it’s easy. The central Angle is 360 degrees divided by the number of sides.

const centerAngel = (Math.PI * 2) / totalSides;
Copy the code
    • For the results

So now we have the Angle and the distance, because each side has a different Angle, and using the X-axis as our reference axis, the Angle between the center of each side and the X-axis is exactly n times the Angle of the center, so we add currentIndex * centerAngel,

Final y coordinate:

const y = Math.sin(centerAngel / 2 + currentIndex * centerAngel) * shortR;
Copy the code

Similarly, the coordinates of x can be obtained:

const x = Math.cos(centerAngel / 2 + currentIndex * centerAngel) * shortR;
Copy the code
  • Second, graphic alignment
  1. Rotate the center cylinder so that the Angle is in the X-axis

When calculating the edge alignment between the semicircle and the polygon, the Angle of the polygon is located on the X-axis. However, when the polygon is 6-sided, the X-axis just bisects one side, so the Angle does not fall on the X-axis, resulting in the asymmetry between the semicircle and the edge of the polygon. Therefore, the Angle of any polygon can only be rotated to the x axis. After testing, we found that any polygon created by Threejs always has an Angle on the y axis, so we only need to rotate the graph 90 degrees.

The code is as follows:

const isRotateCenter = 90 % (360/ totalSides) ! = =0;
  if (isRotateCenter) {
    // Solve the center and edge mismatch problem
      mooncakeCenterGeo.rotateZ(Math.PI / 2);
  }
Copy the code

Since the graph was originally created face up, for easy adjustment, the face should be parallel to the x and y axes 2. By default, semicircle sides are perpendicular to the x axis, so to align with the sides of the larger circle you need to rotate by an Angle equal to Angle 2. For the other sides, each additional side increases the Angle by an additional degree of the central Angle, so the rotated Angle for each side is as follows:

sideGeometry.rotateZ(Math.PI / totalSides + centerAngel * currentIndex);
Copy the code

Math.PI*2 / (totalSides*2

Finally, translate the x and y coordinates into effect

sideGeometry.translate(x, y, 0);
Copy the code

Casts a shadow on the border

  1. Material needs to be usedMeshLambertMaterialcreate
  2. Set up themesh.castShadow = true;

Add the group

Provides a group of three concepts, is multiple figures can be packaged together and then operating together, as a moon cake, edge and center as a whole, of course, so in the create method, will generate multiple edge and center cylinder is packaged into a group, facilitate subsequent operations for the entire element position, pay attention to: Now that the mesh has been added to a group, we pass in the group as a parameter instead of passing in the mesh object

At this point, a standard moon cake was made!!

Realization of circular moon cake

Analysis:

To make a moon cake similar to ‘Zilai Red’ is actually a half sphere facing up, the sphere is actually composed of multiple sides, if the face is cut enough, it is enough round, each side is a triangle, in order to achieve a half sphere, only need from 0 degrees to 90 degrees longitudinal, transverse to 360 degrees rotation

implementation

Use SphereGeometry to construct spheres

 const geometry = new SphereGeometry(
      this.r,
      30.// Number of horizontal shards
      30.// The number of vertical shards
      0.// Specify the horizontal starting Angle
      Math.PI * 2.// Horizontal terminating Angle
      0.// Vertical starting Angle
      Math.PI / 2// Vertical termination Angle
    );
Copy the code

The above implementation can create a hemisphere, but the bottom is empty. Check the relevant API, there is no operation of sealing the bottom, so we can only recreate a TWO-DIMENSIONAL plane as the bottom, sealing. Since the created plane is created in the X and Y coordinate system, the whole plane is perpendicular to the Z axis, and to make it parallel to the Z axis, we need to rotate 90 degrees around the X axis

const geometry = new CircleGeometry(this.r, 30);
const material = new MeshBasicMaterial({ color: this.color });
const circle = new Mesh(geometry, material);
circle.rotateX(Math.PI / 2);
this.group.add(circle);
Copy the code

Create a multi-angle mooncake

Analysis:

Implementation approach basic and standard of moon cakes, just to edge into the shape of a triangle, positioning difference, because the center of the triangle and round circle is different, the standard of moon cake on the center of the circle is just at the edge of the polygon, and the center of the triangle is outside the polygon edges, thus calculate the coordinates of the more complicated it, in order to facilitate understanding, please refer to the below

Key points:

  • Calculate triangular coordinates

So if you want to figure out the x and y coordinates, you just multiply the distance from the center of the circle times the sine and cosine of Angle 2, but the distance from the center of the circle is in two pieces, and we already figured out the first one, so you just have to figure out the second one, and you just add them. Since they are all cylinders, the triangles created are also equilateral triangles, so the center of the triangle (the intersection of the red line) and the lines of each Angle will bisect each Angle. Originally, each Angle is 60 degrees, but after bisecting it, it is 30 degrees, and Angle 3 is 30 degrees. So deltaX (green line) is the radius of the triangle times sin30 degrees. Now you just need to figure out the radius of the triangle. According to the method of finding the radius of the standard moon cake, you can get half of the sides of the polygon, and it happens that this value divided by the radius of the triangle is equal to cosine 30 degrees. So it’s easy to say that the radius of the triangle is zero

const sideR = (cos * r) / cos30;
Copy the code

So deltaX (the green line) is sideR * sin30 and the final distance between the centers is the green line plus the blue line:

sideR * sin30 + shortR;
Copy the code
  • Rotating graphics

If the triangle is not rotated and the Angle is facing down, in order to be exactly flush with the sides, the triangle should be rotated to the right by a certain Angle, which is half of the inner Angle minus 60 degrees, plus the fixed offset of each side. The code is as follows:

 sideGeometry.rotateZ(
  centerAngel * currentIndex - (perAngel / 2 - Math.PI / 3));Copy the code

Create a simple factory to produce mooncakes

By passing in parameters, generate different types of moon cakes and create a simple factory class

export default class MooncakeFactory {
  makeOne(data: IMooncakeParam) {
    let mooncake: BaseMoonCake;
    switch (data.type) {
      case TYPE.ROUND:
        mooncake = new MooncakeRound(data);
        break;
      case TYPE.STANDARD:
        mooncake = new MooncakeStand(data);
        break;
      case TYPE.TRINGLE:
        mooncake = new MooncakeTringle(data);
        break;
    }
    returnmooncake; }}Copy the code

To make the moon cake box

Analysis:

First of all, a moon cake is a box, the length and width of the box are greater than the diameter of the moon cake plus the diameter of the side, the number of moon cake boxes should be limited, not unlimited addition, which needs to specify several rows and columns when creating, define the length, width and height of each box

implementation

First generate boxes based on the number of rows and columns, and space between boxes create a cube box as follows

 const geometry = new BoxGeometry(
   this.perWidth,
   this.perLong,
   this.height
);
Copy the code

The box is generated from left to right, so only the x value of the box needs to be dynamically changed. For the box of the second layer, only the y value needs to be changed. The code is as follows:

 for (let r = 0; r < this.rows; r++) {
      for (let i = 0; i < this.cols; i++) {
            cubeLine.position.x = i * (this.perLong + 1);
            cubeLine.position.y = r * (this.perWidth + 1); }}Copy the code

When adding moon cakes, according to the number of existing moon cakes, if the number is full, the prompt will be given to determine which box to add to the current, dynamic calculation of the location implementation code is as follows:

	
  addMooncake(mooncake: BaseMoonCake) {
    if (this.mooncakes.length === this.rows * this.cols) {
      throw new Error('It won't fit.');
    } else {
      mooncake.group.position.x =
        (this.mooncakes.length % this.cols) * (this.perLong + 1);
      mooncake.group.position.y =
        Math.floor(this.mooncakes.length / this.cols) * this.perWidth;
        this.mooncakes.push(mooncake); }}Copy the code

Note: the coordinates of the newly added moon cake must be +1 than the previous coordinates, so directly use the previous total as the coordinates of the new moon cake. For example, there are three in a row, then when adding the fourth moon cake, the actual coordinates are (3,1), which should be placed in the first one of the second row, so its starting coordinate is 0, and the vertical coordinate should be +1

Delete the moon cake

This is very simple, that is, delete the group corresponding to the moon cake directly from the scene. This method is written in the base class. It should be noted that after deletion, it should be re-rendered, otherwise it will not take effect

destroy(): void {
    scene.remove(this.group);
    render();
  }
Copy the code

pending

  • Perfect scene, add more Mid-Autumn festival elements
  • Add texture to the outer part of the gift box
  • Mooncakes support custom text or pictures
  • Manipulate the location of the moon cake
  • Use color pickers to customize colors

The last

Through this development, the basic understanding of Threejs how to build the model, the use of basic graphics, the basic principle of surface implementation, and texture mapping implementation, feel if make the kind of 3D effect similar to animation, it is really quite difficult, of course, that effect may need professional modeling tools, rather than purely manual JS creation, Threejs can import files exported by the modeling tool, which comes in handy. First contact with 3d field used some junior high school geometry knowledge, it seems that in-depth will involve more geometric knowledge, so lay a good mathematical foundation is very necessary!!

Reference Documents:

www.webgl3d.cn/Three.js/