Skip to content

Web exporting #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 41 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
3055166
scenery: `export_js` for exporting model as inlinable JS
obiwac Sep 22, 2023
14b7855
bfm: Return `Scenery` object when calling `add_scenery`
obiwac Sep 22, 2023
0449adb
web: Start work on templates
obiwac Nov 18, 2023
3b152c1
vers: Bump version number to 0.2.0
obiwac Nov 18, 2023
1bab014
examples/deformation: Make caching better
obiwac Nov 18, 2023
e852a42
sim: Start proper work on web exporting
obiwac Nov 18, 2023
8332c0e
shaders: Move shader version over to `300 es`
obiwac Nov 18, 2023
abbfc35
gitignore: Ignore `index.html` output from web exporting
obiwac Nov 18, 2023
26f391a
web: Inject shader code into template
obiwac Nov 18, 2023
1d0be86
web: Create/use scenery shader
obiwac Nov 18, 2023
00eaa7f
sim: Automatically wrap with `onload` window event listener to remove…
obiwac Nov 18, 2023
4249d08
scenery: Put both coordinates and normals into the same VBO
obiwac Nov 18, 2023
70e7491
bfm: Move web exporting from sim
obiwac Nov 18, 2023
f77153f
scenery: Factor out `vbo_data` cached property
obiwac Nov 18, 2023
3d1aff0
scenery: Export JS as a string
obiwac Nov 18, 2023
556b5d3
bfm: Include scenery models in web export
obiwac Nov 18, 2023
ce01850
web: Load and render scenery models
obiwac Nov 18, 2023
8742a11
web: MVP matrix so we can actually see something
obiwac Nov 18, 2023
2204966
readme: Typo
obiwac Nov 18, 2023
769b3c6
bfm: Allow BFM to be started headless
obiwac Nov 19, 2023
a84e026
web: Camera parameters and animations
obiwac Nov 19, 2023
a95f815
web: Camera rotation
obiwac Nov 19, 2023
f8bbd42
web: Recoil control
obiwac Nov 19, 2023
49f1828
web: Rudimentary panning
obiwac Nov 19, 2023
1c42c62
gitignore: Don't ignore `web/index.html`
obiwac Nov 19, 2023
27145eb
web: Change how shaders are injected
obiwac Nov 19, 2023
684ed08
web: `Model` -> `Scenery` (s.t. I can create `Instance`)
obiwac Nov 19, 2023
89e9134
web: Type annotations on some attributes on `Scenery`
obiwac Nov 19, 2023
0c8fd3e
web: Remove useless buffer copies
obiwac Nov 19, 2023
c1712ff
instance: Implement `export_js`
obiwac Nov 19, 2023
bff6359
bfm: Write out instances
obiwac Nov 19, 2023
d8a24d2
bfm: Fix output path when web exporting
obiwac Nov 19, 2023
0f6b6c1
web: `Instance` class
obiwac Nov 19, 2023
39d827e
web: `mvp` function on `Shader`
obiwac Nov 19, 2023
68be0ee
web: Render instances
obiwac Nov 19, 2023
14b91c9
web: Global `cur_shader` variable
obiwac Nov 19, 2023
50a61c0
web: Fix delta time
obiwac Nov 19, 2023
74e228b
web: Animate effect amount
obiwac Nov 19, 2023
127291a
web: Normalize JSDoc syntax
obiwac Nov 19, 2023
894a4ec
web: Respect aspect ratio of canvas
obiwac Nov 19, 2023
71808d3
web: Compress lists
obiwac Nov 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ build
__pycache__
libbfm.c
*.core
/index.html
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Then, you may install the `bfm` Python module itself:
pip install --user ./pybfm
```

Ubuntu 22.04, set the `DEB_PYTHON_INSTALL_LAYOUT` environment variable to `deb_system` when running the above command:
On Ubuntu 22.04, set the `DEB_PYTHON_INSTALL_LAYOUT` environment variable to `deb_system` when running the above command:

```console
DEB_PYTHON_INSTALL_LAYOUT=deb_system pip install --user ./pybfm
Expand Down
16 changes: 9 additions & 7 deletions examples/deformation.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import faulthandler
faulthandler.enable()

import math

from bfm import Bfm, Condition, Force_linear, Instance, Mesh_lepl1110, Mesh_wavefront, Material, Obj, Rule_gauss_legendre, Sim
from bfm import Bfm, Condition, Force_linear, Instance, Mesh_wavefront, Material, Obj, Rule_gauss_legendre, Sim

# create initial BFM context
# TODO should this be renamed something a little clearer, e.g. Scene?

print("Starting BFM")

bfm = Bfm()
bfm = Bfm(headless=True)

bfm.set_default_recoil(1.7000000000000006)
bfm.set_default_rotation([-0.42500000000000016, -0.4450000000000002])
Expand Down Expand Up @@ -57,11 +55,14 @@ def cache_cross():
def read_cross():
global cross

with open("meshes/cross.py", "r") as f:
with open("meshes/cross.py") as f:
cross = eval(f.read())

# cache_cross()
read_cross()
try:
read_cross()

except:
cache_cross()

def is_boundary(mesh, coord):
x, y = coord
Expand Down Expand Up @@ -124,3 +125,4 @@ def is_boundary(mesh, coord):
# resulting effects from the simulation will automatically be applied to the instance we added to our scene previously

bfm.show(sim)
bfm.export()
2 changes: 1 addition & 1 deletion libbfm/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.9)
project(bfm VERSION 0.1.0 DESCRIPTION "Big F'ing Matrix")
project(bfm VERSION 0.2.0 DESCRIPTION "Big F'ing Matrix")

# library setup

Expand Down
139 changes: 121 additions & 18 deletions pybfm/bfm/bfm.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import html
import math

import pyglet
Expand All @@ -13,21 +14,28 @@
from .shader import Shader
from .sim import Sim

class State:
def __init__(self):
# scene

self.current_sim: Sim | None = None
self.scenery: list[Scenery] = []

class Window(pyglet.window.Window):
def __init__(self, **args):
self.state: State = args["state"]
del args["state"]

super().__init__(**args)
pyglet.clock.schedule_interval(self.update, 1.0 / 60)

# scene

self.current_sim: Sim = None
self.scenery: list[Scenery] = []
# scenery

self.scenery_shader = Shader("shaders/scenery.vert", "shaders/scenery.frag")

# orbit camera

self.default_recoil = 1
self.default_recoil = 1.
self.default_rotation = [0, 0]
self.default_origin = [0, 0, 0]

Expand Down Expand Up @@ -94,15 +102,15 @@ def on_draw(self):
self.scenery_shader.use()
self.scenery_shader.mvp_matrix(mvp_matrix)

for scenery in self.scenery:
for scenery in self.state.scenery:
scenery.draw()

# draw simulation

anim = math.sin(self.time) / 2 + .5

if self.current_sim is not None:
self.current_sim.draw(mvp_matrix, (0, 1, anim)[self.anim_state])
if self.state.current_sim is not None:
self.state.current_sim.draw(mvp_matrix, (0, 1, anim)[self.anim_state])

def on_resize(self, width, height):
print(f"Resize {width} * {height}")
Expand Down Expand Up @@ -154,32 +162,127 @@ def on_key_release(self, key, modifiers):
...

class Bfm:
def __init__(self):
try:
self.config = gl.Config(double_buffer = True, major_version = 3, minor_version = 3, depth_size = 16, sample_buffers = 1, samples = 4)
self.window = Window(config = self.config, width = 480, height = 480, caption = "BFM", resizable = True, vsync = False)
def __init__(self, headless=False):
self.headless = headless
self.state = State()

if not self.headless:
try:
self.config = gl.Config(double_buffer=True, major_version=3, minor_version=3, depth_size=16, sample_buffers=1, samples=4)
self.window = Window(config=self.config, width=480, height=480, caption="BFM", resizable=True, vsync=False, state=self.state)

