Githbub Codebase
PYPI Package

Houdini Engine是Houdini的一个无图形界面简化版,可以通过houdini engine与houdini内核交互,完成houdini fx中的操作。

官方有For Maya, For Max, For Unity, For Unreal四个插件,可以在这四个host程序中调用houdini engine。这是极好的,意味着开发的HDA资源可以直接应用在host程序中。不过笔者并不满足于此,是否可以不用host环境直接调用houdini engine?当然可以,官方提供了一套C API  https://www.sidefx.com/docs/hengine/ . 只不过笔者看着它就头疼,主要两个问题

  • 面向过程,不面向对象
  • 语法繁琐

这也是笔者开发pyhapi的初衷,希望面向对象,简化语法。

怎么安装

已经发布到PyPI,安装直接使用pip即可

1
pip install pyhapi

要求py>3.6, 因为用了asyncio异步库

numpy>1.15

Houdini17.5测试没问题,需要commercial license. Houdini18尚未测试

当然要注意的是,请确保houdini dll路径在PATH中

使用

目前支持的功能:

  • 实例化节点,HDA
  • 节点连接操作
  • 节点参数设置
  • 节点异步cook
  • 传入/出 曲线,模型

还不支持的:

  • 传入/出 体素
  • PDG

举例来说,如果想实例化一个HDA,传入参数,cook,可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# -*- coding: utf-8 -*-
"""Example of getting/setting params of HDA asset
Author  : Maajor
Email   : hello_myd@126.com
"""
import pyhapi as ph

def main():
    session = ph.HSessionManager.get_or_create_default_session()

    #load hda asset and instantiate
    hda_asset = ph.HAsset(session, "hda/SideFX_spaceship.otl")
    asset_node = hda_asset.instantiate(node_name="Spaceship")

    #Set node's parameters
    asset_node.set_param_value("seed", 1.0)
    asset_node.set_param_value("rop_geometry1_sopoutput", "$HIP/spaceship.obj")

    #Press button
    asset_node.press_button("rop_geometry1_execute", status_report_interval=1.0)

    session.save_hip("spaceship.hip")

if __name__ == "__main__":
    main()

执行完以后就可以输出spaceship.obj模型啦

对比一下C API的官方案例Parameters.c

两百多行其实就干了一件事:实例化一个HDA,打印出它所有参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#include <HAPI/HAPI.h>
#include <iostream>
#include <string>

#define ENSURE_SUCCESS( result )
if ( (result) != HAPI_RESULT_SUCCESS )
{
    std::cout << "Failure at " << __FILE__ << ": " << __LINE__ << std::endl;
    std::cout << getLastError() << std::endl;
    exit( 1 );
}

#define ENSURE_COOK_SUCCESS( result )
if ( (result) != HAPI_RESULT_SUCCESS )
{
    std::cout << "Failure at " << __FILE__ << ": " << __LINE__ << std::endl;
    std::cout << getLastCookError() << std::endl;
    exit( 1 );
}

static std::string getLastError();
static std::string getLastCookError();
static std::string getString( HAPI_StringHandle stringHandle );

