導言

關於地形更新我們還差最後一步了啦!

對於按component提交更新而言,不方便的地方還是很顯而易見的,所以我們需要祭出最好用的大殺器,按選取更新。這樣我們就可以最細緻的對一塊地形提交修改了。

當然,還有兩個遺留問題會在本文中提到。

關於選區

UE4自帶了一個畫刷選區的功能,我們就是利用這個功能,把選出來的區域單獨的提交修改。

提交選區

選區更新的話,我們就不再需要去一塊一塊的點選component了,我們可以直接通過GetSelectedRegionComponents() 函數來獲得選區所在的componet。

//.........
if ( bExportOnlySelected )
{
const ULandscapeInfo * LandscapeInfo = LandscapeProxy->GetLandscapeInfo();
if ( LandscapeInfo && !LandscapeInfo->IsPendingKill() )
{
// Get the currently selected components
SelectedComponents = LandscapeInfo->GetSelectedComponents();
}

////////////////////////////////
if (SelectedComponents.Num() <= 0)
{
// if no components is selected, maybe use region to fetch components
SelectedComponents = LandscapeInfo->GetSelectedRegionComponents();
}
////////////////////////////////

if ( bAutoSelectComponents && SelectedComponents.Num() <= 0 && AssetBounds.IsValid )
//.......

然後我們通過下面的函數來把選區的數據提取出來,可以看到,UE4地形的選區居然是float 0~1的,我們就不需要再去轉換了,可以直接作為一個mask提交到houdini engine中。(提交的方法可以完全參照其他mask)

bool FHoudiniLandscapeUtils::GetLandscapeRegionLayerData(ULandscapeInfo* LandscapeInfo, int32 MinX, int32 MinY, int32 SizeX, int32 SizeY, TArray<float>& LayerData, HAPI_VolumeInfo& LayerVolumeInfo)
{
LayerData.Empty();
bool HasRegionInfo = false;
for (int32 Y = MinY; Y < MinY + SizeY; Y++)
{
for (int32 X = MinX; X < MinX + SizeX; X++)
{
float RegionSelect = LandscapeInfo->SelectedRegion.FindRef(FIntPoint(X, Y));
LayerData.Add(RegionSelect);
if(RegionSelect > 0.0f)
{
HasRegionInfo = true;
}
}
}
if (!HasRegionInfo)
{
// no region info
return false;
}
// init VolumeInfo
}

更新地形

通過上面的方法我們已經可以把選區作為一個mask提交到houdini engine中了,我們可以在hda里利用這個mask來做更新,當然,這麼做顯然不夠方便。

所以這裡我們把返回的數據用插件做一個mask功能,只更新選區內的數據,選區外的數據依然保留原狀

void FHoudiniLandscapeUtils::AdjustHeightDataByRegion(ULandscapeInfo* LandscapeInfo, TArray<uint16>& HeightData, ComponentRectInfo& rect, int32& UnrealXStride, TArray<float>& RegionData)
{
int XSize,YSize;
TArray<uint16> OldHeightData;
GetLandscapeData(LandscapeInfo, rect.MinX, rect.MinY, rect.MaxX, rect.MaxY, OldHeightData, XSize, YSize);

// new height data and old height data should be same size
check(HeightData.Num() == OldHeightData.Num())

for (int32 x = 0; x < XSize; x++)
{
for (int32 y = 0; y < YSize; y++)
{
int32 OriginIndex = (y + rect.RelativeMinY) * UnrealXStride + (x + rect.RelativeMinX);
float RegionValue = RegionData[OriginIndex];
int32 i = y * XSize + x;
// adjust the final value by region value
// 把原有高度和更新後高度的差值乘以region的值,讓region不為1的地方能夠有漸變。
HeightData[i] = OldHeightData[i] + (uint16)(((float)HeightData[i] - (float)OldHeightData[i]) * RegionValue);
}
}
}

對於layer就省事兒一些,我們只需要把更新的weight值乘上region的強度就行了。

void FHoudiniLandscapeUtils::AdjustWeightDataByRegion(TArray<uint8>& WeightData, ComponentRectInfo& rect, int32& UnrealXStride, TArray<float>& RegionData)
{
int XSize = rect.MaxX - rect.MinX + 1;
int YSize = rect.MaxY - rect.MinY + 1;

for (int32 x = 0; x < XSize; x++)
{
for (int32 y = 0; y < YSize; y++)
{
int32 OriginIndex = (y + rect.RelativeMinY) * UnrealXStride + (x + rect.RelativeMinX);
float RegionValue = RegionData[OriginIndex];
int32 i = y * XSize + x;
// adjust the final value by region value
WeightData[i] = (uint8)((float)WeightData[i] * RegionValue);
}
}
}

當然,更新高度之前先獲取選區信息,我們的選區也不用生成真實layer。

做完後我們可以就看到地形按照我們的選區做出了修改。

遺留問題1——法線不連續

法線不連續是什麼樣呢,就是如下圖所示:

可以看到中間有一個很明顯的十字,如同更新地形的時候把她們遺漏掉了一樣。

是的沒錯就是遺漏掉了。在LandscapeEdit.SetHeightData中,component的邊緣是不會去計演算法線的,因為UE4官方認為邊緣法線會涉及其他區域所以乾脆不管了。

那麼我們一塊一塊component去提交就會發現component之間的法線都是沒有被處理到的,雖然我們之前已經將選區邊緣做了鎖進來避免整體外緣的法線問題,但是多個選區交接的法線還是不好處理。

苦於沒有什麼好辦法,在不修改引擎源碼的前提下,只能辛苦自己重構代碼了。

借鑒地形雕刻工具的設計思路,我們把所有選區的原有數據先作為一個cache,然後在cache上面更新我們本次要做的修改,最後整個cache一起提交,就能解決component接縫處的問題。

// copy the origin height data as a cache
TSet<ULandscapeComponent*> CurrentComponents;
int RelativeMaxX = RelativeMinX + UnrealXSize - 1;
int RelativeMaxY = RelativeMinY + UnrealYSize - 1;
PreviousInfo->GetComponentsInRegion(RelativeMinX + 1, RelativeMinY + 1, RelativeMaxX - 1, RelativeMaxY - 1, CurrentComponents);
for (ULandscapeComponent* CurComponent : CurrentComponents)
{
CombineHeighData(CurComponent, RelativeMinX, RelativeMinY, RelativeMaxX, RelativeMaxY, CachedHeightData);
}

// overide the cache with new data
for (ComponentRectInfo& node : ComponentRects)
{
TArray<uint16> ComponentHeightData;
GetDataByComponentRect(IntHeightData, UnrealXSize, node, ComponentHeightData);
RemapHeightData(ComponentHeightData, LandscapeTransform.GetScale3D(), FoundLandscape->GetTransform().GetScale3D(), ZeroInDigit);
if (HasRegionData)
{
AdjustHeightDataByRegion(PreviousInfo, ComponentHeightData, node, UnrealXSize, RegionData);
}
CombineArrayData(CachedHeightData.GetData(), ComponentHeightData.GetData(), 0, 0, UnrealXSize - 1, UnrealYSize - 1,
node.RelativeMinX, node.RelativeMinY, node.RelativeMaxX, node.RelativeMaxY);

}

LandscapeEdit.SetHeightData(RelativeMinX, RelativeMinY, RelativeMaxX, RelativeMaxY, CachedHeightData.GetData(), 0, true);

這樣接縫處的法線不連續問題就得到了解決。

遺留問題2——layer被清除

之前我們提過,houdini engine plugin中在提交地形更新之前會先把由houdini 導入的layer全部刪除。對於整體更新來說影響不大,但是對於選區更新來說,更新一個小區域意味著整個地形的layer都丟了,這可不行。所以我們希望在清除layer的時候也只清楚我們選擇的component內的layer。

bool
FHoudiniLandscapeUtils::RestoreLandscapeFromFile( ALandscapeProxy* LandscapeProxy, bool bLandscapeExportSelectionOnly /*= false*/)
{
//.........
// Delete the added layer
FName LayerName = LandscapeInfo->Layers[LayerIndex].LayerName;
if (!bLandscapeExportSelectionOnly)
{
LandscapeInfo->DeleteLayer(CurrentLayerInfo, LayerName);
}
else
{
TSet< ULandscapeComponent * > SelectedComponents;
// Get the currently selected components
SelectedComponents = LandscapeInfo->GetSelectedComponents();

if (SelectedComponents.Num() <= 0)
{
// if no components is selected, maybe use region to fetch components
SelectedComponents = LandscapeInfo->GetSelectedRegionComponents();
}

for (ULandscapeComponent* Comp : SelectedComponents)
{
FLandscapeEditDataInterface LandscapeEdit(LandscapeInfo);
Comp->DeleteLayer(CurrentLayerInfo, LandscapeEdit);
}
}

//.........
}

總結

到這裡,Houdini Terrian & UE4部分基本就告一段落了。Terrian部分的功能肯定不止這麼多,houdini還支持植被散布等功能,等後續涉及到的時候再回頭來寫了。

當然。我肯定是去研究更加黑科技的功能啦!是什麼呢,點個關注敬請期待吧~


推薦閱讀:
相关文章