except pyglet.window.NoSuchConfigException:
self.config = gl.Config(double_buffer = True, major_version = 3, minor_version = 3, depth_size = 16)
self.window = Window(config = self.config, width = 480, height = 480, caption = "BFM (no AA)", resizable = True, vsync = False)
except pyglet.window.NoSuchConfigException:
self.config = gl.Config(double_buffer=True, major_version=3, minor_version=3, depth_size=16)
self.window = Window(config=self.config, width=480, height=480, caption="BFM (no AA)", resizable=True, vsync=False, state=self.state)

def set_default_recoil(self, recoil: float):
if self.headless:
return

self.window.default_recoil = recoil
self.window.orbit_defaults()

def set_default_rotation(self, rotation: list[float]):
if self.headless:
return

*self.window.default_rotation, = rotation
self.window.orbit_defaults()

def set_default_origin(self, origin: list[float]):
if self.headless:
return

*self.window.default_origin, = origin
self.window.orbit_defaults()

def add_scenery(self, mesh: Mesh):
def add_scenery(self, mesh: Mesh) -> Scenery:
scenery = Scenery(mesh)
self.window.scenery.append(scenery)
self.state.scenery.append(scenery)

return scenery

def show(self, sim: Sim):
sim.show()
self.window.current_sim = sim
self.state.current_sim = sim

if self.headless:
return

pyglet.app.run()

# exporting

def export(self, out_path="index.html", title="BFM Web Export", width: int=1280, height: int=720):
def read(path):
with open(path) as f:
return f.read()

# read templates

src_html = read("web/index.html")
src_js = read("web/index.js")
src_matrix_js = read("web/matrix.js")

# generate scenery

scenery_loading_js = "\nconst scenery = ["

for scenery in self.state.scenery:
scenery_loading_js += f"new Scenery({scenery.export_js()}),"

scenery_loading_js += "]\n"

# generate instances

instance_loading_js = "\nconst instances = ["

if self.state.current_sim is not None:
for instance in self.state.current_sim.instances:
instance_loading_js += f"new Instance({instance.export_js()}),"

instance_loading_js += "]\n"

# generate JS source

src_js = f"""
{src_matrix_js}
window.addEventListener("load", () => {{
{src_js}
}})
"""

src_js = src_js.replace("$SCENERY_LOADING", scenery_loading_js)
src_js = src_js.replace("$INSTANCE_LOADING", instance_loading_js)

# generate HTML source

src_html = src_html.replace("$TITLE", html.escape(title))
src_html = src_html.replace("$JS_SRC", src_js)

src_html = src_html.replace("$WIDTH", str(width))
src_html = src_html.replace("$HEIGHT", str(height))

# add shaders

shaders = (
("scenery", "shaders/scenery"),
("deformation", "shaders/sim/deformation"),
("line_deformation", "shaders/sim/line_deformation"),
)

shaders_src = ""

for name, path in shaders:
src_vert = read(path + ".vert")
src_frag = read(path + ".frag")

shaders_src += f"<script id='{name}-vert' type='x-shader/x-vertex'>{src_vert}</script>"
shaders_src += f"<script id='{name}-frag' type='x-shader/x-fragment'>{src_frag}</script>"

src_html = src_html.replace("$SHADERS", shaders_src)

# write output

with open(out_path, "w") as f:
f.write(src_html)
12 changes: 10 additions & 2 deletions pybfm/bfm/instance.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from .condition import Condition
from .libbfm import lib, ffi
from .mesh import Mesh
from .obj import Obj
from .shader import Shader
from .state import default_state
from .util import jsify_list

import ctypes
import functools
Expand Down Expand Up @@ -95,7 +95,6 @@ def effects(self):

@functools.cached_property
def max_effect(self):
mesh = self.obj.mesh
max_effect = 0

for i in range(self.obj.mesh.c_mesh.n_nodes):
Expand Down Expand Up @@ -130,6 +129,15 @@ def draw(self, shader: Shader, lines = False):
gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.ibo)
gl.glDrawElements(gl.GL_TRIANGLES, len(self.obj.indices), gl.GL_UNSIGNED_INT, None)

