소개
이 문서에서는 이미지 시퀀스에 밉매핑을 사용하는 방법을 간략하게 살펴봅니다.
밉맵
밉맵(mipmap)은 이미지 시퀀스에서 로딩되는 데이터 양을 줄이는 데 사용됩니다.
밉맵은 언리얼 엔진에 노멀 텍스처로 임포트되지 않으므로 EXR 밉맵용으로 생성되는 UAsset은 없습니다. 따라서 언리얼 엔진에서 사용하려면 우선 밉 레벨을 직접 생성해야 합니다. 밉맵을 직접 생성하는 방법은 Nuke 및 Python 스크립트를 사용하여 EXR 밉맵 생성하기 섹션을 참고하세요.
제한 사항
현재는 EXR 파일만 지원됩니다.
애니소트로픽 밉은 지원되지 않습니다.
필수 밉 레벨을 로딩하는 방법을 통해서만 로딩되는 데이터를 줄일 수 있습니다.
GPU를 사용하여 언리얼 엔진으로 스트리밍하는 작업을 최적화하려면 모든 EXR 이미지 시퀀스가 압축되어 있지 않아야 합니다.
EXR이 압축되어 있지 않은 경우에만 로딩 시 GPU 가속 기능을 활용할 수 있습니다.
밉 레벨은 모두 압축되어 있거나 압축되어 있지 않아야 합니다. 압축된 밉 레벨과 그렇지 않은 밉 레벨이 섞여 있으면 체인이 끊어집니다.
파일 디렉터리 구조
Cineon 명명 규칙과 산업 표준에 따라 파일 디렉터리 구조는 다음과 같습니다.
Smoke_Element/2048x2048/Smoke_Element.00001.exr
Smoke_Element/1024x1024/Smoke_Element.00001.exr
Smoke_Element/512x512/Smoke_Element.00001.exr
파일 및 폴더명은 다음 규칙을 따라야 합니다.
모든 폴더명은 1024x512와 같이 2의 거듭 제곱이어야 합니다.
밉 레벨 이미지는 소스 이미지와 정확히 동일하게 명명되어야 합니다.
에디터 구성
밉맵 기능을 사용하려면 이미지를 표시하는 모든 오브젝트에 ImgMediaPlayback
컴포넌트를 추가해야 합니다. 이 컴포넌트가 오브젝트에 없는 경우 해당 오브젝트는 밉 레벨 사용 여부를 결정하는 데 사용되지 않습니다.
다음 콘솔 명령으로 디버깅을 활성화할 수 있습니다.
ImgMedia.MipMapDebug 1
이 명령은 각 이미지 시퀀스가 현재 어떤 밉 레벨을 사용하고 있는지 표시합니다.
밉 레벨 선택
밉 레벨은 이미지를 표시하는 각 오브젝트의 텍셀 밀도 예상 픽셀을 기반으로 선택됩니다.
이러한 계산에는 카메라 위치가 사용되므로 카메라가 빠르게 움직일 경우 계산 시 더 많은 오류가 발생할 수 있습니다. 계산 결과를 개선하려면 카메라 움직임을 고려하는 등 추가 작업이 필요합니다.
선택된 밉 레벨은 ImgMediaPlayback
컴포넌트의 LODBias 세팅을 사용하여 수동으로 조정할 수 있습니다.
Nuke 및 Python 스크립트를 사용하여 EXR 밉맵 생성하기
Nuke 및 Python 스크립트를 사용하면 밉 레벨을 자동으로 생성하는 데 유용합니다. 각 스크립트는 이 페이지에서 다운로드 및 확인할 수 있습니다.
nukeMipMap.py는 Nuke에서 실행하여 적절한 LOD 생성 트리를 구성할 수 있는 Python 스크립트입니다.
스크립트를 사용하려면 EXR 시퀀스를 선택하고 원하는 밉 레벨의 수를 설정합니다.
스크립트 맨 위에서 밉맵을 생성할 읽기 노드를 선택하고 실행합니다.
그러면 Nuke에서 필요한 리포맷 및 쓰기 노드가 모두 생성됩니다.
밉 렌더별로 필요한 해상도 폴더도 모두 생성됩니다.
밉 렌더의 경로는 실행 시 선택된 읽기 노드를 기반으로 합니다.
# 모든 시퀀스는 이미지의 해상도를 따라 명명된 폴더에 있어야 합니다.
# 예시 - D:/Perforce/EXR_Sequences/Smoke/2048x2048/
D:/Perforce/EXR_Sequences/Smoke/1024x1024/
D:/Perforce/EXR_Sequences/Smoke/512x512/
# GPU를 사용하여 언리얼 엔진으로 스트리밍하는 작업을 최적화하려면 모든 EXR 이미지 시퀀스가 압축되어 있지 않아야 합니다.
# EXR 파일이 압축되어 있지 않은 경우에만 로딩 시 GPU 가속 기능을 활용할 수 있습니다.
# 언리얼 엔진에 더 빠르게 로딩하려면 압축되지 않은 포맷을 사용하는 것이 좋습니다.
# 밉 레벨은 모두 압축되어 있거나 압축되어 있지 않아야 합니다. 압축된 밉 레벨과 그렇지 않은 밉 레벨이 섞여 있으면 체인이 끊어집니다.
# 모든 밉 레벨은 소스와 정확히 동일하게 명명되어야 합니다.
# 스크립트를 사용하려면 EXR 시퀀스를 선택하고 원하는 밉 레벨의 수를 설정합니다.
# 스크립트 맨 위에서 밉맵을 생성할 읽기 노드를 선택하고 실행합니다.
# 그러면 Nuke에서 필요한 리포맷 및 쓰기 노드가 모두 생성됩니다.
# 밉 렌더별로 필요한 해상도 폴더도 모두 생성됩니다.
# 모든 폴더명은 2의 거듭 제곱(예: 128, 256, 512, 1024)이어야 합니다.
# 밉 렌더의 경로는 실행 시 선택된 읽기 노드를 기반으로 합니다.
import nuke
import os
#필요한 밉 레벨의 수
mipLevels = 3
#노드 선택 구하기
selectedRead = nuke.selectedNodes()
addLevel = mipLevels + 1
#이미지 시퀀스의 높이 및 너비 구하기
def getHeightWidth(read):
getFormat = []
getHeight = []
getWidth = []
getFormat = read.format()
getHeight = getFormat.height()
getWidth = getFormat.width()
dirResName = str(getWidth) + 'x' + str(getHeight)
return dirResName
def getFilePathName(readNode):
getName = readNode['file'].value()
return getName
#디렉터리 생성
def createDirectories(readNode,read):
getNameLocal = getFilePathName(readNode)
getHeightWidthLocal = getHeightWidth(read)
getSequenceName = []
parentPath = []
dirResName = []
dirName = []
setRenderPathName = []
getSequenceName = getNameLocal.split('/')[-1]
parentPath = getNameLocal.split(getSequenceName)[0]
dirName = parentPath + getHeightWidthLocal
#렌더 경로 이름 설정
setRenderPathName = dirName + '/' + getSequenceName
isThere = os.path.isdir(dirName)
if isThere == False:
os.makedirs(dirName)
return setRenderPathName
#리포맷 생성
def createReformatNodes(connectReformat):
createScale = nuke.nodes.Reformat()
createScale['type'].setValue("scale")
createScale['scale'].setValue(0.5)
createScale.connectInput(1,connectReformat)
return createScale
#쓰기 노드 생성
def createWriteNodes(path,connect):
createWrite = nuke.nodes.Write()
createWrite['file'].setValue(path)
createWrite['file_type'].setValue('exr')
createWrite['compression'].setValue('none')
createWrite.connectInput(1,connect)
return createWrite
#트리 생성
if len(selectedRead) > 0:
for x in selectedRead:
getFilePathName(x)
for index in range(addLevel):
if index == 0:
getHeightWidth(x)
setPathLocal = createDirectories(x,x)
createWriteLocal = createWriteNodes(setPathLocal,x)
else:
createScaleLocal = createReformatNodes(createWriteLocal)
getHeightWidth(createScaleLocal)
setPathLocal = createDirectories(x,createScaleLocal)
createWriteLocal = createWriteNodes(setPathLocal,createScaleLocal)
else:
nuke.alert("Nothing selected. Please select your EXR sequence READ NODES to generate mipmaps")
Unreal_ExrMipMap_GenerationExample.nk는 미리 생성된 트리를 갖춘 샘플 Nuke 스크립트로서, 적절한 LOD 스케일링을 생성하기 위한 밉매핑에 필요한 폴더 구조를 보여 주며, 압축 없는 쓰기 노드 환경설정을 표시합니다.
#! C:/Program Files/Nuke12.0v3/nuke-12.0.3.dll -nx
version 12.0 v3
define_window_layout_xml {<?xml version="1.0" encoding="UTF-8"?>
<layout version="1.0">
<window x="-1" y="-8" w="2560" h="1377" maximized="1" screen="0">
<splitter orientation="1">
<split size="40"/>
<dock id="" hideTitles="1" activePageId="Toolbar.1">
<page id="Toolbar.1"/>
</dock>
<split size="2516" stretch="1"/>
<splitter orientation="2">
<split size="1333"/>
<dock id="" activePageId="DAG.1" focus="true">
<page id="DAG.1"/>
<page id="Curve Editor.1"/>
<page id="DopeSheet.1"/>
</dock>
</splitter>
</splitter>
</window>
<window x="3219" y="212" w="1885" h="746" screen="1">
<splitter orientation="2">
<split size="746"/>
<dock id="" activePageId="Viewer.1">
<page id="Viewer.1"/>
</dock>
</splitter>
</window>
</layout>
}
Root {
inputs 0
name C:/Users/Desktop/EXR_Mipmap/Unreal_ExrMipMap_GenerationExample.nk
format "2048 1556 0 0 2048 1556 1 2K_Super_35(full-ap)"
proxy_type scale
proxy_format "1024 778 0 0 1024 778 1 1K_Super_35(full-ap)"
colorManagement Nuke
workingSpaceLUT linear
monitorLut sRGB
int8Lut sRGB
int16Lut sRGB
logLut Cineon
floatLut linear
}
BackdropNode {
inputs 0
name LOD1
tile_color 0x999dbcff
gl_color 0x3f4cccff
label "\t- Scaling by 0.5 to create second mip level aka LOD1\n\t- Images are required to reside in a folder with the new image resolution.\n\t- The image resolution folder is required to reside in the same directory as the source element (LOD0)\n\t- Example: D:/Perforce/EXR_Sequences/Smoke/1024x1024/\n\t- The images should be named exactly the same as the source and have the same compression type"
xpos -584
ypos -19
bdwidth 633
bdheight 157
}
BackdropNode {
inputs 0
name LOD2
tile_color 0x96c499ff
gl_color 0x73cc71ff
label "- Scaling by 0.25 to create third mip level aka LOD2\n- Images are required to reside in a folder with the new image resolution\n- The image resolution folder is required to reside in the same directory as the source element (LOD0)\n- Example: D:/Perforce/EXR_Sequences/Smoke/512x512/\n- The images should be named exactly the same as the source and have the same compression type"
xpos -1057
ypos 180
bdwidth 587
bdheight 160
}
BackdropNode {
inputs 0
name LOD3
tile_color 0xb790aaff
label "\t- Scaling by 0.125 to create fourth mip level aka LOD3\n\t- Images are required to reside in a folder with the new image resolution\n\t- The image resoltion folder is required to reside in the same directory at the source element (LOD0)\n\t- Example: D:/Perforce/EXR_Sequences/Smoke/256x256/\n\t- The images should be named exactly the same as the source and have the same compression type"
xpos -586
ypos 398
bdwidth 662
bdheight 156
}
BackdropNode {
inputs 0
name Source_Element__LOD0
tile_color 0xaf9f9fff
label "Source Element\n - Source images need to live in a folder with the image resolution in the name.\n - Example - D:/Perforce/EXR_Sequences/Smoke/2048x2048/\n - For GPU enhanced optimized streaming into Unreal, all Exr Sequences should be uncompressed\n - A mix of uncompressed and uncompressed in the mip levels will break the chain"
selected true
xpos -751
ypos -347
bdwidth 592
bdheight 204
}
Read {
inputs 0
file_type exr
file D:/Perforce/Project/Movies/EXR_Sequences/AtmosSmoke_003/smoke_003.####.exr
format "1152 2048 0 0 1152 2048 1 "
first 100
last 360
origfirst 100
origlast 360
origset true
in_colorspace scene_linear
out_colorspace scene_linear
name LOD0
selected true
xpos -573
ypos -230
disable true
}
Reformat {
type scale
scale 0.5
name Scale_LOD1
xpos -573
ypos 87
}
set N9841b000 [stack 0]
Reformat {
type scale
scale 0.5
name Scale_LOD2
xpos -573
ypos 287
}
set N9841a800 [stack 0]
Reformat {
type scale
scale 0.5
name ScaleLOD3
xpos -573
ypos 507
}
Write {
file D:/Perforce/EXR_Sequences/Smoke/256x256/smoke.####.exr
file_type exr
compression none
first_part rgba
version 5
in_colorspace scene_linear
out_colorspace scene_linear
name LOD3_Write
xpos -463
ypos 501
}
push $N9841b000
Write {
file D:/Perforce/EXR_Sequences/Smoke/1024x1024/smoke.####.exr
file_type exr
compression none
first_part rgba
version 6
in_colorspace scene_linear
out_colorspace scene_linear
name LOD1_Write
xpos -368
ypos 81
}
push $N9841a800
Write {
file D:/Perforce/EXR_Sequences/Smoke/512x512/smoke.####.exr
file_type exr
compression none
first_part rgba
version 5
in_colorspace scene_linear
out_colorspace scene_linear
name LOD2_Write
xpos -791
ypos 281
}
아래의 생성된 이미지는 Unreal_ExrMipMap_GenerationExample.nk
스크립트의 샘플 스크린샷입니다.
autoGenEXR_mipmap.py는 Nuke가 없는 사용자를 위한 스크립트입니다. Python 스크립트는 디스크에 밉 레벨을 스케일링하고 쓰며 필수 폴더도 자동으로 생성합니다.
import os
os.environ["OPENCV_IO_ENABLE_OPENEXR"]="1"
import cv2 as cv
import numpy as np
import glob
import shutil
setMipLevel = 3
fileInDir = glob.glob("C:\\Users\\User\\Desktop\\smokeCards\*.exr")
#Gets file path to parent sequence
grabFirst = fileInDir[0]
splitFile = grabFirst.split('\\')[-1]
getParentPath = grabFirst.replace(splitFile,'')
#Getting image resolution
img = cv.imread(grabFirst, cv.IMREAD_UNCHANGED)
height = np.size(img, 0)
width = np.size(img, 1)
#Creates folders
def createFolders(dirName):
isThere = os.path.isdir(dirName)
if isThere == False:
os.makedirs(dirName)
#Builds folder path for LOD0 and copies sequence to LOD0 folder
LOD0_folderName = getParentPath + str(width) + 'x' + str(height)
createFolders(LOD0_folderName)
for x in fileInDir:
getFileName = x.split('\\')[-1]
newFile = LOD0_folderName + '\\' + getFileName
if os.path.isfile(newFile) == 0:
shutil.copyfile(x, newFile)
print('copying ' + newFile + ' to correct file path')
#Creates mipped EXR files
def createFiles(file, mipWidth, mipHeight, folderPath):
getName = file.split('\\')[-1]
imageSize = (int(mipWidth), int(mipHeight))
readFile = cv.imread(file, cv.IMREAD_UNCHANGED)
resizeFile = cv.resize(readFile, imageSize, interpolation = cv.INTER_LANCZOS4)
newFile = folderPath + '\\' + getName
cv.imwrite(newFile, resizeFile, [cv.IMWRITE_EXR_TYPE, cv.IMWRITE_EXR_TYPE_HALF])
print("saving mipped file to " + newFile)
#Does math for the mip levels, creates mipped folders and files
for index in range(setMipLevel):
if index == 0:
newWidth = width / 2
newHeight = height / 2
LOD1FolderPath = getParentPath + str(int(newWidth)) + 'x' + str(int(newHeight))
createFolders(LOD1FolderPath)
for file in fileInDir:
createFiles(file, newWidth, newHeight, LOD1FolderPath)
else:
mipWidth = newWidth / 2
mipHeight = newHeight / 2
newWidth = mipWidth
newHeight = mipHeight
lowerMipFolderPath = getParentPath + str(int(newWidth)) + 'x' + str(int(newHeight))
createFolders(lowerMipFolderPath)
for file in fileInDir:
createFiles(file, mipWidth, mipHeight, lowerMipFolderPath)