Skip to content
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

Spherical volume rendering #64

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f4e9c3b
Adding temp PR for spherical rendering
sochowski Dec 3, 2021
ab78cab
Merged grid_position and spherical_grid_position to switch vertex sha…
sochowski Dec 8, 2021
af7663d
Temporary PR for spherical rendering 2
sochowski Dec 20, 2021
ba3ed48
Update to do transformations in geometry shader
matthewturk Dec 20, 2021
ca6dbfb
add constant color shader
matthewturk Dec 20, 2021
d840d3f
Organized spherical ray-tracing
sochowski Jan 21, 2022
83d793f
Merge remote-tracking branch 'upstream/main' into spherical_refactoring
chrishavlin Aug 2, 2022
4cd83b3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 2, 2022
726aa82
spherical example to its own, resetn cartesian
chrishavlin Aug 2, 2022
3cf8186
experimenting...
chrishavlin Dec 1, 2022
c9a5bff
working? now only converting to cartesian for GLpos, etc.
chrishavlin Dec 1, 2022
065eb6d
add a shell fragment to the example, skipped linting
chrishavlin Dec 1, 2022
ddb69fd
swap the angles in the transformation
chrishavlin Dec 2, 2022
0501c6f
simplify the example
chrishavlin Dec 2, 2022
2cb14a8
cleanup
chrishavlin Dec 2, 2022
ad96eb3
Merge remote-tracking branch 'upstream/main' into spherical_vol_rende…
chrishavlin Dec 2, 2022
fb80cea
bad merge: add back the constant color shader
chrishavlin Dec 2, 2022
65a6abf
more cleaning
chrishavlin Dec 2, 2022
f43d577
move the spherical uniforms to child
chrishavlin Dec 6, 2022
d9a9771
add a bbox command line arg for example
chrishavlin Dec 16, 2022
1ffffb5
Merge remote-tracking branch 'upstream/main' into spherical_vol_rende…
chrishavlin Dec 21, 2022
0474454
in progress: spherical ray intersections
chrishavlin Jan 6, 2023
0031ee8
vectorize normal plane calc, only do it if spherical
chrishavlin Jan 9, 2023
4e4fdec
lingering changes, they look OK
chrishavlin Apr 12, 2023
dbd7240
Merge remote-tracking branch 'upstream/main' into spherical_vol_rende…
chrishavlin Jun 6, 2023
afcef53
add a simple test
chrishavlin Jun 6, 2023
4f73cdd
only rescale for cartesian
chrishavlin Jun 8, 2023
ac9cd00
move shader constants to include
chrishavlin Jun 8, 2023
3cef5b3
Merge remote-tracking branch 'upstream/main' into spherical_vol_rende…
chrishavlin Jun 8, 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# temp files
*.swp
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
50 changes: 50 additions & 0 deletions examples/amr_spherical_volume_rendering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import sys

import numpy as np
import yt

import yt_idv

# yt reminder: phi is the polar angle (0 to 2pi)
# theta is the angle from north (0 to pi)


# coord ordering here will be r, phi, theta

bbox_options = {
"partial": np.array([[0.5, 1.0], [0.0, np.pi / 4], [np.pi / 4, np.pi / 2]]),
"whole": np.array([[0.1, 1.0], [0.0, 2 * np.pi], [0, np.pi]]),
"north_hemi": np.array([[0.1, 1.0], [0.0, 2 * np.pi], [0, 0.5 * np.pi]]),
"south_hemi": np.array([[0.1, 1.0], [0.0, 2 * np.pi], [0.5 * np.pi, np.pi]]),
"ew_hemi": np.array([[0.1, 1.0], [0.0, np.pi], [0.0, np.pi]]),
}


sz = (256, 256, 256)
fake_data = {"density": np.random.random(sz)}

if __name__ == "__main__":
if len(sys.argv) > 1:
bbox_type = sys.argv[1]
else:
bbox_type = "partial"

bbox = bbox_options[bbox_type]

ds = yt.load_uniform_grid(
fake_data,
sz,
bbox=bbox,
nprocs=4096,
geometry=("spherical", ("r", "phi", "theta")),
length_unit="m",
)

rc = yt_idv.render_context(height=800, width=800, gui=True)
dd = ds.all_data()
dd.max_level = 1
sg = rc.add_scene(ds, ("index", "r"), no_ghost=True)
# sg = rc.add_scene(ds, ("index", "theta"), no_ghost=True)
# sg = rc.add_scene(ds, ("index", "phi"), no_ghost=True)
sg.camera.focus = [0.0, 0.0, 0.0]
rc.run()
33 changes: 33 additions & 0 deletions examples/spherical_subset_volume_rendering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import numpy as np
import yt

