posted by cimple 2016. 3. 31. 16:57

Maya 에서 특정 프레임 f 에서 object 의 world position 을 얻는 가장 간단한 방법은 이것이다.


import maya.cmds as mc
mc.currentTime(f)  # f is specific frame
mc.xform(obj, q=True, t=True, ws=True), #obj is specific object

그러나, 이 방식의 경우 'currentTime()' 커맨드로 특정 프레임으로 이동한 다음 계산하므로 씬이 전체적으로 업데이트 되어야 해서 만약 다수의 프레임에 대해 처리를 해야 할 경우 속도가 매우 느리다.


'currentTime()' 커맨드를 쓰지 않고 위치정보를 받아올 수 있는 다른 방법으로는 'getAttr()' 커맨드를 이용하는 방식이 있는데, 이 방식은 말그대로 attribute 의 값을 가져올 뿐이므로, object 의 world position 같은 정보를 직접적으로 얻어올 수는 없다.


이를 위하여, 아래는 Maya Python API 2.0 을 활용하여 특정 ojbect 의 world position 을 내가 원하는 frame 에서 찾는 Python Script 이다.


import maya.cmds as mc
import maya.api.OpenMaya as om

def mayaTimeUnitTypeQuery():
    mayaTimeUnitDic = {'100fps':25, '10fps':18, '1200fps':38, '120fps':26, '125fps':27, '12fps':19, '1500fps':39, '150fps':28, '16fps':20, '2000fps':40, '200fps':29, '20fps':21, '240fps':30, '250fps':31, '2fps':12, '3000fps':41, '300fps':32, '375fps':13, '400fps':34, '40fps':22, '4fps':14, '500fps':35, '5fps':15, '6000fps':42, '600fps':36, '6fps':16, '750fps':37, '75fps':23, '80fps':24, '8fps':17, 'film':6, 'game':15, 'hour':1, 'millisec':4, 'sec':3, 'min':2, 'ntsc':8, 'ntscf':11, 'palf':10, 'pal':7, 'show':9} 
    curTimeUnit = mc.currentUnit(q=True, time=True)
    try:
        mayaTimeUnit = mayaTimeUnitDic[curTimeUnit]
    except:
        raise NameError('Invalid Maya time unit. %s is not in the Maya unit dictionary.'%(curTimeUnit))
    return mayaTimeUnit
    

def getWorldPosAtTime(obj, frame):
    sList = om.MSelectionList()
    sList.add(obj)
    dagPath = sList.getDagPath(0)
    transFn = om.MFnDagNode(dagPath)
    matrixPlugArr = transFn.findPlug('worldMatrix', 0)
    matrixPlugArr.evaluateNumElements()
    matrixPlug = matrixPlugArr.elementByPhysicalIndex(0)
    
    curTimeUnit = mayaTimeUnitTypeQuery()
    mayaTime = om.MTime(frame, unit=curTimeUnit)
    timeContext = om.MDGContext(mayaTime)
    
    plugObj = matrixPlug.asMObject(timeContext)   
    #print plugObj.apiTypeStr  #kMatrixData
    plugObjFn = om.MFnMatrixData(plugObj)
    worldMatrix = plugObjFn.transformation()
    worldPos = worldMatrix.translation(om.MSpace.kWorld)
    return worldPos

mayaTimeUnitTypeQuery() 함수는 현재 Maya 에 설정되어 있는 FPS 값을 미리 상수로 예약되어 있는 MayaTimeUnit 값으로 리턴해준다. Maya 에 예약된 모든 FPS 를 포함하는 함수이므로 유용할 것이다. 이 함수에서 오류가 난다면 버그가 있는 것이므로 제보바람.

getWorldPosAtTime() 함수가 우리가 원하는 일을 해 주는 함수인데, 위치를 알고싶은 obj 와 frame 을 입력하면 해당 frame 에서의 world position 을 반환해준다.
Object DagNode 의 worldMatrix plug 에서 matrix data 형태로 어떻게 가지고 오는지,
그리고 MTime 과 MDGContext timeContext 객체를 만들어 어떻게 특정 frame 에서의 value 를 retrieve 하는지가 주목할 만한 부분이다.



posted by cimple 2014. 12. 31. 03:02

