Creating a 3D Turbine Visualization with Vite, TypeScript, and Three.js
This tutorial walks through building an interactive 3D turbine scene using Vite, TypeScript, and Three.js, covering model loading, lighting, orthographic camera setup, trackball controls, edge‑line generation, bloom post‑processing, GUI tweaking, and animated turbine rotation, with full source links.
After a long break from writing visualization posts, the author was inspired by an image and decided to recreate it using the current web stack: vite + typescript + threejs .
Inspiration Image
The reference picture is displayed to guide the visual style.
Preparation
Finding a Model
A suitable 3D turbine model was downloaded from Sketchfab ( https://sketchfab.com/3d-models/turbine-01-e03a3c7ce147460e948f56573d1fdf87 ).
Main Work
Loading the Model
The model is loaded with a custom gltfloader function, then rendered using the scene embedded in the GLTF file.
async function loadModel() {
const res = await loadGltf('../src/assets/models/机械零件/1/scene.gltf')
// Use the original scene from the model
scene = res.scene
init()
animate();
}Lighting
Both AmbientLight and DirectionalLight are added. The directional light follows the camera position so the model is always illuminated.
const ambientLight = new AmbientLight(0xffffff, 40);
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 40);
directionalLight.position.copy(camera.position);
scene.add(directionalLight);Camera
An orthographic camera is used for a clean, non‑perspective view of the small model.
const width = window.innerWidth, height = window.innerHeight;
const offset = 500;
camera = new THREE.OrthographicCamera(width / -offset, width / offset, height / offset, height / -offset, 0.001, 100000);
camera.position.copy(new THREE.Vector3(0, 0, -3));Controls
TrackballControls allow free manipulation of the scene, and the directional light position is updated on every control change.
controls = new TrackballControls(camera, renderer.domElement);
controls.rotateSpeed = 4.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.8;
controls.addEventListener('change', () => {
directionalLight.position.copy(camera.position);
});Creating Edge Lines
After the model is loaded, each mesh is traversed, its world transform is captured, and an edge line is generated using EdgesGeometry and LineSegments . All lines share a single LineBasicMaterial to save memory.
const lineGroup = new THREE.Group();
Sketchfab_model.traverse((mesh) => changeModelMaterial(mesh, lineGroup));
scene.add(lineGroup);
export const changeModelMaterial = (object, lineGroup) => {
const mesh = object as any;
if (mesh.isMesh) {
const quaternion = new THREE.Quaternion();
const worldPos = new THREE.Vector3();
const worldScale = new THREE.Vector3();
mesh.getWorldQuaternion(quaternion);
mesh.getWorldPosition(worldPos);
mesh.getWorldScale(worldScale);
mesh.material.transparent = true;
mesh.material.opacity = 0.4;
const line = getLine(mesh, 30);
line.quaternion.copy(quaternion);
line.position.copy(worldPos);
line.scale.copy(worldScale);
lineGroup.add(line);
}
};Line Helper Function
let color = new THREE.Color('#0fb1fb');
const material = new THREE.LineBasicMaterial({ color: new THREE.Color(color), depthTest: true, transparent: true });
export const getLine = (object, thresholdAngle = 1, color = new THREE.Color('#ff0ff0'), opacity = 1) => {
const edges = new THREE.EdgesGeometry(object.geometry, thresholdAngle);
const line = new THREE.LineSegments(edges);
material.opacity = opacity;
line.material = material;
return line;
};Bloom Effect (Glow)
The author integrates post‑processing passes from Three.js examples: RenderPass , EffectComposer , UnrealBloomPass , ShaderPass , and OutputPass . Parameters such as threshold, strength, radius, and exposure are exposed via a GUI.
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass';
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass';
const params = { threshold: 1, strength: 1, radius: 0.84, exposure: 1.55 };
export const unreal = (scene, camera, renderer, width, height) => {
const renderScene = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(new THREE.Vector2(width, height), 1.5, 0.4, 0.85);
bloomPass.threshold = params.threshold;
bloomPass.strength = params.strength;
bloomPass.radius = params.radius;
const bloomComposer = new EffectComposer(renderer);
bloomComposer.addPass(renderScene);
bloomComposer.addPass(bloomPass);
const mixPass = new ShaderPass(new THREE.ShaderMaterial({
uniforms: { baseTexture: { value: null }, bloomTexture: { value: bloomComposer.renderTarget2.texture } },
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`,
fragmentShader: `
uniform sampler2D baseTexture;
uniform sampler2D bloomTexture;
varying vec2 vUv;
void main() {
gl_FragColor = texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv );
}
`
}), 'baseTexture');
mixPass.needsSwap = true;
const outputPass = new OutputPass();
const finalComposer = new EffectComposer(renderer);
finalComposer.addPass(renderScene);
finalComposer.addPass(mixPass);
finalComposer.addPass(outputPass);
return { finalComposer, bloomComposer, renderScene, bloomPass };
};GUI Controls
function gui() {
const gui = new GUI();
const bloomFolder = gui.addFolder('bloom');
bloomFolder.add(params, 'threshold', 0.0, 1.0).onChange(value => { bloomPass.threshold = Number(value); });
bloomFolder.add(params, 'strength', 0.0, 3.0).onChange(value => { bloomPass.strength = Number(value); });
gui.add(params, 'radius', 0.0, 1.0).step(0.01).onChange(value => { bloomPass.radius = Number(value); });
}Animating the Turbine
The turbine meshes are retrieved by name and rotated each frame to simulate motion.
// Retrieve turbine objects
hull_turbine = scene.getObjectByName('hull_turbine');
hull_turbine_line = lineGroup.getObjectByName('hull_turbine_line');
blades_turbine_003 = scene.getObjectByName('blades_turbine_003');
blades_turbine_003_line = scene.getObjectByName('blades_turbine_003_line');
let rotationX = 0.03;
function render() {
// ... other updates ...
if (hull_turbine && hull_turbine_line) {
hull_turbine.rotation.x += rotationX;
hull_turbine_line.rotation.x += rotationX;
}
if (blades_turbine_003) {
blades_turbine_003.rotation.x += rotationX;
}
blades_turbine_003_line && (blades_turbine_003_line.rotation.x += rotationX);
}Advanced Ideas
The author mentions extending the scene into a turbine monitoring system where IoT data could trigger visual alerts (e.g., turning parts red).
All code versions are hosted on Gitee, with links to v1.0.2 through v1.0.5 and the master 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.