/
node3d.go
460 lines (393 loc) · 15.2 KB
/
node3d.go
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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
// Copyright (c) 2019, The GoKi Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gi3d
import (
"fmt"
"image"
"log"
"reflect"
"sync"
"github.com/goki/gi/gi"
"github.com/goki/gi/oswin"
"github.com/goki/gi/oswin/mouse"
"github.com/goki/ki/ki"
"github.com/goki/ki/kit"
"github.com/goki/mat32"
)
// Node3D is the common interface for all gi3d scenegraph nodes
type Node3D interface {
gi.Node
// IsSolid returns true if this is an Solid node (else a Group)
IsSolid() bool
// AsNode3D returns a generic Node3DBase for our node -- gives generic
// access to all the base-level data structures without requiring
// interface methods.
AsNode3D() *Node3DBase
// AsSolid returns a node as Solid (nil if not)
AsSolid() *Solid
// Validate checks that scene element is valid
Validate(sc *Scene) error
// UpdateWorldMatrix updates this node's local and world matrix based on parent's world matrix
// This sets the WorldMatrixUpdated flag but does not check that flag -- calling
// routine can optionally do so.
UpdateWorldMatrix(parWorld *mat32.Mat4)
// UpdateMVPMatrix updates this node's MVP matrix based on given view and prjn matrix from camera
// Called during rendering.
UpdateMVPMatrix(viewMat, prjnMat *mat32.Mat4)
// UpdateMeshBBox updates the Mesh-based BBox info for all nodes.
// groups aggregate over elements. called from FuncDownMeLast traversal
UpdateMeshBBox()
// UpdateBBox2D updates this node's 2D bounding-box information based on scene
// size and other scene bbox info from scene
UpdateBBox2D(size mat32.Vec2, sc *Scene)
// RayPick converts a given 2D point in scene image coordinates
// into a ray from the camera position pointing through line of sight of camera
// into *local* coordinates of the solid.
// This can be used to find point of intersection in local coordinates relative
// to a given plane of interest, for example (see Ray methods for intersections).
RayPick(pos image.Point, sc *Scene) mat32.Ray
// WorldMatrix returns the world matrix for this node, under read-lock protection.
WorldMatrix() *mat32.Mat4
// NormDCBBox returns the normalized display coordinates bounding box
// which is used for clipping. This is read-lock protected.
NormDCBBox() mat32.Box3
// IsVisible provides the definitive answer as to whether a given node
// is currently visible. It is only entirely valid after a render pass
// for widgets in a visible window, but it checks the window and viewport
// for their visibility status as well, which is available always.
// Non-visible nodes are automatically not rendered and not connected to
// window events. The Invisible flag is one key element of the IsVisible
// calculus -- it is set by e.g., TabView for invisible tabs, and is also
// set if a widget is entirely out of render range. But again, use
// IsVisible as the main end-user method.
// For robustness, it recursively calls the parent -- this is typically
// a short path -- propagating the Invisible flag properly can be
// very challenging without mistakenly overwriting invisibility at various
// levels.
IsVisible() bool
// IsTransparent returns true if solid has transparent color
IsTransparent() bool
// Init3D does 3D intialization
Init3D(sc *Scene)
// Style3D does 3D styling using property values on nodes
Style3D(sc *Scene)
// UpdateNode3D does arbitrary node updating during render process
UpdateNode3D(sc *Scene)
// RenderClass returns the class of rendering for this solid.
// used for organizing the ordering of rendering
RenderClass() RenderClasses
// Render3D is called by Scene Render3D on main thread,
// everything ready to go..
Render3D(sc *Scene, rc RenderClasses, rnd Render)
// ConnectEvents3D: setup connections to window events -- called in
// Render3D if in bounds. It can be useful to create modular methods for
// different event types that can then be mix-and-matched in any more
// specialized types.
ConnectEvents3D(sc *Scene)
// Convenience methods for external setting of Pose values with appropriate locking
// SetPosePos sets Pose.Pos position to given value, under write lock protection
SetPosePos(pos mat32.Vec3)
// SetPoseScale sets Pose.Scale scale to given value, under write lock protection
SetPoseScale(scale mat32.Vec3)
// SetPoseQuat sets Pose.Quat to given value, under write lock protection
SetPoseQuat(quat mat32.Quat)
}
// Node3DBase is the basic 3D scenegraph node, which has the full transform information
// relative to parent, and computed bounding boxes, etc.
// There are only two different kinds of Nodes: Group and Solid
type Node3DBase struct {
gi.NodeBase
Pose Pose `desc:"complete specification of position and orientation"`
PoseMu sync.RWMutex `view:"-" copy:"-" json:"-" xml:"-" desc:"mutex on pose access -- needed for parallel updating"`
MeshBBox BBox `desc:"mesh-based local bounding box (aggregated for groups)"`
WorldBBox BBox `desc:"world coordinates bounding box"`
NDCBBox mat32.Box3 `desc:"normalized display coordinates bounding box, used for frustrum clipping"`
}
var KiT_Node3DBase = kit.Types.AddType(&Node3DBase{}, Node3DBaseProps)
var Node3DBaseProps = ki.Props{
"base-type": true, // excludes type from user selections
"EnumType:Flag": KiT_NodeFlags,
}
// NodeFlags extend gi.NodeFlags to hold 3D node state
type NodeFlags int
//go:generate stringer -type=NodeFlags
var KiT_NodeFlags = kit.Enums.AddEnumExt(gi.KiT_NodeFlags, NodeFlagsN, kit.BitFlag, nil)
const (
// WorldMatrixUpdated means that the Pose.WorldMatrix has been updated
WorldMatrixUpdated NodeFlags = NodeFlags(gi.NodeFlagsN) + iota
// VectorsUpdated means that the rendering vectors information is updated
VectorsUpdated
NodeFlagsN
)
// KiToNode3D converts Ki to a Node3D interface and a Node3DBase obj -- nil if not.
func KiToNode3D(k ki.Ki) (Node3D, *Node3DBase) {
if k == nil || k.This() == nil { // this also checks for destroyed
return nil, nil
}
nii, ok := k.(Node3D)
if ok {
return nii, nii.AsNode3D()
}
return nil, nil
}
// KiToNode3DBase converts Ki to a *Node3DBase -- use when known to be at
// least of this type, not-nil, etc
func KiToNode3DBase(k ki.Ki) *Node3DBase {
return k.(Node3D).AsNode3D()
}
func (nb *Node3DBase) CopyFieldsFrom(frm interface{}) {
fr := frm.(*Node3DBase)
nb.NodeBase.CopyFieldsFrom(&fr.NodeBase)
nb.Pose = fr.Pose
nb.MeshBBox = fr.MeshBBox
nb.WorldBBox = fr.WorldBBox
nb.NDCBBox = fr.NDCBBox
}
// AsNode3D returns a generic Node3DBase for our node -- gives generic
// access to all the base-level data structures without requiring
// interface methods.
func (nb *Node3DBase) AsNode3D() *Node3DBase {
return nb
}
func (nb *Node3DBase) BaseIface() reflect.Type {
return reflect.TypeOf((*Node3DBase)(nil)).Elem()
}
func (nb *Node3DBase) IsSolid() bool {
return false
}
func (nb *Node3DBase) AsSolid() *Solid {
return nil
}
func (nb *Node3DBase) Validate(sc *Scene) error {
return nil
}
func (nb *Node3DBase) IsVisible() bool {
if nb == nil || nb.This() == nil || nb.IsInvisible() {
return false
}
if nb.Par == nil || nb.Par.This() == nil {
return false
}
sc := nb.Par.Embed(KiT_Scene)
if sc != nil {
return sc.(*Scene).IsVisible()
}
return nb.Par.This().(Node3D).IsVisible()
}
func (nb *Node3DBase) IsTransparent() bool {
return false
}
func (nb *Node3DBase) WorldMatrixUpdated() bool {
return nb.HasFlag(int(WorldMatrixUpdated))
}
// UpdateWorldMatrix updates this node's world matrix based on parent's world matrix.
// If a nil matrix is passed, then the previously-set parent world matrix is used.
// This sets the WorldMatrixUpdated flag but does not check that flag -- calling
// routine can optionally do so.
func (nb *Node3DBase) UpdateWorldMatrix(parWorld *mat32.Mat4) {
nb.PoseMu.Lock()
defer nb.PoseMu.Unlock()
nb.Pose.UpdateMatrix() // note: can do this in special ways to bake in other
// automatic transforms as needed
nb.Pose.UpdateWorldMatrix(parWorld)
nb.SetFlag(int(WorldMatrixUpdated))
}
// UpdateMVPMatrix updates this node's MVP matrix based on given view, prjn matricies from camera.
// Called during rendering.
func (nb *Node3DBase) UpdateMVPMatrix(viewMat, prjnMat *mat32.Mat4) {
nb.PoseMu.Lock()
nb.Pose.UpdateMVPMatrix(viewMat, prjnMat)
nb.PoseMu.Unlock()
}
// UpdateBBox2D updates this node's 2D bounding-box information based on scene
// size and min offset position.
func (nb *Node3DBase) UpdateBBox2D(size mat32.Vec2, sc *Scene) {
nb.BBoxMu.Lock()
defer nb.BBoxMu.Unlock()
off := mat32.Vec2{}
nb.PoseMu.RLock()
nb.WorldBBox.BBox = nb.MeshBBox.BBox.MulMat4(&nb.Pose.WorldMatrix)
nb.NDCBBox = nb.MeshBBox.BBox.MVProjToNDC(&nb.Pose.MVPMatrix)
nb.PoseMu.RUnlock()
Wmin := nb.NDCBBox.Min.NDCToWindow(size, off, 0, 1, true) // true = flipY
Wmax := nb.NDCBBox.Max.NDCToWindow(size, off, 0, 1, true) // true = filpY
// BBox is always relative to scene
nb.BBox = image.Rectangle{Min: image.Point{int(Wmin.X), int(Wmax.Y)}, Max: image.Point{int(Wmax.X), int(Wmin.Y)}}
// note: BBox is inaccurate for objects extending behind camera
isvis := sc.Camera.Frustum.IntersectsBox(nb.WorldBBox.BBox)
if isvis { // filter out invisible at objbbox level
scbounds := image.Rectangle{Max: sc.Geom.Size}
bbvis := nb.BBox.Intersect(scbounds)
nb.ObjBBox = bbvis.Add(sc.BBox.Min)
nb.VpBBox = nb.ObjBBox.Add(sc.ObjBBox.Min.Sub(sc.BBox.Min)) // move amount
nb.VpBBox = nb.VpBBox.Intersect(sc.VpBBox)
if nb.VpBBox != image.ZR {
nb.WinBBox = nb.VpBBox.Add(sc.WinBBox.Min.Sub(sc.VpBBox.Min))
} else {
nb.WinBBox = nb.VpBBox
}
} else {
// fmt.Printf("not vis: %v wbb: %v\n", nb.Name(), nb.WorldBBox.BBox)
nb.ObjBBox = image.ZR
nb.VpBBox = image.ZR
nb.WinBBox = image.ZR
}
}
// RayPick converts a given 2D point in scene image coordinates
// into a ray from the camera position pointing through line of sight of camera
// into *local* coordinates of the solid.
// This can be used to find point of intersection in local coordinates relative
// to a given plane of interest, for example (see Ray methods for intersections).
// To convert mouse window-relative coords into scene-relative coords
// subtract the sc.ObjBBox.Min which includes any scrolling effects
func (nb *Node3DBase) RayPick(pos image.Point, sc *Scene) mat32.Ray {
nb.PoseMu.RLock()
sc.Camera.CamMu.RLock()
sz := sc.Geom.Size
size := mat32.Vec2{float32(sz.X), float32(sz.Y)}
fpos := mat32.Vec2{float32(pos.X), float32(pos.Y)}
ndc := fpos.WindowToNDC(size, mat32.Vec2{}, true) // flipY
var err error
ndc.Z = -1 // at closest point
cdir := mat32.NewVec4FromVec3(ndc, 1).MulMat4(&sc.Camera.InvPrjnMatrix)
cdir.Z = -1
cdir.W = 0 // vec
// get world position / transform of camera: matrix is inverse of ViewMatrix
wdir := cdir.MulMat4(&sc.Camera.Pose.Matrix)
wpos := sc.Camera.Pose.Matrix.Pos()
sc.Camera.CamMu.RUnlock()
invM, err := nb.Pose.WorldMatrix.Inverse()
nb.PoseMu.RUnlock()
if err != nil {
log.Println(err)
}
lpos := mat32.NewVec4FromVec3(wpos, 1).MulMat4(invM)
ldir := wdir.MulMat4(invM)
ldir.SetNormal()
ray := mat32.NewRay(mat32.Vec3{lpos.X, lpos.Y, lpos.Z}, mat32.Vec3{ldir.X, ldir.Y, ldir.Z})
return *ray
}
// WorldMatrix returns the world matrix for this node, under read lock protection
func (nb *Node3DBase) WorldMatrix() *mat32.Mat4 {
nb.PoseMu.RLock()
defer nb.PoseMu.RUnlock()
return &nb.Pose.WorldMatrix
}
// NormDCBBox returns the normalized display coordinates bounding box
// which is used for clipping. This is read-lock protected.
func (nb *Node3DBase) NormDCBBox() mat32.Box3 {
nb.BBoxMu.RLock()
defer nb.BBoxMu.RUnlock()
return nb.NDCBBox
}
func (nb *Node3DBase) Init3D(sc *Scene) {
// nop by default -- could connect to scene for update signals or something
}
func (nb *Node3DBase) Style3D(sc *Scene) {
pagg := nb.ParentCSSAgg()
if pagg != nil {
gi.AggCSS(&nb.CSSAgg, *pagg)
} else {
nb.CSSAgg = nil // restart
}
gi.AggCSS(&nb.CSSAgg, nb.CSS)
}
func (nb *Node3DBase) UpdateNode3D(sc *Scene) {
}
func (nb *Node3DBase) Render3D(sc *Scene, rc RenderClasses, rnd Render) {
// nop
}
// SetPosePos sets Pose.Pos position to given value, under write lock protection
func (nb *Node3DBase) SetPosePos(pos mat32.Vec3) {
nb.PoseMu.Lock()
nb.Pose.Pos = pos
nb.PoseMu.Unlock()
}
// SetPoseScale sets Pose.Scale scale to given value, under write lock protection
func (nb *Node3DBase) SetPoseScale(scale mat32.Vec3) {
nb.PoseMu.Lock()
nb.Pose.Scale = scale
nb.PoseMu.Unlock()
}
// SetPoseQuat sets Pose.Quat to given value, under write lock protection
func (nb *Node3DBase) SetPoseQuat(quat mat32.Quat) {
nb.PoseMu.Lock()
nb.Pose.Quat = quat
nb.PoseMu.Unlock()
}
/////////////////////////////////////////////////////////////////
// Events
// Default node can be selected / manipulated per the Scene SelMode settings
func (nb *Node3DBase) ConnectEvents3D(sc *Scene) {
nb.ConnectEvent(sc.Win, oswin.MouseEvent, gi.RegPri, func(recv, send ki.Ki, sig int64, d interface{}) {
me := d.(*mouse.Event)
if me.Action != mouse.Press || !nb.IsVisible() || nb.IsInactive() {
return
}
sci, err := recv.ParentByTypeTry(KiT_Scene, ki.Embeds)
if err != nil {
return
}
ssc := sci.Embed(KiT_Scene).(*Scene)
ni := nb.This().(Node3D)
if ssc.CurSel != ni {
ssc.SetSel(ni)
me.SetProcessed()
}
})
}
// ConnectEvent connects this node to receive a given type of GUI event
// signal from the parent window -- typically connect only visible nodes, and
// disconnect when not visible
func (nb *Node3DBase) ConnectEvent(win *gi.Window, et oswin.EventType, pri gi.EventPris, fun ki.RecvFunc) {
win.EventMgr.ConnectEvent(nb.This(), et, pri, fun)
}
// DisconnectEvent disconnects this receiver from receiving given event
// type -- pri is priority -- pass AllPris for all priorities -- see also
// DisconnectAllEvents
func (nb *Node3DBase) DisconnectEvent(win *gi.Window, et oswin.EventType, pri gi.EventPris) {
win.EventMgr.DisconnectEvent(nb.This(), et, pri)
}
// DisconnectAllEvents disconnects node from all window events -- typically
// disconnect when not visible -- pri is priority -- pass AllPris for all priorities.
// This goes down the entire tree from this node on down, as typically everything under
// will not get an explicit disconnect call because no further updating will happen
func (nb *Node3DBase) DisconnectAllEvents(win *gi.Window, pri gi.EventPris) {
nb.FuncDownMeFirst(0, nb.This(), func(k ki.Ki, level int, d interface{}) bool {
_, ni := KiToNode3D(k)
if ni == nil {
return ki.Break // going into a different type of thing, bail
}
win.EventMgr.DisconnectAllEvents(ni.This(), pri)
return ki.Continue
})
}
// TrackCamera moves this node to pose of camera
func (nb *Node3DBase) TrackCamera(sc *Scene) {
nb.PoseMu.Lock()
sc.Camera.CamMu.RLock()
nb.Pose.CopyFrom(&sc.Camera.Pose)
sc.Camera.CamMu.RUnlock()
nb.PoseMu.Unlock()
}
// TrackLight moves node to position of light of given name.
// For SpotLight, copies entire Pose. Does not work for Ambient light
// which has no position information.
func (nb *Node3DBase) TrackLight(sc *Scene, lightName string) error {
nb.PoseMu.Lock()
defer nb.PoseMu.Unlock()
lt, ok := sc.Lights[lightName]
if !ok {
return fmt.Errorf("gi3d Node: %v TrackLight named: %v not found", nb.Path(), lightName)
}
switch l := lt.(type) {
case *DirLight:
nb.Pose.Pos = l.Pos
case *PointLight:
nb.Pose.Pos = l.Pos
case *SpotLight:
nb.Pose.CopyFrom(&l.Pose)
}
return nil
}