Search

Python으로 모델링 정리 1. Lego Brick

요약
모델링의 특정 요소로 동일한 모델링을 찾아 정리하기
분류
script
태그
python
작성일
2020/08/10
1 more property

개요. summary

C4D는 오브젝트가 많을때(보통은 1만개 이상부터) 느려지는 경우가 많습니다. 보통은 캐드 파일을 변환할 때 자주 겪는 일 일것입니다. 이때는 오브젝트를 합쳐 수를 줄이거나 동일한 모델링을 Instance 나 Cloner 로 처리하면 작업속도 향상과 애니메이션에서 많은 이점이 있습니다. 모델링의 특정 요소를 찾아 반복적인 작업을 스크립트로 자동화 처리하는 방법을 설명합니다.

비교해 볼 수 있는 속성들. Comparison factors

1.
오브젝트 이름
2.
오브젝트 컬러
3.
그룹 된 오브젝트의 수
4.
포인트 수 일치
5.
포인트 위치 일치
6.
폴리곤 수 일치
7.
바운딩박스 크기 일치
8.
포인트들 간의 거리 기준
이외에도 다양한 방법을 발견할 수 있을 것입니다.

C4D Python

C4D에서 파이썬을 사용하려면 기본적인 파이썬 문법을 알아야합니다. 기본기 위에 시포디용 명령을 이용하는 것이기에 아래 설명하는 내용을 제대로 이해하려면 파이썬 기본지식이 필요합니다. 인터넷에 기초 강좌가 너무나 많이 공개되어있으니 틈나는대로 계속 공부를 해주세요.

기본구조. Base structure

하단의 if 문이 실행될때 def main(): 이 실행됩니다.
# c4d 모듈 불러오기 import c4d # main 함수 def main(): return # main 함수를 실행하기위한 코드 if __name__ == "__main__": main()
Python
복사

검색함수. Search funtion

특정 오브젝트를 기준으로 하위 오브젝트 전체를 찾는 함수입니다. GetChildren() 함수가 있지만, 바로 아래 자식오브젝트들만 검색되어 손자 오브젝트들 이상의 하위 오브젝트는 검색하지 않습니다.
def GetAllChildren(op, ops=[]): if ops == []:ops = [op] ops.extend(op.GetChildren()) for o in op.GetChildren(): if o.GetDown(): GetAllChildren(o, ops) return ops
Python
복사

Sublime Text

코드 작성에는 Sublime Text 를 이용합니다. C4D 와 연동을 위해 플러그인 설치가 필요하고 코드힌트를 보여주는 부가 기능들을 설치해야 합니다. 다음의 Sublime Text 강좌 4편을 보고 세팅해주세요.

스크립트 실행 방법. How to run script

Sublime Text 를 사용하기 힘들다면 아래 코드들을 script manager(shift + F11) 에서 file > new script 하여 붙여넣고 오른쪽 아래 Excute 버튼으로 실행할 수 있습니다.
아래 작성하는 코드는 과정별로 분리되어있지만, 최종적으로 한번에 실행이 가능한 코드입니다. 단계별로 필요한 코드들을 설명하고 마지막에 합치고 정리한 내용의 최종코드를 공개하였습니다.

레고 모델링 정리. Optimizing lego brick modeling

샘플 파일. Sample file

아래 링크에서 맘에 드는 모델을 다운받습니다. Studio 에 블럭 정보가 없다면 누락되는 블럭이 생길 수 있습니다. 설명을 위해 42111 doms dodge charger 를 선택했습니다.
위의 모델 파일을 아래 Studio 프로그램으로 열어 Export 합니다. Collada로 저장해야 모델링 정보가 포함됩니다. 다른 포멧은 C4D에 불러올수도 없지만, 블럭 번호와 위치 정보만 저장되는 형식입니다.

0단계 : 파일 열고 정리하기. File open and manual optimizing

콜라다 로드 옵션은 기본으로 유지합니다. OK 를 누릅니다.
필요없는 오브젝트와 재질, 재질 태그를 삭제하고 Instance 는 Make Editable 해줍니다. null 과 폴리곤 오브젝트만 있도록 정리합니다.
정리에 아래 두가지 스크립트를 사용했습니다. 오브젝트 컬러를 변경하여 모델링을 보기 쉽게 만들었습니다.

