本投稿は TECOTEC Advent Calendar 2020 の14日目の記事です。
コンテンツ開発事業部のUnityクライアントエンジニア岩崎です。
本記事では、Unityとオープンソースを用いて、ガラス調のオブジェクトの破壊エフェクトサンプルを作成してみようと思います。
目次
- 使用ソフト
- エフェクトの流れ
- 使用する素材を揃える
- Unityでサンプルを開き、テストしやすいようにシーン内を整理する
- オブジェクトをガラス調にする
- 破壊エフェクトのスクリプト処理
- 破片が散るエフェクト
- オブジェクトの破壊テスト
- 完成版の破壊エフェクト
- 最後に
使用ソフト
- Unity 2019.4.13.1f
エフェクトの流れ
- オブジェクトをクリック
- クリックされたオブジェクトのポリゴンを全て3角錐のパーツで複製&重力等の設定
- より細かい破片が散るエフェクトを再生
- クリックされた実際のオブジェクトを削除
- クローンが設定した重力により自由落下
上記の流れで処理が行われている為、極端な形のオブジェクトでもない限り、別の形のオブジェクトでも自然な破壊エフェクトが実現できます。
使用する素材を揃える
今回は以下のサイト及びオープンソースを用いて導入コストを削減したいと思います。
ガラス調のシェーダー www.atmarkit.co.jp
破壊エフェクトのサンプル github.com
Unityでサンプルを開き、テストしやすいようにシーン内を整理する
サンプルの中には複数のオブジェクトが用意されておりますので、挙動をテストしやすいよう不要なものは非表示にしておきます。 また、挙動が確認しやすいように背景や地面も明るくしました。
オブジェクトをガラス調にする
オブジェクトに前述のガラス調のシェーダーを適応させます。
【ガラス調のシェーダー】
Shader "Custom/GlassShader" { Properties{ _Color("Color", Color) = (1,1,1,1) _MainTex("Albedo (RGB)", 2D) = "white" {} _DotProduct("Rim effect", Range(-1,1)) = 0.25 } SubShader{ Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" } LOD 200 Cull Off CGPROGRAM #pragma surface surf Lambert alpha:fade sampler2D _MainTex; fixed4 _Color; float _DotProduct; struct Input { float2 uv_MainTex; float3 worldNormal; float3 viewDir; }; void surf(Input IN, inout SurfaceOutput o) { float4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; float border = 1 - (abs(dot(IN.viewDir, IN.worldNormal))); float alpha = (border * (1 - _DotProduct) + _DotProduct); o.Alpha = c.a * alpha; } ENDCG } FallBack "Diffuse" }
ガラス…に見えなくもないですが、せっかくですのでもう少しガラス感を出してみます。
色やライト周りを少し変えて、ガラスっぽく透明感を足してみました!
これで見た目の準備が整いました。
破壊エフェクトのスクリプト処理
今回の破壊エフェクトは、クリック時、カメラからカーソル位置に飛ばしたレイに当たったオブジェクトを破壊します。 したがって、ゲーム内カメラにオープンソースより参照した下記スクリプトをアタッチします。
using UnityEngine; using UnityEngine.SceneManagement; public class SimpleMeshExploder : MonoBehaviour { // extra effects to be instantiated public Transform exploPrefab; // public Transform smokePrefab; Camera cam; void Start() { cam = Camera.main; } void Update() { // for testing only, R to reset scene if (Input.GetKeyDown(KeyCode.R)) { Debug.Log("Scene Reload"); SceneManager.LoadScene(0); } // use left mouse button to explode objects if (Input.GetMouseButtonDown(0)) { RaycastHit hit; if (Physics.Raycast(cam.ScreenPointToRay(Input.mousePosition), out hit)) { if (hit.transform.CompareTag("Explodable")) { Explode(hit.transform); } } } } void Explode(Transform target) { // fx Transform clone = Instantiate(exploPrefab, target.position, Quaternion.identity) as Transform; Destroy(clone.gameObject, 5); // clone = Instantiate(smokePrefab, target.position, Quaternion.identity) as Transform; // Destroy(clone.gameObject, 10); Mesh mesh = target.GetComponent<MeshFilter>().mesh; Vector3[] vertices = mesh.vertices; Vector3[] normals = mesh.normals; int[] triangles = mesh.triangles; Vector2[] uvs = mesh.uv; int index = 0; // remove collider from original target.GetComponent<Collider>().enabled = false; // get each face for (int i = 0; i < triangles.Length; i += 3) { // TODO: inherit speed, spin...? Vector3 averageNormal = (normals[triangles[i]] + normals[triangles[i + 1]] + normals[triangles[i + 2]]).normalized; Vector3 s = target.GetComponent<Renderer>().bounds.size; float extrudeSize = ((s.x + s.y + s.z) / 3) * 0.3f; CreateMeshPiece(extrudeSize, target.transform.position, target.GetComponent<Renderer>().material, index, averageNormal, vertices[triangles[i]], vertices[triangles[i + 1]], vertices[triangles[i + 2]], uvs[triangles[i]], uvs[triangles[i + 1]], uvs[triangles[i + 2]]); index++; } // destroy original Destroy(target.gameObject); } void CreateMeshPiece(float extrudeSize, Vector3 pos, Material mat, int index, Vector3 faceNormal, Vector3 v1, Vector3 v2, Vector3 v3, Vector2 uv1, Vector2 uv2, Vector2 uv3) { GameObject go = new GameObject("piece_" + index); Mesh mesh = go.AddComponent<MeshFilter>().mesh; go.AddComponent<MeshRenderer>(); go.tag = "Explodable"; // set this only if should be able to explode this piece also go.GetComponent<Renderer>().material = mat; go.transform.position = pos; Vector3[] vertices = new Vector3[3 * 4]; int[] triangles = new int[3 * 4]; Vector2[] uvs = new Vector2[3 * 4]; // get centroid Vector3 v4 = (v1 + v2 + v3) / 3; // extend to backwards v4 = v4 + (-faceNormal) * extrudeSize; // not shared vertices // orig face //vertices[0] = (v1); vertices[0] = (v1); vertices[1] = (v2); vertices[2] = (v3); // right face vertices[3] = (v1); vertices[4] = (v2); vertices[5] = (v4); // left face vertices[6] = (v1); vertices[7] = (v3); vertices[8] = (v4); // bottom face vertices[9] = (v2); vertices[10] = (v3); vertices[11] = (v4); // orig face triangles[0] = 0; triangles[1] = 1; triangles[2] = 2; // right face triangles[3] = 5; triangles[4] = 4; triangles[5] = 3; // left face triangles[6] = 6; triangles[7] = 7; triangles[8] = 8; // bottom face triangles[9] = 11; triangles[10] = 10; triangles[11] = 9; // orig face uvs[0] = uv1; uvs[1] = uv2; uvs[2] = uv3; // todo // right face uvs[3] = uv1; uvs[4] = uv2; uvs[5] = uv3; // todo // left face uvs[6] = uv1; uvs[7] = uv3; uvs[8] = uv3; // todo // bottom face (mirror?) or custom color? or fixed from uv? uvs[9] = uv1; uvs[10] = uv2; uvs[11] = uv1; // todo mesh.vertices = vertices; mesh.uv = uvs; mesh.triangles = triangles; mesh.RecalculateBounds(); mesh.RecalculateNormals(); CalculateMeshTangents(mesh); go.AddComponent<Rigidbody>(); MeshCollider mc = go.AddComponent<MeshCollider>(); mc.sharedMesh = mesh; mc.convex = true; go.AddComponent<MeshFader>(); } // source: http://answers.unity3d.com/questions/7789/calculating-tangents-vector4.html void CalculateMeshTangents(Mesh mesh) { //speed up math by copying the mesh arrays int[] triangles = mesh.triangles; Vector3[] vertices = mesh.vertices; Vector2[] uv = mesh.uv; Vector3[] normals = mesh.normals; //variable definitions int triangleCount = triangles.Length; int vertexCount = vertices.Length; Vector3[] tan1 = new Vector3[vertexCount]; Vector3[] tan2 = new Vector3[vertexCount]; Vector4[] tangents = new Vector4[vertexCount]; for (long a = 0; a < triangleCount; a += 3) { long i1 = triangles[a + 0]; long i2 = triangles[a + 1]; long i3 = triangles[a + 2]; Vector3 v1 = vertices[i1]; Vector3 v2 = vertices[i2]; Vector3 v3 = vertices[i3]; Vector2 w1 = uv[i1]; Vector2 w2 = uv[i2]; Vector2 w3 = uv[i3]; float x1 = v2.x - v1.x; float x2 = v3.x - v1.x; float y1 = v2.y - v1.y; float y2 = v3.y - v1.y; float z1 = v2.z - v1.z; float z2 = v3.z - v1.z; float s1 = w2.x - w1.x; float s2 = w3.x - w1.x; float t1 = w2.y - w1.y; float t2 = w3.y - w1.y; float r = 1.0f / (s1 * t2 - s2 * t1); Vector3 sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r); Vector3 tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r); tan1[i1] += sdir; tan1[i2] += sdir; tan1[i3] += sdir; tan2[i1] += tdir; tan2[i2] += tdir; tan2[i3] += tdir; } for (int a = 0; a < vertexCount; ++a) { Vector3 n = normals[a]; Vector3 t = tan1[a]; Vector3.OrthoNormalize(ref n, ref t); tangents[a].x = t.x; tangents[a].y = t.y; tangents[a].z = t.z; tangents[a].w = (Vector3.Dot(Vector3.Cross(n, t), tan2[a]) < 0.0f) ? -1.0f : 1.0f; } mesh.tangents = tangents; } }
これで挙動確認の準備が整いました。
と、思いたいですが、もう少し細かい部分に手を加えましょう。
破片が散るエフェクト
実際に物が壊れる場合、その破片は大きいものばかりではないと思います。
そこで、前述のスクリプト内から生成される破片より小さい破片を、
破片が散るエフェクトとして表現しておきます。
これで準備完了です。
オブジェクトの破壊テスト
実際にUnity内でオブジェクトを破壊してみます。
今回用いたオープンソースのサンプルでは、元々石を破壊するイメージだったようですので、
ガラスとしては少し不自然なスモークが出てしまっています。
凍えるほど寒い氷のステージのオブジェクトを破壊する時などはこのままでも良いかもしれませんね。
しかし、今回はあくまで普通のガラスを割ってみる試みですので、前述のスクリプト内からスモークを出現させる処理をコメントアウトしてみます。
完成版の破壊エフェクト
スモークを表示しないよう修正した完成版になります。
破片が自由落下なのは、生成したパーツに特別力を加えていない為で、
よりリアリティを追求する上で、カメラからのレイの方向に力を与えてやるように処理を追加することで、気持ちの良いエフェクトになるのではないでしょうか。
最後に
今回はオープンソース等を使って極力導入のコストを減らしてみました。
ゲームの表現も多種多様に増えてくる一方で、先人の方々の素晴らしい作品の力をお借りし、我々エンジニアも驚くほど作業コストを削減できるようになっていると改めて実感しました。
私も今後、個人的に自作したいシェーダーなどがあればぜひ挑戦してみようと思います。 なおオープンソース等を利用する際は、あらかじめその素材のライセンスの確認を忘れずにしましょう!
ここまでご覧いただき、ありがとうございました。