I am currently in the middle of making A foliage system and its basically done at this point. its a little bit messy but its just a prototype
[Serializable]
public class FoliageAssetItem {
public GameObject ColiderPrefab;
public byte Priority = 255;
public Mesh LOD0;
public Material[] LOD0Mat;
public Mesh LOD1;
public Material[] LOD1Mat;
public Mesh LOD2;
public Material[] LOD2Mat;
}
public class Foliage_Helper
{
public DataList DesiredChunks;
//Temporary
public static bool LoadJson(String data, DataList FoliageChunks) {
DataToken FoliageDataToken;
if (VRCJson.TryDeserializeFromJson(data, out FoliageDataToken))
{
DataDictionary FoliageDataDict = FoliageDataToken.DataDictionary;
if (!FoliageDataDict.TryGetValue("Chunks", out DataToken ChunkList)) return false;
FoliageChunks = ChunkList.DataList;
return true;
}
else
{
return false;
}
}
// Temporary
public static void LoadFoliage(TextAsset foliageData, DataList ChunkData)
{
String FoliageJson = foliageData.text;
if (!LoadJson(FoliageJson, ChunkData)) {
Debug.LogError("Failed to parse foliage data");
}
}
public static Vector3 GetVec3FromList(DataList Vector) {
return new Vector3(Vector[0].Float, Vector[1].Float, Vector[2].Float);
}
public static string FormatVec2int(Vector2Int vector) {
return vector.x.ToString() + "," + vector.y.ToString();
}
public static DataList BuildDesiredChunkList(int Depth) {
DataList DesiredList = new DataList();
for (int radius = 0; radius <= Depth; radius++)
{
for (int x = -radius; x <= radius; x++)
{
for (int y = -radius; y <= radius; y++)
{
bool edge = Mathf.Abs(x) == radius || Mathf.Abs(y) == radius;
if (!edge)
continue;
DataDictionary position = new DataDictionary();
position.Add("X", x);
position.Add("Y", y);
DesiredList.Add(position);
}
}
}
return DesiredList;
}
//Needs to be re written for byte reading
public static DataDictionary GetChunkData(Vector2Int chunkPosition, DataList FoliageChunks)
{
for (int i = 0; i < FoliageChunks.Count; i++)
{
DataDictionary ChunkData = FoliageChunks[i].DataDictionary;
if (!ChunkData.TryGetValue("ChunkX", out DataToken chunkXDT)) { Debug.LogError("Failure to get Chunk X"); return null; }
if (chunkXDT.Int == chunkPosition.x)
{
if (!ChunkData.TryGetValue("ChunkY", out DataToken chunkYDT)) { Debug.LogError("Failure to get Chunk y"); return null; }
if (chunkYDT.Int == chunkPosition.y) return ChunkData;
}
}
return null;
}
public static Matrix4x4[][] BuildChunkData(DataList ChunkData, byte TreeTypes) {
if (ChunkData == null) { return null; }
Matrix4x4[][] TreeItemList = new Matrix4x4[TreeTypes][];
ushort[] TypeCount = new ushort[TreeTypes];
//Count Types
for (int Tree = 0; Tree < ChunkData.Count; Tree++) {
DataDictionary TreeItem = ChunkData[Tree].DataDictionary;
TreeItem.TryGetValue("TreeType", out DataToken TreeType);
TypeCount[TreeType.Int]++;
}
// Allocate Types
int ItemIndx = 0;
foreach (ushort item in TypeCount) {
TreeItemList[ItemIndx] = new Matrix4x4[item];
ItemIndx++;
}
ushort[] TypeInsertions = new ushort[TreeTypes];
// Create Matrixis
for (int Tree = 0; Tree < ChunkData.Count; Tree++)
{
DataDictionary TreeItem = ChunkData[Tree].DataDictionary;
TreeItem.TryGetValue("TreeType", out DataToken TreeType);
TreeItem.TryGetValue("Position", out DataToken PositionDict);
Vector3 Position = GetVec3FromList(PositionDict.DataList);
TreeItem.TryGetValue("Rotation", out DataToken RotationDict);
Vector3 Rotation = GetVec3FromList(RotationDict.DataList);
TreeItem.TryGetValue("Scale", out DataToken Scale);
TreeItemList[TreeType.Int][TypeInsertions[TreeType.Int]] = Matrix4x4.TRS(Position, Quaternion.Euler(Rotation), Vector3.one * Scale.Float);
TypeInsertions[TreeType.Int]++;
}
return TreeItemList;
}
public static bool ChunkShouldBeLoaded(Vector2Int PlayerChunk ,Vector2Int ChunkPosition, int ChunkRadius) {
Vector2Int CheckingChunk = ChunkPosition - PlayerChunk;
return Mathf.Abs(CheckingChunk.x) <= ChunkRadius && Mathf.Abs(CheckingChunk.y) <= ChunkRadius;
}
}
public class Foliage_Manager : UdonSharpBehaviour
{
DataList TreeData;
DataList LoadOrder;
DataList LoadMask;
DataDictionary LoadedChunks;
VRCPlayerApi localPlayer;
public FoliageAssetItem[] TreeAssets;
public byte ChunkSize = 16;
public byte ChunksPerTick = 5;
public int LOD0Radius = 100;
public int LOD1Radius = 250;
public int LOD2Radius = 500;
public TextAsset TerrainTrees = null;
// 1: Chunk 2: TreeType 3 : TreeMatrix
Matrix4x4[][][] TreeRenderMatrix;
private void Start()
{
Foliage_Helper.LoadFoliage(TerrainTrees, TreeData);
LoadOrder = Foliage_Helper.BuildDesiredChunkList(LOD2Radius / ChunkSize);
LoadMask = new DataList();
LoadedChunks = new DataDictionary();
localPlayer = Networking.LocalPlayer;
TreeRenderMatrix = new Matrix4x4[LOD2Radius / ChunkSize][][];
}
Vector2Int LastPlayerChunk = Vector2Int.zero;
private void FixedUpdate()
{
Vector3 playerPosition = localPlayer.GetPosition();
//Difference check
Vector2Int PlayerChunkPosition = new Vector2Int(Mathf.CeilToInt(playerPosition.x / ChunkSize), Mathf.CeilToInt(playerPosition.z / ChunkSize));
if (!PlayerChunkPosition.Equals(LastPlayerChunk)) {
LastPlayerChunk = PlayerChunkPosition;
Debug.Log("Player entered new chunk");
Debug.Log(LastPlayerChunk);
//Build the chunk mask for the new layers
LoadMask.Clear();
for (int i = 0; i < LoadOrder.Count; i++) {
DataDictionary chunkPos = LoadOrder[i].DataDictionary;
LoadMask.Add(chunkPos);
}
}
//Loading and cacheing results
int ItemsLoadedThisTick = 0;
while (ItemsLoadedThisTick < ChunksPerTick) {
// Getting an unloaded chunk
int Chunk = 0;
for (int chunk = 0; chunk < LoadMask.Count; chunk++) {
DataDictionary ChunkPos = LoadMask[chunk].DataDictionary;
ChunkPos.TryGetValue("X", out DataToken XToken);
ChunkPos.TryGetValue("Y", out DataToken YToken);
Vector2Int ChunkPosition = new Vector2Int(XToken.Int, YToken.Int);
if (!LoadedChunks.ContainsKey(Foliage_Helper.FormatVec2int(ChunkPosition)))
{
Foliage_Helper.GetChunkData(ChunkPosition, TreeData).TryGetValue("Trees", out DataToken Dt);
Matrix4x4[][] mat = Foliage_Helper.BuildChunkData(Dt.DataList, (byte)TreeAssets.Length);
bool Loaded = false;
// Check if any of the chunks are null
for (int i = 0; i < LOD2Radius / ChunkSize; i++)
{
if (TreeRenderMatrix[i] == null) {
//LoadedChunks.TryGetValue(Foliage_Helper.FormatVec2int(ChunkPosition), out DataToken index);
TreeRenderMatrix[i] = mat;
LoadedChunks.Add(Foliage_Helper.FormatVec2int(ChunkPosition), i);
Loaded = true;
break;
}
}
if (!Loaded) {
//Find an empty chunk if none are availible
for (int FindEmptyChunk = 0; FindEmptyChunk < TreeRenderMatrix.Length; FindEmptyChunk++)
{
DataList Keys = LoadedChunks.GetKeys();
string ChunkCoordsString = Keys[FindEmptyChunk].String;
string[] SplitItems = ChunkCoordsString.Split(",");
Vector2Int ChunkVector = new Vector2Int(int.Parse(SplitItems[0]), int.Parse(SplitItems[1]));
if (!Foliage_Helper.ChunkShouldBeLoaded(PlayerChunkPosition, ChunkVector, LOD2Radius / ChunkSize))
{
LoadedChunks.TryGetValue(ChunkCoordsString, out DataToken index);
TreeRenderMatrix[index.Int] = mat; // This needs to go in there to overwrite it
LoadedChunks.Add(Foliage_Helper.FormatVec2int(ChunkPosition), index.Int);
LoadedChunks.Remove(ChunkCoordsString);
break;
}
}
}
}
}
ItemsLoadedThisTick++;
}
}
private void Update()
{
//Render
for (int Chunks = 0; Chunks < TreeRenderMatrix.Length; Chunks++) {
if (TreeRenderMatrix[Chunks] == null) { continue; }
for (int Trees = 0; Trees < TreeAssets.Length; Trees++)
{
//FoliageAssetItem item = TreeAssets[Trees];
//Mesh TreeMesh = item.LOD0;
//Material[] mats = item.LOD0Mat;
//for (int submesh = 0; submesh < TreeMesh.subMeshCount; submesh++) {
// VRCGraphics.DrawMeshInstanced(TreeMesh, submesh, mats[submesh], TreeRenderMatrix[Chunks][Trees]);
//}
}
}
}
}
The thing is this system is broken entirely because of one issue. in the final few lines of this code are these two lines
Mesh TreeMesh = item.LOD0;
Material[] mats = item.LOD0Mat;
These lines are basically taking my serializable class and reading the data from them such as lod mesh and materials for the draw func. I have done it this way so i don’t have to deal with flattened arrays that are really hard to work with especially when i am planning to have multiple types of object in this list. I would really like to not flatten this but if that’s the only way. i have a feature request for udon :l
I have tryed so many different things. It was originaly a scriptable object then it was a private class then public and nothing has really been able to fix my issue. If you know a fix I wont have to go through the hell of managing 8 different data arrays at the same time.