import yt_idv

# Spherical Test (to line 20)

NDIM = 32

bbox = np.array(
[[0.0, 0.5], [np.pi / 8, 2 * np.pi / 8], [2 * np.pi / 8, 3 * np.pi / 8]]
)

fake_data = {"density": np.random.random((NDIM, NDIM, NDIM))}
ds = yt.load_uniform_grid(
fake_data,
[NDIM, NDIM, NDIM],
bbox=bbox,
geometry="spherical",
)

rc = yt_idv.render_context(height=800, width=800, gui=True)
dd = ds.all_data()
dd.max_level = 1
sg = rc.add_scene(ds, ("index", "r"), no_ghost=True)
sg.camera.focus = [0.0, 0.0, 0.0]
rc.run()

# Cartesian Test (to line 25)
# ds = yt.load_sample("IsolatedGalaxy")
# rc = yt_idv.render_context(height=800, width=800, gui=True)
# sg = rc.add_scene(ds, "density", no_ghost=True)
# rc.run()
44 changes: 44 additions & 0 deletions tests/test_spherical_vol_rendering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import os

import numpy as np
import pytest
import yt

import yt_idv


@pytest.fixture()
def osmesa_fake_spherical():
"""Return an OSMesa context that has a "fake" AMR dataset added, with "radius"
as the field.
"""

sz = (10, 10, 10)
fake_data = {"density": np.random.random(sz)}

bbox = np.array([[0.1, 1.0], [0.0, 2 * np.pi], [0, np.pi]])

ds = yt.load_uniform_grid(
fake_data,
sz,
bbox=bbox,
nprocs=1,
geometry=("spherical", ("r", "phi", "theta")),
length_unit="m",
)
dd = ds.all_data()
rc = yt_idv.render_context("osmesa", width=1024, height=1024)
rc.add_scene(dd, "density", no_ghost=True)
yield rc
rc.osmesa.OSMesaDestroyContext(rc.context)


def test_spherical(osmesa_fake_spherical, tmp_path):
# just checking that it runs here
image = osmesa_fake_spherical.run()

outdir = tmp_path / "snapshots"
outdir.mkdir()
fn = str(outdir / "testout.png")
_ = yt.write_bitmap(image, fn)
assert os.path.exists(fn)
2 changes: 1 addition & 1 deletion yt_idv/scene_components/base_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class SceneComponent(traitlets.HasTraits):
# These attributes are
cmap_min = traitlets.CFloat(None, allow_none=True)
cmap_max = traitlets.CFloat(None, allow_none=True)
cmap_log = traitlets.Bool(True)
cmap_log = traitlets.Bool(False)
scale = traitlets.CFloat(1.0)

@traitlets.observe("display_bounds")
Expand Down
9 changes: 9 additions & 0 deletions yt_idv/scene_components/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,15 @@ def draw(self, scene, program):
GL.glDrawArrays(GL.GL_POINTS, tex_ind * each, each)

def _set_uniforms(self, scene, shader_program):
shader_program._set_uniform(
"is_spherical", self.data.data_source.ds.geometry == "spherical"
)
if self.data.data_source.ds.geometry == "spherical":
axis_id = self.data.data_source.ds.coordinates.axis_id
shader_program._set_uniform("id_theta", axis_id["theta"])
shader_program._set_uniform("id_r", axis_id["r"])
shader_program._set_uniform("id_phi", axis_id["phi"])

shader_program._set_uniform("box_width", self.box_width)
shader_program._set_uniform("sample_factor", self.sample_factor)
shader_program._set_uniform("ds_tex", np.array([0, 0, 0, 0, 0, 0]))
Expand Down
37 changes: 37 additions & 0 deletions yt_idv/scene_data/_geometry_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import numpy as np


def phi_normal_planes(edge_coordinates, axis_id, cast_type: str = None):
# for spherical geometries, calculates the cartesian normals and constants
# defining the planes normal to a fixed-phi value. The phi normal plane for
# a given spherical coordinate (r, theta, phi) will contain the given
# coordinate and the z-axis.
#
# edge_coordinates: 3D array of shape (N, 3) containing the spherical
# coordinates for which we want the phi-normal.
# axis_id: dictionary that maps the spherical coordinate axis names to the
# index number.

phi = edge_coordinates[:, axis_id["phi"]]
theta = edge_coordinates[:, axis_id["theta"]]
r = edge_coordinates[:, axis_id["r"]]

# get the cartesian values of the coordinates
z = r * np.cos(theta)
r_xy = r * np.sin(theta)
x = r_xy * np.cos(phi)
y = r_xy * np.sin(phi)
xyz = np.column_stack([x, y, z])

