SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git
python3.13libs/hc/hccam.py (9.9K)
1 import hou
2 from .hcframing import orthographic_frame_width, perspective_frame_distance
3 from .hcgeo import HCGeo
4 from .hcsceneviewer import HCSceneViewer
5
6
7 class HCCam:
8 def __init__(self, cam_node, scene_viewer):
9 self.cam_node = cam_node
10 self.scene_viewer = scene_viewer
11
12 self._t = hou.Vector3(0, 0, 0)
13 self._r = hou.Vector3(0, 0, 0)
14 self._p = hou.Vector3(0, 0, 0)
15 self._ow = 10
16 self._projection = 'perspective'
17 self._resx = 1000
18 self._resy = 1000
19 self._aspect_ratio = 1
20 self.local_x = hou.Vector3(1, 0, 0)
21 self.local_y = hou.Vector3(0, 1, 0)
22 self.local_z = hou.Vector3(0, 0, 1)
23 self.global_x = hou.Vector3(1, 0, 0)
24 self.global_y = hou.Vector3(0, 1, 0)
25 self.global_z = hou.Vector3(0, 0, 1)
26 # self.zoom = 10
27 self.delta_t = 1
28 self.delta_r = 15
29 self.delta_zoom = 1
30 self.target = None
31
32 self.fitAspectRatio()
33 self.lock()
34 self.reset()
35
36
37 """ Movement """
38
39 def center(self):
40 geo = self.scene_viewer.geo()
41 centroid = geo.boundingBox().center() if geo else hou.Vector3(0, 0, 0)
42 self.t = hou.Vector3(centroid)
43 self.p = hou.Vector3(centroid)
44
45 def frame(self):
46 geo = self.geo()
47 if not geo:
48 return
49
50 bbox = geo.boundingBox()
51 centroid = bbox.center()
52 size = bbox.sizevec()
53
54 self.p = hou.Vector3(centroid)
55 self._syncFromCameraNode()
56
57 if self.projection == 'perspective':
58 dist = perspective_frame_distance(
59 bbox_size=(size[0], size[1], size[2]),
60 aperture=self.cam_node.parm('aperture').eval(),
61 focal=self.cam_node.parm('focal').eval(),
62 aspect_ratio=self.aspect_ratio,
63 )
64 self.t = hou.Vector3(centroid) + (self.local_z * dist)
65 else:
66 self.ow = orthographic_frame_width((size[0], size[1], size[2]))
67 self.t = hou.Vector3(centroid) + (self.local_z * max((self.t - self.p).length(), 0.001))
68
69 self._syncFromCameraNode()
70
71 def home(self):
72 geo = self.geo()
73 centroid = geo.boundingBox().center() if geo else hou.Vector3(0, 0, 0)
74 self.t = centroid
75 self.p = centroid
76 # self.ow = 10
77 # self.setZoom(6)
78
79 # def movePivot(self):
80 # If origin
81 # if self.target == 0:
82 # self.t = [0, 0, self.zoom]
83 # self.r = [45, 45, 0]
84 # self.p = [0, 0, self.zoom * -1]
85 # self.ow = 10
86
87 def rotateUp(self):
88 self._syncFromCameraNode()
89 delta = hou.Vector3(self.delta_r, 0, 0)
90 self.r += delta
91 m = hou.hmath.buildRotateAboutAxis(self.local_x, self.delta_r)
92 self.t -= self.p
93 self.t *= m
94 self.t += self.p
95 self.local_x *= m
96 self.local_y *= m
97 self.local_z *= m
98
99 def rotateDown(self):
100 self._syncFromCameraNode()
101 delta = hou.Vector3(-self.delta_r, 0, 0)
102 self.r += delta
103 m = hou.hmath.buildRotateAboutAxis(self.local_x, -self.delta_r)
104 self.t -= self.p
105 self.t *= m
106 self.t += self.p
107 self.local_x *= m
108 self.local_y *= m
109 self.local_z *= m
110
111 def rotateLeft(self):
112 self._syncFromCameraNode()
113 delta = hou.Vector3(0, -self.delta_r, 0)
114 self.r += delta
115 m = hou.hmath.buildRotateAboutAxis(self.global_y, -self.delta_r)
116 self.t -= self.p
117 self.t *= m
118 self.t += self.p
119 self.local_x *= m
120 self.local_y *= m
121 self.local_z *= m
122
123 def rotateRight(self):
124 self._syncFromCameraNode()
125 delta = hou.Vector3(0, self.delta_r, 0)
126 self.r += delta
127 m = hou.hmath.buildRotateAboutAxis(self.global_y, self.delta_r)
128 self.t -= self.p
129 self.t *= m
130 self.t += self.p
131 self.local_x *= m
132 self.local_y *= m
133 self.local_z *= m
134
135 def setZoom(self, amt):
136 self._syncFromCameraNode()
137 move = self.local_z * amt
138 self.t += move
139
140 def translateUp(self):
141 self._syncFromCameraNode()
142 move = self.local_y * self.delta_t
143 self.t += move
144 self.p += move
145
146 def translateDown(self):
147 self._syncFromCameraNode()
148 move = self.local_y * self.delta_t * -1
149 self.t += move
150 self.p += move
151
152 def translateLeft(self):
153 self._syncFromCameraNode()
154 move = self.local_x * self.delta_t * -1
155 self.t += move
156 self.p += move
157
158 def translateRight(self):
159 self._syncFromCameraNode()
160 move = self.local_x * self.delta_t
161 self.t += move
162 self.p += move
163
164 def zoom(self, dir):
165 self._syncFromCameraNode()
166 dir_map = {'out': 1, 'in': -1}
167 move = self.local_z * self.delta_zoom * dir_map[dir]
168 self.t += move
169
170 def zoomOrtho(self, dir):
171 self._syncFromCameraNode()
172 dir_map = {'out': 1, 'in': -1}
173 self.ow += self.delta_zoom * dir_map[dir]
174
175
176 """ Util """
177
178 def fitAspectRatio(self):
179 self.resx = 1000
180 self.resy = 1000
181 viewport = self.currentViewport()
182 ratio = viewport.size()[2] / viewport.size()[3]
183 self.aspect_ratio = ratio
184
185 def resetAspectRatio(self):
186 self.aspect_ratio = 1.0
187
188 @staticmethod
189 def captureViewportState(viewport):
190 """Capture the current viewport's camera state before keycam takes over."""
191 xform = viewport.viewTransform()
192 is_persp = viewport.type() == hou.geometryViewportType.Perspective
193 parts = xform.explode(transform_order='srt', rotate_order='xyz')
194 return {
195 't': hou.Vector3(parts['translate']),
196 'r': hou.Vector3(parts['rotate']), # XYZ order to match keycam xOrd=0
197 'p': hou.Vector3(viewport.viewPivot()),
198 'projection': 'perspective' if is_persp else 'ortho',
199 }
200
201 def initFromState(self, state):
202 """Initialize camera from a previously captured viewport state."""
203 self.t = state['t']
204 self.r = state['r']
205 self._p = state['p']
206 if state['projection'] != self._projection:
207 self.projection = state['projection']
208 self._syncFromCameraNode()
209
210 def geo(self):
211 return self.scene_viewer.geo()
212
213 def lock(self):
214 viewport = self.currentViewport()
215 viewport.setCamera(self.cam_node)
216 viewport.lockCameraToView(1)
217
218 def reset(self):
219 self.t = hou.Vector3(0, 0, 2)
220 self.r = hou.Vector3(0, 0, 0)
221 self.p = hou.Vector3(0, 0, 0)
222 # self.zoom = 10
223 self.ow = 10
224 self.delta_t = 1
225 self.delta_r = 15
226 self.delta_zoom = 1
227 self.local_x = hou.Vector3(1, 0, 0)
228 self.local_y = hou.Vector3(0, 1, 0)
229 self.local_z = hou.Vector3(0, 0, 1)
230 self.global_x = hou.Vector3(1, 0, 0)
231 self.global_y = hou.Vector3(0, 1, 0)
232 self.global_z = hou.Vector3(0, 0, 1)
233
234 def _syncFromCameraNode(self):
235 self._t = hou.Vector3(self.cam_node.parmTuple('t').eval())
236 self._r = hou.Vector3(self.cam_node.parmTuple('r').eval())
237 self._ow = self.cam_node.parm('orthowidth').eval()
238 self._projection = self.cam_node.parm('projection').evalAsString()
239
240 world_xform = self.cam_node.worldTransform()
241 self.local_x = hou.Vector3(world_xform.at(0, 0), world_xform.at(0, 1), world_xform.at(0, 2)).normalized()
242 self.local_y = hou.Vector3(world_xform.at(1, 0), world_xform.at(1, 1), world_xform.at(1, 2)).normalized()
243 self.local_z = hou.Vector3(world_xform.at(2, 0), world_xform.at(2, 1), world_xform.at(2, 2)).normalized()
244
245 def setView(self):
246 view_map = {
247 'top': hou.Vector3(270, 0, 0),
248 'bottom': hou.Vector3(90, 0, 0),
249 'front': hou.Vector3(0, 180, 0),
250 'back': hou.Vector3(0, 0, 0),
251 'right': hou.Vector3(0, 90, 0),
252 'left': hou.Vector3(0, 270, 0)
253 }
254 self._syncFromCameraNode()
255 distance = max((self.t - self.p).length(), 0.001)
256 self.r = view_map[self.view]
257 self._syncFromCameraNode()
258 self.t = self.p + (self.local_z * distance)
259 self._syncFromCameraNode()
260
261 def toggleProjection(self):
262 projection_map = {
263 'perspective': 'ortho',
264 'ortho': 'perspective'
265 }
266 self.projection = projection_map[self.projection]
267
268 def unlock(self):
269 self.viewport().lockCameraToView(0)
270
271 def currentViewport(self):
272 return self.scene_viewer.currentViewport()
273
274
275 """ Props """
276
277 @property
278 def t(self):
279 return self._t
280 @t.setter
281 def t(self, val):
282 self._t = val
283 self.cam_node.parmTuple('t').set(val)
284
285 @property
286 def r(self):
287 return self._r
288 @r.setter
289 def r(self, val):
290 self._r = val
291 self.cam_node.parmTuple('r').set(val)
292
293 @property
294 def p(self):
295 return self._p
296 @p.setter
297 def p(self, val):
298 self._p = val
299
300 @property
301 def ow(self):
302 return self._ow
303 @ow.setter
304 def ow(self, val):
305 self._ow = val
306 self.cam_node.parm('orthowidth').set(val)
307
308 @property
309 def projection(self):
310 return self._projection
311 @projection.setter
312 def projection(self, val):
313 self._projection = val
314 self.cam_node.parm('projection').set(val)
315
316 @property
317 def resx(self):
318 return self._resx
319 @resx.setter
320 def resx(self, val):
321 self._resx = val
322 self.cam_node.parm('resx').set(val)
323
324 @property
325 def resy(self):
326 return self._resy
327 @resy.setter
328 def resy(self, val):
329 self._resy = val
330 self.cam_node.parm('resy').set(val)
331
332 @property
333 def aspect_ratio(self):
334 return self._aspect_ratio
335 @aspect_ratio.setter
336 def aspect_ratio(self, val):
337 self._aspect_ratio = val
338 self.cam_node.parm('aspect').set(val)