def export_js(self) -> str:
return f"""{{
indices: new Uint32Array({jsify_list(self.obj.indices)}),
line_indices: new Uint32Array({jsify_list(self.obj.line_indices)}),
coords: new Float32Array({jsify_list(self.obj.coords)}),
effects: new Float32Array({self.effects}),
max_effect: {self.max_effect},
}}"""

class Instance(CInstance):
def __init__(self, obj: Obj):
c_instance = ffi.new("bfm_instance_t*")
Expand Down
49 changes: 30 additions & 19 deletions pybfm/bfm/scenery.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .mesh import Mesh
from .util import jsify_list

import ctypes
import functools
Expand Down Expand Up @@ -118,43 +119,47 @@ def normals(self):

return normals

@functools.cached_property
def vbo_data(self):
n = len(self.coords) + len(self.normals)
data = [0] * n

for i in range(0, len(self.coords), 3):
data[i * 2 + 0] = self.coords[i + 0]
data[i * 2 + 1] = self.coords[i + 1]
data[i * 2 + 2] = self.coords[i + 2]

data[i * 2 + 3] = self.normals[i + 0]
data[i * 2 + 4] = self.normals[i + 1]
data[i * 2 + 5] = self.normals[i + 2]

return data

def gen_buffers(self):
# create VAO

self.vao = gl.GLuint(0)
gl.glGenVertexArrays(1, ctypes.byref(self.vao))
gl.glBindVertexArray(self.vao)

# create coords VBO
# create VBO

self.vbo = gl.GLuint(0)
gl.glGenBuffers(1, ctypes.byref(self.vbo))
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vbo)

coords_t = gl.GLfloat * len(self.coords)
data_t = gl.GLfloat * len(self.vbo_data)
float_size = ctypes.sizeof(gl.GLfloat)

gl.glBufferData(gl.GL_ARRAY_BUFFER,
ctypes.sizeof(coords_t),
(coords_t) (*self.coords),
ctypes.sizeof(data_t),
(data_t) (*self.vbo_data),
gl.GL_STATIC_DRAW)

gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, 0)
gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, float_size * 6, 0)
gl.glEnableVertexAttribArray(0)

# create normals VBO

self.normals_vbo = gl.GLuint(0)
gl.glGenBuffers(1, ctypes.byref(self.normals_vbo))
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.normals_vbo)

normals_t = gl.GLfloat * len(self.normals)

gl.glBufferData(gl.GL_ARRAY_BUFFER,
ctypes.sizeof(normals_t),
(normals_t) (*self.normals),
gl.GL_STATIC_DRAW)

gl.glVertexAttribPointer(1, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, 0)
gl.glVertexAttribPointer(1, 3, gl.GL_FLOAT, gl.GL_FALSE, float_size * 6, float_size * 3)
gl.glEnableVertexAttribArray(1)

# create IBO
Expand All @@ -173,3 +178,9 @@ def gen_buffers(self):
def draw(self):
gl.glBindVertexArray(self.vao)
gl.glDrawElements(gl.GL_TRIANGLES, len(self.indices), gl.GL_UNSIGNED_INT, None)

def export_js(self) -> str:
return f"""{{
indices: new Uint32Array({jsify_list(self.indices)}),
vbo_data: new Float32Array({jsify_list(self.vbo_data)}),
}}"""
4 changes: 4 additions & 0 deletions pybfm/bfm/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from typing import List

def jsify_list(l: List):
return "[" + ",".join(map(lambda x: str(round(x, 3)), l)) + "]"
2 changes: 1 addition & 1 deletion pybfm/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "bfm"
version = "0.1.0"
version = "0.2.0"
description = "Python wrapper around BFM library for FEM/FEA"
authors = [
{name = "Alexandre Dewilde", email = "alexandredewilde.contact@gmail.com"},
Expand Down
4 changes: 3 additions & 1 deletion shaders/scenery.frag
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#version 330
#version 300 es

precision mediump float;

out vec4 out_colour;

Expand Down
Loading