基于THREE.JS的50×50类SimCity网格地形高度过渡平滑化技术咨询
Hey there! Let's work through this smooth terrain transition problem for your 50×50 SimCity-style grid in Three.js. I’ve got a couple of practical, actionable approaches that keep your grid structure intact while fixing that blocky height issue, plus ways to support terrain type edits.
Approach 1: Subdivided Plane Geometry with Vertex Height Interpolation
Instead of using BoxGeometry for each cell, switch to a subdivided PlaneGeometry for every grid cell. This lets you adjust intermediate vertices to blend heights between adjacent cells naturally.
How it works:
- For each 50×50 cell, create a plane with horizontal/vertical subdivisions (e.g., 4x4 segments).
- Fetch height data from the current cell and its four neighbors (top, bottom, left, right).
- Use bilinear interpolation to calculate smooth Y-values for each vertex in the plane.
- Assign materials based on the cell's terrain type.
Code Example:
const gridSize = 50; const cellSize = 1; const subdivisions = 4; // More subdivisions = smoother transitions // Assume gridData is your 50x50 array of {x, z, y, terrainType} for (let x = 0; x < gridSize; x++) { for (let z = 0; z < gridSize; z++) { const cell = gridData[x][z]; // Create plane and rotate to XZ plane (default is XY) const geometry = new THREE.PlaneGeometry(cellSize, cellSize, subdivisions, subdivisions); geometry.rotateX(-Math.PI / 2); // Get heights of current cell and neighbors (handle edge cases) const currentH = cell.y; const topH = z > 0 ? gridData[x][z-1].y : currentH; const bottomH = z < gridSize-1 ? gridData[x][z+1].y : currentH; const leftH = x > 0 ? gridData[x-1][z].y : currentH; const rightH = x < gridSize-1 ? gridData[x+1][z].y : currentH; // Adjust each vertex's Y height geometry.vertices.forEach(vertex => { // Convert local vertex coords from [-0.5, 0.5] to [0, 1] const localX = vertex.x + 0.5; const localZ = vertex.z + 0.5; // Bilinear interpolation for smooth height blending const height = THREE.MathUtils.lerp( THREE.MathUtils.lerp(leftH, rightH, localX), THREE.MathUtils.lerp(topH, bottomH, localX), localZ ); vertex.y = height; }); geometry.verticesNeedUpdate = true; // Assign terrain-specific material let material; switch(cell.terrainType) { case 'grass': material = new THREE.MeshLambertMaterial({ color: 0x7cb342 }); break; case 'dirt': material = new THREE.MeshLambertMaterial({ color: 0x8d6e63 }); break; case 'stone': material = new THREE.MeshLambertMaterial({ color: 0x757575 }); break; default: material = new THREE.MeshLambertMaterial({ color: 0xcccccc }); } // Position the cell in the grid const mesh = new THREE.Mesh(geometry, material); mesh.position.set(x * cellSize, 0, z * cellSize); scene.add(mesh); } }
Approach 2: Shader-Based Height Blending (Better Performance)
For larger grids or smoother dynamic updates, use a ShaderMaterial to handle height blending in the GPU. This avoids modifying geometry vertices directly and keeps performance high.
How it works:
- Convert your grid data into two textures: a height map (stores cell Y-values) and a terrain type map (stores grass/dirt/stone as color channels).
- Use a vertex shader to sample the height map and interpolate heights across the grid.
- Use a fragment shader to apply terrain colors and height-based shading.
Code Example:
// Vertex Shader const vertexShader = ` varying vec2 vUv; uniform sampler2D heightMap; void main() { vUv = uv; // Sample height from texture (normalized 0-1, adjust to your actual height range) float height = texture2D(heightMap, vUv).r * 10.0; vec3 pos = position; pos.y = height; gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); } `; // Fragment Shader const fragmentShader = ` varying vec2 vUv; uniform sampler2D terrainMap; uniform sampler2D heightMap; void main() { float terrainType = texture2D(terrainMap, vUv).r; float height = texture2D(heightMap, vUv).r; vec3 color; // Map terrain type values to colors if (terrainType == 0.0) color = vec3(0.486, 0.702, 0.259); // Grass else if (terrainType == 0.5) color = vec3(0.557, 0.431, 0.388); // Dirt else if (terrainType == 1.0) color = vec3(0.459, 0.459, 0.459); // Stone else color = vec3(0.8); // Default // Add subtle height-based shading color *= 0.8 + height * 0.2; gl_FragColor = vec4(color, 1.0); } `; // Convert grid data to textures const heightArray = new Float32Array(gridSize * gridSize); const terrainArray = new Float32Array(gridSize * gridSize); for (let x = 0; x < gridSize; x++) { for (let z = 0; z < gridSize; z++) { const idx = x + z * gridSize; heightArray[idx] = gridData[x][z].y / 10.0; // Normalize to 0-1 switch(gridData[x][z].terrainType) { case 'grass': terrainArray[idx] = 0.0; break; case 'dirt': terrainArray[idx] = 0.5; break; case 'stone': terrainArray[idx] = 1.0; break; default: terrainArray[idx] = 0.0; } } } const heightTexture = new THREE.DataTexture(heightArray, gridSize, gridSize, THREE.RedFormat, THREE.FloatType); heightTexture.needsUpdate = true; heightTexture.wrapS = heightTexture.wrapT = THREE.ClampToEdgeWrapping; const terrainTexture = new THREE.DataTexture(terrainArray, gridSize, gridSize, THREE.RedFormat, THREE.FloatType); terrainTexture.needsUpdate = true; // Create single grid mesh const geometry = new THREE.PlaneGeometry(gridSize, gridSize, gridSize-1, gridSize-1); geometry.rotateX(-Math.PI / 2); const material = new THREE.ShaderMaterial({ uniforms: { heightMap: { value: heightTexture }, terrainMap: { value: terrainTexture } }, vertexShader, fragmentShader }); const gridMesh = new THREE.Mesh(geometry, material); gridMesh.position.set(gridSize/2, 0, gridSize/2); scene.add(gridMesh);
Supporting User Edits
To let users modify terrain type or height:
- For Approach 1: When a cell is edited, re-calculate its vertices' heights and set
geometry.verticesNeedUpdate = true. - For Approach 2: Update the corresponding values in
heightArrayorterrainArray, then setheightTexture.needsUpdate = trueorterrainTexture.needsUpdate = true.
内容的提问来源于stack exchange,提问作者AKB2

