티스토리 뷰

개인공부/FDS

FDS 2 UNREAL

소심야채 2023. 10. 30. 22:00

사용되는 프로그램

- FDS

- PyroSim

- Houdini

- UE 5.3

 

 

0. 개요

FDS 시뮬레이션 결과를 PyroSim 뷰어를 통해 확인한 동영상입니다.

 

UE에 적용할 FDS 주요 파라미터로는 Physical Dimensions, Grid Dimensions이 있습니다.

Cell size = 0.2 x 0.2 x 0.2 (m)로 설정하였기 때문에 Grid Cells 갯수가 50*165*25 입니다.

 

UE 5.3에서 볼륨 시뮬레이션 데이터를 표현가능하도록 OpenVDB를 임포트 하는 기능을 실험적으로 도입하였습니다. (https://docs.unrealengine.com/5.3/en-US/unreal-engine-5.3-release-notes/)

포스팅 시점에서는 FDS2OpenVDB는 FDS Road Map에서 잠재적 연구 주제로 선정되었지만 출시가 되지 않은 것 같습니다.. (https://github.com/firemodels/fds/wiki/FDS-Road-Map)

 

복잡하지만 적용 가능한 방법으로는 FDS -> VTK -> CSV -> OpenVDB 파일을 변환하고 UE에서 머테리얼 설정을 통해 시뮬레이션 결과를 UE에 적용할 수 있습니다.

 

1. FDS 2 VTK

import numpy as np
import os
import sys
import argparse

def getGridFromSMV(path):
    with open(path) as f:
        lines = [l.strip() for l in f.readlines()]
        lines = [l for l in lines if l!=""]
    #Get the begin
    begin = [0,0,0]
    for i,l in enumerate(lines):
        for j,kwd in enumerate(["TRNX", "TRNY", "TRNZ"]):
            if kwd in l:
                begin[j] = i+2
    #Write the X, Y and Z coords
    currentInd = 0
    X, Y, Z = [], [], []
    for i,l in enumerate(lines):
        if i>=begin[currentInd]:
            try:
                if currentInd==0:
                    X.append(float(l.split()[1]))
                if currentInd==1:
                    Y.append(float(l.split()[1]))
                if currentInd==2:
                    Z.append(float(l.split()[1]))
            except:
                if currentInd == 2:
                    break
                else:
                    currentInd+=1
    print(len(X), len(Y), len(Z))
    return X,Y,Z
def getOutputFilesFromSMV(path):
    with open(path) as f:
        files = []
        lines = [l.strip() for l in f.readlines()]
        lines = [l for l in lines if l!=""]
        for i in range(len(lines)):
            if lines[i].split()[0]=="SMOKF3D":
                files.append([lines[i+2].split()[0], lines[i+1].strip()])
            if lines[i].split()[0]=="SLCF":
                files.append([lines[i+2].strip(), lines[i+1].strip()])
        return files
def toInt(byte):
    return int(byte.encode('hex'), 16)
def readS3D(s3dfile, frames, start=0, end=None):
    if end is None:
        end = len(frames)

    VALUES = []
    with open(s3dfile, "rb") as f:
        #First offset
        X = 36
        f.seek(X,1)

        #Skipping until the start
        for frame in frames[:start]:
            f.seek(72-X, 1)
            f.seek(int(frame[2]), 1)

        #Reading from start to end
        if start!=0 or end!=len(frames):
            print ("Reading the frames " + str(start) + " to " + str(end))
        for ind, frame in enumerate(frames[start:end]):
            f.seek(72-X, 1)
            bits  = f.read(int(frame[2]))
            vals  = []
            i     = 0
            while(i<len(bits)):
                if toInt(bits[i]) == 255:
                    val = toInt(bits[i+1])
                    n   = toInt(bits[i+2])
                    for j in range(n):
                        vals.append(val)
                    i+=3
                else:
                    val = toInt(bits[i])
                    vals.append(val)
                    i+=1
            VALUES.append(vals)
    return VALUES
def writeVTKArray(arr, f):
    for i in range(len(arr)/6 + 1):
        for j in range(6):
            if 6*i + j < len(arr):
                f.write(str(arr[6*i + j]) + " ")
        f.write("\n")
def exportVTK(filepath, name, x,y,z,values):
    with open(filepath, "w") as f:
        writeVTKArray(values, f)
def arguments():
    parser = argparse.ArgumentParser(description='Converts FDS outputs to vtk.')
    parser.add_argument('--input',   '-i', help='simulation root path', required=False, default="C:\\FDS_6FLOOR")
    parser.add_argument('--run',     '-r', help='really run', action="store_true", default=True)
    parser.add_argument('--output',  '-o', help='output folder (default to input folder)', default="C:\\FDS_6FLOOR\\output")
    parser.add_argument('--start',   '-s', help='frame to start extraction (default to first)', type=int, default=0)
    parser.add_argument('--end',     '-e', help='frame to end extraction (default to last available)', type=int, default=0)
    args = parser.parse_args()

    if not os.path.isdir(args.input):
        print (args.input + " not a directory")
        sys.exit()
    if len([f for f in os.listdir(args.input) if f[-4:]==".smv"])==0:
        print ("No .smv file in " + args.input)
        sys.exit()
    args.input = os.path.abspath(args.input)

    if args.output is not None:
        if not os.path.isdir(args.output):
            print (args.output + " does not exist, please create it")
            sys.exit()
    else:
        args.output = args.input
    args.output = os.path.abspath(args.output)

    return args

if __name__=="__main__":
    # 0 - Argument parsing
    args = arguments()
    case = [f[:-4] for f in os.listdir(args.input) if f[-4:]==".smv"][0]

    # 1 - Get the grid info
    _x,_y,_z = getGridFromSMV(os.path.join(args.input, case + ".smv"))

    # 2 - Get the output data files (.s3d and .sf)
    files = getOutputFilesFromSMV(os.path.join(args.input, case + ".smv"))

    # 3 - Loop on the data files
    if args.run:
        for f in files:
            #Read the .s3d files
            if f[1][-4:] == ".s3d":
                folder_path = os.path.join(args.output, f[0])
                if not os.path.exists(folder_path):
                    os.makedirs(folder_path)

                with open( os.path.join(args.input, f[1] + ".sz") ) as fsz:
                    frames = np.array([[float(x) for x in l.split()] for l in fsz.readlines()[1:]])

                args.end = len(frames)

                values = readS3D( os.path.join(args.input, f[1]) , frames , args.start, args.end)
                print (len(values))

                for i,v in enumerate(values):
                    vtkFile = os.path.join(folder_path, str(args.start + i) + ".txt")
                    print ("Writing " + vtkFile)
                    exportVTK(vtkFile , f[0], _x, _y, _z, v)


    else:
        print (args)
        print (case)
        print (len(_x), len(_y), len(_z))
        for f in files:
            print (f)
        print ("To run the script, add the --run option")

- 참고한 코드 : https://github.com/ISCDdocs/FDS-to-unreal/blob/master/fds2ascii.py

- 파이썬 버전 : 2.7

코드에서 주의해야할 부분은 커맨드로 실행하지 않는 이상 input 디폴트 파일경로를 fds 파일이 존재하는 폴더경로로 설정해주어야 합니다.

 

코드 실행결과 HRRPUV, SOOT, TEMPERATURE 폴더가 생성됩니다.

HRRPUV -> 단위 부피당 열방출률을 나타내는 지표로써 UE에서 Emissive Color를 나타낼 때 사용됩니다.

SOOT -> 화재시 발생하는 연기를 나타낼 때 사용됩니다.

 

세 폴더는 공통적으로 0부터 끝 프레임까지의 포인트 데이터가 txt파일 형태로 기록됩니다.

주의해야할 점으로는 각 차원에서 1씩 더해 Grid Cells 갯수가 달라집니다.

해당 파일에서는 51 x 166 x 26으로 총 241,696개 입니다.

 

 

2. VTK 2 CSV

import os
import re
from fractions import Fraction
from multiprocessing import Pool, cpu_count


def count_files_in_directory(directory_path):
    return sum([1 for item in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, item))])


