Skip to content

rendering -- 3D user interface

This module provides a render pipeline system featuring:

  • Class Scene to gather the data to render
  • Widget View that actually renders the scene
  • The display protocol, that allows any object to define its Display subclass to be rendered in a scene.

The view is for window integration and user interaction. Scene is only to manage the objects to render. Almost all madcad data types can be rendered to scenes being converted into an appropriate subclass of Display. Since the conversion from madcad data types into display instance is automatically handled via the display protocol, you usually don't need to deal with displays directly.

show(scene, interest=None, size=uvec2(400, 400), projection=None, navigation=True, enable_alpha=False, **options)

    Easy and convenient way to create a window containing a `View3D` on a `Scene`

Parameters:

Name Type Description Default
scene Scene | dict | list

a mapping (dict or list) giving the objects to render in the scene

required
interest Box

the region of interest to zoom on at the window initialization

None
size

the window size (pixel)

uvec2(400, 400)
projection

an object handling the camera projection (aka intrinsic parameters), see QView3D.projection

None
navigation

an object handling the camera movements, see QView3D.navigation

True
options

options to set in Scene.options

Parameters: scene: a mapping (dict or list) giving the objects to render in the scene interest: the region of interest to zoom on at the window initialization size: the window size (pixel) projection: an object handling the camera projection (aka intrinsic parameters), see QView3D.projection navigation: an object handling the camera movements, see QView3D.navigation enable_alpha: if enabled, the view background is transparent options: options to set in Scene.options

Tip: For integration in a Qt window or to manipulate the view, you should directly use View

{}
Source code in madcad/rendering/__init__.py
 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
	def show(scene:Scene|dict|list, interest: Box = None, size=uvec2(400, 400), projection=None, navigation=True, enable_alpha=False, **options):
		'''
		Easy and convenient way to create a window containing a `View3D` on a `Scene`

        Parameters:
            scene:     a mapping (dict or list) giving the objects to render in the scene
            interest:  the region of interest to zoom on at the window initialization
            size:      the window size (pixel)
            projection: an object handling the camera projection (aka intrinsic parameters), see `QView3D.projection`
            navigation: an object handling the camera movements, see `QView3D.navigation`
            options:   options to set in `Scene.options`

		Parameters:
			scene:     a mapping (dict or list) giving the objects to render in the scene
			interest:  the region of interest to zoom on at the window initialization
			size:      the window size (pixel)
			projection: an object handling the camera projection (aka intrinsic parameters), see `QView3D.projection`
			navigation: an object handling the camera movements, see `QView3D.navigation`
			enable_alpha: if enabled, the view background is transparent
			options:   options to set in `Scene.options`

		Tip:
			For integration in a Qt window or to manipulate the view, you should directly use `View`
		'''
		import sys

		if not isinstance(scene, Scene):
			scene = Scene(scene, options)

		# detect existing QApplication
		app = QApplication.instance()
		created = False
		if not app:
			app = QApplication(sys.argv)
			created = True

		# use the Qt color scheme if specified
		if settings.display['system_theme']:
			settings.use_qt_colors()

		# create the scene as a window
		view = QView3D(scene, projection=projection, navigation=navigation, enable_alpha=enable_alpha)
		view.resize(*size)
		view.show()
		scene.prepare()

		# make the camera see everything
		if not interest:	
			interest = view.scene.root.box
		view.center(interest.center)
		view.adjust(interest)

		# run eventually created QApplication
		if created:
			err = app.exec()
			if err != 0:
				print('error: Qt exited with code', err)

Display protocol

A displayable is an object that implements the signature of Display.

Display(*args)

base class for objects displayed in a scene

Source code in madcad/rendering/base.py
354
355
def __init__(self, *args): 
	pass

display(scene)

Displays are obviously displayable as themselves, this should be overriden

Source code in madcad/rendering/base.py
357
358
359
def display(self, scene:Scene) -> Self:
	''' Displays are obviously displayable as themselves, this should be overriden '''
	return self

stack(scene)

Rendering functions to insert in the renderpipeline.

The expected result can be any iterable providing tuples (key, target, priority, callable) such as:

