Blender On Acid
Tags: blender, python
Published: 2020-02-28 06:52:23

BlenderOnAcid release v4 is coming soon, and will include the full source code of MiniMario for educational purposes. In this post i will cover how most of this demo works, you will learn about: TPython, JavaScript, ODE, bpy, and inter-process communication.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/loehhmbyofxuueixll19.png)

Blender has its own builtin text editor, and the .blend file format can hold text objects, which can be referenced by other objects. Above is all of the demo source code, along with the generated 3D data.  For learning how something works, it helps to have it all in a single .blend file.

Personally I do not like writing code in Blender's text editor, and instead prefer a fully data-driven style, where I generate the single .blend file from another Python script, this generator script sets up the scene, creates the text objects, and links them all together.

```python
import bpy, os, sys, math

thispath = os.path.split(__file__)[0]

world = bpy.data.worlds[0]
world['threejs_canvas'] = None
script = bpy.data.texts.new(name='minimario_main')
world.threejs_script = script

src = open(os.path.join(thispath,'minimario/minimario_escapes_main.py'),'rb').read().decode('utf-8')
script.from_string( src )

js = bpy.data.texts.new(name='minimario_escapes_js')
src = open(os.path.join(thispath,'minimario/minimario_escapes_js.py'),'rb').read().decode('utf-8')
js.from_string( src )
script.include0 = js

shared = bpy.data.texts.new(name='minimario_shared')
src = open(os.path.join(thispath,'minimario/minimario_shared.py'),'rb').read().decode('utf-8')
shared.from_string( src )
script.include1 = shared

draw = bpy.data.texts.new(name='minimario_draw')
src = open(os.path.join(thispath,'minimario/minimario_draw.py'),'rb').read().decode('utf-8')
draw.from_string( src )
script.include2 = draw


blender_start = bpy.data.texts.new(name='minimario_startup')
world.blender_startup_script = blender_start
blender_start.include0 = shared

src = open(os.path.join(thispath,'minimario/minimario_startup.py'),'rb').read().decode('utf-8')
blender_start.from_string( src )

onupdate = '''
import math

M = bpy.data.objects['mario_collision']
MR = bpy.data.objects['marioroot']

if M.location[0] + 0.1 < M['px']:
	if MR.rotation_euler.z < math.radians( 180 ):
		MR.rotation_euler.z += 0.5
else:
	MR.rotation_euler.z *= 0.75

M['px'] = M.location[0]

if M.location.x > 9 and M.location.z > 8 and M.location.z < 13:
	bname = 'brick-%s' % int(M.location.x)
	if bname not in bpy.data.objects:
		bname = 'brick-%s' % int(M.location.x+0.5)
	if bname not in bpy.data.objects:
		bname = 'brick-%s' % int(M.location.x-0.5)
	if bname in bpy.data.objects:
		brick = bpy.data.objects[ bname ]
		if 'broken' not in brick.keys():
			print('breaking new brick')
			brick['broken'] = True
			bpy.ops.ode.cell_fracture(object_name=bname)
'''

blender_onupdate = bpy.data.texts.new(name='minimario_onupdate')
world.blender_script = blender_onupdate
blender_onupdate.from_string(onupdate)


```
Above you can see that basically this script just reads other python scripts from a sub-folder and creates the text objects.  Some code is inline, the onupdate script, this gets called every frame in Blender.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/yofrh1jswvx60lgwo0ju.gif)

Above Mario starts out in the Emscripten|SDL window, which he jumps out of into the Emscripten debugger, changing from SDL drawn pixels into CSS ASCII Art, when he leaves the web browser window and enters Blender he transforms into 3D ASCII Art. He then breaks some blocks using ODE physics and my customized cell fracture script. Finally he returns to the browser window and knocks the SDL window from below and jumps back into the SDL window.

The CSS Mario is simply drawn underneath the SDL window, his position is synchronized from TPython with this function call.
```python
set_ascii_mario( B.getPosition()[0]-60, -float(B.getPosition()[1]+320), state["direction"] )
```
The variable `B` is the ODE Body used to simulate his physics. The definition of `set_ascii_mario` is below.
```python
def set_ascii_mario(x,y, direction):
	bump *= 0.75
	canvas.style.top = (75 - bump) + 'px'
	if direction == 1:
		marioR.hidden = true
		mariodiv.hidden = false
		mariodiv.style.left = (x + canvas.offsetLeft) + 'px'
		mariodiv.style.top  = y + 'px'
	else:
		mariodiv.hidden = true
		marioR.hidden = false
		marioR.style.left = (x + canvas.offsetLeft) + 'px'
		marioR.style.top  = y + 'px'
```
Above you were probably expecting to see JavaScript instead of Python.  While pure JavaScript is allowed to be mixed with your python script running in the TPython WASM compiled interpreter, it is preferred that you write your JS code in a Python-style, and let the TPython toolchain transpile it to JS, this is because then the JS function will be automatically exposed to your TPython script for easy calling.