int
main( int argc, char ** argv )
{
    const char * hdaFile = argc == 2 ? argv[ 1 ] : "examples/SideFX_spaceship.otl";
    
    HAPI_CookOptions cookOptions = HAPI_CookOptions_Create();

    HAPI_Session session;

    HAPI_CreateInProcessSession( &session );

    ENSURE_SUCCESS( HAPI_Initialize( &session,
                     &cookOptions,
                     true,
                     -1,
                     nullptr,
                     nullptr,
                     nullptr,
                     nullptr,
                     nullptr ) );

    HAPI_AssetLibraryId assetLibId;
    ENSURE_SUCCESS( HAPI_LoadAssetLibraryFromFile( &session, hdaFile, true, &assetLibId ) );

    int assetCount;
    ENSURE_SUCCESS( HAPI_GetAvailableAssetCount( &session, assetLibId, &assetCount ) );

    if (assetCount > 1)
    {
    std::cout << "Should only be loading 1 asset here" << std::endl;
    exit ( 1 );
    }
    
    HAPI_StringHandle assetSh;
    ENSURE_SUCCESS( HAPI_GetAvailableAssets( &session, assetLibId, &assetSh, assetCount ) );

    std::string assetName = getString( assetSh );

    HAPI_NodeId nodeId;
    ENSURE_SUCCESS( HAPI_CreateNode( &session, -1, assetName.c_str(), "AnAsset", false, &nodeId ) );

    ENSURE_SUCCESS( HAPI_CookNode( &session, nodeId, &cookOptions ) );
    
    int cookStatus;
    HAPI_Result cookResult;

    do
    {
    cookResult = HAPI_GetStatus( &session, HAPI_STATUS_COOK_STATE, &cookStatus );
    }
    while (cookStatus > HAPI_STATE_MAX_READY_STATE && cookResult == HAPI_RESULT_SUCCESS);

    ENSURE_SUCCESS( cookResult );
    ENSURE_COOK_SUCCESS( cookStatus );
    
    HAPI_NodeInfo nodeInfo;
    ENSURE_SUCCESS( HAPI_GetNodeInfo( &session, nodeId, &nodeInfo ) );
    
    HAPI_ParmInfo * parmInfos = new HAPI_ParmInfo[ nodeInfo.parmCount ];
    ENSURE_SUCCESS( HAPI_GetParameters( &session, nodeId, parmInfos, 0, nodeInfo.parmCount ) );

    // Print parameter info
    std::cout << "Parameters: " << std::endl;

    for( int i = 0; i < nodeInfo.parmCount; ++i )
    {
    std::cout << "==========" << std::endl;

    std::cout << "   Name: "
          << getString( parmInfos[ i ].nameSH )
          << std::endl;

    std::cout << "   Values: (";

    if ( HAPI_ParmInfo_IsInt( &parmInfos[ i ] ) )
        {
        int parmIntCount = HAPI_ParmInfo_GetIntValueCount( &parmInfos[ i ] );
        
        int * parmIntValues = new int[ parmIntCount ];
        
        ENSURE_SUCCESS( HAPI_GetParmIntValues( &session,
                           nodeId, parmIntValues,
                           parmInfos[ i ].intValuesIndex,
                           parmIntCount ) );

        for ( int v = 0; v < parmIntCount; ++v )
        {
        if ( v != 0 )
            std::cout << ", ";

        std::cout << parmIntValues[ v ];
        }
        
        delete [] parmIntValues;
    }
    else if ( HAPI_ParmInfo_IsFloat( &parmInfos[ i ] ) )
        {
        int parmFloatCount = HAPI_ParmInfo_GetFloatValueCount( &parmInfos[ i ] );

        float * parmFloatValues = new float[ parmFloatCount ];
        
        ENSURE_SUCCESS( HAPI_GetParmFloatValues( &session,
                             nodeId, parmFloatValues,
                             parmInfos[ i ].floatValuesIndex,
                             parmFloatCount ) );
        
        for ( int v = 0; v < parmFloatCount; ++v )
        {
        if ( v != 0 )
            std::cout << ", ";

        std::cout << parmFloatValues[ v ];
        }
        
        delete [] parmFloatValues;
    }
    else if ( HAPI_ParmInfo_IsString( &parmInfos[ i ] ) )
    {
        int parmStringCount = HAPI_ParmInfo_GetStringValueCount( &parmInfos[ i ] );

        HAPI_StringHandle * parmSHValues = new HAPI_StringHandle[ parmStringCount ];
    
        ENSURE_SUCCESS( HAPI_GetParmStringValues( &session,
                              nodeId,
                              true, parmSHValues,
                              parmInfos[ i ].stringValuesIndex,
                              parmStringCount ) );
        
        for ( int v = 0; v < parmStringCount; ++v )
        {
        if ( v != 0 )
            std::cout << ", ";
        
        std::cout << getString( parmSHValues[ v ] );
        }

        delete [] parmSHValues;
    }
        
    std::cout << ")" << std::endl;
    }

    delete [] parmInfos;

    char in;
    std::cout << "Press any key to exit" << std::endl;
    std::cin >> in;
    
    HAPI_Cleanup( &session );
    
    return 0;
}

static std::string
getLastError()
{
    int bufferLength;
    HAPI_GetStatusStringBufLength( nullptr,
                   HAPI_STATUS_CALL_RESULT,
                   HAPI_STATUSVERBOSITY_ERRORS,
                   &bufferLength );

    char * buffer = new char[ bufferLength ];

    HAPI_GetStatusString( nullptr, HAPI_STATUS_CALL_RESULT, buffer, bufferLength );

    std::string result( buffer );
    delete [] buffer;

    return result;
}

static std::string
getLastCookError()
{
    int bufferLength;
    HAPI_GetStatusStringBufLength( nullptr,
                   HAPI_STATUS_COOK_RESULT,
                   HAPI_STATUSVERBOSITY_ERRORS,
                   &bufferLength );

    char * buffer = new char[ bufferLength ];

    HAPI_GetStatusString( nullptr, HAPI_STATUS_COOK_RESULT, buffer, bufferLength );

    std::string result( buffer );
    delete[] buffer;

    return result;
}

static std::string
getString( HAPI_StringHandle stringHandle )
{
    if ( stringHandle == 0 )
    {
    return "";
    }

    int bufferLength;
    HAPI_GetStringBufLength( nullptr,
                   stringHandle,
                   &bufferLength );

    char * buffer = new char[ bufferLength ];

    HAPI_GetString ( nullptr, stringHandle, buffer, bufferLength );

    std::string result( buffer );
    delete [] buffer;

    return result;
}

用pyhapi的话十行以内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pyhapi as ph

def main():
    session = ph.HSessionManager.get_or_create_default_session()

    #load hda asset and instantiate
    hda_asset = ph.HAsset(session, "hda/SideFX_spaceship.otl")
    asset_node = hda_asset.instantiate(node_name="Spaceship")

    #Query node's parameters
    for name in asset_node.get_param_names():
        print("Query param: {0} has value: {1}".format(name, asset_node.get_param_value(name)))

if __name__ == "__main__":
    main()

当然我们还可以把一个模型/曲线,传入/取出houdini engine,比如mesh_marshall_input.py这个例子,传进去一个方块,然后做了细分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# -*- coding: utf-8 -*-
"""Example of setting mesh data into hengine
Author  : Maajor
Email   : hello_myd@126.com
"""
import numpy as np
import pyhapi as ph

def main():
    """Main
    """
    session = ph.HSessionManager.get_or_create_default_session()

    #create an inputnode where you can set geometry
    geo_inputnode = ph.HInputNode(session, "Cube")

    #create a geomesh
    cube_geo = ph.HGeoMesh(
        vertices=np.array(
            [[0.0, 0.0, 0.0],
             [0.0, 0.0, 1.0],
             [0.0, 1.0, 0.0],
             [0.0, 1.0, 1.0],
             [1.0, 0.0, 0.0],
             [1.0, 0.0, 1.0],
             [1.0, 1.0, 0.0],
             [1.0, 1.0, 1.0]], dtype=np.float32),
        faces=np.array(
            [[0, 2, 6, 4],
             [2, 3, 7, 6],
             [2, 0, 1, 3],
             [1, 5, 7, 3],
             [5, 4, 6, 7],
             [0, 4, 5, 1]], dtype=np.int32))

    #set this geomesh as geometry of inputnode
    geo_inputnode.set_geometry(cube_geo)

    #create a node whose input is inputnode
    ph.HNode(session, "Sop/subdivide", "Cube Subdivider").connect_node_input(geo_inputnode)

    session.save_hip()

if __name__ == "__main__":
    main()

内部怎么实现的?

Python可以和C很好地实现交互

类似其他几个平台,

Unity 用的c#,dllimport就行

Unreal就是cpp,include进来就行

Python用ctypes就能加载dll

1
2
3
4
5
sys = platform.system()
if sys == "Windows":
    HAPI_LIB = ctypes.cdll.LoadLibrary("libHAPIL")
elif sys == "Linux":
    HAPI_LIB = ctypes.cdll.LoadLibrary("libHAPIL.so")

这就直接可以调用libHAPIL的方法了

1
HAPI_LIB.HAPI_IsSessionValid(byref(session))

源码里hapi.py里全部是对libHAPIL的封装,hdata.py里全是houdini engine的struct

除此以外,hsession封装session对象,hnode封装node对象,hgeo封装geo对象,hasset封装HDA对象

总共就这六个文件

可以干什么?

为了Houdini Pipeline咯

可以做

  • 资产自动化处理
  • HDA自动化测试
  • HDA管理
  • etc…

祝读者春节快乐,百病不侵!

https://www.sidefx.com/products/houdini-engine/