The view contains the uniforms, rendering targets and the scene for common resources

Source code in madcad/rendering/base.py
368
369
370
371
372
373
374
375
def stack(self, scene:Scene) -> Iterable[Step]:
	''' Rendering functions to insert in the renderpipeline.

		The expected result can be any iterable providing tuples `(key, target, priority, callable)` such as:

		The view contains the uniforms, rendering targets and the scene for common resources
	'''
	return empty

__getitem__(key)

Get a child display by its key in this display (like in a scene)

Source code in madcad/rendering/base.py
392
393
394
def __getitem__(self, key) -> Display:
	''' Get a child display by its key in this display (like in a scene) '''
	raise IndexError('{} has no sub displays'.format(type(self).__name__))

update(scene, src)

Update the current displays internal datas with the given displayable .

If the display cannot be upgraded, it must return False to be replaced by a fresh new display created from the displayable

Source code in madcad/rendering/base.py
361
362
363
364
365
366
def update(self, scene:Scene, src:object) -> bool:
	''' Update the current displays internal datas with the given displayable .

		If the display cannot be upgraded, it must return False to be replaced by a fresh new display created from the displayable
	'''
	return False

control(view, key, sub, evt)

Handle input events occuring on the area of this display (or of one of its subdisplay). For subdisplay events, the parents control functions are called first, and the sub display controls are called only if the event is not accepted by parents

Parameters:

Name Type Description Default
key tuple

the key path for the current display

required
sub tuple

the key path for the subdisplay

required
evt QEvent

the Qt event (see Qt doc)

required
Source code in madcad/rendering/base.py
377
378
379
380
381
382
383
384
385
386
def control(self, view, key:tuple, sub:tuple, evt:QEvent):
	''' Handle input events occuring on the area of this display (or of one of its subdisplay).
		For subdisplay events, the parents control functions are called first, and the sub display controls are called only if the event is not accepted by parents

	Parameters:
		key:    the key path for the current display
		sub:    the key path for the subdisplay
		evt:    the Qt event (see Qt doc)
	'''
	pass

Rendering system

Note

As the GPU native precision is f4 (float 32 bits), all the vector stuff regarding rendering is made using simple precision types: fvec3, fvec4, fmat3, fmat4, ... instead of the usual double precision vec3

Scene(src=None, options=None, context=None, overrides=None)

rendering pipeline for madcad displayable objects

This class is gui-agnostic, it only relies on OpenGL, and the context has to be created by the user.

When an object is added to the scene, a Display is not immediately created for it, the object is put into the queue and the Display is created at the next render. If the object is removed from the scene before the next render, it is dequeued.

Parameters:

Name Type Description Default
src object

the root object of the scene, it must be a displayable and is usually a dict

None
options dict

scene rendering parameters, overriding settings.scene

None
context Context

the openGL context to use to send rendering instructions to the GPU

None
Source code in madcad/rendering/base.py
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
def __init__(self, src:object=None, options:dict=None, context:mgl.Context=None, overrides:dict=None):
	'''
	Args:
		src:        the root object of the scene, it must be a displayable and is usually a `dict`
		options:    scene rendering parameters, overriding `settings.scene`
		context:    the openGL context to use to send rendering instructions to the GPU
	'''
	self.root = None
	self.overrides = deepcopy(Scene.overrides)
	if overrides:
		self.overrides.update(overrides)
	self.stacks = {}
	self.options = deepcopy(settings.scene)
	self.shared = {}
	self.selection = set()
	self.touched = False
	self._hover = None
	self._memo = set()
	if options:   self.options.update(options)

	global global_context
	if context:
		self.context = context
	elif global_context:
		self.context = global_context
	else:
		try:
			self.context = global_context = mgl.create_standalone_context(requires=opengl_version)
		except Exception:
			self.context = global_context = mgl.create_standalone_context(requires=opengl_version, backend='egl')
		self.context.gc_mode = 'auto'
		self.context.line_width = settings.display["line_width"]

	self.update(src)

root = None instance-attribute

the root display of the scene, usually a Group

stacks = {} instance-attribute

a list of the display callbacks for each target frame buffers

touched = False instance-attribute

trigger for restacking render steps

overrides = deepcopy(Scene.overrides) class-attribute instance-attribute

attribute allowing to alter how the displayable are transformed into displays

  • the class attribute stores default overrides
  • the instance attribute stores the effective overrides for the scene

options = deepcopy(settings.scene) instance-attribute

scene rendering parameters, deriving from settings.scene

shared = {} instance-attribute

strong references to the resources we don't want to free when no display is using them

context = mgl.create_standalone_context(requires=opengl_version) instance-attribute

opengl context used in this scene's displays. It is assumed to have gc_mode = auto

selection = set() instance-attribute

current set of selected displays (readonly, use the scene methods to alter the selection)

hover = property(_get_hover, _set_hover) class-attribute instance-attribute

current display being hovered (readwrite)

update(src)

update the root display of the scene

Source code in madcad/rendering/base.py
93
94
95
96
97
98
99
def update(self, src:object):
	''' update the root display of the scene '''
	if src is None:
		src = {}
	self.root = self.display(src, self.root)
	self.root.key = ()
	self.touch()

touch()

inform the scene than its composition changed and that it needs to recompute its call stack

Source code in madcad/rendering/base.py
101
102
103
def touch(self):
	''' inform the scene than its composition changed and that it needs to recompute its call stack '''
	self.touched = True

display(obj, former=None)

Create a display for the given object for the current scene.

This is the actual function converting objects into displays. You don't need to call this method if you just want to add an object to the scene, use add() instead

Source code in madcad/rendering/base.py
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
def display(self, obj, former:Display=None) -> Display:
	''' Create a display for the given object for the current scene.

		This is the actual function converting objects into displays.
		You don't need to call this method if you just want to add an object to the scene, use add() instead
	'''
	# prevent reference-loop in groups (groups are taken from the execution env, so the user may not want to display it however we are trying to)
	ido = id(obj)
	assert ido not in self._memo, 'there should not be recursion loops in cascading displays'
	self._memo.add(ido)

	try:
		# no refresh if object has not changed
		if former and hasattr(former, 'source') and (former.source is obj or former.source == obj):
			disp = former
		# refresh cleverly if a previous displays proposes it
		elif former and former.update(self, obj):
			disp = former
		# create it with overrides
		elif type(obj) in self.overrides:
			disp = self.overrides[type(obj)](self, obj)
		# create it with the displayable protocol
		elif hasattr(obj, 'display'):
			if isinstance(obj, type):
				raise TypeError('types are not displayable')
			if isinstance(obj.display, type):
				disp = obj.display(self, obj)
			elif callable(obj.display):
				disp = obj.display(self)
			else:
				raise TypeError("member 'display' must be a method or a type, on {}".format(type(obj).__name__))
		else:
			raise TypeError('type {} is not displayable'.format(type(obj).__name__))

		if not isinstance(disp, Display):
			raise TypeError('the display for {} is not a subclass of Display: {}'.format(type(obj).__name__, type(disp)))

	finally:
		self._memo.remove(ido)

	# keeping a reference of the source object may increas the RAM used but avoid to refresh displays when updating the scene with the same constant value
	if self.options['track_source']:
		disp.source = obj
	return disp

displayable(obj)

return whether the given object could be displayed in the scene

Source code in madcad/rendering/base.py
150
151
152
153
154
155
156
157
158
159
160
161
def displayable(self, obj) -> bool:
	''' return whether the given object could be displayed in the scene '''
	if type(obj) in self.overrides:
		return True
	elif hasattr(obj, 'display'):
		if isinstance(obj, type):
			return False
		if isinstance(obj.display, type):
			return True
		elif callable(obj.display):
			return True
	return False

share(key, generator=None)

Get a shared resource loaded or load it using the function func. If func is not provided, an error is raised

Source code in madcad/rendering/base.py
163
164
165
166
167
168
169
170
171
172
173
174
def share(self, key, generator=None):
	''' Get a shared resource loaded or load it using the function func.
		If func is not provided, an error is raised
	'''
	if key in self.shared:	
		return self.shared[key]
	elif callable(generator):
		res = generator(self)
		self.shared[key] = res
		return res
	else:
		raise KeyError("resource {} doesn't exist or is not loaded".format(repr(key)))

