Recreating the Xiaomi SU7 Showcase Webpage with three.js and Custom Shaders
This article walks through reproducing the stunning Xiaomi SU7 showcase webpage by using three.js, custom GLSL shaders, and several helper libraries to implement dynamic environment map switching, reflective ground, tunnel traversal, bloom, and camera shake effects, providing complete source code and explanations.
This guide demonstrates how to replicate the impressive Xiaomi SU7 showcase webpage using three.js and custom shader techniques.
Libraries Used
In addition to the core three.js library, the following helper libraries are employed:
kokomi.js – a thin wrapper around three.js .
lygia – a collection of useful shader functions.
postprocessing – provides various post‑processing filter effects.
gsap – a popular JavaScript animation library.
Basic Scene Setup
The initial scene loads the SU7 3D model from Sketchfab and displays it with a simple camera and lighting configuration.
Environment Map Switching
To transition the car from night to day, an environment map is swapped using an off‑screen framebuffer object ( FBO ) that renders a full‑screen quad sampling two cubemap textures and interpolating them with a mix function.
class DynamicEnv extends kokomi.Component {
constructor(base, config = {}) {
super(base);
const { envmap1, envmap2 } = config;
const envData = envmap1?.source.data;
const fbo = new kokomi.FBO(this.base, { width: envData.width, height: envData.height });
this.fbo = fbo;
this.envmap.mapping = THREE.CubeUVReflectionMapping;
const material = new THREE.ShaderMaterial({
vertexShader: dynamicEnvVertexShader,
fragmentShader: dynamicEnvFragmentShader,
uniforms: {
uEnvmap1: { value: envmap1 },
uEnvmap2: { value: envmap2 },
uWeight: { value: 0 },
uIntensity: { value: 1 },
},
});
this.material = material;
const quad = new kokomi.FullScreenQuad(material);
this.quad = quad;
}
update() {
this.base.renderer.setRenderTarget(this.fbo.rt);
this.quad.render(this.base.renderer);
this.base.renderer.setRenderTarget(null);
}
}The fragment shader for the FBO performs the interpolation:
uniform sampler2D uEnvmap1;
uniform sampler2D uEnvmap2;
uniform float uWeight;
uniform float uIntensity;
void main(){
vec2 uv=vUv;
vec3 envmap1=texture(uEnvmap1,uv).xyz;
vec3 envmap2=texture(uEnvmap2,uv).xyz;
vec3 col=mix(envmap1,envmap2,uWeight)*uIntensity;
gl_FragColor=vec4(col,1.);
}Reflective Ground
A custom reflective material is built on top of kokomi.js by feeding the reflected vector and a reflection texture into a shader, then adding normal‑map based roughness and Fresnel blending to achieve a realistic look.
uniform float iTime;
uniform vec2 iResolution;
uniform vec2 iMouse;
varying vec2 vUv_;
varying vec4 vWorldPosition;
void main(){
vec3 viewDir=vViewPosition;
float d=length(viewDir);
viewDir=normalize(viewDir);
vec2 distortion=surfaceNormal.xz*(.001+1./d);
vec4 reflectPoint=uReflectMatrix*vWorldPosition;
reflectPoint=reflectPoint/reflectPoint.w;
vec3 reflectionSample=texture(uReflectTexture,reflectPoint.xy+distortion).xyz;
// ... apply Fresnel and roughness ...
}Tunnel Traversal Effect
The tunnel animation is created entirely with fragment‑shader logic, using a simplex noise function, random color generation, and smoothstep masking to draw moving lines on the inner surface of a cylinder.
// Simplex noise implementation (from Shadertoy)
vec2 hash(vec2 p){
p=vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3)));
return -1.+2.*fract(sin(p)*43758.5453123);
}
float noise(in vec2 p){
const float K1=.366025404;
const float K2=.211324865;
vec2 i=floor(p+(p.x+p.y)*K1);
vec2 a=p-i+(i.x+i.y)*K2;
float m=step(a.y,a.x);
vec2 o=vec2(m,1.-m);
vec2 b=a-o+K2;
vec2 c=a-1.+2.*K2;
vec3 h=max(.5-vec3(dot(a,a),dot(b,b),dot(c,c)),0.);
vec3 n=h*h*h*h*vec3(dot(a,hash(i+0.)),dot(b,hash(i+o)),dot(c,hash(i+1.)));
return dot(n,vec3(70.));
}Random colors are generated per‑pixel using a custom pos2col function that samples three independent random values, and a bilinear interpolation routine creates smooth color noise.
vec3 pos2col(vec2 i){
i+=vec2(9.,0.);
float r=random(i+vec2(12.,2.));
float g=random(i+vec2(7.,5.));
float b=random(i);
return vec3(r,g,b);
}Time‑based offsets on the UV coordinates animate the lines, while additional post‑processing such as bloom and a custom mip‑mapped reflection texture add depth.
Flow‑Light Effect and Camera Shake
A secondary FBO rendered with a CubeCamera provides a dynamic environment map that creates a flowing light overlay. Camera shake is achieved by generating per‑axis FBM noise, scaling it, and applying a smooth GSAP tween to the camera position.
class CameraShake extends kokomi.Component {
constructor(base, config = {}) {
super(base);
const { intensity = 1 } = config;
this.intensity = intensity;
this.tweenedPosOffset = new THREE.Vector3(0,0,0);
}
update(){
const t = this.base.clock.elapsedTime;
const posOffset = new THREE.Vector3(
fbm({frequency: t*0.5 + THREE.MathUtils.randFloat(-10000,0), amplitude:2}),
fbm({frequency: t*0.5 + THREE.MathUtils.randFloat(-10000,0), amplitude:2}),
fbm({frequency: t*0.5 + THREE.MathUtils.randFloat(-10000,0), amplitude:2})
);
posOffset.multiplyScalar(0.1 * this.intensity);
gsap.to(this.tweenedPosOffset,{x:posOffset.x,y:posOffset.y,z:posOffset.z,duration:1.2});
this.base.camera.position.add(this.tweenedPosOffset);
}
}Final Result
The combined techniques produce a faithful replica of the original SU7 showcase, featuring dynamic day/night transitions, reflective ground with roughness, an immersive tunnel of animated lines, bloom glow, flowing light, and subtle camera motion.
Source Code
The complete project is available on GitHub: https://github.com/alphardex/su7-replica .
Contact
For questions or further discussion, reach out via WeChat (ID: Blacklurker ) or follow the author’s public account “技术干货”.
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.