[ 프랙탈 만들기 ]
Instantiate game objects.
Work with recursion.
Use coroutines.
Add randomness.
여기서는 프랙탈이 만들어지는 간단한 스크립트를 작성한다.
[ 1 How to Make a Fractal ]
새로운 프로젝트 만들기
Menu : File >> New Scene
Menu : GameObject >> Create Empty
Name : Fractal
Project Window : Create >> C# Script
Name : Fractal
Fractal(Script) 드래그 >> (to) Fractal(GameObject)
[ 2 Showing Something ]
public class Fractal : MonoBehaviour {
public Mesh mesh;
public Material material;
private void Start () {
gameObject.AddComponent<MeshFilter>().mesh = mesh;
gameObject.AddComponent<MeshRenderer>().material = material;
}
}
Fractal(Script) : Mesh >> Cube 메쉬를 찾아서 대입
Fractal(Script) : Material >> Default-Material을 찾아서 대입
Mesh
폴리곤으로 이루어진 복합체(3D Object)
Material
Object가 보여지는 속성(매질) - 색깔, 질감 등
Start
Update()가 호출되기 전에 한번만 호출되는 함수
AddComponent
새로운 컴퍼넌트를 GameObject에 Add하는 함수
[ 3 Making Children ]
Child Object 생성하기
public class Fractal : MonoBehaviour {
public Mesh mesh;
public Material material;
private void Start () {
gameObject.AddComponent<MeshFilter>().mesh = mesh;
gameObject.AddComponent<MeshRenderer>().material = material;
//new GameObject ("Fractal Child");
new GameObject ("Fractal Child").AddComponent<Fractal> ();
}
}
새로운 GameObject(Fractal Child)를 만들고 거기에 "Fractal(Script)"를 추가
Fractal Script의 Start함수가 구동되면서 무한정 Fractal을 만들고 있음
new GameObject ("Fractal Child"); 로 실행시키면 Empty Object의 Fractal만 만들어짐
만들어지는 자식의 개수를 제한해 보기
public int maxDepth;
private int depth;
private void Start () {
gameObject.AddComponent<MeshFilter>().mesh = mesh;
gameObject.AddComponent<MeshRenderer>().material = material;
if (depth < maxDepth) {
new GameObject ("Fractal Child").AddComponent<Fractal> ();
depth++;
}
}
maxDepth에 4를 대입하고 실행 (Editor창의 Fractal(GameObject) >> Inspector : Fractal(script) : maxDepth)
maxDepth와 depth가 독립적인 객체라 공유가 안됨
결과적으로 한개의 Child Object만 생성
또한, 기존에 mesh, material 정보도 넘어오지 않음
public class Fractal : MonoBehaviour {
public Mesh mesh;
public Material material;
public int maxDepth;
private int depth;
private void Start () {
gameObject.AddComponent<MeshFilter>().mesh = mesh;
gameObject.AddComponent<MeshRenderer>().material = material;
if (depth < maxDepth) {
new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this);
}
}
private void Initialize (Fractal parent) {
mesh = parent.mesh;
material = parent.material;
maxDepth = parent.maxDepth;
depth = parent.depth + 1;
//transform.parent = parent.transform;
}
}
Initialize()함수를 통해 기존의 mesh, material, depth, MaxDepth의 정보를 자식오브젝트에 넘겨줌
this
현재 자신의 클래스로 생성된 객체의 레퍼런스
GameObject를 만들게 되면 Awake()와 OnEnable() 함수가 호출되게 되며, 그 이후에 AddCompnent를 통해 Fractal 스크립트를 추가하고 있다. 만약 Start() 함수가 아닌 Awake()함수에서 진행하게 되면, Awake()가 끝나지 않는 시점에서 계속 재귀 호출을 하는 현상이 발생하게 됨 (Start()함수에서 실행하게 되면 모든 일들이 끝마친 이후에 자식 단계로 넘어 갈 수 있게 됨)
transform.parent = parent.transform;
[ 4 Shaping Children ]
public float childScale;
private void Initialize (Fractal parent) {
mesh = parent.mesh;
material = parent.material;
maxDepth = parent.maxDepth;
depth = parent.depth + 1;
childScale = parent.childScale;
transform.parent = parent.transform;
transform.localScale = Vector3.one * childScale;
transform.localPosition = Vector3.up * (0.5f + 0.5f * childScale);
}
Fractal Inspector >> Fractal(Script) : Child Scale 에 0.5 대입
transform.localPosition = Vector3.up * (0.5f + 0.5f * childScale);
parent의 크기의 반 위치와 child의 크기 위치의 반
[ 5 Making Multiple Children ]
두 방향으로 뻗어나가는 프랙탈 만들기
private void Start () {
gameObject.AddComponent<MeshFilter>().mesh = mesh;
gameObject.AddComponent<MeshRenderer>().material = material;
if (depth < maxDepth) {
new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Vector3.up);
new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Vector3.right);
}
}
public float childScale;
private void Initialize (Fractal parent, Vector3 direction) {
mesh = parent.mesh;
material = parent.material;
maxDepth = parent.maxDepth;
depth = parent.depth + 1;
childScale = parent.childScale;
transform.parent = parent.transform;
transform.localScale = Vector3.one * childScale;
transform.localPosition = direction * (0.5f + 0.5f * childScale);
}
Coroutine을 사용해서 자식 객체들이 일정 간격으로 생겨날 수 있도록 구현하기
private void Start () {
gameObject.AddComponent<MeshFilter>().mesh = mesh;
gameObject.AddComponent<MeshRenderer>().material = material;
if (depth < maxDepth) {
StartCoroutine(CreateChildren());
}
}
private IEnumerator CreateChildren () {
yield return new WaitForSeconds(0.5f);
new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Vector3.up);
yield return new WaitForSeconds(0.5f);
new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Vector3.right);
}
세 방향으로 뻗어나가는 프랙탈 만들기
private IEnumerator CreateChildren () {
yield return new WaitForSeconds(0.5f);
new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Vector3.up);
yield return new WaitForSeconds(0.5f);
new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Vector3.right);
yield return new WaitForSeconds(0.5f);
new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Vector3.left);
}
overdraw(Scene Menu : overdraw)를 보면 보이지 않는 객체들이 만들어져 있는 것을 볼 수 있음
이러한 문제가 발생한 이유는 부모 객체와 똑같은 방향(상, 좌, 우)으로만 만들어지고 있기 때문
만들어지는 자식들을 회전함으로써 이를 해결
private IEnumerator CreateChildren () {
yield return new WaitForSeconds(0.5f);
new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Vector3.up, Quaternion.identity);
yield return new WaitForSeconds(0.5f);
new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Vector3.right, Quaternion.Euler(0f, 0f, -90f));
yield return new WaitForSeconds(0.5f);
new GameObject("Fractal Child").AddComponent<Fractal>().Initialize(this, Vector3.left, Quaternion.Euler(0f, 0f, 90f));
}
public float childScale;
private void Initialize (Fractal parent, Vector3 direction, Quaternion orientation) {
mesh = parent.mesh;
material = parent.material;
maxDepth = parent.maxDepth;
depth = parent.depth + 1;
childScale = parent.childScale;
transform.parent = parent.transform;
transform.localScale = Vector3.one * childScale;
transform.localPosition = direction * (0.5f + 0.5f * childScale);
transform.localRotation = orientation;
}
Quaternion
UP방향으로 가는 것은 그대로 두고, Right, Left 방향으로 생겨나는 자식 객체들은 회전을 할 수 있도록 함
그래도 자세히 보면 Scale이 0.5로 줄이다보니, 작은 것들이 겹치는 현상이 발생함
이를 해결하기 위해서는 Scale을 줄이거나 메시를 Sphere로 하면 됨
[ 6 Better Code For More Children ]
코드를 좀 더 직관적으로 정리하기
public class Fractal : MonoBehaviour {
public Mesh mesh;
public Material material;
public int maxDepth;
private int depth;
private static Vector3[] childDirections = {
Vector3.up,
Vector3.right,
Vector3.left
};
private static Quaternion[] childOrientations = {
Quaternion.identity,
Quaternion.Euler(0f, 0f, -90f),
Quaternion.Euler(0f, 0f, 90f)
};
private void Start () {
gameObject.AddComponent<MeshFilter>().mesh = mesh;
gameObject.AddComponent<MeshRenderer>().material = material;
if (depth < maxDepth) {
StartCoroutine(CreateChildren());
}
}
private IEnumerator CreateChildren () {
for (int i = 0; i < childDirections.Length; i++) {
yield return new WaitForSeconds(0.5f);
new GameObject("Fractal Child").AddComponent<Fractal>().
Initialize(this, i);
}
}
public float childScale;
private void Initialize (Fractal parent, int childIndex) {
mesh = parent.mesh;
material = parent.material;
maxDepth = parent.maxDepth;
depth = parent.depth + 1;
childScale = parent.childScale;
transform.parent = parent.transform;
transform.localScale = Vector3.one * childScale;
transform.localPosition = childDirections[childIndex] * (0.5f + 0.5f * childScale);
transform.localRotation = childOrientations[childIndex];
}
}
static 멤버 변수는 (Fractal 클래스로 만들어진) 모든 객체가 동일하게 접근 가능
5방향으로 뻗어나가는 프랙탈 만들기
private static Vector3[] childDirections = {
Vector3.up,
Vector3.right,
Vector3.left,
Vector3.forward,
Vector3.back
};
private static Quaternion[] childOrientations = {
Quaternion.identity,
Quaternion.Euler(0f, 0f, -90f),
Quaternion.Euler(0f, 0f, 90f),
Quaternion.Euler(90f, 0f, 0f),
Quaternion.Euler(-90f, 0f, 0f)
};
[ 7 Explosive Growth ]
f(0) = 1, f(n) = 5 * f(n-1) + 1 (1, 6, 31, 156, 781, 3906, 19531, 97656 ... )
기하급수적으로 늘어나는 객체들 때문에 depth가 4 이상이면 Frame Rate가 급속하게 떨어질 것임
자식 객체를 0.5초마다 생성하게 하지말고, 0.1 ~ 0.5초 사이에 랜덤하게 생성할 수 있도록 하자.
Random 함수 ( int와 float)
Random.Range( ... )
private IEnumerator CreateChildren () {
for (int i = 0; i < childDirections.Length; i++) {
yield return new WaitForSeconds(Random.Range(0.1f, 0.5f));
new GameObject("Fractal Child").AddComponent<Fractal>().
Initialize(this, i);
}
}
[ 8 Adding Color ]
단조로운 색상을 Lerp 함수를 이용하여 그라이데이션 같은 색상을 주자
Lerp : 보간 함수
Lerp(a, b, t) : a + (b -a) * t (t : 0 ~ 1)
private void Start () {
gameObject.AddComponent<MeshFilter>().mesh = mesh;
gameObject.AddComponent<MeshRenderer>().material = material;
GetComponent<MeshRenderer>().material.color =
Color.Lerp(Color.white, Color.yellow, (float)depth / maxDepth);
if (depth < maxDepth) {
StartCoroutine(CreateChildren());
}
}
Dynamic Batching
유니티에서 draw call을 일괄적으로 처리하기 위한 방법으로 동일한 material을 공유하는 메시들을 하나의 Mesh로 만들어 사용될 수 있게끔 만들어 줌
단순한 모양의 Mesh에서는 성능을 발휘하지만, 복잡한 형태의 메시는 작동하지 않음
depth별로 같은 Material을 사용하는 Mesh는 하나의 Material만 만들어서 사용할 수 있도록 하자.
private static Material[] materials;
private void InitializeMaterials () {
materials = new Material[maxDepth + 1];
for (int i = 0; i <= maxDepth; i++) {
materials[i] = new Material(material);
materials[i].color =
Color.Lerp(Color.white, Color.yellow, (float)i / maxDepth);
}
}
private void Start () {
if (materials == null) {
InitializeMaterials();
}
gameObject.AddComponent<MeshFilter>().mesh = mesh;
gameObject.AddComponent<MeshRenderer>().material = materials[depth]; // Depth에 따라 만들어진 Material 사용
if (depth < maxDepth) {
StartCoroutine(CreateChildren());
}
}
...
private void Initialize (Fractal parent, int childIndex) {
mesh = parent.mesh;
//materials = parent.materials;
maxDepth = parent.maxDepth;
... ..
마지막 노드로 생성되는 메시의 색깔을 magenta로 바꿔 보자
private void InitializeMaterials () {
materials = new Material[maxDepth + 1];
for (int i = 0; i <= maxDepth; i++) {
float t = i / (maxDepth - 1f);
t *= t;
materials[i] = new Material(material);
materials[i].color = Color.Lerp(Color.white, Color.yellow, t);
}
materials[maxDepth].color = Color.magenta;
}
Material의 색깔을 Cyan과 Yellow를 랜덤하게 설정되게 하고, Depth에 따라 농도를 white ~ cyan, white ~ yellow 로 하기
마지막 카라는 magenta 혹은 red가 되도록 하고 있음
private static Material[,] materials;
private void InitializeMaterials () {
materials = new Material[maxDepth + 1, 2];
for (int i = 0; i <= maxDepth; i++) {
float t = i / (maxDepth - 1f);
t *= t;
materials[i, 0] = new Material(material);
materials[i, 0].color = Color.Lerp(Color.white, Color.yellow, t);
materials[i, 1] = new Material(material);
materials[i, 1].color = Color.Lerp(Color.white, Color.cyan, t);
}
materials[maxDepth, 0].color = Color.magenta;
materials[maxDepth, 1].color = Color.red;
}
private void Start () {
if (materials == null) {
InitializeMaterials();
}
gameObject.AddComponent<MeshFilter>().mesh = mesh;
gameObject.AddComponent<MeshRenderer>().material =
materials[depth, Random.Range(0, 2)];
if (depth < maxDepth) {
StartCoroutine(CreateChildren());
}
}
[ 9 Randomizing Meshes ]
[ 10 Making the Fractal Irregular ]
[ 11 Rotating the Fractal ]
[ 12 Adding More Chaos ]