diff --git a/examples/amr_volume_rendering_with_planes.py b/examples/amr_volume_rendering_with_planes.py new file mode 100644 index 00000000..3b1cb914 --- /dev/null +++ b/examples/amr_volume_rendering_with_planes.py @@ -0,0 +1,71 @@ +# example showing how to add a static cutting plane to an amr rendering +import numpy as np +import yt +from unyt import unyt_quantity + +import yt_idv +from yt_idv.scene_components.plane import Plane # NOQA +from yt_idv.scene_data.plane import PlaneData # NOQA + +ds = yt.load_sample("IsolatedGalaxy") + +# add the volume rendering to the scene +rc = yt_idv.render_context(height=800, width=800, gui=True) +sg = rc.add_scene(ds, "density", no_ghost=True) + +field = ("index", "z") + +# add some planes (these are independent objects, not linked to the volume rendering) + +# slice +slc = ds.slice(0, 0.5) +slc_data = PlaneData(data_source=slc) +slc_data.add_data(field, 1.0, (400, 400)) +slc_render = Plane(data=slc_data, cmap_log=True) +rc.scene.data_objects.append(slc_data) +rc.scene.components.append(slc_render) + +# a second slice, off-center +slc = ds.slice(1, 0.5) +slc_data = PlaneData(data_source=slc) +slc_data.add_data( + field, + unyt_quantity(400, "kpc"), + (400, 400), + center=np.array([0.65, slc.coord, 0.5]), +) +slc_render = Plane(data=slc_data, cmap_log=True) +rc.scene.data_objects.append(slc_data) +rc.scene.components.append(slc_render) + +# another slice, lower width +slc = ds.slice(2, 0.5) +slc_data = PlaneData(data_source=slc) +slc_data.add_data( + field, + unyt_quantity(400, "kpc"), + (400, 400), +) +slc_render = Plane(data=slc_data, cmap_log=True) +rc.scene.data_objects.append(slc_data) +rc.scene.components.append(slc_render) + +# cutting plane +normal = np.array([1.0, 1.0, 0.0], dtype="float64") +center = ds.domain_center.to("code_length").value +cut = ds.cutting(normal, center) +cut_data = PlaneData(data_source=cut) +cut_data.add_data(field, 1.0, (400, 400)) +cut_render = Plane(data=cut_data, cmap_log=True) +rc.scene.data_objects.append(cut_data) +rc.scene.components.append(cut_render) + +# projection +proj = ds.proj(field, 0) +proj_data = PlaneData(data_source=proj) +proj_data.add_data(field, 1.0, (400, 400)) +proj_render = Plane(data=proj_data, cmap_log=True) +rc.scene.data_objects.append(proj_data) +rc.scene.components.append(proj_render) + +rc.run() diff --git a/examples/plane_textures_arbitrary.py b/examples/plane_textures_arbitrary.py new file mode 100644 index 00000000..30914b76 --- /dev/null +++ b/examples/plane_textures_arbitrary.py @@ -0,0 +1,37 @@ +# plots arbitrary data on a plane + +import numpy as np + +from yt_idv import render_context +from yt_idv.cameras.trackball_camera import TrackballCamera +from yt_idv.scene_components.plane import Plane +from yt_idv.scene_data.plane import BasePlane +from yt_idv.scene_graph import SceneGraph + +# create an empty scene with a camera +rc = render_context(height=800, width=800, gui=True) +c = TrackballCamera(position=[3.5, 3.5, 3.5], focus=[0.0, 0.0, 0.0]) +rc.scene = SceneGraph(camera=c) + +# create some arbitrary 2d data +x, y = np.meshgrid(np.linspace(0, 1, 200), np.linspace(0, 1, 300)) +dist = np.sqrt((x - 0.5) ** 2 + (y - 0.5) ** 2) +test_data = np.exp(-((dist / 0.25) ** 2)) + +# create a plane for our 2d data, add the data +image_plane = BasePlane( + normal=np.array([1.0, 0.0, 0.0]), + center=np.array([0.0, 0.0, 0.0]), + data=test_data, + width=1, + height=1, +) +image_plane.north_vec = np.array([0.0, 0.0, 1.0]) +image_plane.add_data() + +# add the rendering object, data to the scene +plane_render = Plane(data=image_plane, cmap_log=False) +rc.scene.data_objects.append(image_plane) +rc.scene.components.append(plane_render) + +rc.run() diff --git a/examples/plane_textures_arbitrary_image.py b/examples/plane_textures_arbitrary_image.py new file mode 100644 index 00000000..697ac409 --- /dev/null +++ b/examples/plane_textures_arbitrary_image.py @@ -0,0 +1,70 @@ +import numpy as np +import requests +import yt +from PIL import Image + +import yt_idv +from yt_idv.scene_annotations.grid_outlines import GridOutlines # NOQA +from yt_idv.scene_components.plane import Plane +from yt_idv.scene_data.grid_positions import GridPositions # NOQA +from yt_idv.scene_data.plane import BasePlane + +# create a volume rendering +ds = yt.load("IsolatedGalaxy/galaxy0030/galaxy0030") +rc = yt_idv.render_context(height=800, width=800, gui=True) +sg = rc.add_scene(ds, "density", no_ghost=True) + +# pull in an image file, convert to greyscale and ignore resulting alpha channel +im_url = "https://raw.githubusercontent.com/yt-project/website/master/img/yt_logo.png" +im = np.array(Image.open(requests.get(im_url, stream=True).raw).convert("LA"))[:, :, 0] + + +# create a plane for our 2d data, add the data +image_plane = BasePlane( + normal=np.array([1.0, 1.0, 1.0]), + center=np.array([0.5, 0.5, 0.5]), + data=im, + width=0.5, + height=0.5, +) +# image_plane.east_vec = np.array([0.0, 1.0, 0.0]) +# image_plane.north_vec = np.array([0.0, 0.0, 1.0]) +image_plane.add_data() + +# add the rendering object, data to the scene +plane_render = Plane(data=image_plane, cmap_log=False) +rc.scene.data_objects.append(image_plane) +rc.scene.components.append(plane_render) + +# for norm in [ +# np.array([1.0, 0.0, 0.0]), +# np.array([0.0, 1.0, 0.0]), +# np.array([0.0, 0.0, 1.0]) +# ]: +# +# # create a plane for our 2d data, add the data +# image_plane = BasePlane( +# normal=norm, +# center=np.array([.5, .5, .5]), +# data=im, +# width=.5, +# height=.5, +# ) +# # image_plane.east_vec = np.array([0.0, 1.0, 0.0]) +# # image_plane.north_vec = np.array([0.0, 0.0, 1.0]) +# image_plane.add_data() +# +# # add the rendering object, data to the scene +# plane_render = Plane(data=image_plane, cmap_log=False) +# rc.scene.data_objects.append(image_plane) +# rc.scene.components.append(plane_render) + +# add grids +grids = ds.index.grids.tolist() +gp = GridPositions(grid_list=grids) +rc.scene.data_objects.append(gp) +go = GridOutlines(data=gp) +rc.scene.components.append(go) + + +rc.run() diff --git a/examples/plane_textures_cutting_plane.py b/examples/plane_textures_cutting_plane.py new file mode 100644 index 00000000..9285faf7 --- /dev/null +++ b/examples/plane_textures_cutting_plane.py @@ -0,0 +1,35 @@ +import numpy as np +import yt + +from yt_idv import render_context +from yt_idv.cameras.trackball_camera import TrackballCamera +from yt_idv.scene_annotations.grid_outlines import GridOutlines # NOQA +from yt_idv.scene_components.plane import Plane # NOQA +from yt_idv.scene_data.grid_positions import GridPositions # NOQA +from yt_idv.scene_data.plane import PlaneData # NOQA +from yt_idv.scene_graph import SceneGraph + +rc = render_context(height=800, width=800, gui=True) +c = TrackballCamera(position=[3.5, 3.5, 3.5], focus=[0.0, 0.0, 0.0]) +rc.scene = SceneGraph(camera=c) + +ds = yt.load("Enzo_64/RD0005/RedshiftOutput0005") + +normal = np.array([1.0, 1.0, 0.0], dtype="float64") +center = ds.domain_center.to("code_length").value +cut = ds.cutting(normal, center) +cut_data = PlaneData(data_source=cut) +cut_data.add_data(("enzo", "Density"), 1.0, (400, 400)) +cut_render = Plane(data=cut_data, cmap_log=True) + +rc.scene.data_objects.append(cut_data) +rc.scene.components.append(cut_render) + +# add grids +grids = ds.index.grids.tolist() +gp = GridPositions(grid_list=grids) +rc.scene.data_objects.append(gp) +go = GridOutlines(data=gp) +rc.scene.components.append(go) + +rc.run() diff --git a/examples/plane_textures_multiple.py b/examples/plane_textures_multiple.py new file mode 100644 index 00000000..48d22548 --- /dev/null +++ b/examples/plane_textures_multiple.py @@ -0,0 +1,58 @@ +import numpy as np +import yt + +from yt_idv import render_context +from yt_idv.cameras.trackball_camera import TrackballCamera +from yt_idv.scene_components.plane import Plane # NOQA +from yt_idv.scene_data.plane import PlaneData # NOQA +from yt_idv.scene_graph import SceneGraph + +rc = render_context(height=800, width=800, gui=True) +c = TrackballCamera(position=[3.5, 3.5, 3.5], focus=[0.0, 0.0, 0.0]) +rc.scene = SceneGraph(camera=c) + +ds = yt.load("Enzo_64/RD0005/RedshiftOutput0005") + +# add a projection +proj = ds.proj(("enzo", "Density"), 1.0) +proj_data = PlaneData(data_source=proj) +proj_data.add_data(("enzo", "Density"), 1.0, (400, 400)) +proj_render = Plane(data=proj_data, cmap_log=True) +rc.scene.data_objects.append(proj_data) +rc.scene.components.append(proj_render) + +# add another projection +proj_2 = ds.proj(("enzo", "Density"), 0.0) +proj_data_2 = PlaneData(data_source=proj_2) +proj_data_2.add_data(("enzo", "Density"), 1.0, (400, 400)) +proj_render_2 = Plane(data=proj_data_2, cmap_log=True) +rc.scene.data_objects.append(proj_data_2) +rc.scene.components.append(proj_render_2) + +# add another projection, shifted to other axis +proj_3 = ds.proj(("enzo", "Density"), 1.0) +proj_data_3 = PlaneData(data_source=proj_3) +proj_data_3.add_data(("enzo", "Density"), 1.0, (400, 400), translate=-1.0) +proj_render_3 = Plane(data=proj_data_3, cmap_log=True) +rc.scene.data_objects.append(proj_data_3) +rc.scene.components.append(proj_render_3) + +# add an axis-normal slice +slc = ds.slice(0, 0.5) +slice_data = PlaneData(data_source=slc) +slice_data.add_data(("enzo", "Density"), 1.0, (400, 400)) +slice_render = Plane(data=slice_data, cmap_log=True) +rc.scene.data_objects.append(slice_data) +rc.scene.components.append(slice_render) + +# add a cutting plane +normal = np.array([1.0, 1.0, 0.0], dtype="float64") +center = ds.domain_center.to("code_length").value +cut = ds.cutting(normal, center) +cut_data = PlaneData(data_source=cut) +cut_data.add_data(("enzo", "Density"), 1.0, (400, 400)) +cut_render = Plane(data=cut_data, cmap_log=True) +rc.scene.data_objects.append(cut_data) +rc.scene.components.append(cut_render) + +rc.run() diff --git a/examples/plane_textures_projection.py b/examples/plane_textures_projection.py new file mode 100644 index 00000000..0d5a009b --- /dev/null +++ b/examples/plane_textures_projection.py @@ -0,0 +1,33 @@ +import yt + +from yt_idv import render_context +from yt_idv.cameras.trackball_camera import TrackballCamera +from yt_idv.scene_annotations.grid_outlines import GridOutlines # NOQA +from yt_idv.scene_components.plane import Plane # NOQA +from yt_idv.scene_data.grid_positions import GridPositions # NOQA +from yt_idv.scene_data.plane import PlaneData # NOQA +from yt_idv.scene_graph import SceneGraph + +rc = render_context(height=800, width=800, gui=True) +c = TrackballCamera(position=[3.5, 3.5, 3.5], focus=[0.0, 0.0, 0.0]) +rc.scene = SceneGraph(camera=c) + +ds = yt.load("Enzo_64/RD0005/RedshiftOutput0005") + +for proj_axis in [0.0, 1.0, 2.0]: + proj = ds.proj(("enzo", "Density"), proj_axis) + proj_data = PlaneData(data_source=proj) + proj_data.add_data(("enzo", "Density"), 1.0, (400, 400)) + proj_render = Plane(data=proj_data, cmap_log=True) + + rc.scene.data_objects.append(proj_data) + rc.scene.components.append(proj_render) + +# add grids +grids = ds.index.grids.tolist() +gp = GridPositions(grid_list=grids) +rc.scene.data_objects.append(gp) +go = GridOutlines(data=gp) +rc.scene.components.append(go) + +rc.run() diff --git a/examples/plane_textures_slice.py b/examples/plane_textures_slice.py new file mode 100644 index 00000000..2d6d6a25 --- /dev/null +++ b/examples/plane_textures_slice.py @@ -0,0 +1,62 @@ +import numpy as np +import yt +from unyt import unyt_quantity + +from yt_idv import render_context +from yt_idv.cameras.trackball_camera import TrackballCamera +from yt_idv.scene_annotations.grid_outlines import GridOutlines # NOQA +from yt_idv.scene_components.plane import Plane # NOQA +from yt_idv.scene_data.grid_positions import GridPositions # NOQA +from yt_idv.scene_data.plane import PlaneData # NOQA +from yt_idv.scene_graph import SceneGraph + +rc = render_context(height=800, width=800, gui=True) +c = TrackballCamera(position=[3.5, 3.5, 3.5], focus=[0.0, 0.0, 0.0]) +rc.scene = SceneGraph(camera=c) + +ds = yt.load("Enzo_64/RD0005/RedshiftOutput0005") + +for slice_axis in [0, 1]: + slc = ds.slice(slice_axis, 0.5) + slice_data = PlaneData(data_source=slc) + slice_data.add_data(("enzo", "Density"), 1.0, (400, 400)) + slice_render = Plane(data=slice_data, cmap_log=True) + + rc.scene.data_objects.append(slice_data) + rc.scene.components.append(slice_render) + +# another slice covering a smaller distance +slc = ds.slice(2, 0.5) +slice_data = PlaneData(data_source=slc) +slice_data.add_data( + ("enzo", "Density"), + unyt_quantity(45, "Mpc"), + (400, 400), + height=unyt_quantity(45, "Mpc"), +) +slice_render = Plane(data=slice_data, cmap_log=True) +rc.scene.data_objects.append(slice_data) +rc.scene.components.append(slice_render) + +# another small slice, at a different center +slc = ds.slice(2, 0.25) +slice_data = PlaneData(data_source=slc) +slice_data.add_data( + ("enzo", "Density"), + unyt_quantity(45, "Mpc"), + (400, 400), + height=unyt_quantity(45, "Mpc"), + center=np.array([0.75, 0.75, 0.25]), +) +slice_render = Plane(data=slice_data, cmap_log=True) +rc.scene.data_objects.append(slice_data) +rc.scene.components.append(slice_render) + +# add grids +grids = ds.index.grids.tolist() +gp = GridPositions(grid_list=grids) +rc.scene.data_objects.append(gp) +go = GridOutlines(data=gp) +rc.scene.components.append(go) + +rc.run() diff --git a/yt_idv/scene_components/base_component.py b/yt_idv/scene_components/base_component.py index b2599cd9..a9e2142c 100644 --- a/yt_idv/scene_components/base_component.py +++ b/yt_idv/scene_components/base_component.py @@ -339,3 +339,6 @@ def _reset_cmap_bounds(self): else: print(f"Computed new cmap values {self.cmap_min} - {self.cmap_max}") self._cmap_bounds_invalid = False + + +_cmaps = ["arbre", "viridis", "magma", "doom"] diff --git a/yt_idv/scene_components/plane.py b/yt_idv/scene_components/plane.py new file mode 100644 index 00000000..ee516dd6 --- /dev/null +++ b/yt_idv/scene_components/plane.py @@ -0,0 +1,46 @@ +import traitlets +from OpenGL import GL + +from yt_idv.scene_components.base_component import SceneComponent, _cmaps +from yt_idv.scene_data.plane import BasePlane + + +def take_log_checkbox(imgui, scene_obj): + # imgui UI element for setting the log boolean + changed, scene_obj.cmap_log = imgui.checkbox("Take log", scene_obj.cmap_log) + return changed + + +def colormap_list(imgui, scene_obj): + # imgui UI element for colormap list and selection + changed, cmap_index = imgui.listbox( + "Colormap", _cmaps.index(scene_obj.colormap.colormap_name), _cmaps + ) + if changed: + scene_obj.colormap.colormap_name = _cmaps[cmap_index] + return changed + + +class Plane(SceneComponent): + name = "image_plane" + data = traitlets.Instance(BasePlane) + + def draw(self, scene, program): + with self.data.texture_object.bind(0): + GL.glDisable(GL.GL_CULL_FACE) # default, two-sided rendering + GL.glDrawElements(GL.GL_TRIANGLES, self.data.size, GL.GL_UNSIGNED_INT, None) + GL.glEnable(GL.GL_CULL_FACE) # back to what it was + + def _set_uniforms(self, scene, shader_program): + cam = scene.camera + shader_program._set_uniform("projection", cam.projection_matrix) + shader_program._set_uniform("modelview", cam.view_matrix) + shader_program._set_uniform("to_worldview", self.data.to_worldview) + + def render_gui(self, imgui, renderer, scene): + changed, self.visible = imgui.checkbox("Visible", self.visible) + _ = take_log_checkbox(imgui, self) + changed = changed or _ + _ = colormap_list(imgui, self) + changed = changed or _ + return changed diff --git a/yt_idv/scene_data/plane.py b/yt_idv/scene_data/plane.py new file mode 100644 index 00000000..10009447 --- /dev/null +++ b/yt_idv/scene_data/plane.py @@ -0,0 +1,293 @@ +import numpy as np +import traitlets +from OpenGL import GL +from unyt import unyt_quantity +from unyt.exceptions import UnitParseError +from yt.data_objects.construction_data_containers import YTProj +from yt.data_objects.data_containers import YTDataContainer +from yt.data_objects.selection_objects.slices import YTCuttingPlane, YTSlice +from yt.utilities.orientation import Orientation + +from yt_idv.opengl_support import Texture2D, VertexArray, VertexAttribute +from yt_idv.scene_data.base_data import SceneData + + +class BasePlane(SceneData): + """ + base class for a rendering a plane in 3d space using a 2d texture. + + While BasePlane can be used to plot arbitrary image data (see + plane_textures_arbitrary.py and plane_textures_arbitrary_images.py in examples/), + the child class PlaneData is easier to use if plotting yt slice, cutting or + projection objects. + + Parameters + ---------- + normal: ndarray with shape (3,) + normal vector to the plane in 3d cartesian image coordinates + center: ndarray with shape (3,) + the center point within the plane in 3d cartesian image coordinates + width: float + the width of our 2d plane (the euclidean distance covered along the + "east" dimension by our plane) + height: float + the height of our 2d plane (the euclidean distance covered along the + "north" axis by our plane) + data: 2D ndarray + the data to sample on the plane. + + Some important attributes + ------------------------- + + north_vec: ndarray with shape (3,) + the north vector lying within the plane in 3d cartesian image coordinates + + Both of these must be set manually if the normal vector is not aligned with + the usual cartesian axes. + + """ + + name = "image_plane_data" + + # calculated or sterilized: + plane_normal = None + east_vec = None + north_vec = None + + # shader-related objects + texture_object = traitlets.Instance(Texture2D) + texture_id = traitlets.CInt() + size = traitlets.CInt(-1) + + # required arguments + normal = traitlets.Instance(np.ndarray, allow_none=False) + center = traitlets.Instance(np.ndarray, allow_none=False) + data = traitlets.Instance(np.ndarray, allow_none=False) + width = traitlets.Float(allow_none=False) + height = traitlets.Float(allow_none=False) + + def _set_transformation(self): + """ + here we build the transformation matrix that when applied to the in-plane + coordinates returns the world coordinates. + """ + # set the in-plane coordinate vectors, basis_u = east, basis_v = north + orientation = Orientation(self.normal, north_vector=self.north_vec) + unit_vecs = orientation.unit_vectors + unit_east = unit_vecs[0, :] + unit_north = unit_vecs[1, :] + unit_normal = unit_vecs[2, :] + + # total transformation + # M * [U, V, 0., 1], where M = T * W * S + # S = scale matrix, to scale distance from UV texture coord to in-plane coord + # W = projection matrix to go from in-plane coords to cartesian coords + # centered at the world origin + # T = translation matrix to go from world origin to plane center + + # homogenous scaling matrix from UV texture coords to in-plane coords + S = np.eye(4) + S[0, 0] = self.width + S[1, 1] = self.height + + # homogenous projection matrix from in-plane coords to world at origin + W = np.eye(4) + W[0:3, 0] = unit_east + W[0:3, 1] = unit_north + W[0:3, 2] = unit_normal + + to_world = np.matmul(W, S) + + # homogenous translation matrix + T = np.eye(4) + # align the center of the UV coords with the true center + current_center = np.matmul(to_world, np.array([0.5, 0.5, 0.0, 1.0]).T) + T[0:3, 3] = self.center - current_center[0:3] + + # combined homogenous projection matrix + self.to_worldview = np.matmul(T, to_world) + self.to_worldview = self.to_worldview.astype("f4") + + def add_data(self): + + self._set_transformation() + # our in-plane coordinates. same as texture coordinates + verts = np.array([[1, 0], [0, 0], [0, 1], [1, 1]]) + + i = np.array([[0, 1, 2], [0, 2, 3]]) + i.shape = (i.size, 1) + + self.vertex_array.attributes.append( + VertexAttribute(name="model_vertex", data=verts.astype("f4")) + ) + + self.vertex_array.indices = i.astype("uint32") + self.size = i.size + self.build_textures() + + @traitlets.default("vertex_array") + def _default_vertex_array(self): + return VertexArray(name="simple_slice", each=0) + + def build_textures(self): + tex_id = GL.glGenTextures(1) + bitmap = self.data.astype("f4") + texture = Texture2D( + texture_name=tex_id, data=bitmap, boundary_x="clamp", boundary_y="clamp" + ) + self.texture_id = tex_id + self.texture_object = texture + + +class PlaneData(BasePlane): + """ + a 2D plane built from a yt slice, cutting plane or projection. + + Parameters + ---------- + data_source: YTDataContainer + Must be a ds.slice, ds.cutting or ds.proj, where ds is a yt dataset. + + To add a projection to an existing volume rendering scene: + + >>> import yt + >>> import yt_idv + >>> from yt_idv.scene_components.planes import Plane + >>> from yt_idv.scene_data.plane import PlaneData + + >>> ds = yt.load_sample("IsolatedGalaxy") + + >>> # add the volume rendering to the scene + >>> rc = yt_idv.render_context(height=800, width=800, gui=True) + >>> sg = rc.add_scene(ds, "density", no_ghost=True) + + >>> # calculate and add a projection through the volume to our 3D rendering + >>> proj = ds.proj("density", 0) + >>> proj_data = PlaneData(data_source=proj) + >>> proj_data.add_data("density", 1.0, (400, 400)) + >>> proj_render = Plane(data=proj_data, cmap_log=True) + >>> rc.scene.data_objects.append(proj_data) + >>> rc.scene.components.append(proj_render) + + >>> rc.run() + """ + + data_source = traitlets.Instance(YTDataContainer) + _calculate_translation = True + + def _sanitize_length_var(self, var): + # pulls out the code_length value for var if it is a unyt quantity + if hasattr(var, "units"): + try: + var = var.to("code_length") + except UnitParseError: + var = unyt_quantity( + var.value, var.units, registry=self.data_source.ds.unit_registry + ) + var = var.to("code_length").d + return var + + def add_data( + self, + field, + width=1.0, + resolution=(400, 400), + height=None, + translate=0.0, + center=None, + ): + """ + generates a fixed resolution buffer from the data_source and adds it + as a plane in 3D space. + + Parameters + ---------- + field: tuple + a yt field tuple, ("field_type", "field") + translate: float in "code_length" or unyt quantity + moves the plane by this value in the direction normal to the plane. + useful for controlling where a projection through a 3D volume is + displayed. + + The remaining parameters all control the fixed resolution buffer (frb) + generated from the data_source. See yt's documentation on .to_frb() for + more details. + + width: float in "code_length" or unyt quantity + the width of the frb + resolution: (int, int) + the resolution of the frb + height: float in "code_length" or unyt quantity + the height of the frb + center: None, ndarray in "code_length", or unyt array + the center of the frb, must lie within the plane of the data_source + default None. + + """ + # get our image plane data + frb_kw_args = dict(resolution=resolution) + for kw, val in [("height", height), ("center", center)]: + if np.any(val): + frb_kw_args[kw] = val + + frb = self.data_source.to_frb(width, **frb_kw_args) + self.data = frb[field] + if np.any(center): + center = self._sanitize_length_var(center) + + def calc_center(axis_coord_val): + center = np.zeros((3,)) + for ax, dim in enumerate(["x", "y", "z"]): + if ax == self.data_source.axis: + center[ax] = axis_coord_val + else: + val = np.mean( + [ + frb.limits[dim][0].to("code_length").value, + frb.limits[dim][1].to("code_length").value, + ] + ) + val = val / self.data_source.ds.domain_width[ax] + center[ax] = val + return center + + # store the parameters defining the plane + dstype = type(self.data_source) + if dstype == YTSlice: + normal = np.zeros((3,)) + normal[self.data_source.axis] = 1.0 + if np.any(center) is None: + center = calc_center(self.data_source.coord) + elif dstype == YTCuttingPlane: + self.north_vec = self.data_source.orienter.north_vector + self.north_vec = self.north_vec / np.linalg.norm(self.north_vec) + normal = self.data_source.orienter.normal_vector + normal = normal / np.linalg.norm(normal) # make sure it's a unit normal + self.east_vec = np.cross(normal, self.north_vec) + if np.any(center) is None: + center = self.data_source.center.value + elif isinstance(self.data_source, YTProj): + normal = np.zeros((3,)) + normal[self.data_source.axis] = 1.0 + if np.any(center) is None: + center = calc_center(1.0) + else: + raise ValueError( + f"Unexpected data_source type. data_source must be one of" + f" YTSlice or YTproj but found {dstype}." + ) + + if translate != 0: + center += self._sanitize_length_var(translate) * normal + + self.center = center + self.normal = normal + if height is None: + height = width + + # need to set the width and height relative to the screen coordinates + for dimstr, dim in [("width", width), ("height", height)]: + dim_s = float(self._sanitize_length_var(dim)) + setattr(self, dimstr, dim_s) + + super().add_data() diff --git a/yt_idv/shaders/plane.frag.glsl b/yt_idv/shaders/plane.frag.glsl new file mode 100644 index 00000000..7f47c67d --- /dev/null +++ b/yt_idv/shaders/plane.frag.glsl @@ -0,0 +1,10 @@ +out vec4 output_color; +in vec2 UV; // our in-plane coordinates + +void main() +{ + float val = texture(fb_tex, UV).r; + gl_FragDepth = 0.0; + if(val == 0) discard; + output_color = vec4(val); +} diff --git a/yt_idv/shaders/plane.vert.glsl b/yt_idv/shaders/plane.vert.glsl new file mode 100644 index 00000000..92e55cb1 --- /dev/null +++ b/yt_idv/shaders/plane.vert.glsl @@ -0,0 +1,15 @@ +in vec2 model_vertex; // position is in-plane coordinates +out vec2 UV; +uniform mat4 to_worldview; // homogenous projection matrix from in-plane to world + +void main() +{ + // first get the vertex position in world coordinates from the in-plane coords + vec4 world_vertex = to_worldview * vec4(model_vertex, 0., 1.0); + + // calculate and return the final screen view position + gl_Position = projection * modelview * world_vertex; + + // our in-plane coordinates ARE the texture coordinates, just pass those out + UV = model_vertex; +} diff --git a/yt_idv/shaders/shaderlist.yaml b/yt_idv/shaders/shaderlist.yaml index 52f52eb7..b69cfebf 100644 --- a/yt_idv/shaders/shaderlist.yaml +++ b/yt_idv/shaders/shaderlist.yaml @@ -62,6 +62,14 @@ shader_definitions: - one - zero blend_equation: func add + plane: + info: A fragment shader for a 2D plane in plane-coordinates. + source: plane.frag.glsl + depth_test: less + blend_func: + - src alpha + - dst alpha + blend_equation: func add noop: info: A second pass fragment shader that performs no operation. Usually used if the first pass already took care of applying proper color to the data @@ -142,6 +150,9 @@ shader_definitions: info: A vertex shader used for unstructured mesh rendering. source: mesh.vert.glsl depth_test: less + plane: + info: A vertex shader for a 2D plane in plane-coordinates. + source: plane.vert.glsl passthrough: info: A second pass vertex shader that performs no operations on vertices source: passthrough.vert.glsl @@ -275,6 +286,14 @@ component_shaders: first_fragment: text_overlay second_vertex: passthrough second_fragment: passthrough + image_plane: + default_value: default + default: + description: Default + first_vertex: plane + first_fragment: plane + second_vertex: passthrough + second_fragment: apply_colormap mesh_rendering: default_value: default default: