Houdini ApprenticeでVAT出力

環境

Houdini Apprentice 19.5.534

python3.9 Fbx Sdk

概要

Houdiniでパーティクルを独自VATとしてExr形式のファイルに出力します

Unity上で再生できるプログラムを作ったところ、同じように動作しました

パーティクルは参考の動画をみて作成しました。バージョンが上がり多少違いましたが、同じような感じに出来ました

また動画はライフに基づいてカラーを0に近づけていますが、こちらはAlphaのアトリビュートを追加してそれに対して行っています

Alphaというアトリビュートを追加すると勝手にアルファブレンドになる模様

import OpenEXR
import numpy as np
import Imath
import json

fps = int(hou.fps())
node = hou.pwd()
targetNode = hou.node(node.parm("node").eval())
startFrame = node.parm("startFrame").eval()
endFrame = node.parm("endFrame").eval()
imageWidth = node.parm("imageWidth").eval()
imageHeight = node.parm("imageHeight").eval()
outputDir = node.parm("outputDir").eval()

imageSize = imageWidth * imageHeight
channelNum = 4
strideTexelNum = 2
pixelType = Imath.PixelType.HALF
dataType = np.float16
texels = np.zeros((imageSize, channelNum), dtype=dataType)
texIndex = 0
texelIndex = 0

def writeImage():
    header = OpenEXR.Header(imageWidth, imageHeight)
    header['channels'] = {
        'R': Imath.Channel(Imath.PixelType(pixelType)),
        'G': Imath.Channel(Imath.PixelType(pixelType)),
        'B': Imath.Channel(Imath.PixelType(pixelType)),
        'A': Imath.Channel(Imath.PixelType(pixelType))
    }
    header['compression'] =Imath.Compression(Imath.Compression.NO_COMPRESSION)
    outputPath = f'{outputDir}/output{texIndex}.exr';
    print(outputPath)
    output = OpenEXR.OutputFile(outputPath, header)
    output.writePixels({
        'R': texels[:,0].astype(dataType).tostring(),
        'G': texels[:,1].astype(dataType).tostring(),
        'B': texels[:,2].astype(dataType).tostring(),
        'A': texels[:,3].astype(dataType).tostring()
    })
    output.close()
    
def writePoints(frame, maxPointNum):
    global texels, texIndex, texelIndex
    
    geo = targetNode.geometry()
    pointNum = len(geo.points())
    texelNum = maxPointNum * strideTexelNum
    if (texelIndex + texelNum) > imageSize:
        writeImage()
        texIndex += 1
        texelIndex = 0
        texels[:,] = [0.0, 0.0, 0.0, 0.0]

    for point in geo.points():
        pos = point.position()
        texels[texelIndex + 0,] = [pos.x(), pos.y(), pos.z(), 0.0]
        color = point.attribValue('Cd')
        alpha = point.attribValue('Alpha')
        texels[texelIndex + 1,] = [color[0], color[1], color[2], alpha]
        texelIndex += strideTexelNum

    dummyNum = maxPointNum - pointNum
    for i in range(dummyNum):
        texels[texelIndex + 0,] = [0.0, 0.0, 0.0, 0.0]
        texels[texelIndex + 1,] = [0.0, 0.0, 0.0, 0.0]
        texelIndex += strideTexelNum

def main():
    global texIndex, texelIndex
    
    maxPointNum = 0
    frameNum = (endFrame - startFrame) + 1
    for i in range(frameNum):
        frame = startFrame + i
        hou.setFrame(frame)
        geo = targetNode.geometry()
        maxPointNum = max(maxPointNum, len(geo.points()))

    texIndex = 0
    texelIndex = 0
    for i in range(frameNum):
        frame = startFrame + i
        hou.setFrame(frame)
        writePoints(frame, maxPointNum)
    if texelIndex > 0 :
        writeImage()
        texIndex += 1
    
    paramDict = {}
    paramDict['texNum'] = texIndex;
    paramDict['fps'] = fps
    paramDict['frameNum'] = frameNum;
    paramDict['maxPointNumInFrame'] = maxPointNum;
    jsonData = json.dumps(paramDict, ensure_ascii=False)
    print(jsonData)
    outputPath = f'{outputDir}/output.json';
    f = open(outputPath, 'w')
    f.write(jsonData)
    f.close()

main()

参考

Houdini ダイナミクス基本講座 Part1:ダイナミクスシミュレーションの流れ - YouTube

プリミティブ版

HdrpVatExample/Fluid.hip at master · keijiro/HdrpVatExample · GitHub

に三角形分割、Color、Normal、UVを追加したものを使用してテスト

import OpenEXR
import numpy as np
import Imath
import json

