diff --git a/examples/amr_volume_rendering_isocontours.py b/examples/amr_volume_rendering_isocontours.py new file mode 100644 index 0000000..5533e36 --- /dev/null +++ b/examples/amr_volume_rendering_isocontours.py @@ -0,0 +1,19 @@ +import yt + +import yt_idv +from yt_idv.scene_components.isolayers import Isolayers + +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) + +iso = Isolayers(data=sg.components[0].data) +sg.components.append(iso) + + +# default behavior will treat these values as base-10 exponents +sg.components[1].iso_layers[0] = -27.0 +sg.components[1].iso_tolerance[0] = 1.2 # tolerance in percent + +rc.run() diff --git a/yt_idv/scene_components/base_component.py b/yt_idv/scene_components/base_component.py index 82baa8d..648d351 100644 --- a/yt_idv/scene_components/base_component.py +++ b/yt_idv/scene_components/base_component.py @@ -1,4 +1,3 @@ -import numpy as np import traitlets from OpenGL import GL @@ -29,11 +28,6 @@ class SceneComponent(traitlets.HasTraits): priority = traitlets.CInt(0) visible = traitlets.Bool(True) use_db = traitlets.Bool(False) # use depth buffer - iso_tolerance = traitlets.CFloat(-1) # the tolerance for finding isocontours - iso_tol_is_pct = traitlets.Bool(False) # if True, the tolerance is a fraction - iso_log = traitlets.Bool(True) # if True, iso values are base 10 exponents - iso_layers = traitlets.List() # the target values for isocontours - iso_layers_alpha = traitlets.List() # the transparency of isocontours display_bounds = traitlets.Tuple( traitlets.CFloat(), traitlets.CFloat(), @@ -109,28 +103,8 @@ def render_gui(self, imgui, renderer, scene): _ = add_popup_help(imgui, "Click to reset the colorbounds of the current view.") changed = changed or _ - if self.render_method == "isocontours": - _ = self._render_isolayer_inputs(imgui) - changed = changed or _ - return changed - @traitlets.observe("iso_log") - def _switch_iso_log(self, change): - # if iso_log, then the user is setting 10**x, otherwise they are setting - # x directly. So when toggling this checkbox we convert the existing - # values between the two forms. - if change["old"]: - # if True, we were taking the log, but now are not: - self.iso_tolerance = 10**self.iso_tolerance - new_iso_layers = [10**iso_val for iso_val in self.iso_layers] - self.iso_layers = new_iso_layers - else: - # we were not taking the log but now we are, so convert to the exponent - self.iso_tolerance = np.log10(self.iso_tolerance) - new_iso_layers = [np.log10(iso_val) for iso_val in self.iso_layers] - self.iso_layers = new_iso_layers - @traitlets.default("display_name") def _default_display_name(self): return self.name @@ -149,14 +123,6 @@ def _change_render_method(self, change): self.colormap_vertex = new_combo["second_vertex"] self.colormap_fragment = new_combo["second_fragment"] - @traitlets.observe("render_method") - def _add_initial_isolayer(self, change): - # this adds an initial isocontour entry when the render method - # switches to isocontours and if there are no layers yet. - if change["new"] == "isocontours" and len(self.iso_layers) == 0: - self.iso_layers.append(0.0) - self.iso_layers_alpha.append(1.0) - @traitlets.default("fb") def _fb_default(self): return Framebuffer() @@ -253,18 +219,6 @@ def program2(self): self._program2_invalid = False return self._program2 - def _set_iso_uniforms(self, p): - # these could be handled better by watching traits. - p._set_uniform("iso_num_layers", int(len(self.iso_layers))) - isolayervals = self._get_sanitized_iso_layers() - p._set_uniform("iso_layers", isolayervals) - p._set_uniform("iso_layer_tol", self._get_sanitized_iso_tol()) - avals = np.zeros((32,), dtype="float32") - avals[: len(self.iso_layers)] = np.array(self.iso_layers_alpha) - p._set_uniform("iso_alphas", avals) - p._set_uniform("iso_min", float(self.data.min_val)) - p._set_uniform("iso_max", float(self.data.max_val)) - def run_program(self, scene): # Store this info, because we need to render into a framebuffer that is the # right size. @@ -276,8 +230,6 @@ def run_program(self, scene): with self.program1.enable() as p: scene.camera._set_uniforms(scene, p) self._set_uniforms(scene, p) - if self.render_method == "isocontours": - self._set_iso_uniforms(p) with self.data.vertex_array.bind(p): self.draw(scene, p) @@ -306,65 +258,6 @@ def run_program(self, scene): def draw(self, scene, program): raise NotImplementedError - def _get_sanitized_iso_layers(self, normalize=True): - # returns an array of the isocontour layer values, padded with 0s out - # to max number of contours (32). - iso_vals = np.asarray(self.iso_layers) - if self.iso_log: - iso_vals = 10**iso_vals - - if normalize: - iso_vals = self.data._normalize_by_min_max(iso_vals) - - full_array = np.zeros(32, dtype="float32") - full_array[: len(self.iso_layers)] = iso_vals - return full_array - - def _get_sanitized_iso_tol(self): - # isocontour selection conditions: - # - # absolute difference - # d - c <= eps - # or percent difference - # (d - c) / c * 100 <= eps_pct - # - # where d is a raw data value, c is the target isocontour, eps - # is an absolute difference, eps_f is a percent difference - # - # The data textures available on the shaders are normalized values: - # d_ = (d - min) / (max - min) - # where max and min are the global min and max values across the entire - # volume (e.g., over all blocks, not within a block) - # - # So in terms of normalized values, the absoulte difference condition - # becomes - # d_ - c_ <= eps / (max - min) - # where c_ is the target value normalized in the same way as d_. - # - # And the percent difference becomes - # (d_ - c_) * (max - min) / c * 100 <= eps_pct - # or - # d_ - c_ <= eps_pct / 100 * c / (max - min) - # so that the allowed tolerance is a function of the raw target value - # and so will vary with each layer. - - if self.iso_log: - # the tol value is an exponent, convert - tol = 10 ** float(self.iso_tolerance) - else: - tol = float(self.iso_tolerance) - # always normalize tolerance - tol = tol / self.data.val_range - - if self.iso_tol_is_pct: - # tolerance depends on the layer value - tol = tol * 0.01 - raw_layers = self._get_sanitized_iso_layers(normalize=False) - final_tol = raw_layers * tol - else: - final_tol = np.full((32,), tol, dtype="float32") - return final_tol - def _recompile_shader(self) -> bool: # removes existing shaders, invalidates shader programs shaders = ( @@ -381,77 +274,6 @@ def _recompile_shader(self) -> bool: self._program1_invalid = self._program2_invalid = True return True - def _render_isolayer_inputs(self, imgui) -> bool: - changed = False - if imgui.tree_node("Isocontours"): - _, self.iso_log = imgui.checkbox("set exponent", self.iso_log) - _ = add_popup_help( - imgui, "If checked, will treat isocontour values as base-10 exponents." - ) - changed = changed or _ - - imgui.columns(2, "iso_tol_cols", False) - - _, self.iso_tolerance = imgui.input_float( - "tol", - self.iso_tolerance, - flags=imgui.INPUT_TEXT_ENTER_RETURNS_TRUE, - ) - _ = add_popup_help(imgui, "The tolerance for selecting an isocontour.") - changed = changed or _ - - imgui.next_column() - _, self.iso_tol_is_pct = imgui.checkbox("%", self.iso_tol_is_pct) - _ = add_popup_help(imgui, "If checked, the tolerance is a percent.") - changed = changed or _ - imgui.columns(1) - - if imgui.button("Add Layer"): - if len(self.iso_layers) < 32: - changed = True - self.iso_layers.append(0.0) - self.iso_layers_alpha.append(1.0) - _ = self._construct_isolayer_table(imgui) - changed = changed or _ - imgui.tree_pop() - return changed - - def _construct_isolayer_table(self, imgui) -> bool: - imgui.columns(3, "iso_layers_cols", False) - - i = 0 - changed = False - while i < len(self.iso_layers): - _, self.iso_layers[i] = imgui.input_float( - f"Layer {i + 1}", - self.iso_layers[i], - flags=imgui.INPUT_TEXT_ENTER_RETURNS_TRUE, - ) - _ = add_popup_help(imgui, "The value of the isocontour layer.") - changed = changed or _ - - imgui.next_column() - _, self.iso_layers_alpha[i] = imgui.input_float( - f"alpha {i}", - self.iso_layers_alpha[i], - flags=imgui.INPUT_TEXT_ENTER_RETURNS_TRUE, - ) - _ = add_popup_help(imgui, "The opacity of the isocontour layer.") - changed = changed or _ - - imgui.next_column() - if imgui.button("Remove##rl" + str(i + 1)): - self.iso_layers.pop(i) - self.iso_layers_alpha.pop(i) - i -= 1 - _ = True - changed = changed or _ - imgui.next_column() - i += 1 - imgui.columns(1) - - return changed - def _reset_cmap_bounds(self): data = self.fb.data if self.use_db: diff --git a/yt_idv/scene_components/blocks.py b/yt_idv/scene_components/blocks.py index ab9f503..85ee5d3 100644 --- a/yt_idv/scene_components/blocks.py +++ b/yt_idv/scene_components/blocks.py @@ -62,6 +62,11 @@ def render_gui(self, imgui, renderer, scene): gp = GridPositions(grid_list=grids) scene.data_objects.append(gp) scene.components.append(GridOutlines(data=gp)) + if imgui.button("Add Isocontours"): + from .isolayers import Isolayers + + iso = Isolayers(data=self.data) + scene.components.append(iso) if self.render_method == "transfer_function": # Now for the transfer function stuff imgui.image_button( diff --git a/yt_idv/scene_components/isolayers.py b/yt_idv/scene_components/isolayers.py new file mode 100644 index 0000000..1a9bf61 --- /dev/null +++ b/yt_idv/scene_components/isolayers.py @@ -0,0 +1,231 @@ +import numpy as np +import traitlets +from OpenGL import GL + +from yt_idv.gui_support import add_popup_help +from yt_idv.opengl_support import TransferFunctionTexture +from yt_idv.scene_components.base_component import SceneComponent +from yt_idv.scene_data.block_collection import BlockCollection + + +class Isolayers(SceneComponent): + name = "block_isocontours" + render_method = "isocontours" + + data = traitlets.Instance(BlockCollection) + iso_tol_is_pct = traitlets.Bool(True) # if True, the tolerance is a fraction + iso_log = traitlets.Bool(True) # if True, iso values are base 10 exponents + iso_tolerance = traitlets.List() # the tolerance for finding isocontours + iso_layers = traitlets.List() # the target values for isocontours + iso_layers_alpha = traitlets.List() # the transparency of isocontours + box_width = traitlets.CFloat(0.1) + sample_factor = traitlets.CFloat(1.0) + transfer_function = traitlets.Instance(TransferFunctionTexture) + tf_min = traitlets.CFloat(0.0) + tf_max = traitlets.CFloat(1.0) + tf_log = traitlets.Bool(True) + + @traitlets.observe("iso_log") + def _switch_iso_log(self, change): + # if iso_log, then the user is setting 10**x, otherwise they are setting + # x directly. So when toggling this checkbox we convert the existing + # values between the two forms. + if change["old"]: + # if True, we were taking the log, but now are not: + new_tol = [10**iso_tol for iso_tol in self.iso_tolerance] + self.iso_tolerance = new_tol + new_iso_layers = [10**iso_val for iso_val in self.iso_layers] + self.iso_layers = new_iso_layers + else: + # we were not taking the log but now we are, so convert to the exponent + new_tol = [np.log10(iso_tol) for iso_tol in self.iso_tolerance] + self.iso_tolerance = new_tol + new_iso_layers = [np.log10(iso_val) for iso_val in self.iso_layers] + self.iso_layers = new_iso_layers + + @traitlets.default("iso_layers") + def _default_iso_layer(self): + val = (self.data.min_val + self.data.max_val) / 2.0 + if self.iso_log: + val = np.log10(val) + return [ + val, + ] + + @traitlets.default("iso_tolerance") + def _default_iso_layer_tol(self): + return [ + 0.0, + ] + + @traitlets.default("iso_layers_alpha") + def _default_iso_layers_alpha(self): + return [ + 1.0, + ] + + def _set_uniforms(self, scene, shader_program): + + shader_program._set_uniform("iso_num_layers", int(len(self.iso_layers))) + isolayervals = self._get_sanitized_iso_layers() + shader_program._set_uniform("iso_layers_min", isolayervals[0]) + shader_program._set_uniform("iso_layers_max", isolayervals[1]) + avals = np.zeros((32,), dtype="float32") + avals[: len(self.iso_layers)] = np.array(self.iso_layers_alpha) + shader_program._set_uniform("iso_alphas", avals) + + 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])) + shader_program._set_uniform("bitmap_tex", 1) + shader_program._set_uniform("tf_tex", 2) + shader_program._set_uniform("tf_min", self.tf_min) + shader_program._set_uniform("tf_max", self.tf_max) + shader_program._set_uniform("tf_log", float(self.tf_log)) + + def render_gui(self, imgui, renderer, scene): + changed = False + + _, self.iso_log = imgui.checkbox("set exponent", self.iso_log) + _ = add_popup_help( + imgui, "If checked, will treat isocontour values as base-10 exponents." + ) + changed = changed or _ + + imgui.next_column() + _, self.iso_tol_is_pct = imgui.checkbox( + "set tolerance in %", self.iso_tol_is_pct + ) + _ = add_popup_help(imgui, "If checked, the tolerance is a percent.") + changed = changed or _ + imgui.columns(1) + + if imgui.button("Add Layer"): + if len(self.iso_layers) < 32: + changed = True + self.iso_layers.append(0.0) + self.iso_layers_alpha.append(1.0) + self.iso_tolerance.append(0.0) + _ = self._construct_isolayer_table(imgui) + changed = changed or _ + + _ = super().render_gui(imgui, renderer, scene) + changed = changed or _ + + return changed + + def _construct_isolayer_table(self, imgui) -> bool: + imgui.columns(4, "iso_layers_cols", False) + if len(self.iso_layers) > 0: + # column text headers + imgui.text("Value") + imgui.next_column() + imgui.text("Tolerance") + imgui.next_column() + imgui.text("alpha") + imgui.next_column() + imgui.text("delete") + imgui.next_column() + + i = 0 + changed = False + while i < len(self.iso_layers): + _, self.iso_layers[i] = imgui.input_float( + f"##layer_{i}", + self.iso_layers[i], + flags=imgui.INPUT_TEXT_ENTER_RETURNS_TRUE, + ) + _ = add_popup_help(imgui, "The value of the isocontour layer.") + changed = changed or _ + + imgui.next_column() + _, self.iso_tolerance[i] = imgui.input_float( + f"##tol_{i}", + self.iso_tolerance[i], + flags=imgui.INPUT_TEXT_ENTER_RETURNS_TRUE, + ) + _ = add_popup_help(imgui, "The tolerance of the isocontour layer.") + changed = changed or _ + + imgui.next_column() + _, self.iso_layers_alpha[i] = imgui.input_float( + f"##alpha_{i}", + self.iso_layers_alpha[i], + flags=imgui.INPUT_TEXT_ENTER_RETURNS_TRUE, + ) + _ = add_popup_help(imgui, "The opacity of the isocontour layer.") + changed = changed or _ + + imgui.next_column() + if imgui.button(f"X##remove_{i}"): + self._remove_layer(i) + i -= 1 + _ = True + changed = changed or _ + imgui.next_column() + i += 1 + imgui.columns(1) + + return changed + + def _remove_layer(self, layer_id): + self.iso_layers.pop(layer_id) + self.iso_layers_alpha.pop(layer_id) + self.iso_tolerance.pop(layer_id) + + @property + def _iso_layer_array(self): + iso_vals = np.asarray(self.iso_layers, dtype="float32") + if self.iso_log: + iso_vals = 10**iso_vals + return iso_vals + + def _get_sanitized_iso_layers(self, normalize=True): + # returns an array of the isocontour layer values, padded with 0s out + # to max number of contours (32). + iso_vals = self._iso_layer_array + + tols = self._get_sanitized_iso_tol() + iso_min_max = [iso_vals - tols / 2.0, iso_vals + tols / 2.0] + + min_max_outputs = [] + for id in range(2): + vals = iso_min_max[id] + if normalize: + vals = self.data._normalize_by_min_max(vals) + + full_array = np.zeros(32, dtype="float32") + full_array[: len(self.iso_layers)] = vals.astype("float32") + min_max_outputs.append(full_array) + + return min_max_outputs + + def _get_sanitized_iso_tol(self): + tol = np.asarray(self.iso_tolerance) + if self.iso_log: + # the tol value is an exponent, convert + tol = 10**tol + + if self.iso_tol_is_pct: + # tolerance depends on the layer value + tol = np.asarray(tol) * 0.01 + iso_vals = self._iso_layer_array + final_tol = iso_vals * tol + else: + final_tol = tol + return final_tol + + @traitlets.default("transfer_function") + def _default_transfer_function(self): + tf = TransferFunctionTexture(data=np.ones((256, 1, 4), dtype="u1") * 255) + return tf + + def draw(self, scene, program): + each = self.data.vertex_array.each + GL.glEnable(GL.GL_CULL_FACE) + GL.glCullFace(GL.GL_BACK) + with self.transfer_function.bind(target=2): + for tex_ind, tex, bitmap_tex in self.data.viewpoint_iter(scene.camera): + with tex.bind(target=0): + with bitmap_tex.bind(target=1): + GL.glDrawArrays(GL.GL_POINTS, tex_ind * each, each) diff --git a/yt_idv/shaders/isocontour.frag.glsl b/yt_idv/shaders/isocontour.frag.glsl index 245af5f..d1d459d 100644 --- a/yt_idv/shaders/isocontour.frag.glsl +++ b/yt_idv/shaders/isocontour.frag.glsl @@ -124,10 +124,12 @@ void main() // current_data_value = (raw block value - iso_min) / (iso_range); // the iso_layer values below also come in already normalized. for (int i = 0; i < iso_num_layers; i++) { - if (abs(data_value - iso_layers[i]) <= iso_layer_tol[i]) { - is_layer = true; - curr_color.a = iso_alphas[i]; - break; + if (data_value >= iso_layers_min[i]){ + if (data_value <= iso_layers_max[i]){ + is_layer = true; + curr_color.a = iso_alphas[i]; + break; + } } } if (is_layer) { diff --git a/yt_idv/shaders/known_uniforms.inc.glsl b/yt_idv/shaders/known_uniforms.inc.glsl index 220d5d1..fc96507 100644 --- a/yt_idv/shaders/known_uniforms.inc.glsl +++ b/yt_idv/shaders/known_uniforms.inc.glsl @@ -59,6 +59,6 @@ uniform vec4 curve_rgba; // isocontour control uniform int iso_num_layers; -uniform float iso_layers[32]; -uniform float iso_layer_tol[32]; +uniform float iso_layers_min[32]; +uniform float iso_layers_max[32]; uniform float iso_alphas[32]; diff --git a/yt_idv/shaders/shaderlist.yaml b/yt_idv/shaders/shaderlist.yaml index 91a1282..c072be8 100644 --- a/yt_idv/shaders/shaderlist.yaml +++ b/yt_idv/shaders/shaderlist.yaml @@ -207,18 +207,20 @@ component_shaders: first_fragment: transfer_function second_vertex: passthrough second_fragment: passthrough - isocontours: - description: Isocontours + slice: + description: Slice first_vertex: grid_position first_geometry: grid_expand - first_fragment: isocontour + first_fragment: slice_sample second_vertex: passthrough second_fragment: apply_colormap - slice: - description: Slice + block_isocontours: + default_value: isocontours + isocontours: + description: Isocontours first_vertex: grid_position first_geometry: grid_expand - first_fragment: slice_sample + first_fragment: isocontour second_vertex: passthrough second_fragment: apply_colormap octree_block_rendering: diff --git a/yt_idv/tests/test_yt_idv.py b/yt_idv/tests/test_yt_idv.py index 11fa8c3..abe27d8 100644 --- a/yt_idv/tests/test_yt_idv.py +++ b/yt_idv/tests/test_yt_idv.py @@ -11,6 +11,7 @@ import yt_idv from yt_idv import shader_objects from yt_idv.scene_components.curves import CurveCollectionRendering, CurveRendering +from yt_idv.scene_components.isolayers import Isolayers from yt_idv.scene_data.curve import CurveCollection, CurveData @@ -125,7 +126,15 @@ def test_annotate_text(osmesa_empty, image_store): def test_isocontour_functionality(osmesa_fake_amr, image_store): - osmesa_fake_amr.scene.components[0].render_method = "isocontours" + sc = osmesa_fake_amr.scene + iso = Isolayers(data=sc.components[0].data) + sc.components.append(iso) + + sc.components[0].visible = False + sc.components[1].iso_log = False + sc.components[1].iso_tol_is_pct = True + sc.components[1].iso_tolerance[0] = 2.0 + sc.components[1].iso_layers[0] = 0.5 image_store(osmesa_fake_amr)