옛날 'Maya2014에서 numpy 사용하기' 라는 글에서 약간 얼렁뚱땅 Maya2014 의 Python library 사용문제를 넘어갔었는데, 결국 언젠가는 굴러굴러 문제로 닥칠수밖에 없는 일. 하루를 모두 소진해서야 문제를 해결할 수 있었다.


욕을 하지 않으려고 해도 지랄맞은(...) 마야의 Python 시스템 때문에 정말 수도없는 테스트와 테스트 끝에 해결을 하고 나니 진이 다 빠지는 느낌이지만, 정리를 하지 않으면 누군가가 또 고생할 것이고 인간은 망각의 동물이니 나도 고생할 수 있기 때문에....


단지 Maya2014 에서 python nlopt 를 사용하는 문제뿐만 아니라, python library 를 사용하는데 있어 비슷한 문제가 발생했을 때에 해결하는 단초를 찾았다는 생각이다.





시간없는 사람들을 위해


먼저 일단 차치하고 Maya2014 에서 사용할수 있는 nlopt 라이브러리


nlopt_maya2014.7z


기존처럼 C:\Program Files\Autodesk\Maya2014\Python\Lib\site-packages 에 복사해 넣으면 된다.

(물론 Numpy 도 설치되어 있어야 돌아간다. Numpy 는 위 링크글 참조...)





1. 문제의 유형 및 발생원인


Maya2012 까지는 간편하게 Numpy 와 nlopt 를 설치할 수 있다. 비공식이긴 하지만 64bit python 컴파일된 binary 를 distribute 하기 때문이다. 이걸 설치하고 Maya 의 python 의 site-package 폴더에 복사만 해주면 된다.


numpy 링크

http://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy


nlopt 링크

http://www.lfd.uci.edu/~gohlke/pythonlibs/#nlopt



그런데 이게 Maya2013 부터 numpy 부터 시작해서 라이브러리들이 import 가 잘 안되기 시작했는데, 그 이유가 Maya 의 Python 을 빌드한 컴파일러 문제이다. 이것을 알아보기 위해서 다음 명령어를 

import sys
print sys.version

각각 다른 버전의 Maya Script Editor 에 쳐서 확인해보면 다음과 같은 결과를 얻을 수 있다.


Maya2012


Maya2013


Maya2014


보다시피 Maya2013 부터 Python 컴파일러를 MSC v.1600, 즉 VS2010 compiler 로 컴파일했다는 사실을 알 수 있다. 


이게 아주 쓰레기같은게 뭐냐면 우리가 Python 홈페이지에서 받을 수 있는 파이선 2.x 버전들을 체크 해보면 MSC v.1500 로 컴파일되어 있다. 즉 VS2008 에서 컴파일한 버전을 배포하고 있다. v.1600 은 Python 3.x 버전 이후부터이다.


그런데 이 샹늠의 Maya 는 뭐가 잘났다고 혼자 독특하고 고고하게 VS2010 에서 컴파일한 파이선 2.x 버전을 사용하고 있다. 그것도 Maya2014 부터는 Python 2.7 버전을 사용한다. 아마도 추후에는 Python 3.x 버전으로 업그레이드를 할 목적이긴 하겠지만, 이것이 문제를 발생시키는 가장 큰 원인이었다.


이 문제 때문에 어렵사리 컴파일한 nlopt 를 사용할 수가 없었다. 가장 고생한 에러메시지는 바로 이것일 것이다.


ImportError: DLL load failed: %1은(는) 올바른 Win32 응용 프로그램이 아닙니다.


이걸 해결하기 위해서, 'Python2.7 자체를 VS2010 컴파일러로 다시 컴파일' 해야만 했다. 왜 그래야만 했는지는 아래에 설명...



2. python nlopt 컴파일하기


일단 내 입맛에 맞는 python nlopt 라이브러리의 binary 버전은 없다. 애초에 Window 에서 쓰라고 만든것도 아니고 Python nlopt 는 Wrap 해서 배포할 뿐이지 원래 c++ 라이브러리다. 직접 만들어 써야 한다고 보면 된다. 찾으려고 괜히 애쓰지 말고 컴파일 하는 것이 오히려 이득이다.


다음과 같은 순서를 따르면 된다.


1) 일단 nlopt 소스파일을 다운로드 받아서 압축을 풀어놓는다. 위치는 상관없다. 어차피 필요한건 컴파일한 다음 라이브러리 파일이다.

http://ab-initio.mit.edu/wiki/index.php/NLopt#Download_and_installation


2) 다음은 아래 페이지에서

http://ab-initio.mit.edu/wiki/index.php/NLopt_on_Windows


nlopt-2.4.2-dll64.zip (64-bit) 압축파일을 받아서 압축을 푼다. 미리 컴파일해둔 win64 용 .dll 파일이 있고 어쩌고 하는데 어차피 못쓴다. MinGW 로 컴파일한것이기 때문이다. 어차피 MSVC 로 다시 컴파일해야 한다. 이 압축파일의 용도는, nlopt 의 라이브러리파일 .lib 가 만들어지면, python 으로 컴파일할 수 있는 setup.py 파일이 포함되어 있기 때문이다.


3) 위 2) 의 링크 NLopt on Windows 페이지에서 CMake 관련파일 2개를 받는다. CMakeLists.txt 와 config.cmake.h.in 이다. 이것들을 1) 의 압축 풀어놓은 폴더에 넣는다.


cmake 로 VS 프로젝트를 생성-> 컴파일해서 Shared library (.dll) 생성 -> 이걸 Python 으로 컴파일 하는 것이 전체적인 얼개이다.


4) Cmake 로 해당 폴더를 Configure 하면 설정을 잘 잡아주는데, 주의해야 할 것은 

BUILD_SHARED_LIBS 옵션을 체크해 주어야 한다는 것이다. 여기서 컴파일된 .dll 파일을 가지고 python 에서 다시 컴파일한다.


5) Cmake 에서 Generate 하면 VS2010 프로젝트가 생성된다. 열어서 빌드하면 nlopt-0.dll 이라는 dll 이 생성될 것이다.


6) 이제 이것을 2) 에서 받았던 폴더에 넣는데, 원래 있던 dll 인 libnlopt-0.dll 을 지우고 그 이름으로 넣는다.


7) 이제 dll 을 가지고 lib 를 만들 차례이다. (사실 컴파일 자체는 해당 폴더의 README-WINDOWS 를 읽으면 있는 내용이다.) 명령 프롬프트를 써야 하는데, Visual Studio Win64 명령 프롬프트(x64) 를 실행시킨 다음 해당 폴더로 찾아들어간다. 그리고 아래 명령어 입력.


lib /def:libnlopt-0.def


8) 그럼 libnlopt-0.lib 파일이 만들어진다. 이제 python 으로 컴파일한다.


python setup.py build_ext --inplace


위 명령어로 컴파일하면 되는데, 여기가 지랄맞다!!!! 그 이유는, 여기서 python 으로 컴파일하는 것은 내가 환경변수에 등록된 python 으로 컴파일하는데, 이 python 이 처음에 말했지만 흔히 배포되는 Python2.x 의 MSC v.1500 컴파일된 python 을 사용하면 maya 에서 이 nlopt 를 쓸 수가 없다.


따라서 MSC v.1600 으로 컴파일된 python 으로 이 라이브러리를 빌드해야 한다. 그러기 위해서는 VS2010 에서 컴파일된 python2.x 버전을 사용해야 하는데, 나를 도와준 고마운 링크는 아래...


http://www.p-nand-q.com/python/building-python-27-with-vs2010.html


그냥 여기서 주는 python2.7 다운받아 썼다....지만 근데 site-package 를 뭔 이렇게 쓸데없이 깔아서 배포했는지 너무 무겁다; python 직접 컴파일하셔도 됨. 시간많으시면.


어쨌든 이 MSC v.1600 으로 컴파일된 python 으로 위 명령어를 통해 python 으로 컴파일하면, _nlopt.pyd 파일이 생성된다.


9) _nlopt.pyd, nlopt.py, nlopt-0.dll 3개의 파일을 maya 의 site-package 폴더에 넣으면 Maya 에서 잘 돌아가는 nlopt 를 감상할 수 있다.






써놓고 보니 간단해 보이는데 정말 python/라이브러리/심지어 Maya2014 까지 다시깔고, 컴파일 이런 저런 옵션 찾아가며 또해보고, document 읽고 또 읽고... 


해결은 했지만 이거 정말 Maya 문제 있다는 생각이다....





덧붙임1. NLopt 의 installation guide 에서 python_plugin 파트를 읽어보면


http://ab-initio.mit.edu/wiki/index.php/NLopt_Installation#Python_plugins


'Shared library 로 컴파일을 하면 자동으로 python plugin 을 설치해 줍니다!' (오오??) 라고 하는데 Windows 사용자들은 헛된기대 하지 말자... Linux make 파일에서 제공해주는 설정들이다. 실제로 make 파일과 configure 파일을 뜯어보면 python version 의 site-package 에 설치해주는 부분들이 있어서 한참 들여다봤지만 결국 Windows 에서 CMake 로 만든 VS Project 로 컴파일하는 유저들에게는 그런거 없는거로.....



posted by cimple 2011. 6. 30. 17:11



비둘기 프로젝트를 하는데 다음과 같은 조건을 충족시킬 문제가 떨어졌다.

1. 타임라인이 움직이면 깃털들이 랜덤하게 팔락거렸으면 좋겠다.
2. 추가로 플러그인을 만들지 말고 마야에 있는 기능으로 만들었으면 좋겠다.

이런 문제에서는 Maya Expression 이 적당하다. 각각의 깃털들에 대해서 서로 다른 Expression 이 적용되어야 하므로, Python Script 를 이용해서 Expression 을 만들었다.

코드는 다음과 같이 간단하다.



먼저 날개 mesh 들을 모두 선택한 다음, 날개 메쉬 각각에 expression 을 만들어 주는 것이다.

물론 이러면 항상 일정한 값으로 (0.0~3.0 사이 랜덤하게) rotateX 값이 깃털에 들어가게 되는데

controllable 하게 만들어 줄 수도 있다.

하지만 이 정도면 python script 상에서 어떻게 expression 을 만들면 되는지 hello world 는 찍을 수 있을 것이다.


ThEnd.





p.s.

결국 추가로 최종적으로 만든 expression 은 다음과 같다.

expression 안에 다른 attribute 를 넣고 싶으면 getAttr 같은 것을 쓸 필요 없이 바로 쓰면 되고,

if 문으로 조건문 제어가 가능하다.

Python script 로 2줄 이상의 expression 을 넣고 싶으면 일단은 다음과 같이 죽 붙여 쓰는 형태로 짜야 하는 것 같은데

다른 방법이 있는지 찾아봐야겠다.








p.s.2. 3중 따옴표 (""") 의 사용

재밌는 기능을 발견했다. Maya Python Script 에만 있는 기능은 아니고, Python 자체에 있는 기능인데 3중 따옴표를 쓰면 여러 줄의 문자열을 한 string 에 입력할 수 있다.



여러 줄의 expression 을 입력하고 싶을 때 스크립트로 다른 expression 을 입력해서 기존의 expression 을 수정하는 방법은 없는 것 같다. 그러면 또 다른 expression 이 생겨버린다. expression 을 수정하기 위해서는 expression editor 를 이용하는 방법 뿐인듯.

ThEnd.

posted by cimple 2011. 1. 11. 03:42



Solid Skeleton 이 하루만에 버전 2.0 이 나왔습니다. 사용자 입장에서 조금 큰 폭으로 바뀌었기에 아예 버전을 2.0 으로 한 단계 높였습니다. 강화된 기능은 다음과 같습니다.


1. UI 제공
2. 조인트의 크기와 모양을 실시간으로 변환 가능


다음은 시연 동영상입니다.






쉐이더까지 자동으로 입힐 수 있으나, 렌더링은 각자 용도에 맞게 하시면 되리라 생각되어 구태여 넣지 않았습니다.

그럼 유용하게 사용하세요!





Version 2.1 로 업그레이드 되었습니다.

수정 사항은

- 캐릭터가 하나의 조인트 구조가 아닌 여러 개로 따로 떨어진 조인트 구조를 가졌을 경우, 각각의 조인트의 root 를 선택하고 generate 버튼을 계속 누르는 방식으로 추가할 수 있습니다.

- 물론 처음부터 root 에 해당하는 모든 조인트를 선택하고 generate 버튼을 눌러서 생성할 수도 있습니다.

아래는 시연 동영상입니다.




ThEnd.

posted by cimple 2010. 11. 15. 23:31

Maya 에서 스크립트를 작성하다 보면 무한 루프에 빠지는 경우가 있다.

for 문이나 while 문 (특히 while 이놈!) 을 잘못 쓰면 무한 루프에 빠져 버리는 경우가 있는데,

ctrl+break 가 통하는 것도 아니고 esc 를 아무리 연타해도 안 되고, 마야가 먹통이 되어 버려서 결국에는 작업관리자를 켜야만 하는 처지에 놓이게 된다.

이 때, 반복문 내에 무한루프를 방지할 수 있는 간단한 장치를 마련할 수 있는데, 방법이 조금 독특하다.

'외부 파일을 이용' 하는 방법이다. 


사용 방법은 다음과 같다.




1. 위에 첨부된 파일(BREAKMEL.bat) 를 받아서, 소중한 곳에 잘 간직해둔다.
2. 마야에서 MEL scripting 을 하면서, 무한루프가 의심되는 반복문 안에 다음 구문을 넣어준다.

import os
if os.path.exists("c:/breakMel") : break
3. 만약 스크립팅을 하다가 무한 루프에 빠지면, 첨부된 파일을 실행하기만 하면 반복문이 종료될 것이다.



간단한 예제는 다음과 같다.



위와 같은 구문을 스크립트 에디터에서 실행하면, 아마 지구가 멸망할 때까지 1을 찍어댈 것이다.
이 때, 첨부된 BREAKMEL.bat 를 실행시키기만 하면 무한루프를 중지시킨다.
이 때 임시로 생성되는 파일은 자동으로 삭제되니 신경쓰지 않아도 된다.




이 방식의 원리는 다음과 같다.

1. 반복문이 계속 돌면서, 컴퓨터 내부에 특정 파일이 존재하는지 계속 검사를 한다.
2. 만약 무한루프에 빠지면, 그 특정 파일을 생성시킴으로써 반복문을 종료시킬 수 있다.


여기에서 '컴퓨터 내부를 계속 검사한다' 라는 부분이 상당히 찜찜하게 느껴질 것이다.
하지만, 이 방식을 처음 제시한 저자에 따르면 거의 퍼포먼스의 저하가 없다고 한다.

첨부파일인 BREAKMEL.bat 는 뭐 복잡한 구조를 가지고 있는 것이 아니라, 다음이 전부이다.


즉, c:\ 에 breakMel 이라는 가상의 파일을 하나 만들고, 아무 키나 누르면 그걸 지운다는 거다.


재밌는 방식이고, 꽤나 괜찮은 아이디어이다.
하지만 퍼포먼스의 저하가 '전무' 하다는 것은 사실 믿을 수 없다;; 약간 궁여지책처럼 느껴지기도 하고, 의심이 많이 가는 반복문에서만 사용하는 것이 좋을 것 같다.



아이디어는 다음 블로그 포스팅에서 참고하였다.

http://www.naughtynathan.co.uk/?p=59



ThEnd.

posted by cimple 2010. 8. 13. 16:03

(어쩐지 좀 두서없는 포스팅같기는 한데;; 그리고 요즘 R 프로그래밍을 함께 공부하고 있는데 Python 과 문법이 유사하다는 생각을 무척 많이 하고 있다.)


1. Python 의 List 생성.

Python 의  List 는 데이터를 저장하는데 훌륭한 자료구조를 제공해주고 있다.
일단 Python List 에 데이터를 저장하기 위해서는 다음과 같이 저장한다.


myFirstList = ["red", 21.00, 1]


myFirstList 라는 list 이름을 정해주고, 대괄호 [] 로 묶어주면 그 안에 원하는 data 를 넣어주면 된다. 이 때 data 의 type 이 위의 예제처럼 달라도 상관이 없다.

MEL 에서는 이것이 불가능했지만, python list 에서는 가능하고, 심지어 list 도 list 안에 들어가기 때문에 간편하게 데이터를 관리할 수 있다.


list 에 있는 데이터에 접근하기 위해서는 c 의 배열에 접근하는 방법과 비슷하게 접근하면 된다.

myFirstList[0];

위에는 "red" 가 들어있게 된다.



재미있는 것은, maya 에서 현재 내가 선택하고 있는 object 를 list 안에 등록할 수 있다.
다음과 같이 등록하면 된다.

import maya.cmds as mc;
listObject = mc.ls(sl=True);

MEL 커맨드로 ls -sl 을 하면 현재 내가 선택하고 있는 object 를 반환하는 기능을 이용한 것이다.
print 해 보면 다음과 같이 나온다.

print listObject;

[u'pSphere1', u'pCube1']


pSphere1 을 먼저 선택하고, pCube1 을 다음에 선택한 다음 list 에 등록하면 위와 같이 들어간다.

u가 붙은 이유는 잘 알수 없으나, 실제로 값을 빼 보면 u 는 빠져있고 pSphere1 만 나온다.






2. List 의 크기 받아오기

언제나 자료의 크기를 아는 것은 중요하다. Python Script 에서는 len() 이라는 함수를 사용한다.

len(custom_list);

이것으로 사용자가 생성한 list 의 크기를 알 수 있다.







3. List 의 data 에 iterative 하게 접근하기

List 의 data 에 대하여 iterative 하게 접근할 수 있다. 이는 for 문으로 가능한데, Python Script 에서 for  문은 다음과 같은 형태를 가진다.


for i in range(start, end, increment)


그리고 Python Script 의 for 문은 조금 독특한 문법을 가지고 있는데, for 문 다음에 콤마 (:) 로 시작해서 들여쓰기로 해당 scope 를 표시한다. 즉


for i in range(0, 5, 1) :
          print i
print "done"



위와 같은 코드가 있을 때, i 는 1, 2, 3, 4, 5 로 반복문을 돌면서 찍히겠지만 "done" 은 한 번만 찍힌다.


for i in range(0, 5, 1) :
          print i
          print "done"


위와 같은 경우는 done 도 5번 찍히는 것이다.

이 반복문을 이용해서 list 안에 있는 data 들에 접근하기 위해서는



for i in range(0, len(custom_list), 1):
          print(custion_list[i])




이렇게 하면 list 에 있는 요소들에 순차적으로 간편하게 접근할 수 있다.







ThEnd.




p.s. 다음 링크로 가면 파이썬의 기본 데이터 유형인 사전, 리스트, 터플에 대해서 소개하고 있다.

http://coreapython.hosting.paran.com/dive/chap03.html#d0e5887
 
posted by cimple 2010. 8. 12. 19:52


1. 변수의 선언

Maya Python Script 에서 변수는 따로 자료형을 선언해주지 않고, 바로 이름을 적고 값을 할당해 주면 된다.

myFirstVariable = 10
myFirstVariable = "Text"


MEL 과 같이 자동으로 자료형이 정해지며, 숫자로 시작하거나 이미 커맨드로 등록되어 있는 단어는 사용할 수 없다.

Python 의 변수는 MEL 보다 더 많은 자료형을 담을 수 있어서 더 강력하다고 하는데, 그건 더 활용해봐야 할 일인듯.


2. 변수의 활용 예제

특정 script 가 반환하는 자료형을 변수에 담아서 스크립트에 활용할 수 있다.
예를 들어, polyCube 같은 경우는 반환값이 string 형태이다. 그리고 이 string 은 해당 object 의 이름을 반환한다.

특정 명령어의 반환값과 그 type 을 알고 싶다면, 해당 명령어를 드래그>우클릭 하여 Quick Help 탭을 연 후, 그 Quick Help 창 위에서 다시 우클릭>Show Command Documentation 하면 바로 Documentation 에 연결되어 해당 명령어가 Return 하는 값에 대한 정보를 볼 수 있다.

cubeToBevel = mc.polyCube( w=1 , h=1, ... , ch=1)

이런 형태로 polyCube 가 반환하는 오브젝트 이름의 string 을 cubeToBevel 이라는 사용자 정의 변수에 담아 놓고,

mc.polyBevel(cubeToBevel, offset=0.5, ... , ch=1)

이런 식으로 쌍따옴표 " "  형태의 string 이 들어가던 부분에 변수를 넣어 놓는 형태로 스크립트를 작성할 수 있다.


3. 변수의 출력

각종 변수를 출력하는 방법은 전통적인 C 의 문법과 크게 다르지 않다.
다음과 같이 출력하면 된다.

intLife = 42
fPI = 3.14
hexColor = 0xFFCC00

print "My values are %d and %f" % (intLife, fPI)
print "The value of hexColor is ... %f" % hexColor



출력할 스트링 뒤에 % 를 붙여주고, 2개 이상일 경우에는 괄호로 묶어주면 된다.


ThEnd.