prepare()

convert all displayable to displays and bufferize everything for comming renderings

Source code in madcad/rendering/base.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def prepare(self):
	''' convert all displayable to displays and bufferize everything for comming renderings '''
	if self.touched:
		self.touched = False
		self.active = None
		self.hover = None
		with self.context:
			for stack in self.stacks.values():
				stack.clear()
			for step in self.root.stack(self):
				if isinstance(step, tuple):
					step = Step(*step)
				elif not isinstance(step, Step):
					raise TypeError('stack step must be of type Step, not {}'.format(type(step)))
				if not isinstance(step.display, Display):
					raise TypeError('step.display must be a Display, not {}'.format(type(step.display)))
				if step.target not in self.stacks:	
					self.stacks[step.target] = []
				stack = self.stacks[step.target]
				stack.append(step)
			# sort the stack using the specified priorities
			for stack in self.stacks.values():
				stack.sort(key=attrgetter('priority'))

render(view)

Render to the view targets.

This must be called by the view widget, once the OpenGL context is set.

Source code in madcad/rendering/base.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def render(self, view):
	''' Render to the view targets. 

		This must be called by the view widget, once the OpenGL context is set.
	'''
	# restack and bufferize
	try:
		self.prepare()
	except Exception:
		traceback.print_exc()
	with self.context:
		# render everything
		for target, frame, setup in view.targets:
			try:
				view.target = frame
				frame.use()
				setup()
				for step in self.stacks.get(target,empty):
					step.render(view)
			except Exception:
				traceback.print_exc()

selection_add(display, sub=None)

select the given display

Source code in madcad/rendering/base.py
222
223
224
225
226
227
228
229
230
231
232
233
def selection_add(self, display:Display, sub:int=None):
	''' select the given display '''
	if not hasattr(display, 'selected'):
		raise TypeError('{} is not selectable'.format(type(display).__name__))
	if isinstance(display.selected, set):
		display.selected.add(sub)
		display.selected = display.selected
		self.selection.add(display)
	else:
		display.selected = True
		self.selection.add(display)
	self.touch()

selection_remove(display, sub=None)

deselect the given display

Source code in madcad/rendering/base.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
def selection_remove(self, display:Display, sub:int=None):
	''' deselect the given display '''
	if not hasattr(display, 'selected'):
		raise TypeError('{} is not selectable'.format(type(display).__name__))
	if isinstance(display.selected, set):
		if sub is None:
			display.selected.clear()
		else:
			display.selected.discard(sub)
		display.selected = display.selected
		if not display.selected:
			self.selection.discard(display)
	else:
		display.selected = False
		self.selection.discard(display)
	self.touch()

selection_toggle(display, sub=None)

Source code in madcad/rendering/base.py
252
253
254
255
256
257
258
259
260
261
262
def selection_toggle(self, display:Display, sub:int=None):
	if not hasattr(display, 'selected'):
		raise TypeError('{} is not selectable'.format(type(display).__name__))
	if isinstance(display.selected, set):
		selected = sub in display.selected
	else:
		selected = display.selected
	if selected:
		self.selection_remove(display, sub)
	else:
		self.selection_add(display, sub)

selection_clear()

deselect all previously selected displays

Source code in madcad/rendering/base.py
264
265
266
267
268
269
270
271
272
273
274
def selection_clear(self):
	''' deselect all previously selected displays '''
	for display in self.selection:
		if isinstance(display.selected, set):
			display.selected.clear()
			display.selected = display.selected
		else:
			display.selected = False
	if self.selection:
		self.touch()
		self.selection.clear()

__getitem__(key)

Source code in madcad/rendering/base.py
308
309
def __getitem__(self, key):
	return self.root[key]

__setitem__(key, value)

Source code in madcad/rendering/base.py
311
312
def __setitem__(self, key, value):
	self.root[key] = value

item(key)

Source code in madcad/rendering/base.py
314
315
316
317
def item(self, key:tuple):
	node = self.root
	for sub in key:
		node = node[sub]