1단계 : 로고 블럭 찾아 그룹핑, 삭제. Find logo bricks and delete.

로고마다 색이 다른걸 보니 각각의 오브젝트 상태이고, 텍스쳐로 표현한다고 가정하겠습니다. 로고의 포인트 수는 3169 개 이니 포인트 수를 검사해서 분리하겠습니다.
import c4d def GetAllChildren(op, ops=[]): if ops == []:ops = [op] ops.extend(op.GetChildren()) for o in op.GetChildren(): if o.GetDown(): GetAllChildren(o, ops) return ops def main(): # 씬의 첫 오브젝트를 찾습니다. # Node-42111__doms__dodge__charger_dot_io 을 root 에 지정됩니다. root = doc.GetFirstObject() # objs 에 root 아래 있는 모든 오브젝트를 찾아 list 로 만듭니다. objs = GetAllChildren(root) # 로고 모델링을 담을 null 을 생성합니다. 현재는 메모리에 존재합니다. logoNull = c4d.BaseObject(c4d.Onull) # 메모리에 있는 null 의 이름을 변경해줍니다. logoNull.SetName("Logos") # 메모리에 있던 null 을 root 아래쪽에 추가합니다. logoNull.InsertAfter(root) # 전체오브젝트를 하나씩 obj 에 지정해주고 아래 코드를 실행합니다. for obj in objs: # obj의 타입을 검사해서 폴리곤 오브젝트일때만 아래 코드를 실행합니다. if type(obj) == c4d.PolygonObject: # 포인트 수를 검사해서 일치할때만 아래 코드를 실행합니다. if obj.GetPointCount() == 3169: # 현재의 월드좌표를 mg 에 담아둡니다. mg = obj.GetMg() # obj 를 추가한 null 의 자식으로 넣어줍니다. obj.InsertUnder(logoNull) # 이동시 부모가 달라져 상대값인 좌표계산으로 위치가 달라집니다. # 저장해두었던 월드 좌표를 적용합니다. obj.SetMg(mg) # 씬을 새로고침합니다. c4d.EventAdd() if __name__ == "__main__": main()
Python
복사
스크립트 실행 후 모습으로 204개의 로고 모델링을 찾아 그룹 된 모습

2단계 : 블럭 모델링과 Null 분리하기. Grouping

맨위의 'Node-42111__doms__dodge__charger_dot_io' null 아래에는 널과 블럭 모델링이 들어있습니다. 이 구조를 단순하게 'brick' null 을 만들고 로고처럼 모아놓으려고합니다. 불필요한 null 은 삭제합니다.
import c4d def GetAllChildren(op, ops=[]): if ops == []:ops = [op] ops.extend(op.GetChildren()) for o in op.GetChildren(): if o.GetDown(): GetAllChildren(o, ops) return ops def main(): # 씬의 첫 오브젝트를 찾습니다. # Node-42111__doms__dodge__charger_dot_io 을 root 에 지정됩니다. root = doc.GetFirstObject() # objs 에 root 아래 있는 모든 오브젝트를 찾아 list 로 만듭니다. # 아래에서 블럭(폴리곤 오브젝트)를 새 null 아래로 이동시킬때 # 그 오브젝트의 자식오브젝트들도 같이 이동됩니다. # 이 예제에서는 null은 삭제, 블럭은 이동을 하려고합니다. # 리스트는 위에서 아래로 담기는데 이걸 뒤집어주면 이 코드에서는 문제가 발생하지 않습니다. objs = GetAllChildren(root) objs.reverse() # brick 을 담을 null 을 메모리에 생성합니다. brickNull = c4d.BaseObject(c4d.Onull) # 메모리에 있는 null 의 이름을 변경해줍니다. brickNull.SetName("Brick") # 메모리에 있던 null 을 root 아래쪽에 추가합니다. brickNull.InsertAfter(root) # 순서를 뒤집은 전체오브젝트를 하나씩 obj 에 지정해주고 아래 코드를 실행합니다. for obj in objs: # obj의 타입을 검사해서 폴리곤 오브젝트일때만 아래 코드를 실행합니다. if type(obj) == c4d.PolygonObject: # 현재의 월드좌표를 mg 에 담아둡니다. mg = obj.GetMg() # obj 를 추가한 null 의 자식으로 넣어줍니다. obj.InsertUnder(brickNull) # 이동시 부모가 달라져 상대값인 좌표계산으로 위치가 달라집니다. # 저장해두었던 월드 좌표를 적용합니다. obj.SetMg(mg) else: # 폴리곤이 아닐경우 삭제합니다. obj.Remove() # 씬을 새로고침합니다. c4d.EventAdd() if __name__ == "__main__": main()
Python
복사
스크립트 실행 전
스크립트 실행 후

3단계 : 동일한 부품 찾기. Find same brick

로고를 찾을때와 비슷하지만, 다른 정보를 비교할 것입니다. 이유는 블럭중에 대칭부품의 경우, 포인트 수는 동일하지만 형태는 다릅니다. 추가한 조건은 모든 포인트의 위치값까지 같은지 비교하는 것입니다.
포인트 위치값이 동일한지 비교하는것은 포인트 수를 비교하는 효과도 생깁니다.
대칭 부품
위치값은 벡터타입으로 c4d.Vector(0,0,0) 의 형태입니다. 비교하는 데이터가 늘어난 만큼 2,3초 정도의 스크립트실행 시간이 더 필요합니다. 오브젝트가 많고 비교하는 정보량에 따라 스크립트 실행시 몇분이 소요되기도합니다. 이때는 무한 반복에 빠지는 경우도 있기때문에 C4D 파일이나 스크립트를 저장하는 것이 좋습니다. Sublime Text 를 사용할 경우 C4D는 죽더라도 스크립트는 영향을 받지 않습니다.

스크립트 진행 과정을 보면. Process

1.
'Instance' null 을 추가합니다.
2.
'Reference' null 을 추가합니다.
3.
root를 찾고 하위 오브젝트를 리스트로 만듭니다.
4.
첫번째 블럭의 포인트 포지션 값을 확인합니다.
5.
첫번째 블럭을 'Reference' null 로 이동합니다.
6.
첫번째 블럭을 Instance 로 만들어 'Instance' null 로 이동합니다.
7.
나머지 블럭과 비교하여 동일한 조건인 블럭은 삭제하고 그 위치에 Instance 를 만들어 주고 참조로 첫번째 블럭을 지정한다. 'Instance' null로 이동시킨다.
import c4d def GetAllChildren(op, ops=[]): if ops == []:ops = [op] ops.extend(op.GetChildren()) for o in op.GetChildren(): if o.GetDown(): GetAllChildren(o, ops) return ops def main(): # 첫오브젝트를 찾습니다. 'bricks' null 을 root 로 지정합니다. root = doc.GetFirstObject() # instance 를 담을 null 을 씬 추가합니다. insNull = c4d.BaseObject(c4d.Onull) insNull.SetName("Instances") insNull.InsertAfter(root) # reference 를 담을 null 을 메모리에 생성합니다. refNull = c4d.BaseObject(c4d.Onull) refNull.SetName("References") refNull.InsertAfter(root) # root 아래 전체오브젝트를 찾고 [1:] 로 root 를 제외시킵니다. objs = GetAllChildren(root)[1:] # 리스트의 첫번째 오브젝트를 first 에 지정합니다. first = objs[0] # first의 포인트 위치정보를 찾습니다. firstPointsPosition = first.GetAllPoints() # 리스트에서 비교군인 두번째부터를 others 지정합니다. others = objs[1:] # first 를 instace 로 만들고 이동시킵니다. ins = c4d.BaseObject(c4d.Oinstance) ins.InsertUnder(insNull) ins[c4d.INSTANCEOBJECT_LINK] = first ins.SetMg(first.GetMg()) first.InsertUnder(refNull) # 포인트의 위치를 비교하고 같다면 인스턴스로 만들고 삭제한다. for obj in others: if obj.GetAllPoints() == firstPointsPosition: ins = c4d.BaseObject(c4d.Oinstance) ins.InsertUnder(insNull) ins[c4d.INSTANCEOBJECT_LINK] = first ins.SetMg(obj.GetMg()) obj.Remove() # 씬을 새로고침합니다. c4d.EventAdd() if __name__ == "__main__": main()
Python
복사
8.
3번부터 6번까지 반복합니다. while 문을 통해 반복할 수 있는 구조로 변경합니다. 모든 블럭을 찾고나면 그룹안에는 아무것도 없는 상태가 됩니다. if objs == []: 일때는 맨위에 null 을 삭제하고 while 문을 중지시킵니다. 안그러면 무한 루프나 오류가 발생 할 수 있으니 while 문을 사용할 때는 주의가 필요합니다. 스크립트는 작성중인 노트북으로 10초 정도 계산시간이 필요했습니다.
import c4d def GetAllChildren(op, ops=[]): if ops == []:ops = [op] ops.extend(op.GetChildren()) for o in op.GetChildren(): if o.GetDown(): GetAllChildren(o, ops) return ops def main(): # 첫오브젝트를 찾습니다. 'bricks' null 을 root 로 지정합니다. root = doc.GetFirstObject() # instance 를 담을 null 을 씬 추가합니다. insNull = c4d.BaseObject(c4d.Onull) insNull.SetName("Instances") insNull.InsertAfter(root) # reference 를 담을 null 을 메모리에 생성합니다. refNull = c4d.BaseObject(c4d.Onull) refNull.SetName("References") refNull.InsertAfter(root) while True: objs = GetAllChildren(doc.GetFirstObject())[1:] if objs == []: doc.GetFirstObject().Remove() break first = objs[0] firstPointsPosition = first.GetAllPoints() others = objs[1:] ins = c4d.BaseObject(c4d.Oinstance) ins.InsertUnder(insNull) ins[c4d.INSTANCEOBJECT_LINK] = first ins.SetMg(first.GetMg()) first.InsertUnder(refNull) for obj in others: if obj.GetAllPoints() == firstPointsPosition: ins = c4d.BaseObject(c4d.Oinstance) ins.InsertUnder(insNull) ins[c4d.INSTANCEOBJECT_LINK] = first ins.SetMg(obj.GetMg()) obj.Remove() # 씬을 새로고침합니다. c4d.EventAdd() if __name__ == "__main__": main()
Python
복사

