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

WIP: Isolayer updates #140

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
14 changes: 14 additions & 0 deletions examples/amr_volume_rendering_isocontours.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import yt
import yt_idv

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)
sg.components[0].render_method = 'isocontours'

# default behavior will treat these values as base-10 exponents
sg.components[0].iso_layers[0] = -29.0
sg.components[0].iso_tolerance[0] = 1.0 # tolerance in percent

rc.run()
176 changes: 1 addition & 175 deletions yt_idv/scene_components/base_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,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(),
Expand Down Expand Up @@ -109,28 +104,9 @@ 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
Expand All @@ -149,14 +125,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()
Expand Down Expand Up @@ -253,17 +221,7 @@ 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
Expand All @@ -276,8 +234,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)

Expand Down Expand Up @@ -306,65 +262,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 = (
Expand All @@ -381,77 +278,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:
Expand Down
8 changes: 7 additions & 1 deletion yt_idv/scene_components/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
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_components.isolayers import Isolayers
from yt_idv.scene_data.block_collection import BlockCollection
from yt_idv.shader_objects import component_shaders


class BlockRendering(SceneComponent):
class BlockRendering(SceneComponent, Isolayers):
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 any opinio on using a multi-inheritence structure here? was the most convenient way i saw to add the isolayer functionalty to the block rendering while allowing future re-use with octree blocks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(which is why we had initially added it to the base component I think)

Copy link
Member

Choose a reason for hiding this comment

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

hmm, good point. I guess I'm OK with it, but I also wonder if this should just be either a different shader (which I guess would open up multiple issues with having new uniforms that are conditionally modified) or a different component referencing the same data object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think a different component is actually a great idea. And we could have a "add isolayer" button similar to the button for adding a grid outline. And it would allow viewing both isolayers and a volume rendering at once (though I'm not sure this would work right away... Offhand I don't remember how the different components are blended, might be some depth buffer issues...).

"""
A class that renders block data. It may do this in one of several ways,
including mesh outline. This allows us to render a single collection of
Expand Down Expand Up @@ -120,6 +121,9 @@ def render_gui(self, imgui, renderer, scene):
changed = changed or _
_ = add_popup_help(imgui, "The normal vector of the slicing plane.")
changed = changed or _
elif self.render_method == "isocontours":
_ = self._render_isolayer_inputs(imgui)
changed = changed or _

return changed

Expand Down Expand Up @@ -149,3 +153,5 @@ def _set_uniforms(self, scene, shader_program):
shader_program._set_uniform("tf_log", float(self.tf_log))
shader_program._set_uniform("slice_normal", np.array(self.slice_normal))
shader_program._set_uniform("slice_position", np.array(self.slice_position))
if self.render_method == "isocontours":
self._set_iso_uniforms(shader_program)
Loading