def getNums(file_path: str):
    with open(file_path, 'r') as file:
        file_contents = file.read()
        nums = re.findall(r"\d+", file_contents)

    return nums


def process_step(args):
    current_step, point_count, x_gap, y_gap, z_gap, x_count, y_count, z_count = args

    cur_soot_path = os.path.join("SOOT", f"{current_step}.txt")
    cur_temperature_path = os.path.join("HRRPUV", f"{current_step}.txt")

    cur_soot = getNums(cur_soot_path)
    cur_temperature = getNums(cur_temperature_path)

    # 이 프로세스에서의 cur_soot와 cur_temperature의 최대, 최소 값을 찾습니다.
    max_soot = max(map(int, cur_soot))
    min_soot = min(map(int, cur_soot))
    max_temperature = max(map(int, cur_temperature))
    min_temperature = min(map(int, cur_temperature))

    with open(f"csv/dataset_{current_step:06}.csv", "w") as file:
        # first line
        file.write("Points:0,Points:1,Points:2,density,temperature\n")

        x, y, z = Fraction(0, 1), Fraction(0, 1), Fraction(0, 1)
        for point in range(0, point_count):
            file.write(f"{float(x)},{float(y)},{float(z)},{cur_soot[point]},{cur_temperature[point]}\n")
            x += x_gap
            if (x > x_gap * (x_count - 1)):
                x = 0
                y += y_gap

            if (y > y_gap * (y_count - 1)):
                y = 0
                z += z_gap

        print(f"csv/dataset_{current_step:06}.csv finish")

    # 최대, 최소 값을 반환합니다.
    return max_soot, min_soot, max_temperature, min_temperature


