Welcome to the Runtime Mesh World Sample repository! This repository is a script sample that takes a three-dimensional byte array as input and places cubes on non-zero elements to construct a building world.
- Optimization: The use of the Job System and async Task syntax is employed to maximize background multithreading, minimizing the load on the main thread.
Follow these steps to try out the Sample:
- Clone this repository.
- Open the project from the cloned folder using Unity Hub.
- Play the sample scene to understand how mesh merging is performed.
- Pressing the space key triggers world generation with shifted positions.
ランタイム メッシュワールドサンプル リポジトリへようこそ!
このリポジトリは三次元 byte 配列を入力に、0 でない要素にキューブを配置して、建築世界を構築するスクリプトサンプルです。
- 最適化: Job System と async Task 構文を用いて極力バックグラウンドマルチスレッド化がほどこされ、メインスレッドに負荷を与えません。
以下の手順に従って、サンプルをお試しできます。
- このリポジトリをクローンします。
- Unity Hub 経由でクローンしたフォルダからプロジェクトを開きます。
- サンプルシーンを開いて再生することでメッシュの結合サンプル処理を理解します。
- スペースキーを押すと、位置をずらした世界生成が行われます
usingSystem;usingSystem.Collections.Generic;usingSystem.IO;usingSystem.Threading.Tasks;usingUnity.Collections;usingUnity.Jobs;usingUnity.Mathematics;usingUnityEngine;usingUnityEngine.Rendering;namespaceSimplestarGame{publicclassSimpleMeshWorldSample:MonoBehaviour{[SerializeField]stringmeshFileName="BasicCube.caw";[SerializeField]stringdataFileName="world000.gz";[Range(0,3)][SerializeField]intchunkCount=0;[SerializeField]Materialmaterial;[SerializeField]Transform[]parents;intparetentIndex=0;constintdataEdgeCubeCount=256;classWorldData{publicNativeArray<byte>voxelData;publicNativeArray<XYZ>xyz;publicNativeArray<int>countOffsets;}voidAwake(){Application.targetFrameRate=60;GraphicsSettings.useScriptableRenderPipelineBatching=true;}asyncvoidStart(){// キューブのメッシュデータソースを読み込みvarcubeData=awaitTask.Run(()=>CAWFile.ReadCAWFile(Path.Combine(Application.streamingAssetsPath,this.meshFileName)));// ワールドのボクセルデータ読み込みvarcompressedData=awaitTask.Run(()=>File.ReadAllBytes(Path.Combine(Application.streamingAssetsPath,this.dataFileName)));// ワールドのデータ解凍varworldDataBytes=awaitTask.Run(()=>GZipCompressor.Unzip(compressedData));// Job用NativeArray確保varedgeCubeCount=Mathf.RoundToInt(16*Mathf.Pow(2,this.chunkCount));varworldData=awaitthis.AllocateDataAsync(worldDataBytes,edgeCubeCount);// Meshオブジェクト作成List<GameObject>meshObjectList=newList<GameObject>();varedgeChunkCount=dataEdgeCubeCount/edgeCubeCount;for(intchunkX=0;chunkX<edgeChunkCount;chunkX++){for(intchunkY=0;chunkY<edgeChunkCount;chunkY++){for(intchunkZ=0;chunkZ<edgeChunkCount;chunkZ++){varchunkInt3=newVector3Int(chunkX,chunkY,chunkZ);meshObjectList.Add(awaitthis.CreateChunkObjectAsync(worldData,cubeData,chunkInt3,edgeCubeCount));}}}// 確保したものを開放worldData.countOffsets.Dispose();worldData.xyz.Dispose();worldData.voxelData.Dispose();cubeData.fileVertexData.Dispose();cubeData.vertexCounts.Dispose();// BakeMeshawaitthis.BakeMeshAsync(meshObjectList);// 配置換えthis.paretentIndex++;if(this.parents.Length==this.paretentIndex){this.paretentIndex=0;}}voidUpdate(){// Space キーを押すと、配置換えしてワールドを構築し直しますif(Input.GetKeyDown(KeyCode.Space)){this.Start();}}/// <summary>/// 計算で毎回使うバッファ、使いまわすために最初に確保/// </summary>/// <param name="worldDataBytes">世界データ</param>/// <param name="edgeCubeCount">チャンクの辺キューブ数</param>/// <returns>確保したバッファ</returns>asyncTask<WorldData>AllocateDataAsync(byte[]worldDataBytes,intedgeCubeCount){returnawaitTask.Run(()=>{varvoxelData=newNativeArray<byte>(worldDataBytes,Allocator.Persistent);varxyz=newNativeArray<XYZ>(edgeCubeCount*edgeCubeCount*edgeCubeCount,Allocator.Persistent);varcountOffsets=newNativeArray<int>(xyz.Length,Allocator.Persistent);for(bytex=0;x<edgeCubeCount;x++){for(bytey=0;y<edgeCubeCount;y++){for(bytez=0;z<edgeCubeCount;z++){varindex=x*edgeCubeCount*edgeCubeCount+y*edgeCubeCount+z;xyz[index]=newXYZ{x=x,y=y,z=z};countOffsets[index]=0;}}}returnnewWorldData{voxelData=voxelData,xyz=xyz,countOffsets=countOffsets};});}/// <summary>/// メッシュの作成/// </summary>/// <param name="meshDataArray">データ設定済みメッシュデータ</param>/// <param name="vertexIndexCount">インデックス数=頂点数</param>/// <param name="bounds">バウンディングボックス情報</param>/// <returns>作成したメッシュ</returns>asyncTask<Mesh>CreateMesh(Mesh.MeshDataArraymeshDataArray,intvertexIndexCount,float3x2bounds){varnewMesh=newMesh();newMesh.name="CustomLayoutMesh";varmeshBounds=newMesh.bounds=newBounds((bounds.c0+bounds.c1)*0.5f,bounds.c1-bounds.c0);awaitTask.Run(()=>{meshDataArray[0].SetSubMesh(0,newSubMeshDescriptor{topology=MeshTopology.Triangles,vertexCount=vertexIndexCount,indexCount=vertexIndexCount,baseVertex=0,firstVertex=0,indexStart=0,bounds=meshBounds},MeshUpdateFlags.DontRecalculateBounds|MeshUpdateFlags.DontValidateIndices|MeshUpdateFlags.DontNotifyMeshUsers);});Mesh.ApplyAndDisposeWritableMeshData(meshDataArray,new[]{newMesh},MeshUpdateFlags.DontRecalculateBounds|MeshUpdateFlags.DontValidateIndices|MeshUpdateFlags.DontNotifyMeshUsers);returnnewMesh;}/// <summary>/// メッシュオブジェクト作成/// </summary>/// <param name="worldData">ワールド全体データ</param>/// <param name="cubeData">キューブの頂点情報</param>/// <param name="chunkOffset">ワールド内のローカル塊オフセット</param>/// <param name="edgeCubeCount">塊の辺キューブ数</param>/// <returns>作成したゲームオブジェクト</returns>asyncTask<GameObject>CreateChunkObjectAsync(WorldDataworldData,CAWFile.CubeDatacubeData,Vector3IntchunkOffset,intedgeCubeCount){// カウントawaitTask.Run(()=>{varcountJobHandle=newCountChunkVertexJob(){vertexCounts=cubeData.vertexCounts,voxelData=worldData.voxelData,xyz=worldData.xyz,results=worldData.countOffsets,heightDepth=edgeCubeCount*edgeCubeCount,width=edgeCubeCount,height=edgeCubeCount,depth=edgeCubeCount,chunkOffset=chunkOffset,dataEdgeCubeCount=dataEdgeCubeCount,}.Schedule(worldData.xyz.Length,8);countJobHandle.Complete();});// 集計varvertexIndexCount=awaitTask.Run(()=>{intvertexIndexCount=0;for(intindex=0;index<worldData.countOffsets.Length;index++){varcounts=worldData.countOffsets[index];worldData.countOffsets[index]=vertexIndexCount;vertexIndexCount+=counts;}returnvertexIndexCount;});if(vertexIndexCount==0){returnnull;}// 確保varmeshDataArray=Mesh.AllocateWritableMeshData(1);Mesh.MeshDatameshData=meshDataArray[0];meshData.subMeshCount=1;meshData.SetVertexBufferParams(vertexIndexCount,CustomLayoutMesh.VERTEX_ATTRIBUTE_DESCRIPTORS);meshData.SetIndexBufferParams(vertexIndexCount,IndexFormat.UInt32);NativeArray<int>indexData=meshData.GetIndexData<int>();// インデックス書き込みvarindexJobHandle=newWriteIndexDataJob(){indexData=indexData}.Schedule(indexData.Length,128);indexJobHandle.Complete();// 頂点データ書き込みNativeArray<CustomVertexLayout>vertexData=meshData.GetVertexData<CustomVertexLayout>(stream:0);awaitTask.Run(()=>{varwriteJobHandle=newWriteChunkDataJob(){vertexCounts=cubeData.vertexCounts,voxelData=worldData.voxelData,xyz=worldData.xyz,countOffsets=worldData.countOffsets,width=edgeCubeCount,height=edgeCubeCount,depth=edgeCubeCount,fileVertexData=cubeData.fileVertexData,vertexData=vertexData,chunkOffset=chunkOffset,dataEdgeCubeCount=dataEdgeCubeCount,}.Schedule(worldData.xyz.Length,8);writeJobHandle.Complete();});// バウンディングボックスfloat3x2bounds=newfloat3x2();bounds.c0=math.min(bounds.c0,newfloat3(-0.5f,-0.5f,-0.5f));bounds.c1=math.max(bounds.c1,newfloat3(edgeCubeCount+0.5f,edgeCubeCount+0.5f,edgeCubeCount+0.5f));// オブジェクト作成MeshnewMesh=awaitthis.CreateMesh(meshDataArray,vertexIndexCount,bounds);vertexData.Dispose();indexData.Dispose();GameObjectnewGameObject=newGameObject("TestCubeMeshObject");newGameObject.transform.SetParent(this.parents[this.paretentIndex],false);newGameObject.transform.localPosition=chunkOffset*edgeCubeCount;newGameObject.isStatic=true;newGameObject.AddComponent<MeshFilter>().sharedMesh=newMesh;newGameObject.AddComponent<MeshRenderer>().sharedMaterial=this.material;returnnewGameObject;}/// <summary>/// MeshCollider 作成/// </summary>/// <param name="meshObjectList">MeshFilter の sharedMesh を入力に MeshCollider を計算します</param>/// <returns>Task</returns>asyncTaskBakeMeshAsync(List<GameObject>meshObjectList){NativeArray<int>meshIds=newNativeArray<int>(meshObjectList.Count,Allocator.Persistent);varmeshIdx=0;foreach(varmeshObjectinmeshObjectList){varmesh=meshObject.GetComponent<MeshFilter>().sharedMesh;meshIds[meshIdx++]=mesh.GetInstanceID();}awaitTask.Run(()=>{varbakeMeshJob=newBakeMeshJob(meshIds);varbakeMeshJobHandle=bakeMeshJob.Schedule(meshIds.Length,1);bakeMeshJobHandle.Complete();meshIds.Dispose();});// Set MeshColliderforeach(varmeshObjectinmeshObjectList){meshObject.AddComponent<MeshCollider>().sharedMesh=meshObject.GetComponent<MeshFilter>().sharedMesh;}}}}This project is licensed under the MIT License.
If you find a bug, have an enhancement idea, or want to contribute in any other way, please open an issue or submit a pull request.
