Three.js 网格地板教程
网格地板 ·Gird Floor· ▶ 在线运行案例
- 案例合集:三维可视化功能案例(threehub.cn)
- 开源仓库github地址:https://github.com/z2586300277/three-cesium-examples
- 400个案例代码:网盘链接
你将学到什么
- ShaderMaterial 自定义着色器实现核心视觉效果
- OrbitControls 相机轨道交互
- 场景雾效增强纵深
requestAnimationFrame渲染循环与resize自适应
效果说明
本案例演示网格地板效果:基于 WebGL 实现「网格地板」可视化效果,附完整可运行源码;核心用到 ShaderMaterial、OrbitControls、场景雾效增强纵深。建议先打开文首在线案例查看动态画面,再对照下方源码逐步理解。
核心概念
- Scene / Camera / WebGLRenderer构成最小渲染闭环;大场景可开
logarithmicDepthBuffer缓解 Z-fighting。 - ShaderMaterial通过
uniforms+ 自定义 GLSL 控制逐像素/逐点效果;透明粒子常配合depthTest: false。 - OrbitControls提供轨道旋转/缩放;开启
enableDamping后需在 animate 中controls.update()。
实现步骤
- 搭建 Scene、PerspectiveCamera、WebGLRenderer,挂载 canvas 并处理
resize - 定义 uniforms / onBeforeCompile 或 ShaderMaterial,编写 GLSL 与材质参数
- 创建 OrbitControls(及 Raycaster 等交互控件,若源码包含)
- 在
requestAnimationFrame循环中更新状态并 render(Cesium 为viewer.render或自动渲染) 代码要点
import * as THREE from 'three'import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' import { Pane } from 'tweakpane'
const baseVertexShader =
#includevarying vec2 vUv;
void main() { gl_Position = projectionMatrixmodelViewMatrixvec4(position, 1.0);
vUv = uv;
#include #include #include }const baseFragmentShader =
#includeuniform float uGridThickness; uniform vec3 uGridColor; uniform float uCrossScale; uniform float uCrossThickness; uniform float uCross; uniform vec3 uCrossColor; uniform vec3 uFloorColor;
varying vec2 vUv;
float gridFloor(vec2 uv, vec2 lineWidth) { //💡 derivatives of original uv // to create anti-aliasing line with smoothstep // how much a specific value is changing between one pixel and the next // width change depending on angle & distance from camera can be found with space partial derivatives // fwidth - approximation of derivatives //float lineAA = fwidth(uv.x); // vec2 uvDeriv = fwidth(uv); vec4 uvDDXY = vec4(dFdx(uv), dFdy(uv)); vec2 uvDeriv = vec2(length(uvDDXY.xz), length(uvDDXY.yw));
// 💡 Invert Line Trick // since 0.5 clamp was use, to handle line thickness > 0.5 // draw black lines on white offset by half a grid width bool invertLine = lineWidth.x > 0.5; vec2 targetWidth = invertLine ? 1.0 - lineWidth : lineWidth;
// 💡 Phone-wire AA // STEP 1: ensure line does not get smaller than one pixel // if so, we will clamp it to one pixel // vec2 drawWidth = max(uvDeriv, lineWidth); // clamp to 0.5 to ensure line fades to grey, not black vec2 drawWidth = clamp(targetWidth, uvDeriv, vec2(0.5));
// 💡 1 pixel wide smoothstep can be too sharp causing aliasing // hence using 1.5 pixel wide smoothstep // AA - anti-aliasing vec2 lineAA = uvDeriv * 1.5; //💡 prepare uv for lines // 0-1(uv) 👉 0-2(multiply) 👉 -1-0-1(shift) 👉 1-0-1(absolute) // 👉 0-1-0(shift) make white at center(0,0) position // (fract) - make sawtooth wave //float lineUV = 1.0 - abs(fract(uv.x) * 2.0 - 1.0); vec2 gridUV = abs(fract(uv) * 2.0 - 1.0); gridUV = invertLine ? gridUV : 1.0 - gridUV;
//💡 repeating lines // use the derivative to make the lines smooth //float line = smoothstep(lineWidth.x + lineAA, lineWidth.x - lineAA,lineUV); vec2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
// 💡 Phone-wire AA // STEP 2: fades the line out as it gets thinner // how thick we want divided by how thick we’re drawing grid2 *= clamp(targetWidth / drawWidth, 0.0, 1.0);
// 💡 Moire Suppresion // grid cells < a pixel(when derivative > 1.0), moire pattern can appear // note: after the 0.5 clamp, moire would be more pronounced, but in my case, i do not see any moire // fade to solid color when 0.5 > derivative > 1.0 // anti-aliased lines start to merge grid2 = mix(grid2, targetWidth, clamp(uvDeriv * 2.0 - 1.0, 0.0, 1.0)); grid2 = invertLine ? 1.0 - grid2 : grid2;
// overlap xy lines float grid = mix(grid2.x, 1.0, grid2.y);
return grid; }
float crossFloor(vec2 uv, float scale, float thickness, float crossIntensity) { vec2 lineWidth = vec2(thickness);
//💡 derivatives of original uv // to create anti-aliasing line with smoothstep // how much a specific value is changing between one pixel and the next // width change depending on angle & distance from camera can be found with space partial derivatives // fwidth - approximation of derivatives //float lineAA = fwidth(uv.x); // vec2 uvDeriv = fwidth(uv); vec4 uvDDXY = vec4(dFdx(uv), dFdy(uv)); vec2 uvDeriv = vec2(length(uvDDXY.xz), length(uvDDXY.yw));
// 💡 Invert Line Trick // since 0.5 clamp was use, to handle line thickness > 0.5 // draw black lines on white offset by half a grid width bool invertLine = lineWidth.x > 0.5; // vec2 targetWidth = invertLine ? 1.0 - lineWidth : lineWidth; vec2 targetWidth = lineWidth;
// 💡 Phone-wire AA // STEP 1: ensure line does not get smaller than one pixel // if so, we will clamp it to one pixel // vec2 drawWidth = max(uvDeriv, lineWidth); // clamp to 0.5 to ensure line fades to grey, not black vec2 drawWidth = clamp(targetWidth, uvDeriv, vec2(0.5));
// 💡 1 pixel wide smoothstep can be too sharp causing aliasing // hence using 1.5 pixel wide smoothstep // AA - anti-aliasing vec2 lineAA = uvDeriv * 1.5; // Cross Intensity float cutOffX = abs(fract(uv.y) * 2.0 - 1.0) > crossIntensity ? 1.0 : 0.0; float cutOffY = abs(fract(uv.x) * 2.0 - 1.0) > crossIntensity ? 1.0 : 0.0;
//💡 prepare uv for lines // 0-1(uv) 👉 0-2(multiply) 👉 -1-0-1(shift) 👉 1-0-1(absolute) // 👉 0-1-0(shift) make white at center(0,0) position // (fract) - make sawtooth wave //float lineUV = 1.0 - abs(fract(uv.x) * 2.0 - 1.0); // vec2 gridUV = abs(fract(uv) * 2.0 - 1.0); // UV float uvX = abs(fract(uv.x) * 2.0 - 1.0) + cutOffX; float uvY = abs(fract(uv.y) * 2.0 - 1.0) + cutOffY; vec2 gridUV = vec2(uvX, uvY);
// gridUV = invertLine ? gridUV : 1.0 - gridUV;
//💡 repeating lines // use the derivative to make the lines smooth //float line = smoothstep(lineWidth.x + lineAA, lineWidth.x - lineAA,lineUV); vec2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
// 💡 Phone-wire AA // STEP 2: fades the line out as it gets thinner // how thick we want divided by how thick we’re drawing grid2 *= clamp(targetWidth / drawWidth, 0.0, 1.0);
// 💡 Moire Suppresion // grid cells < a pixel(when derivative > 1.0), moire pattern can appear // note: after the 0.5 clamp, moire would be more pronounced, but in my case, i do not see any moire // fade to solid color when 0.5 > derivative > 1.0 // anti-aliased lines start to merge grid2 = mix(grid2, targetWidth, clamp(uvDeriv * 2.0 - 1.0, 0.0, 1.0)); // grid2 = invertLine ? 1.0 - grid2 : grid2;
// overlap xy lines float grid = mix(grid2.x, 1.0, grid2.y);
return grid; }
void main() { vec2 lineWidth = vec2(uGridThickness); //💡 scaling uv to get multiple repeating lines vec2 uv = vUv * 20.0;
// grid floor float grid = gridFloor(uv, lineWidth); // mix with floor color vec3 gridColor = mix(uFloorColor, uGridColor, vec3(grid));
// cross grid float crossUv = crossFloor(uv, uCrossScale, uCrossThickness, uCross); // 💡 to add more grids on top, ensure the base is taken from previous gridColor vec3 gridColor2 = mix(gridColor, uCrossColor, vec3(crossUv)); vec3 color = gridColor2;
gl_FragColor = vec4(color, 1.0);
#include }const gui = new Pane()
const debugObject = { color: '#c4d6ff', crossColor: '#7a91df', fogColor: '#c3dce2', backgroundColor: '#e9f6f8', floorColor: '#ffffff', }
// Canvas const box = document.querySelector('#box')
// Scene const scene = new THREE.Scene()
/**
- Test mesh
// Material const material = new THREE.ShaderMaterial({ vertexShader: baseVertexShader, fragmentShader: baseFragmentShader, side: THREE.DoubleSide, transparent: true, uniforms: { // Floor uFloorColor: { value: new THREE.Color(debugObject.floorColor) },
// Grid uGridThickness: { value: 0.02 }, uGridColor: { value: new THREE.Color(debugObject.color) },
// Cross uCrossThickness: { value: 0.02 }, uCross: { value: 0.2 }, uCrossColor: { value: new THREE.Color(debugObject.crossColor) },
// Fog fogColor: { value: new THREE.Color(debugObject.fogColor) }, fogNear: { value: 1 }, fogFar: { value: 5 }, }, fog: true, })
const gridFolder = gui.addFolder({ title: '🌐 Grid Floor' }) gridFolder.addBinding(material.uniforms.uGridThickness, 'value', { label: 'thickness', min: 0, max: 1, step: 0.001, }) gridFolder .addBinding(debugObject, 'color', { label: 'color', }) .on('change', () => { material.uniforms.uGridColor.value.set(debugObject.color) })
const crossFolder = gui.addFolder({ title: '❎ Cross Floor' }) crossFolder.addBinding(material.uniforms.uCrossThickness, 'value', { label: 'thickness', min: 0, max: 1, step: 0.001, }) crossFolder.addBinding(material.uniforms.uCross, 'value', { label: 'cross', min: 0, max: 1, step: 0.01, }) crossFolder .addBinding(debugObject, 'crossColor', { label: 'color', }) .on('change', () => { material.uniforms.uCrossColor.value.set(debugObject.crossColor) })
// Grid Floor const gridFloor = new THREE.Mesh(geometry, material) gridFloor.rotation.x = Math.PI * 0.5 scene.add(gridFloor)
// Fog // color, density scene.fog = new THREE.Fog(debugObject.fogColor, 1, 10) scene.background = new THREE.Color(debugObject.backgroundColor)
const fogFolder = gui.addFolder({ title: '💨 Fog' })
fogFolder.addBinding(scene.fog, 'near', { label: 'near', min: -5, max: 2, step: 0.1, }) fogFolder.addBinding(scene.fog, 'far', { label: 'far', min: 2, max: 50, step: 0.1, }) fogFolder .addBinding(debugObject, 'fogColor', { label: 'color', }) .on('change', () => { scene.fog.color.set(debugObject.fogColor) })
const envFolder = gui.addFolder({ title: '🏡 Environment' }) envFolder .addBinding(debugObject, 'backgroundColor', { label: 'sky', }) .on('change', () => { scene.background = new THREE.Color(debugObject.backgroundColor) })
envFolder .addBinding(debugObject, 'floorColor', { label: 'Floor', }) .on('change', () => { material.uniforms.uFloorColor.value.set(debugObject.floorColor) })
// Axes helper const axesHelper = new THREE.AxesHelper(3) scene.add(axesHelper)
/**
- Sizes
window.addEventListener('resize', () => { // Update sizes sizes.width = window.innerWidth sizes.height = window.innerHeight
// Update camera camera.aspect = sizes.width / sizes.height camera.updateProjectionMatrix()
// Update renderer renderer.setSize(sizes.width, sizes.height) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) })
/**
- Camera
/**
- Renderer
// Controls const controls = new OrbitControls(camera, renderer.domElement) controls.enableDamping = true controls.minDistance = 0.5 controls.maxDistance = 15
/**
- Animate
// Render renderer.render(scene, camera)
// Call tick again on the next frame window.requestAnimationFrame(tick) }
tick()
完整源码:GitHub
小结
- 本文提供网格地板完整 Three.js 源码与在线 Demo,建议先运行案例再改 uniform/参数做二次实验
- 更多 Three.js 实战案例见 three-cesium-examples 合集 与 GitHub 开源仓库