Views classes

GLView3D(scene, size=None, view=None, proj=None, enable_ident=True, enable_alpha=False, **uniforms)

pure openGL 3D view over a madcad scene

Source code in madcad/rendering/d3/view.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def __init__(self, scene, size:uvec2=None, view:fmat4=None, proj:fmat4=None,
		enable_ident=True,
		enable_alpha=False,
		**uniforms):
	self.scene = scene if isinstance(scene, Scene) else Scene(scene)
	self.view = view
	self.proj = proj
	self.uniforms = uniforms
	self.screen = None
	self.ident = None
	self.targets = []
	self.enable_ident = enable_ident
	self.enable_alpha = enable_alpha
	# indentifiers management for the ident map
	self._steps = []  # identifiers before each ident stack step
	self._stepi = 0
	self._step = 0

	self.scene.root.world = fmat4()
	if size:
		self._reallocate(size)

scene = scene if isinstance(scene, Scene) else Scene(scene) instance-attribute

madcad scene to render, it must contain 3D displays (at least a the root)

screen = None instance-attribute

color map (and depth) openGL buffer

ident = None instance-attribute

identification map (and depth) openGL buffer

targets = [] instance-attribute

exposed for Scene

uniforms = uniforms instance-attribute

exposed for Scene

view = view instance-attribute

current view matrix, this will be the default for next rendering

proj = proj instance-attribute

current projection matrix, this will be the default for next rendering

enable_ident = enable_ident instance-attribute

enable self.ident

enable_alpha = enable_alpha instance-attribute

render(size=None, view=None, proj=None)

trigger the rendering of a frame, do not wait for the result

  • the view and proj instance attributes can be changed on the fly without extra cost.
  • a size change will trigger reallocation of the buffers
Source code in madcad/rendering/d3/view.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def render(self, size:uvec2=None, view:fmat4=None, proj:fmat4=None) -> GLView3D:
	''' trigger the rendering of a frame, do not wait for the result 

		- the `view` and `proj` instance attributes can be changed on the fly without extra cost.
		- a `size` change will trigger reallocation of the buffers
	'''
	size = glsize(size)
	self._reallocate(size)
	# setup uniforms
	if view:    self.view = view
	if proj:    self.proj = proj
	self.uniforms['size'] = uvec2(self.screen.size)
	self.uniforms['view'] = self.view
	self.uniforms['proj'] = self.proj
	self.uniforms['projview'] = self.proj * self.view
	# normal rendering of the scene
	self.scene.render(self)
	# downsample the screen buffer to 1 sample per pixel
	self.scene.context.copy_framebuffer(self.screen, self._screen_multisample)

	# self.screen.use()
	# self.screen.clear(1,0,0)
	self.scene.context.finish()
	return self

identstep(nidents)

Updates the amount of rendered idents and return the start ident for the calling rendering pass? Method to call during a renderstep

Source code in madcad/rendering/d3/view.py
136
137
138
139
140
141
142
143
144
def identstep(self, nidents):
	''' Updates the amount of rendered idents and return the start ident for the calling rendering pass?
		Method to call during a renderstep
	'''
	s = self._step
	self._step += nidents
	self._steps[self._stepi] = self._step-1
	self._stepi += 1
	return s

Offscreen3D(scene, size=None, view=None, proj=None, enable_depth=False, enable_ident=False, enable_alpha=False, **uniforms)

3D view giving images accessible to numpy buffers

Source code in madcad/rendering/d3/view.py
171
172
173
174
175
176
177
178
179
180
def __init__(self, scene, size:uvec2=None, view:fmat4=None, proj:fmat4=None, 
		enable_depth=False, 
		enable_ident=False, 
		enable_alpha=False,
		**uniforms):
	self.gl = GLView3D(scene, size, view, proj, enable_ident, enable_alpha, **uniforms)
	self.color = self.depth = self.ident = None
	self.enable_depth = enable_depth
	if size:
		self._reallocate(size)

gl = GLView3D(scene, size, view, proj, enable_ident, enable_alpha, **uniforms) instance-attribute

