Creating Laser‑Cutting Effects with Three.js, SVG Logos and Vite
This tutorial demonstrates how to use Three.js, TypeScript and Vite to load an SVG logo, extract its Bézier curve points, generate evenly spaced laser paths, animate multiple lasers with Line2, and finally separate the logo from a floor using shape holes, providing complete source code and visual results.
Creating Laser‑Cutting Effects with Three.js, SVG Logos and Vite
This article walks through a step‑by‑step implementation of a laser‑cutting visual effect in a WebGL scene. It covers preparation, SVG loading, curve point extraction, dynamic point distribution, floor creation, laser generation, animation, and logo separation.
Preparation
threejs
TypeScript (ts)
Vite
Start by obtaining an SVG file of a logo (e.g., a bird) and adding it to the scene.
Rendering the SVG
// 加载模型
const loadModel = async () => {
svgLoader.load('./svg/logo.svg', (data) => {
const material = new THREE.MeshBasicMaterial({
color: '#000',
});
for (const path of data.paths) {
const shapes = SVGLoader.createShapes(path);
for (const shape of shapes) {
const geometry = new THREE.ShapeGeometry(shape);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh)
}
}
renderer.setAnimationLoop(render)
})
}
loadModel()The loaded shape contains all key points of the logo. These points are later used to build laser motion paths.
Extracting Curve Points
The CubicBezierCurve class provides getPoints(divisions) to sample points along a curve. The default division count is 5.
.getPoints ( divisions : Integer ) : Array divisions -- Number of segments to divide the curve into. Default is 5.
To visualise the points, small cubes are created at each sampled position:
// 加载模型
const loadModel = async () => {
...
for (const curve of shape.curves) {
const points = curve.getPoints(100);
console.log(points);
for (const v2 of points) {
const geometry = new THREE.BoxGeometry(10, 10, 10);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(v2.x, v2.y, 0)
scene.add(cube);
}
}
...
renderer.setAnimationLoop(render)
}
loadModel()Because point density varies with curve length, the number of points is adjusted dynamically using curve.getLength() :
const length = curve.getLength();
const points = curve.getPoints(Math.floor(length / 10));Collecting Point Information
All extracted points are stored in arrays of THREE.Vector2 and later converted to THREE.Vector3 for further processing.
// 新建一个二维数组用于收集组成logo的点位信息
let divisionPoints: THREE.Vector2[] = []
let divisionPoints: THREE.Vector3[] = []
let list: THREE.Vector3[] = []
const length = curve.getLength();
const points = curve.getPoints(Math.floor(length / 20));
for (const v2 of points) {
v2.divideScalar(20) // 缩小 logo
const v3 = new THREE.Vector3(v2.x, 0, v2.y)
list.push(v3)
divisionPoints.push(v2)
}
paths.push(list)Creating the Floor and Centering the Logo
const logoSize = new THREE.Vector2()
const logoCenter = new THREE.Vector2()
const floorHeight = 3
let floor: THREE.Mesh | null
let floorOffset = 8Using the collected points, a bounding box is computed to size the floor and position the logo at the scene centre.
const handlePaths = () => {
const box2 = new THREE.Box2();
box2.setFromPoints(divisionPoints)
box2.getSize(logoSize)
box2.getCenter(logoCenter)
createFloor()
}Laser Generation
Four (or any number) lasers are created. Their start points are placed on a circular arc around the logo, and end points follow the sampled logo points.
// 激光组
const buiGroup = new THREE.Group()
const buiDivide = 3
const buiOffsetH = 30
const buiCount = 10
const createBui = () => {
var R = Math.min(...logoSize.toArray()) / buiDivide; // 圆弧半径
var N = buiCount * 10; // 圆弧点数
const vertices: number[] = []
for (var i = 0; i < N; i++) {
var angle = 2 * Math.PI / N * i;
var x = R * Math.sin(angle);
var y = R * Math.cos(angle);
vertices.push(x, buiOffsetH, y)
}
initArc(vertices)
for (let i = 0; i < buiCount; i++) {
const startPoint = new THREE.Vector3().fromArray(vertices, i * buiCount * 3)
const endPoint = new THREE.Vector3()
endPoint.copy(startPoint.clone().setY(-floorHeight))
const color = new THREE.Color(Math.random() * 0xffffff)
initCube(startPoint, color)
initCube(endPoint, color)
}
}Using Line2 for Adjustable Width
Because the native linewidth property is limited to 1, the tutorial uses Line2 from three‑js examples.
import { Line2 } from "three/examples/jsm/lines/Line2.js";
import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
const createLine2 = (linePoints: number[]) => {
const geometry = new LineGeometry();
geometry.setPositions(linePoints);
const matLine = new LineMaterial({
linewidth: 0.002,
dashed: true,
opacity: 0.5,
color: 0x4cb2f8,
vertexColors: false,
});
let biu = new Line2(geometry, matLine);
biuGroup.add(biu);
}Laser Animation
The biuAnimate function updates each laser’s endpoint by iterating over the divided logo points, splitting them among the lasers, and feeding the data to Line2 at a fixed interval.
const biuAnimate = () => {
const allPoints = [...divisionPoints]
const len = Math.ceil(allPoints.length / biuCount)
for (let i = 0; i < biuCount; i++) {
const points = allPoints.splice(0, len);
const biu = biuGroup.children[i] as Line2;
const biuStartPoint = biu.userData.startPoint
let j = 0;
const interval = setInterval(() => {
if (j < points.length) {
const point = points[j]
const attrPosition = [...biuStartPoint.toArray(), ...new THREE.Vector3(point.x, floorHeight/2, point.y).add(getlogoPos()).toArray()]
uploadBiuLine(biu, attrPosition)
j++
} else {
clearInterval(interval)
}
}, 100)
}
}
const uploadBiuLine = (line2: Line2, attrPosition) => {
const geometry = new LineGeometry();
line2.geometry.setPositions(attrPosition);
}Drawing the Logo with Laser Paths
As each laser moves, a THREE.Line is updated with the visited points, gradually reconstructing the logo.
for (let i = 0; i < biuCount; i++) {
const line = createLine()
scene.add(line)
const interval = setInterval(() => {
if (j < points.length) {
const point = points[j]
const endArray = new THREE.Vector3(point.x, floorHeight/2, point.y).add(getlogoPos()).toArray()
const attrPosition = [...biuStartPoint.toArray(), ...endArray]
// update line geometry
const logoLinePointArray = [...(line.geometry.attributes['position']?.array || [])];
logoLinePointArray.push(...endArray)
line.geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(logoLinePointArray), 3))
j++
} else {
clearInterval(interval)
}
}, 100)
}Logo Separation (Boolean / Shape Hole)
After the laser finishes, the logo is separated from the floor. For simple shapes, threeBSP can be used, but for complex logos the tutorial prefers creating a shape with a hole.
// Create shapes for logo and excess part
const logoShape = new THREE.Shape()
const moreShape = new THREE.Shape()
// In loadModel, build logoShape path
if (i === 0) {
logoShape.moveTo(v2.x, v2.y)
} else {
logoShape.lineTo(v2.x, v2.y)
}
// In createFloor, build floor shape and add logo hole
moreShape.moveTo(floorSize.x / 2, floorSize.y / 2)
moreShape.lineTo(-floorSize.x / 2, floorSize.y / 2)
moreShape.lineTo(-floorSize.x / 2, -floorSize.y / 2)
moreShape.lineTo(floorSize.x / 2, -floorSize.y / 2)
const path = new THREE.Path()
divisionPoints.forEach((point, i) => {
point.add(logoCenter.clone().negate())
if (i === 0) path.moveTo(point.x, point.y)
else path.lineTo(point.x, point.y)
})
moreShape.holes.push(path)
// Extrude shapes to meshes
logoMesh = createLogoMesh(logoShape)
moreMesh = createLogoMesh(moreShape)
scene.add(logoMesh)
scene.add(moreMesh)Final Results
The tutorial showcases the final animated GIFs for several logos (Twitter, Douyin, GitHub) and provides a link to the complete source code repository.
All code versions are referenced as v.logo.1.0.x and can be downloaded from the provided Gitee repository.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.