# construct the planes
z_hat = np.array([0, 0, 1])
# cross product is vectorized, result is shape (N, 3):
normal_vec = np.cross(xyz, z_hat)
# dot product is not vectorized, do it manually via an elemntwise multiplication
# then summation. result will have shape (N,)
d = (normal_vec * xyz).sum(axis=1) # manual dot product

normals_d = np.column_stack([normal_vec, d])
if cast_type is not None:
normals_d = normals_d.astype(cast_type)
return normals_d
49 changes: 42 additions & 7 deletions yt_idv/scene_data/block_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from yt.data_objects.data_containers import YTDataContainer

from yt_idv.opengl_support import Texture3D, VertexArray, VertexAttribute
from yt_idv.scene_data._geometry_utils import phi_normal_planes
from yt_idv.scene_data.base_data import SceneData


Expand Down Expand Up @@ -41,6 +42,7 @@ def add_data(self, field, no_ghost=False):
# Every time we change our data source, we wipe all existing ones.
# We now set up our vertices into our current data source.
vert, dx, le, re = [], [], [], []

self.min_val = +np.inf
self.max_val = -np.inf
if self.scale:
Expand Down Expand Up @@ -77,27 +79,60 @@ def add_data(self, field, no_ghost=False):
LE = np.array([b.LeftEdge for i, b in self.blocks.values()]).min(axis=0)
RE = np.array([b.RightEdge for i, b in self.blocks.values()]).max(axis=0)
self.diagonal = np.sqrt(((RE - LE) ** 2).sum())

# Now we set up our buffer
vert = np.array(vert, dtype="f4")
units = self.data_source.ds.units
ratio = (units.code_length / units.unitary).base_value
dx = np.array(dx, dtype="f4") * ratio
le = np.array(le, dtype="f4") * ratio
re = np.array(re, dtype="f4") * ratio
dx = np.array(dx, dtype="f4")
le = np.array(le, dtype="f4")
re = np.array(re, dtype="f4")
if self._yt_geom_str == "cartesian":
units = self.data_source.ds.units
ratio = (units.code_length / units.unitary).base_value
dx = dx * ratio
le = le * ratio
re = re * ratio
Comment on lines +88 to +93
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthewturk

So I had to put the new scaling from #79 in this if statement....

I think we should still scale in the spherical case -- but I think we should just apply this ratio to the r coordinate. That sound right? I know we do want to scale theta and phi from 0 to 1 for the texture maps... but we don't want to scale theta and phi when calculating positions in model space.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is right. Just scale r.

self._set_geometry_attributes(le, re)
self.vertex_array.attributes.append(
VertexAttribute(name="model_vertex", data=vert)
)
self.vertex_array.attributes.append(VertexAttribute(name="in_dx", data=dx))
self.vertex_array.attributes.append(
VertexAttribute(name="in_left_edge", data=le)
VertexAttribute(name="in_left_edge", data=le.astype("f4"))
)
self.vertex_array.attributes.append(
VertexAttribute(name="in_right_edge", data=re)
VertexAttribute(name="in_right_edge", data=re.astype("f4"))
)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it occurs to me that we should have the cartesian volume rendering as the basic option, and we can/should have spherical as a subclass. Let's not try cramming it all in; @neutrinoceros 's work on having things be visible in index-space coordinates is encouraging me to think of it in both ways.

That's not really specific here though, except that what we may want to do is to have this be a supplemental function that gets called if the component accessing the data is viewing it in spherical coordinates.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya! I agree totally! I just jammed it in here to get things in initially working. Unjamming it and subclassing it relates to your vectorization question I think. If I vectorize that function, it'll be easier to have a sublcass that takes the output from the standard cartesian version to calculate the new spherical-only attributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