opengl renderer

color = None instance-attribute

result image from the previous rendering, showing the

depth = None instance-attribute

result image from the previous rendering, showing the

ident = None instance-attribute

result image from the previous rendering, showing the

enable_alpha = forwardproperty('gl', 'enable_alpha') class-attribute instance-attribute

if enabled, self.color is RGBA else it is 'RGB'

enable_depth = enable_depth instance-attribute

enable self.depth

enable_ident = forwardproperty('gl', 'enable_ident') class-attribute instance-attribute

enable self.ident

scene = forwardproperty('gl', 'scene') class-attribute instance-attribute

view = forwardproperty('gl', 'view') class-attribute instance-attribute

proj = forwardproperty('gl', 'proj') class-attribute instance-attribute

uniforms = forwardproperty('gl', 'uniforms') class-attribute instance-attribute

render(size=None, view=None, proj=None)

render the scene and retreive the result in the color, depth and ident attributes

  • the view and proj instance attributes can be changed on the fly without extra cost.
  • a size change will trigger reallocation of the buffers
Source code in madcad/rendering/d3/view.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def render(self, size:uvec2=None, view:fmat4=None, proj:fmat4=None) -> GLView3D:
	'''
		render the scene and retreive the result in the `color`, `depth` and `ident` attributes

		- the `view` and `proj` instance attributes can be changed on the fly without extra cost.
		- a `size` change will trigger reallocation of the buffers
	'''
	if size:
		size = glsize(size)
		self.gl.render(size, view, proj)
		self._reallocate(size)
	# retreive everything from the GPU to the CPU
	self.gl.screen.read_into(self.color, components=self.color.shape[2])
	# switch vertical axis to convert from opengl image convention to usual (and Qt) image convention
	self.color[:] = self.color[::-1]
	if self.enable_ident:
		self.gl.ident.read_into(self.ident, attachment=0)
		self.ident[:] = self.ident[::-1]
	if self.enable_depth:
		self.gl.screen.read_into(self.depth, attachment=-1)
		self.depth[:] = self.depth[::-1]
	return self

View projections and navigation

Turntable(position=0, distance=1, yaw=0, pitch=0)

Navigation rotating on yaw and pitch around a center

Object used as View.navigation

Source code in madcad/rendering/d3/view.py
753
754
755
756
757
def __init__(self, position:fvec3=0, distance:float=1, yaw:float=0, pitch:float=0):
	self.position = fvec3(position)
	self.yaw = yaw
	self.pitch = pitch
	self.distance = distance

position = fvec3(position) instance-attribute

distance = distance instance-attribute

pitch = pitch instance-attribute

yaw = yaw instance-attribute

matrix()

Source code in madcad/rendering/d3/view.py
759
760
761
762
763
764
def matrix(self) -> fmat4:
	# build rotation from view euler angles
	rot = inverse(fquat(fvec3(pi/2-self.pitch, 0, -self.yaw)))
	mat = translate(fmat4(rot), -self.position)
	mat[3][2] -= self.distance
	return mat

adjust(distance)

Source code in madcad/rendering/d3/view.py
766
767
def adjust(self, distance: float):
	self.distance = distance

look(position)

Source code in madcad/rendering/d3/view.py
769
770
771
772
773
def look(self, position: fvec3):
	dir = position - fvec3(affineInverse(self.matrix())[3])
	self.distance = length(dir)
	self.position = position
	self.sight(dir)

sight(direction)

Source code in madcad/rendering/d3/view.py
775
776
777
778
779
def sight(self, direction: fvec3):
	if length2(direction) <= 1e-6:
		return
	self.yaw = atan2(direction.x, direction.y)
	self.pitch = -atan2(direction.z, length(direction.xy))

center(position)

Source code in madcad/rendering/d3/view.py
781
782
def center(self, position: fvec3):
	self.position = position

zoom(ratio)

Source code in madcad/rendering/d3/view.py
784
785
def zoom(self, ratio: float):
	self.distance *= ratio

pan(offset)

