Yet another iteration on dave1707’s terrain

@dave1707 ’s voxel-terrain algorithm is really great and versatile. Thanks dave. And here’s yet another iteration on it.

No voxels here—this uses the algorithm to generate a craft terrain model as seen above.


-- Global references for scene and terrain
local scene, terrainModel, terrainEntity

function setup()
    perlinH = craft.noise.perlin()
    setupScene()
    generateTerrain()
    prepareTerrainModel()
end

function setupScene()
    -- Create a new Craft scene
    scene = craft.scene()
    -- Add an OrbitViewer for camera control
    local viewer = scene.camera:add(OrbitViewer, vec3(275, 200, 200), 3000, 0, 2000)
    viewer.camera.farPlane = 10000
    viewer.rx, viewer.ry = 50, -40
    
    
    -- Adjust the sun (directional light) properties
    scene.sun.rotation = quat.eulerAngles(45, 0, 116) -- Adjust the direction of the sun
    scene.sun:get(craft.light).intensity = 0.8 -- Adjust sunlight intensity
    
    -- Add an ambient light to the scene
    scene.ambientColor = color(127, 127, 127, 255) -- Adjust the ambient light color and intensity
end

function calculateNormal(v1, v2, v3)
    local edge1 = v2 - v1
    local edge2 = v3 - v1
    return edge1:cross(edge2):normalize()
end

function prepareTerrainModel()
    terrainEntity = scene:entity()
    terrainEntity.model = terrainModel
    terrainEntity.material = craft.material(asset.builtin.Materials.Basic)
    terrainEntity.material.map = readImage(asset.builtin.Surfaces.Desert_Cliff_Color)
end

function draw()
    scene:update(DeltaTime)
    scene:draw()
end

function generateTerrain()
    -- Terrain generation settings
    local pointsPerSide = 400
    local offsetX, offsetZ = math.random(500), math.random(500)
    
    -- Prepare for terrain generation
    local terrainVerts, terrainIndices, terrainNormals, terrainUvs, terrainColors = {}, {}, {}, {}, {}
    local m = 1 / pointsPerSide
    local h = perlinH  
    -- First, generate raw height values
    local rawHeights = {}
    generateHeightValues(rawHeights, pointsPerSide, offsetX, offsetZ, perlinH)
    
    -- Apply a simple smoothing pass to the height values
    local smoothedHeights = smoothHeights(rawHeights, pointsPerSide)
    
    -- Use smoothedHeights for generating terrain vertices, normals, etc.
    generateVertsNormalsUvsColors(pointsPerSide, smoothedHeights, 
    terrainVerts, terrainNormals, terrainUvs, terrainColors)
    
    -- Generate terrain indices for mesh triangles
    generateTerrainIndices(pointsPerSide, terrainIndices)
    
    local smoothedNormals = generateSmoothedNormals(terrainIndices, terrainVerts, terrainNormals)
    
    -- Finalize terrain model
    terrainModel = craft.model()
    terrainModel.positions = terrainVerts
    terrainModel.indices = terrainIndices
    terrainModel.normals = smoothedNormals
    terrainModel.uvs = terrainUvs
    terrainModel.colors = terrainColors
end

function generateHeightValues(
        valuesTable, pointsPerSide, offsetX, offsetZ, perlinH)
    local rawHeights = valuesTable
    local m = 1 / pointsPerSide
    for x = 1, pointsPerSide do
        rawHeights[x] = {}
        for z = 1, pointsPerSide do
            local xx, zz = x * m + offsetX, z * m + offsetZ
            local y = math.abs(perlinH:getValue(xx, 0, zz)) * 150
            rawHeights[x][z] = y > 127 and 127 - (y - 127) or y
        end
    end
end

function generateVertsNormalsUvsColors(pointsPerSide, smoothedHeights, terrainVerts, terrainNormals, terrainUvs, terrainColors)
    -- Determine the maximum height value
    local maxHeight = 0
    for x = 1, pointsPerSide do
        for z = 1, pointsPerSide do
            local height = smoothedHeights[x][z]
            if height > maxHeight then
                maxHeight = height
            end
        end
    end
    
    -- Define the three key levels and their associated colors
    local lowestColor = color(90, 53, 42)
    local middleColor = color(219, 148, 101) 
    local highestColor = color(215, 174, 127) 
    
    -- Generate vertices, normals, uvs, and colors with gradient
    for x = 1, pointsPerSide do
        for z = 1, pointsPerSide do
            local y = smoothedHeights[x][z]
            local index = (x - 1) * pointsPerSide + z
            terrainVerts[index] = vec3(x, y, z)
            terrainNormals[index] = vec3(0, 1, 0)
            terrainUvs[index] = vec2(x / pointsPerSide, z / pointsPerSide)
            
            -- Calculate relative height (0.0 to 1.0)
            local relativeHeight = y / maxHeight
            
            -- Determine the color based on the relative height
            if relativeHeight < 0.5 then
                -- Interpolate between lowestColor and middleColor
                local t = (relativeHeight * 2) -- Scale factor for lower half
                terrainColors[index] = lerpColor(lowestColor, middleColor, t)
            else
                -- Interpolate between middleColor and highestColor
                local t = (relativeHeight - 0.5) * 2 -- Scale factor for upper half
                terrainColors[index] = lerpColor(middleColor, highestColor, t)
            end
        end
    end