if __name__ == "__main__":  # multiprocessing은 이 구문 아래에서 실행해야 합니다.
    max_time_step = count_files_in_directory("SOOT")

    x_count = 51
    y_count = 166
    z_count = 26
    point_count = x_count * y_count * z_count

    x_gap = Fraction(1, 5)  # 0.2 정교한 값을 위해 분수로
    y_gap = Fraction(1, 5)
    z_gap = Fraction(1, 5)

    if not os.path.exists("csv"):
        os.mkdir("csv")

    # 병렬 처리 시작
    pool = Pool(processes=cpu_count())  # 사용 가능한 모든 CPU 코어를 사용
    args = [(step, point_count, x_gap, y_gap, z_gap, x_count, y_count, z_count) for step in range(max_time_step)]

    # 각 프로세스에서 반환된 최대, 최소 값들을 모은다.
    results = pool.map(process_step, args)

    # 모든 프로세스에서 반환된 최대, 최소 값들 중에서 전체 최대, 최소 값을 찾는다.
    overall_max_soot = max(result[0] for result in results)
    overall_min_soot = min(result[1] for result in results)
    overall_max_temperature = max(result[2] for result in results)
    overall_min_temperature = min(result[3] for result in results)

    print(f"Overall Max Soot: {overall_max_soot}")
    print(f"Overall Min Soot: {overall_min_soot}")
    print(f"Overall Max Temperature: {overall_max_temperature}")
    print(f"Overall Min Temperature: {overall_min_temperature}")

    pool.close()
    pool.join()

- 파이썬 버전 : 3.8

 

해당 코드에서 주의해야 할 점으로는 각 차원의 count를 Physical Dimension 갯수 +1씩 적용하여 수동 입력해주어야 합니다.

 

또한 vtk2csv.py 파일경로에 vtk 파일폴더를 넣어줍니다. 해당 코드 실행결과 csv폴더안에 csv파일들이 생성됩니다.

csv파일 구성으로는 xyz 데이터, hrr, soot 스칼라 데이터입니다.

 

 

3. CSV 2 OpenVDB

OpenVDB 파일로 만들기 위해서 Houdini 프로그램이 사용됩니다. 변환 과정을 동영상을 통해 보여드리겠습니다.

 

 

 

4. 언리얼에 OpenVDB 적용

...

'개인공부 > FDS' 카테고리의 다른 글

기본 데이터  (0) 2023.09.23
dxf 파일 구조분석  (0) 2023.09.18
FDS 연산 정지시키는 방법  (0) 2023.09.17
댓글