Source code in madcad/rendering/d3/view.py
787
788
789
def pan(self, offset: fvec2):
	mat = transpose(fmat3(inverse(fquat(fvec3(pi/2-self.pitch, 0, -self.yaw)))))
	self.position += ( mat[0] * -offset.x + mat[1] * offset.y) * self.distance/2

rotate(offset)

Source code in madcad/rendering/d3/view.py
791
792
793
794
795
796
def rotate(self, offset: fvec3):
	if abs(self.pitch) > 3.2:	offset.x = -offset.x
	self.yaw += offset.x*pi
	self.pitch += offset.y*pi
	if self.pitch > pi:	self.pitch -= 2*pi
	if self.pitch < -pi: self.pitch += 2*pi

Orbit(position=0, distance=1, orientation=fquat())

Navigation rotating on the 3 axis around a center.

Object used as View.navigation

Source code in madcad/rendering/d3/view.py
704
705
706
707
def __init__(self, position:fvec3=0, distance:float=1, orientation=fquat()):
	self.position = fvec3(position)
	self.distance = float(distance)
	self.orientation = fquat(orientation)

position = fvec3(position) instance-attribute

distance = float(distance) instance-attribute

orientation = fquat(orientation) instance-attribute

matrix()

Source code in madcad/rendering/d3/view.py
709
710
711
712
def matrix(self) -> fmat4:
	mat = translate(fmat4(self.orientation), -self.position)
	mat[3][2] -= self.distance
	return mat

adjust(distance)

Source code in madcad/rendering/d3/view.py
714
715
def adjust(self, distance: float):
	self.distance = distance

look(position)

Source code in madcad/rendering/d3/view.py
717
718
719
720
721
def look(self, position: fvec3):
	dir = position - fvec3(affineInverse(self.matrix())[3])
	self.distance = length(dir)
	self.position = position
	self.sight(dir)

sight(direction)

Source code in madcad/rendering/d3/view.py
723
724
725
726
727
def sight(self, direction: fvec3):
	if length2(direction, direction) <= 1e-6:
		return
	focal = self.orientation * fvec3(0,0,1)
	self.orientation = quat(direction, focal) * self.orientation

center(position)

Source code in madcad/rendering/d3/view.py
729
730
def center(self, position: fvec3):
	self.position = position

zoom(ratio)

Source code in madcad/rendering/d3/view.py
732
733
def zoom(self, ratio: float):
	self.distance *= ratio

pan(offset)

Source code in madcad/rendering/d3/view.py
735
736
737
def pan(self, offset: vec2):
	x,y,z = transpose(fmat3(self.orientation))
	self.position += (fvec3(x) * -offset.x + fvec3(y) * offset.y) * self.distance/2

rotate(offset)

Source code in madcad/rendering/d3/view.py
739
740
741
def rotate(self, offset: vec3):
	# rotate from view euler angles
	self.orientation = inverse(fquat(fvec3(-offset.y, -offset.x, offset.z) * pi)) * self.orientation

Perspective(fov=None)

Object used as View.projection

Attributes:

Name Type Description
fov float

field of view (rad), defaulting to settings.display['field_of_view']

Source code in madcad/rendering/d3/view.py
806
807
def __init__(self, fov=None):
	self.fov = fov or settings.display['field_of_view']

fov = fov or settings.display['field_of_view'] instance-attribute

matrix(ratio, distance)

Source code in madcad/rendering/d3/view.py
809
810
def matrix(self, ratio, distance) -> fmat4:
	return perspective(self.fov/min(1, ratio), ratio, distance*1e-2, distance*1e4)

adjust(size)

Source code in madcad/rendering/d3/view.py
812
813
def adjust(self, size):
	return size / tan(self.fov/2)

Orthographic(size=None)

Object used as View.projection

Attributes:

Name Type Description
size float

factor between the distance from camera to navigation center and the zone size to display defaulting to tan(settings.display['field_of_view']/2)

Source code in madcad/rendering/d3/view.py
824
825
def __init__(self, size=None):
	self.size = size or tan(settings.display['field_of_view']/2)

size = size or tan(settings.display['field_of_view'] / 2) instance-attribute

matrix(ratio, distance)

