西風之眠

Understand the blend types of Landscape Layer Blend Node in Unreal Engine 5

When I was learning the landscape painting in Unreal Engine 5, I was really confused in these two concepts:

  1. The blend types (LB_WeightBlend, LB_HeightBlend, and LB_AlphaBlend) of the Layer Blend Node in the Landscape Material.
  2. The kinds (Weight-Blended and Non Weight-Blended) of the layer info object when painting the landscape.

After reading Mason Stevenson’s wonder article Blending Landscape Materials in Unreal Engine, I’ve learned they are two different concepts and are not related, dispite the similarity on their names.

This blog mainly talks about the first one, and there might be another blog for the second one.

The layer blend node defines several layers, and for each layer it excepts a weight map when rendering. (The weight map is generated when painting the landscape and is adjusted by the kind of the layer info object).

The source code is located at Engine/Source/Runtime/Landscape/Private/Materials/MaterialExpressionLandscapeLayerBlend.cpp, the logics generate the shading code which run on the GPU. I’ve translated the code into a short psudo code for easy understanding. Please be alarmed that this is not a rough one to one translation, I’ve omited lots of details, please also check the original source code, it’s not that hard to read.

var output = 0 // The final output

var needsRenormalize = false
var weightSum = 0
var weights = [](repeating: none, count: layers.count)

// The first traversal
for (i, layer) in layers.enumerated() {
	let defaultWeight = PreviewWeight if PreviewWeight > 0.0f else none
	let weight = StaticTerrainLayerWeight(layer, defaultWeight) // get painted weight value from landscape layer
	switch layer.BlendType {
	case LB_AlphaBlend:
		break // LB_AlphaBlend will be processed in the last pass
	case LB_WeightBlend:
		weights[i] = weight
		weightSum += weight
	case LB_HeightBlend:
		needsRenormalize = true // as long as one LB_HeightBlend exists, the normalization is required
		let height = {
            if this Layer has a HeightInput, use the HeightInput
            else use the Layer's ConstHeightInput
		}
		weights[i] = clamp(
			add(lerp(-1.f, 1.f, weight), height), 
			0.0001, 
			1.f
		)
		weightSum += weight
	}
}

let invWeightSum = 1.f / weightSum

// The second traversal
for (i, layer) in layers.enumerated() {
	guard weights[i] != none else {
		// for now only LB_AlphaBlend should be none
		continue
	}
	let input = {
        if this Layer has a LayerInput, use the LayerInput
        else use the Layer's ConstLayerInput
	}
	if needsRenormalize {
		// output += input * (invWeightSum * weights[i])
		output = add(output, mul(input, mul(invWeightSum, weights[i])))
	} else {
		// output += input * weights[i]
		output = add(output, mul(input, weights[i]))
	}
}

// The third traversal, here the LB_AlphaBlend is finally processed
for (i, layer) in layers.enumerated() {
	guard layer.BlendType == LB_AlphaBlend else {
		continue
	}
	let defaultWeight = PreviewWeight if PreviewWeight > 0.0f else none
	let weight = StaticTerrainLayerWeight(layer, defaultWeight) // get painted weight value from landscape layer
	let input = {
        if this Layer has a LayerInput, use the LayerInput
        else use the Layer's ConstLayerInput
	}
	output = lerp(output, input, weight) // learp between the current output and the input of this LB_AlphaBlend layer based on the weight value
}