Nvidia Blast - News & General Discussion - Unity Discussions
해당 효과는 위의 글에서 bocs라는 분이 nvidia blast를 unity에서 쉽게 쓸 수 있도록
만든 외부 라이브러리를 사용했습니다.
구현
일단 위의 패키지를 Import 시킵니다.
FractureTool.cs
using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using Random = UnityEngine.Random;
[CreateAssetMenu(fileName = "FractureData", menuName = "Fracture Data", order = 0)]
[Serializable]
public class FractureData : ScriptableObject
{
[HideInInspector]
public GameObject fracturableObject;
public Material insideMaterial, outsideMaterial;
[Min(1)]
public int breakForce;
[Min(1f)]
public float density;
[Range(2, 64)]
public int siteCount;
[HideInInspector]
public bool haveIndividualSettings;
public bool isChunksActive = true;
}
public static class FractureTool
{
private static NvFractureTool fractureTool = new();
private static NvVoronoiSitesGenerator sites;
/// <summary>
/// fratureData를 기반으로 Mesh를 조각내어 반환
/// </summary>
public static List<Mesh> CreateFractureMeshes(GameObject originObj, FractureData data, Mesh mesh)
{
// Seed 값을 현재 시간에 따라 랜덤으로 설정
Random.InitState((int)DateTime.Now.Ticks);
NvBlastExtUnity.setSeed(Random.Range(0, 1000));
#if UNITY_EDITOR
var worldMesh = CreateWorldMesh(originObj, mesh);
// sharedMesh는 center가 Vector3.zero에 있는 거 같아서 Pivot 위치 조정
for (int i = 0; i < worldMesh.vertices.Length; i++)
{
worldMesh.vertices[i] += originObj.transform.position;
}
worldMesh.RecalculateBounds();
#else
var worldMesh = CreateWorldMesh(originObj, mesh);
#endif
var nvMesh = new NvMesh(
worldMesh.vertices,
worldMesh.normals,
worldMesh.uv,
worldMesh.vertexCount,
worldMesh.GetIndices(0),
(int) worldMesh.GetIndexCount(0));
fractureTool.setRemoveIslands(false);
fractureTool.setSourceMesh(nvMesh);
sites = new NvVoronoiSitesGenerator(nvMesh);
sites.uniformlyGenerateSitesInMesh(data.siteCount);
fractureTool.voronoiFracturing(0, sites);
fractureTool.finalizeFracturing();
var meshCount = fractureTool.getChunkCount();
var outsideMeshes = new List<Mesh>(meshCount);
var insideMeshes = new List<Mesh>(meshCount);
for (var i = 0; i < meshCount; i++)
{
var tmp = CreateChunkMesh(fractureTool, i);
var inside = tmp[1];
var outside = tmp[0];
outside.name = $"{originObj.name} Outside Chunk {i}";
outsideMeshes.Add(outside);
insideMeshes.Add(inside);
}
// Outside Mesh에 Inside Mesh도 포함되어 있음.
return outsideMeshes;
}
private static Mesh[] CreateChunkMesh(NvFractureTool fractureTool, int index)
{
var outside = fractureTool.getChunkMesh(index, false);
var inside = fractureTool.getChunkMesh(index, true);
var outsideMesh = outside.toUnityMesh();
var insideMesh = inside.toUnityMesh();
outsideMesh.subMeshCount = 2;
outsideMesh.SetIndices(inside.getIndexes(), MeshTopology.Triangles, 1);
return new Mesh[] { outsideMesh, insideMesh };
}
private static Mesh CreateWorldMesh(GameObject originObj, Mesh originMesh)
{
var newMesh = new Mesh();
var vertices = originMesh.vertices;
for (var i = 0; i < vertices.Length; i++)
{
vertices[i] = originObj.transform.TransformPoint(vertices[i]);
}
newMesh.vertices = vertices;
newMesh.triangles = originMesh.triangles;
newMesh.normals = originMesh.normals;
newMesh.uv = originMesh.uv;
newMesh.RecalculateBounds();
return newMesh;
}
public static void CreateFractureGameObjects(GameObject originObj, FractureData data, List<Mesh> meshes)
{
var chunks = new GameObject("Chunks");
var chunkIndex = 0;
var chunkCount = fractureTool.getChunkCount();
chunks.transform.SetParent(originObj.transform);
for (var i = 1; i < chunkCount; i++)
{
GameObject chunk = new GameObject($"{originObj.gameObject.name} Chunk {chunkIndex++}");
chunk.transform.SetParent(chunks.transform);
chunk.AddComponent<MeshFilter>().mesh = meshes[i];
data.insideMaterial = data.insideMaterial == null ? new Material(Shader.Find("Standard")) : data.insideMaterial;
data.outsideMaterial = data.outsideMaterial == null ? originObj.GetComponent<MeshRenderer>().sharedMaterial : data.outsideMaterial;
chunk.AddComponent<MeshRenderer>().materials = new Material[] { data.outsideMaterial, data.insideMaterial };
Rigidbody rig = chunk.AddComponent<Rigidbody>();
rig.mass = VolumeOfMesh(meshes[i]) * data.density;
// 충돌할 때 원래 오브젝트가 받은 힘을 Chunk들이 받도록 설정
#if !UNITY_EDITOR
rig.velocity = actorRig.velocity;
rig.angularVelocity = actorRig.angularVelocity;
#endif
MeshCollider col = chunk.AddComponent<MeshCollider>();
col.sharedMesh = meshes[i];
col.convex = true;
FixedJoint joint = chunk.AddComponent<FixedJoint>();
joint.breakForce = data.breakForce;
}
chunks.SetActive(data.isChunksActive);
}
/// <summary>
/// 각 조각의 부피를 계산
/// </summary>
// https://discussions.unity.com/t/how-would-one-calculate-a-3d-mesh-volume-in-unity/16895
private static float VolumeOfMesh(Mesh mesh)
{
float volume = 0;
Vector3[] vertices = mesh.vertices;
int[] triangles = mesh.triangles;
for (int i = 0; i < triangles.Length; i += 3)
{
Vector3 p1 = vertices[triangles[i + 0]];
Vector3 p2 = vertices[triangles[i + 1]];
Vector3 p3 = vertices[triangles[i + 2]];
volume += SignedVolumeOfTriangle(p1, p2, p3);
}
return Mathf.Abs(volume);
}
private static float SignedVolumeOfTriangle(Vector3 p1, Vector3 p2, Vector3 p3)
{
float v321 = p3.x * p2.y * p1.z;
float v231 = p2.x * p3.y * p1.z;
float v312 = p3.x * p1.y * p2.z;
float v132 = p1.x * p3.y * p2.z;
float v213 = p2.x * p1.y * p3.z;
float v123 = p1.x * p2.y * p3.z;
return (1.0f / 6.0f) * (-v321 + v231 + v312 - v132 - v213 + v123);
}
}
사용 방법
일단 위 스크립트들을 만들어 줍니다.
그리고 Project에서 Create - 맨 위에 Fracture Data를
클릭해서 Scriptableobject를 만들어 줍니다.
Fracture Data는 조각 정보? 라고 생각하면 되는데요.
하나씩 보면
Inside는 조각난 오브젝트의 안쪽 Material이고
Outside는 바깥족 Material입니다.
참고로 Inside, Outside 둘 다 비워놔도
Inside는 Standard Material
Outside는 조각날 오브젝트, 그러니까 원본 오브젝트의 Material을 가져옵니다.
Break Force는 Joint에서의 Break Force를 말합니다.
이게 높으면 높을수록 쉽게 안 부서집니다.
이건 조각난 오브젝트들에 하나씩 할당되어 있습니다.
원래 여러 개의 joint를 만들어서 Connected Body에 닿아 있는 조각난 오브젝트들을 넣어줘야 자연스럽게 부서지는데
구현 방식에 대해 고민 중이라 안 넣었고 없어도 되길래 일단 Connected Body는 빼줬습니다.
Density는 밀도를 말하는 데 조각난 오브젝트마다 부피를 다르게 계산하기 위해 넣어줬습니다.
Site Count는 조각 개수입니다.
Is Chunks Active는 오브젝트들이 조각나면
이런 식으로 자식들을 구성하게 됩니다.
여기서 중간에 Chunks 오브젝트의 활성화 여부입니다.
public class FractureInPlayMode : MonoBehaviour
{
[Header("Chunk Settings")]
[SerializeField, Tooltip("조각날 오브젝트의 정보")]
private FractureData fractureData;
public FractureData FractureData { get => fractureData; set => fractureData = value; }
[SerializeField, Tooltip("조각날 메쉬")]
private Mesh fractureMesh;
private GameObject chunksParent, chunks;
private void Start()
{
GenerateFracture();
}
public void GenerateFracture()
{
var meshes = FractureTool.CreateFractureMeshes(gameObject, fractureData, fractureMesh);
FractureTool.CreateFractureGameObjects(gameObject, fractureData, meshes);
}
}
위 코드는 조각내는 방법을 예시로 만든 스크립트입니다.
fractureData는 Component 창에서 가져올 수 있고
다른 스크립트에서 ScriptableObject의 CreateInstance를 통해 만든 FractureData를 직접 가져올 수도 있습니다.
fractureMesh는 조각날 메쉬, 원본 메쉬입니다.
FractureTool.CreateFractureMeshes는 조각내고 조각난 오브젝트들의 Mesh를 만듭니다.
FractureTool.CreateFractureGameObjects는 조각난 Mesh들을 가지고 GameObject를 만듭니다.
암튼, 이런 식으로 위 스크립트를 부수고자 하는 오브젝트에 넣어줍니다.
그러고 나서 rigidbody, trigger를 가지는 오브젝트를 가져다 박으면 부서지는 효과를 만들 수 있습니다.
기타
해당 방법은 Play Mode에서 생성하는 방법입니다.
물론 Edit Mode에서 미리 조각내는 방법도 있지만 이건 따로 글 쓸 예정입니다.
'유니티 > Script' 카테고리의 다른 글
Unity Edit Mode에서 오브젝트 부서지는 효과 만들기 (0) | 2025.02.07 |
---|---|
폴더를 클릭하면 Inspector 바뀌는 거 방지 (0) | 2025.01.01 |