Source code in madcad/rendering/d3/view.py
827
828
829
830
831
832
833
def matrix(self, ratio, distance) -> fmat4:
	rf = 1e3 # relative far
	rn = 1e-2 # relative near
	return fmat4(min(1, 1/ratio)/(distance*self.size), 0, 0, 0,
				0,       min(1, ratio)/(distance*self.size), 0, 0,
				0,       0,          -2/(distance*(rf-rn)), 0,
				0,       0,          -(rf+rn)/(rf-rn), 1)

adjust(size)

Source code in madcad/rendering/d3/view.py
835
836
def adjust(self, size):
	return size / self.size

Helpers to trick into the pipeline

Group(scene, src=None, world=1)

Bases: Display

display holding multiple displays associated with a key, just like a dictionnary

world, selected, hovered properties are propagated to its children

Source code in madcad/rendering/base.py
405
406
407
408
409
410
411
412
def __init__(self, scene:Scene, src:dict|list=None, world=1):
	self.displays = {}
	self._pending = None
	self._world = world
	self._selected = False
	self._hovered = False
	if src:
		self.update(None, src)

box property

stack(scene)

Source code in madcad/rendering/base.py
484
485
486
487
488
489
def stack(self, scene):
	self.prepare(scene)
	for key, display in self.displays.items():
		display.world = self.world
		display.key = (*self.key, key)
		yield from display.stack(scene)

__getitem__(key)

Source code in madcad/rendering/base.py
494
495
def __getitem__(self, key):
	return self.displays[key]

update(*args)

    update(scene, src)
    update(src)

    update all child displays with the displayable present in the given dictionnary

    former children that do not match keys in the given dictionnary are dropped
Note

the new content will not be immediately available in self.displays because they will only be buffered at next rendering. it makes this function thread safe and able to run without a reference to the scene

Source code in madcad/rendering/base.py
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
def update(self, *args):
	''' 
		update(scene, src)
		update(src)

		update all child displays with the displayable present in the given dictionnary

		former children that do not match keys in the given dictionnary are dropped

	Note:
		the new content will not be immediately available in `self.displays` because they will only be buffered at next rendering. it makes this function thread safe and able to run without a reference to the scene
	'''
	if len(args) == 1:
		src, = args
	elif len(args) == 2:
		scene, src = args
	else:
		raise TypeError("expected 1 or 2 arguments, got {}".format(len(args)))
	if isinstance(src, list):
		src = dict(enumerate(src))
	if not isinstance(src, dict):
		return False
	self._pending = src
	return True

world()

Source code in madcad/rendering/base.py
420
421
422
423
@writeproperty
def world(self):
	for display in self.displays.values():
		display.world = self.world

Step(display, target, priority, render) dataclass

describes a rendering step for a display

display instance-attribute

the display instance responsible of this callback

target instance-attribute

the name of the render target in the view that will be rendered (see View)

priority instance-attribute

a float that is used to insert the callable at the proper place in the rendering stack

render instance-attribute

a function that renders, signature is func(view)

Displayable(build, *args, **kwargs)

Simple displayable initializing the given Display class with arguments

At the display creation time, it will simply execute build(*args, **kwargs)

Source code in madcad/rendering/base.py
513
514
515
def __init__(self, build, *args, **kwargs):
	self.args, self.kwargs = args, kwargs
	self.build = build

__slots__ = ('build', 'args', 'kwargs') class-attribute instance-attribute

build = build instance-attribute

__eq__(other)

Source code in madcad/rendering/base.py
516
517
518
519
520
521
def __eq__(self, other):
	return self is other or isinstance(other, Displayable) and (
		self.build == other.build
		and self.args == other.args
		and self.kwargs == other.kwargs
		)

__repr__()

Source code in madcad/rendering/base.py
522
523
524
525
526
527
def __repr__(self):
	return '{}({}, {})'.format(
		type(self).__name__, 
		','.join(repr(arg) for arg in self.args), 
		','.join(key+'='+repr(arg)  for key, arg in self.kwargs.items())
		)

display(scene)

Source code in madcad/rendering/base.py
528
529
def display(self, scene):
	return self.build(scene, *self.args, **self.kwargs)