최종 코드. Final code

import c4d def GetAllChildren(op, ops=[]): if ops == []:ops = [op] ops.extend(op.GetChildren()) for o in op.GetChildren(): if o.GetDown(): GetAllChildren(o, ops) return ops def createNull(op, name): null = c4d.BaseObject(c4d.Onull) null.SetName(name) null.InsertAfter(op) return null def main(): # 로고 찾아 그룹핑하기 root = doc.GetFirstObject() objs = GetAllChildren(root) logoNull = createNull(root, "Logos") for obj in objs: if type(obj) == c4d.PolygonObject: if obj.GetPointCount() == 3169: mg = obj.GetMg() obj.InsertUnder(logoNull) obj.SetMg(mg) # 블럭 찾아 그룹핑하고 널은 삭제하기 objs = GetAllChildren(root) objs.reverse() brickNull = createNull(root, "Bricks") for obj in objs: if type(obj) == c4d.PolygonObject: mg = obj.GetMg() obj.InsertUnder(brickNull) obj.SetMg(mg) else: obj.Remove() root.Remove() # 같은 블럭 찾아 인스턴스하고 정리하기 root = doc.GetFirstObject() insNull = createNull(root, "Instances") refNull = createNull(root, "References") while True: objs = GetAllChildren(doc.GetFirstObject())[1:] if objs == []: doc.GetFirstObject().Remove() break first = objs[0] firstPointsPosition = first.GetAllPoints() others = objs[1:] ins = c4d.BaseObject(c4d.Oinstance) ins.InsertUnder(insNull) ins[c4d.INSTANCEOBJECT_LINK] = first ins.SetMg(first.GetMg()) first.InsertUnder(refNull) for obj in others: if obj.GetAllPoints() == firstPointsPosition: ins = c4d.BaseObject(c4d.Oinstance) ins.InsertUnder(insNull) ins[c4d.INSTANCEOBJECT_LINK] = first ins.SetMg(obj.GetMg()) obj.Remove() # 씬 새로고침 c4d.EventAdd() if __name__ == "__main__": main()
Python
복사

시연 영상. Demo