end

-- Function to interpolate between two colors
function lerpColor(c1, c2, t)
    local r = math.floor(lerp(c1.r, c2.r, t))
    local g = math.floor(lerp(c1.g, c2.g, t))
    local b = math.floor(lerp(c1.b, c2.b, t))
    local a = math.floor(lerp(c1.a, c2.a, t))
    return color(r, g, b, a)
end

-- Linear interpolation function
function lerp(a, b, t)
    return a + (b - a) * t
end

function generateSmoothedNormals(terrainIndices, terrainVerts, terrainNormals)
    local smoothingFactor = 1 -- Range from 0 to 1, adjust this to control smoothing
    -- Assume vertices and indices are already populated
    local faceNormals = {}
    local vertexFaces = {}
    
    -- Calculate face normals
    for i = 1, #terrainIndices, 3 do
        local i1, i2, i3 = terrainIndices[i], terrainIndices[i+1], terrainIndices[i+2]
        local v1, v2, v3 = terrainVerts[i1], terrainVerts[i2], terrainVerts[i3]
        local normal = calculateNormal(v1, v2, v3)
        
        faceNormals[#faceNormals + 1] = normal
        
        -- Map vertices to their faces
        vertexFaces[i1] = vertexFaces[i1] or {}
        vertexFaces[i2] = vertexFaces[i2] or {}
        vertexFaces[i3] = vertexFaces[i3] or {}
        table.insert(vertexFaces[i1], #faceNormals)
        table.insert(vertexFaces[i2], #faceNormals)
        table.insert(vertexFaces[i3], #faceNormals)
    end
    
    -- Calculate smoothed vertex normals
    local smoothedNormals = {}
    for i, v in ipairs(terrainVerts) do
        local sumNormal = vec3(0, 0, 0)
        local faces = vertexFaces[i] or {}
        
        for _, fIndex in ipairs(faces) do
            sumNormal = sumNormal + faceNormals[fIndex]
        end
        
        if #faces > 0 then
            local avgNormal = (sumNormal / #faces):normalize()
            -- Interpolate between the original normal and the smoothed normal
            smoothedNormals[i] = lerp(terrainNormals[i], avgNormal, smoothingFactor)
        else
            smoothedNormals[i] = terrainNormals[i]
        end
    end
    return smoothedNormals
end

function generateTerrainIndices(pointsPerSide, terrainIndices)
    for i = 1, pointsPerSide - 1 do
        for j = 1, pointsPerSide - 1 do
            local base = (i - 1) * pointsPerSide + j
            local nextRow = base + pointsPerSide
            -- Triangle 1
            table.insert(terrainIndices, base)
            table.insert(terrainIndices, nextRow)
            table.insert(terrainIndices, base + 1)
            -- Triangle 2
            table.insert(terrainIndices, nextRow)
            table.insert(terrainIndices, nextRow + 1)
            table.insert(terrainIndices, base + 1)
        end
    end
end

function smoothHeights(heights, size)
    local smoothed = {}
    for x = 1, size do
        smoothed[x] = {}
        for z = 1, size do
            local sum = 0
            local count = 0
            for i = -1, 1 do
                for j = -1, 1 do
                    if heights[x + i] and heights[x + i][z + j] then
                        sum = sum + heights[x + i][z + j]
                        count = count + 1
                    end
                end
            end
            smoothed[x][z] = sum / count
        end
    end
    return smoothed
end
2 Likes

looks nice ^^
I played around with it a bit and I don’t see the difference smoothedNormals supposedly make, removing that step the result seems the same to me…?
this is an example without smoothedNormals (I changed the height colors ^^” )

1 Like

It could be redundant, but try looking at it without the texture (map) on the material.

The purpose of both the smooth normals and the height smoothing is to remove some artificial-looking striations or grid lines that are remnants of the height algorithm’s original purpose in duplicating the placement of voxels.

These might be more visible without the texture. You may also need to disable the colors to see it, but it’s also totally possible I have some redundant code.

Nice color changes :slight_smile: