이미지 시퀀스 밉매핑

이 문서에서는 이미지 시퀀스에 밉매핑을 사용하는 방법을 간략하게 살펴봅니다.

소개

이 문서에서는 이미지 시퀀스에 밉매핑을 사용하는 방법을 간략하게 살펴봅니다.

밉맵

밉맵(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.pyNuke에서 실행하여 적절한 LOD 생성 트리를 구성할 수 있는 Python 스크립트입니다.

  • 스크립트를 사용하려면 EXR 시퀀스를 선택하고 원하는 밉 레벨의 수를 설정합니다.

  • 스크립트 맨 위에서 밉맵을 생성할 읽기 노드를 선택하고 실행합니다.

  • 그러면 Nuke에서 필요한 리포맷 및 쓰기 노드가 모두 생성됩니다.

  • 밉 렌더별로 필요한 해상도 폴더도 모두 생성됩니다.

  • 밉 렌더의 경로는 실행 시 선택된 읽기 노드를 기반으로 합니다.

nukeMipMap.py

# 모든 시퀀스는 이미지의 해상도를 따라 명명된 폴더에 있어야 합니다.
# 예시 - 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 스케일링을 생성하기 위한 밉매핑에 필요한 폴더 구조를 보여 주며, 압축 없는 쓰기 노드 환경설정을 표시합니다.

Unreal_ExrMipMap_GenerationExample.nk

#! 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 스크립트의 샘플 스크린샷입니다.

ExampleNukeTree.PNG

autoGenEXR_mipmap.py는 Nuke가 없는 사용자를 위한 스크립트입니다. Python 스크립트는 디스크에 밉 레벨을 스케일링하고 쓰며 필수 폴더도 자동으로 생성합니다.

이 스크립트를 활용하려면 무료 Python 모듈인 numpy, opencvshutil을 설치해야 합니다.

autoGenEXR_mipmap.py

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)