At this point you might be wondering how is the position of Mario synchronized with Blender. This is done using the standard XMLHttpRequest.
```python
def sendmario():
	var msg = [{"name":"mario_collision", "pos":[X,-Z,Y]}]
	var req = new XMLHttpRequest()
	req.addEventListener("load", sendmario)
	req.open("GET", "http://localhost:8080/update?" + JSON.stringify(msg))
	req.send()
setTimeout(sendmario, 100)
```
Above hijacks the builtin server of BlenderOnAcid normally used to synchronize ThreeJS objects, but in this case we are not using ThreeJS. Instead we have created a `mario_collision` object ahead of time in the blender startup script, which is the parent of the 3D ASCII Mario. Below is the blender startup script.
```python
import bpy, math, _bpy

ob = bpy.data.objects['Cube']
ob.name = 'mario_collision'
ob.display_type = "WIRE"
ob.ode_size[0] = 2
ob.ode_size[1] = 2
ob.ode_size[2] = 2
ob.ode_kinematic = True
_bpy.ode_add_object( ob )
ob['px'] = 0.0

root = bpy.data.objects.new(name='marioroot', object_data=None)
root.scale *= 0.12
bpy.context.collection.objects.link(root)
root.parent = ob


cam = bpy.data.objects['Camera']
cam.location = [10, -30, 5]
cam.rotation_euler = [math.radians(90), 0, 0]


def make_mario( asciiart, collection, root ):
	z = 16
	for ln in asciiart:
		x = -16
		for c in ln:
			x += 2
			if c == '_':
				continue
			pix = bpy.data.objects.new(name='pixel', object_data=bpy.data.meshes['Cube'].copy())
			if c not in bpy.data.materials:
				mat = bpy.data.materials.new(name=c)
				mat.use_nodes = False
				if c == '?':
					r,g,b = MarioPal['%']
				else:
					r,g,b = MarioPal[c]
				mat.diffuse_color = [r/255,g/255,b/255,1.0]
			else:
				mat = bpy.data.materials[c]
			pix.data.materials[0]=mat
			collection.objects.link(pix)
			pix.location = [x,0,z]
			pix.scale.y = 2
			pix.modifiers.new(name='bevel', type="BEVEL")
			pix.parent=root

		z -= 2


col = bpy.data.collections.new(name='mariopixels')
bpy.context.collection.children.link(col)
make_mario( Mario, col, root )

def make_bricks():
	mat = bpy.data.materials.new(name='brick')
	mat.use_nodes = False
	mat.diffuse_color = [0.57, 0.15, 0.14, 1.0]
	x = 10
	z = 10
	for i in range(4):
		brick = bpy.data.objects.new(name='brick-%s' %int(x), object_data=bpy.data.meshes['Cube'].copy())
		bpy.context.collection.objects.link(brick)
		brick.location = [x, 0, z]
		brick.data.materials[0] = mat
		brick.ode_size[0] = 2
		brick.ode_size[1] = 2
		brick.ode_size[2] = 2
		brick.ode_kinematic = True
		_bpy.ode_add_object( brick )
		x += 2

make_bricks()
```
Above you can see I setup a secondary ODE physics simulation in Blender.  The primary ODE physics simulation is actually running in the web browser sending position updates for `mario_collision` to this physics simulation here in Blender. Note that in Blender i am making these bodies `ode_kinematic` which means they only have collision, and respond to position updates from the UI or the builtin web server.

Finally, lets review the code which triggers the bricks to break, this happens in the main blender onupdate script.
```python
M = bpy.data.objects['mario_collision']

if M.location.x > 9 and M.location.z > 8 and M.location.z < 13:
	bname = 'brick-%s' % int(M.location.x)
	if bname not in bpy.data.objects:
		bname = 'brick-%s' % int(M.location.x+0.5)
	if bname not in bpy.data.objects:
		bname = 'brick-%s' % int(M.location.x-0.5)
	if bname in bpy.data.objects:
		brick = bpy.data.objects[ bname ]
		if 'broken' not in brick.keys():
			print('breaking new brick')
			brick['broken'] = True
			bpy.ops.ode.cell_fracture(object_name=bname)

```
Why am I bypassing the ODE collision detection callback system, and writing ugly code like above to trigger the cell fracture? Because its simple, and to show you a simple trick for collision detection, where you round positions to integers and lookup an object by that rounded index. This cheat works well if your game-board objects are all about the same size of one.