fps = int(hou.fps())
node = hou.pwd()
targetNode = hou.node(node.parm("node").eval())
startFrame = node.parm("startFrame").eval()
endFrame = node.parm("endFrame").eval()
imageWidth = node.parm("imageWidth").eval()
imageHeight = node.parm("imageHeight").eval()
outputDir = node.parm("outputDir").eval()

imageSize = imageWidth * imageHeight
channelNum = 4
strideTexelNum = 3
pixelType = Imath.PixelType.HALF
dataType = np.float16
texels = np.zeros((imageSize, channelNum), dtype=dataType)
texIndex = 0
texelIndex = 0
pointInface = 3

def writeImage():
    header = OpenEXR.Header(imageWidth, imageHeight)
    header['channels'] = {
        'R': Imath.Channel(Imath.PixelType(pixelType)),
        'G': Imath.Channel(Imath.PixelType(pixelType)),
        'B': Imath.Channel(Imath.PixelType(pixelType)),
        'A': Imath.Channel(Imath.PixelType(pixelType))
    }
    header['compression'] =Imath.Compression(Imath.Compression.NO_COMPRESSION)
    outputPath = f'{outputDir}/output{texIndex}.exr';
    print(outputPath)
    output = OpenEXR.OutputFile(outputPath, header)
    output.writePixels({
        'R': texels[:,0].astype(dataType).tostring(),
        'G': texels[:,1].astype(dataType).tostring(),
        'B': texels[:,2].astype(dataType).tostring(),
        'A': texels[:,3].astype(dataType).tostring()
    })
    output.close()
    
def writeFaces(frame, maxPointNum):
    global texels, texIndex, texelIndex
    
    geo = targetNode.geometry()
    texelNum = maxPointNum * strideTexelNum
    pointNum = len(geo.prims()) * pointInface
    if pointNum > maxPointNum:
        print(f'error pointNum:{pointNum} maxPointNum:{maxPointNum}')
        return
    
    if (texelIndex + texelNum) > imageSize:
        writeImage()
        texIndex += 1
        texelIndex = 0
        texels[:,] = [0.0, 0.0, 0.0, 0.0]

    for prim in geo.prims():
        #print(f'prim{prim.number()}')
        if len(prim.vertices()) != pointInface:
            print('error verticesNum')
            break
        pointInPrim = []
        for vertex in prim.vertices():
            pointInPrim.append(vertex.point())
        if len(pointInPrim) != pointInface:
            print('error pointInPrim')
            break
        for i in range(pointInface):
            point = pointInPrim[int((pointInface - i) % pointInface)]
            pos = point.position()
            #print(f'pos{index}/{point.number()}:{pos}')
            normal = point.attribValue('N')
            color = point.attribValue('Cd')
            alpha = point.attribValue('Alpha')
            uv = point.attribValue('uv')
            texels[texelIndex + 0,] = [pos.x(), pos.y(), pos.z(), uv[0]]
            texels[texelIndex + 1,] = [normal[0], normal[1], normal[2], uv[1]]
            texels[texelIndex + 2,] = [color[0], color[1], color[2], alpha]
            texelIndex += strideTexelNum
        #break

    dummyNum = maxPointNum - pointNum
    for i in range(dummyNum):
        texels[texelIndex + 0,] = [0.0, 0.0, 0.0, 0.0]
        texels[texelIndex + 1,] = [0.0, 0.0, 0.0, 0.0]
        texels[texelIndex + 2,] = [0.0, 0.0, 0.0, 0.0]
        texelIndex += strideTexelNum

def main():
    global texIndex, texelIndex
    
    frameNum = (endFrame - startFrame) + 1
    geo = targetNode.geometry()
    maxPointNum = 0
    frameNum = (endFrame - startFrame) + 1
    for i in range(frameNum):
        frame = startFrame + i
        hou.setFrame(frame)
        geo = targetNode.geometry()
        maxPointNum = max(maxPointNum, len(geo.prims()) * pointInface)

    texIndex = 0
    texelIndex = 0
    for i in range(frameNum):
        frame = startFrame + i
        hou.setFrame(frame)
        print(f'frame:{frame}')
        writeFaces(frame, maxPointNum)
    if texelIndex > 0 :
        writeImage()
        texIndex += 1
    
    paramDict = {}
    paramDict['texNum'] = texIndex;
    paramDict['fps'] = fps
    paramDict['frameNum'] = frameNum;
    paramDict['maxPointNumInFrame'] = maxPointNum;
    paramDict['strideTexelNum'] = strideTexelNum;
    jsonData = json.dumps(paramDict, ensure_ascii=False)
    print(jsonData)
    outputPath = f'{outputDir}/output.json';
    f = open(outputPath, 'w')
    f.write(jsonData)
    f.close()

main()
print('finish')