latest push no longer breaks things for cartesian coords (it's now vectorized and only runs if spherical).

I still want to keep thinking on how to better capture the different geometries though. I initially thought it may make sense to have a SphericalBlockCollection, but implementing it wasn't super straightforward because the calculations rely on the left and right edges, which currently only get saved via the vertex_array attribute after casting to "f4". So a standard construction like:

class SphericalBlockCollection(BlockCollection):
        def add_data(self, field, no_ghost=False):
            super().add_data(field, no_ghost=no_ghost)
            
            << --- calculate phi-normal planes from left, right edges --- >>

would require us also saving the full le and re arrays as attributes. Since they're only needed for an intermediate step, it seemed better to just add a geometry check to BlockCollection.add_data(). But i'm open to ideas here!

# Now we set up our textures
self._load_textures()

@property
def _yt_geom_str(self):
# note: casting to string for compatibility with new and old geometry
# attributes (now an enum member in latest yt),
# see https://github.com/yt-project/yt/pull/4244
return str(self.data_source.ds.geometry)

def _set_geometry_attributes(self, le, re):
# set any vertex_array attributes that depend on the yt geometry type

if self._yt_geom_str == "cartesian":
return
elif self._yt_geom_str == "spherical":
axis_id = self.data_source.ds.coordinates.axis_id
phi_plane_le = phi_normal_planes(le, axis_id, cast_type="f4")
phi_plane_re = phi_normal_planes(re, axis_id, cast_type="f4")
self.vertex_array.attributes.append(
VertexAttribute(name="phi_plane_le", data=phi_plane_le)
)
self.vertex_array.attributes.append(
VertexAttribute(name="phi_plane_re", data=phi_plane_re)
)
else:
raise NotImplementedError(
f"{self.name} does not implement {self._yt_geom_str} geometries."
)

def viewpoint_iter(self, camera):
for block in self.data_source.tiles.traverse(viewpoint=camera.position):
vbo_i, _ = self.blocks[id(block)]
Expand Down
49 changes: 38 additions & 11 deletions yt_idv/shaders/grid_expand.geom.glsl
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
layout ( points ) in;
layout ( triangle_strip, max_vertices = 14 ) out;

// note: all in/out variables below are always in native coordinates (e.g.,
// spherical or cartesian) except when noted.
flat in vec3 vdx[];
flat in vec3 vleft_edge[];
flat in vec3 vright_edge[];
flat in vec4 vphi_plane_le[];
flat in vec4 vphi_plane_re[];

flat out vec3 dx;
flat out vec3 left_edge;
flat out vec3 right_edge;
flat out mat4 inverse_proj;
flat out mat4 inverse_mvm;
flat out mat4 inverse_pmvm;
flat out mat4 inverse_proj; // always cartesian
flat out mat4 inverse_mvm; // always cartesian
flat out mat4 inverse_pmvm; // always cartesian
out vec4 v_model;

flat in mat4 vinverse_proj[];
flat in mat4 vinverse_mvm[];
flat in mat4 vinverse_pmvm[];
flat in mat4 vinverse_proj[]; // always cartesian
flat in mat4 vinverse_mvm[]; // always cartesian
flat in mat4 vinverse_pmvm[]; // always cartesian
flat in vec4 vv_model[];

flat out ivec3 texture_offset;

flat out vec4 phi_plane_le;
flat out vec4 phi_plane_re;

// https://stackoverflow.com/questions/28375338/cube-using-single-gl-triangle-strip
// suggests that the triangle strip we want for the cube is

Expand All @@ -36,26 +43,46 @@ uniform vec3 arrangement[8] = vec3[](

uniform int aindex[14] = int[](6, 7, 4, 5, 1, 7, 3, 6, 2, 4, 0, 1, 2, 3);

vec3 transform_vec3(vec3 v) {
if (is_spherical) {
// in yt, phi is the polar angle from (0, 2pi), theta is the azimuthal
// angle (0, pi). the id_ values below are uniforms that depend on the
// yt dataset coordinate ordering
return vec3(
v[id_r] * sin(v[id_theta]) * cos(v[id_phi]),
v[id_r] * sin(v[id_theta]) * sin(v[id_phi]),
v[id_r] * cos(v[id_theta])
);
} else {
return v;
}
}

void main() {

vec4 center = gl_in[0].gl_Position;
vec4 center = gl_in[0].gl_Position; // always cartesian

vec3 width = vright_edge[0] - vleft_edge[0];

vec4 newPos;
vec3 current_pos;

for (int i = 0; i < 14; i++) {
newPos = vec4(vleft_edge[0] + width * arrangement[aindex[i]], 1.0);
gl_Position = projection * modelview * newPos;
// walks through each vertex of the triangle strip, emit it. need to
// emit gl_Position in cartesian, but pass native coords out in v_model
current_pos = vec3(vleft_edge[0] + width * arrangement[aindex[i]]);
newPos = vec4(transform_vec3(current_pos), 1.0); // cartesian
gl_Position = projection * modelview * newPos; // cartesian
left_edge = vleft_edge[0];
right_edge = vright_edge[0];
inverse_pmvm = vinverse_pmvm[0];
inverse_proj = vinverse_proj[0];
inverse_mvm = vinverse_mvm[0];
phi_plane_le = vphi_plane_le[0];
phi_plane_re = vphi_plane_re[0];
dx = vdx[0];
v_model = newPos;
v_model = vec4(current_pos, 1.0);
texture_offset = ivec3(0);
EmitVertex();
}

}
Loading