diff --git a/examples/histogram_ex.py b/examples/histogram_ex.py new file mode 100644 index 00000000..5d2dbe3d --- /dev/null +++ b/examples/histogram_ex.py @@ -0,0 +1,12 @@ +import yt + +import yt_idv +from yt_idv.scene_annotations.block_histogram import BlockHistogram + +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) +bh = BlockHistogram(data=sg.data_objects[0], bins=64) +sg.annotations = sg.annotations + [bh] +# rc.run() diff --git a/yt_idv/opengl_support.py b/yt_idv/opengl_support.py index f9cb400b..604aae53 100644 --- a/yt_idv/opengl_support.py +++ b/yt_idv/opengl_support.py @@ -93,13 +93,21 @@ 4: (GL.GL_UNSIGNED_BYTE, GL.GL_RGBA8, GL.GL_RGBA), }, "uint32": { - 1: (GL.GL_UNSIGNED_INT, GL.GL_R32UI, GL.GL_RED), + 1: (GL.GL_UNSIGNED_INT, GL.GL_R32UI, GL.GL_RED_INTEGER), 2: (GL.GL_UNSIGNED_INT, GL.GL_RG32UI, GL.GL_RG), 3: (GL.GL_UNSIGNED_INT, GL.GL_RGB32UI, GL.GL_RGB), 4: (GL.GL_UNSIGNED_INT, GL.GL_RGBA32UI, GL.GL_RGBA), }, + "int32": { + 1: (GL.GL_INT, GL.GL_R32I, GL.GL_RED_INTEGER), + 2: (GL.GL_INT, GL.GL_RG32I, GL.GL_RG), + 3: (GL.GL_INT, GL.GL_RGB32I, GL.GL_RGB), + 4: (GL.GL_INT, GL.GL_RGBA32I, GL.GL_RGBA), + }, } +SKIP_MIPMAP = [GL.GL_R32UI, GL.GL_R16UI, GL.GL_R8UI, GL.GL_R32I, GL.GL_R16I, GL.GL_R8I] + def coerce_uniform_type(val, gl_type): # gl_type here must be in const_types @@ -168,6 +176,7 @@ class Texture(traitlets.HasTraits): channels = GLValue("r32f") min_filter = GLValue("linear") mag_filter = GLValue("linear") + image_mode = GLValue("write only") @traitlets.default("texture_name") def _default_texture_name(self): @@ -181,6 +190,34 @@ def bind(self, target=0): _ = GL.glActiveTexture(TEX_TARGETS[target]) GL.glBindTexture(self.dim_enum, 0) + @contextmanager + def clear(self): + with self.bind_as_image(0, GL.GL_WRITE_ONLY): + GL.glClearTexImage(self.texture_name, 0, self.channels, GL.GL_FLOAT, None) + yield + + @contextmanager + def bind_as_image(self, target=0, override_mode=None): + _ = GL.glActiveTexture(TEX_TARGETS[target]) + mode = override_mode or self.image_mode + _ = GL.glBindImageTexture( + target, self.texture_name, 0, False, 0, mode, self.channels + ) + yield + _ = GL.glActiveTexture(TEX_TARGETS[target]) + GL.glBindImageTexture(0, 0, 0, False, 0, mode, self.channels) + + def read_as_image(self): + if len(self.data.shape) == 2: + channels = self.data.shape[-1] + else: + channels = 1 + gl_type, _, type2 = TEX_CHANNELS[self.data.dtype.name][channels] + with self.bind(): + data = np.zeros(self.data.shape, dtype=self.data.dtype) + GL.glGetTexImage(self.dim_enum, 0, type2, gl_type, data) + return data + class Texture1D(Texture): boundary_x = TextureBoundary() @@ -199,7 +236,7 @@ def _set_data(self, change): gl_type, type1, type2 = TEX_CHANNELS[data.dtype.name][channels] GL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1) if not isinstance(change["old"], np.ndarray): - GL.glTexStorage1D(GL.GL_TEXTURE_1D, 6, type1, dx) + GL.glTexStorage1D(GL.GL_TEXTURE_1D, 1, type1, dx) GL.glTexSubImage1D(GL.GL_TEXTURE_1D, 0, 0, dx, type2, gl_type, data) GL.glTexParameterf(GL.GL_TEXTURE_1D, GL.GL_TEXTURE_WRAP_S, self.boundary_x) GL.glTexParameteri( @@ -208,7 +245,8 @@ def _set_data(self, change): GL.glTexParameteri( GL.GL_TEXTURE_1D, GL.GL_TEXTURE_MAG_FILTER, self.mag_filter ) - GL.glGenerateMipmap(GL.GL_TEXTURE_1D) + if type1 not in SKIP_MIPMAP: + GL.glGenerateMipmap(GL.GL_TEXTURE_1D) class ColormapTexture(Texture1D): @@ -268,7 +306,8 @@ def _set_data(self, change): GL.glTexParameteri( GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, self.mag_filter ) - GL.glGenerateMipmap(GL.GL_TEXTURE_2D) + if type1 not in SKIP_MIPMAP: + GL.glGenerateMipmap(GL.GL_TEXTURE_2D) class TransferFunctionTexture(Texture2D): @@ -329,7 +368,8 @@ def _set_data(self, change): GL.glTexParameteri( GL.GL_TEXTURE_3D, GL.GL_TEXTURE_MAG_FILTER, self.mag_filter ) - GL.glGenerateMipmap(GL.GL_TEXTURE_3D) + if type1 not in SKIP_MIPMAP: + GL.glGenerateMipmap(GL.GL_TEXTURE_3D) class VertexAttribute(traitlets.HasTraits): diff --git a/yt_idv/scene_annotations/block_histogram.py b/yt_idv/scene_annotations/block_histogram.py new file mode 100644 index 00000000..34b3c25c --- /dev/null +++ b/yt_idv/scene_annotations/block_histogram.py @@ -0,0 +1,64 @@ +import numpy as np +import traitlets +from OpenGL import GL + +from yt_idv.opengl_support import Texture1D +from yt_idv.scene_annotations.base_annotation import SceneAnnotation +from yt_idv.scene_data.block_collection import BlockCollection + + +class BlockHistogram(SceneAnnotation): + """ + A class that computes and displays a histogram of block data. + """ + + name = "block_histogram" + data = traitlets.Instance(BlockCollection) + output_data = traitlets.Instance(Texture1D) + + bins = traitlets.CInt(64) + min_val = traitlets.CFloat(0.0) + max_val = traitlets.CFloat(1.0) + + _recompute = True + + @traitlets.default("output_data") + def _default_output_data(self): + return Texture1D(data=np.zeros(self.bins, dtype="u4"), channels="r32ui") + + def _set_uniforms(self, scene, shader_program): + pass + + def _set_compute_uniforms(self, scene, shader_program): + shader_program._set_uniform("min_val", self.min_val) + shader_program._set_uniform("max_val", self.max_val) + shader_program._set_uniform("bins", self.bins) + + def compute(self, scene, program): + # We need a place to dump our stuff. + self.visible = False + self.output_data.clear() + total = 0 + with self.output_data.bind_as_image(2): + for _tex_ind, tex, bitmap_tex in self.data.viewpoint_iter(scene.camera): + # We now need to bind our textures. We don't care about positions. + total += bitmap_tex.data.size + with tex.bind(target=0): + with bitmap_tex.bind(target=1): + GL.glUseProgram(program.program) + self._set_compute_uniforms(scene, program) + # This will need to be carefully chosen based on our + # architecture, I guess. That aspect of running compute + # shaders, CUDA, etc, is one of my absolute least favorite + # parts. + GL.glDispatchCompute( + bitmap_tex.data.shape[0], + bitmap_tex.data.shape[1], + bitmap_tex.data.shape[2], + ) + GL.glMemoryBarrier(GL.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT) + + def draw(self, scene, program): + # This will probably need to have somewhere to draw the darn thing. So + # we'll need display coordinates, size, etc. + pass diff --git a/yt_idv/scene_components/base_component.py b/yt_idv/scene_components/base_component.py index 464062e7..4fa1edeb 100644 --- a/yt_idv/scene_components/base_component.py +++ b/yt_idv/scene_components/base_component.py @@ -13,6 +13,7 @@ ) from yt_idv.scene_data.base_data import SceneData from yt_idv.shader_objects import ( + ComputeShaderProgram, ShaderProgram, ShaderTrait, component_shaders, @@ -49,6 +50,7 @@ class SceneComponent(traitlets.HasTraits): clear_region = traitlets.Bool(False) render_method = traitlets.Unicode(allow_none=True) + compute_shader = ShaderTrait(allow_none=True).tag(shader_type="compute") fragment_shader = ShaderTrait(allow_none=True).tag(shader_type="fragment") geometry_shader = ShaderTrait(allow_none=True).tag(shader_type="geometry") vertex_shader = ShaderTrait(allow_none=True).tag(shader_type="vertex") @@ -56,11 +58,14 @@ class SceneComponent(traitlets.HasTraits): colormap_fragment = ShaderTrait(allow_none=True).tag(shader_type="fragment") colormap_vertex = ShaderTrait(allow_none=True).tag(shader_type="vertex") colormap = traitlets.Instance(ColormapTexture) + _compute_program = traitlets.Instance(ShaderProgram, allow_none=True) _program1 = traitlets.Instance(ShaderProgram, allow_none=True) _program2 = traitlets.Instance(ShaderProgram, allow_none=True) _program1_invalid = True _program2_invalid = True _cmap_bounds_invalid = True + _recompute = False + _compute_program_invalid = True display_name = traitlets.Unicode(allow_none=True) @@ -166,6 +171,10 @@ def _add_initial_isolayer(self, change): def _fb_default(self): return Framebuffer() + @traitlets.observe("compute_shader") + def _change_compute(self, change): + self._compute_program_invalid = True + @traitlets.observe("fragment_shader") def _change_fragment(self, change): # Even if old/new are the same @@ -201,6 +210,10 @@ def _default_colormap(self): cm.colormap_name = "arbre" return cm + @traitlets.default("compute_shader") + def _compute_shader_default(self): + return component_shaders[self.name][self.render_method]["compute"] + @traitlets.default("vertex_shader") def _vertex_shader_default(self): return component_shaders[self.name][self.render_method]["first_vertex"] @@ -234,6 +247,16 @@ def _default_base_quad(self): ) return bq + @property + def compute_program(self): + if self._compute_program_invalid: + if self._compute_program is not None: + self._compute_program.delete_program() + self._compute_program = ComputeShaderProgram(self.compute_shader) + self._compute_program_invalid = False + self._recompute = True + return self._compute_program + @property def program1(self): if self._program1_invalid: @@ -270,9 +293,20 @@ def _set_iso_uniforms(self, p): p._set_uniform("iso_min", float(self.data.min_val)) p._set_uniform("iso_max", float(self.data.max_val)) + def compute(self, scene, program): + pass + + def _set_compute_uniforms(self, scene, program): + pass + def run_program(self, scene): # Store this info, because we need to render into a framebuffer that is the # right size. + if self._recompute: + with self.compute_program.enable() as p: + self._set_compute_uniforms(scene, p) + self.compute(scene, p) + self._recompute = False x0, y0, w, h = GL.glGetIntegerv(GL.GL_VIEWPORT) GL.glViewport(0, 0, w, h) if not self.visible: diff --git a/yt_idv/shader_objects.py b/yt_idv/shader_objects.py index 22892985..a9822694 100644 --- a/yt_idv/shader_objects.py +++ b/yt_idv/shader_objects.py @@ -22,7 +22,7 @@ _NULL_SOURCES = { "geometry": r""" -#version 330 core +#version 430 core layout ( points ) in; layout ( points ) out; @@ -32,7 +32,7 @@ } """, "vertex": r""" -#version 330 core +#version 430 core // Input vertex data, different for all executions of this shader. in vec3 vertexPosition_modelspace; @@ -48,7 +48,7 @@ """, "fragment": r""" -#version 330 core +#version 430 core out vec4 color; @@ -124,6 +124,7 @@ def introspect(self): # First get all of the uniforms self.uniforms = {} self.attributes = {} + self.inputs = {} if not bool(GL.glGetProgramInterfaceiv): return @@ -136,18 +137,44 @@ def introspect(self): gl_type = num_to_const[gl_type] self.uniforms[name.decode("utf-8")] = (size, gl_type) - n_attrib = GL.glGetProgramInterfaceiv( - self.program, GL.GL_PROGRAM_INPUT, GL.GL_ACTIVE_RESOURCES - ) + n_attrib = ctypes.pointer(ctypes.c_int(0)) + GL.glGetProgramiv(self.program, GL.GL_ACTIVE_ATTRIBUTES, n_attrib) length = ctypes.pointer(ctypes.c_int()) size = ctypes.pointer(ctypes.c_int()) gl_type = ctypes.pointer(ctypes.c_int()) name = ctypes.create_string_buffer(256) - for i in range(n_attrib): + for i in range(n_attrib[0]): GL.glGetActiveAttrib(self.program, i, 256, length, size, gl_type, name) gl_const = num_to_const[gl_type[0]] self.attributes[name[: length[0]].decode("utf-8")] = (size[0], gl_const) + # We're going to use `n_inputs` here as well. + n_inputs = GL.glGetProgramInterfaceiv( + self.program, GL.GL_PROGRAM_INPUT, GL.GL_ACTIVE_RESOURCES + ) + props = (ctypes.c_int * 2)(GL.GL_TYPE, GL.GL_ARRAY_SIZE) + pprops = ctypes.pointer(props) + params = (ctypes.c_int * 2)(0, 0) + pparams = ctypes.pointer(params) + size = ctypes.pointer(ctypes.c_int()) + gl_type = ctypes.pointer(ctypes.c_int()) + name = ctypes.create_string_buffer(256) + for i in range(n_inputs): + length[0] = 2 + GL.glGetProgramResourceiv( + self.program, GL.GL_PROGRAM_INPUT, i, 2, pprops, 1, size, pparams + ) + length[0] = 0 + GL.glGetProgramResourceName( + self.program, GL.GL_PROGRAM_INPUT, i, 256, length, name + ) + gl_const = num_to_const[params[0]] + input_name = name[: length[0]].decode("utf-8") + if input_name in self.attributes: + # Ignore these ... + continue + self.inputs[input_name] = (params[1], gl_const) + def delete_program(self): if self.program is not None: GL.glDeleteProgram(self.program) @@ -228,6 +255,54 @@ def enable(self): GL.glUseProgram(0) +class ComputeShaderProgram(ShaderProgram): + """ + Wrapper class that compiles and links compute shaders. + + This has very little shared code with a ShaderProgram, and is designed to be + used for compute shaders only. + + Parameters + ---------- + + compute_shader : string + or :class:`yt.visualization.volume_rendering.shader_objects.ComputeShader` + The vertex shader used in the Interactive Data Visualization pipeline. + """ + + def __init__(self, compute_shader): + self.link(compute_shader) + self._uniform_funcs = OrderedDict() + + def link(self, compute_shader): + """ + Links the compute shader to the program. + + Parameters + ---------- + compute_shader : string + or :class:`yt.visualization.volume_rendering.shader_objects.ComputeShader` + The compute shader used in the Interactive Data Visualization pipeline. + """ + self.program = GL.glCreateProgram() + if not isinstance(compute_shader, Shader): + compute_shader = Shader(source=compute_shader) + self.compute_shader = compute_shader + GL.glAttachShader(self.program, self.compute_shader.shader) + GL.glLinkProgram(self.program) + result = GL.glGetProgramiv(self.program, GL.GL_LINK_STATUS) + if not result: + raise RuntimeError(GL.glGetProgramInfoLog(self.program)) + self.compute_shader.delete_shader() + self.introspect() + + @contextlib.contextmanager + def enable(self): + GL.glUseProgram(self.program) + yield self + GL.glUseProgram(0) + + class Shader(traitlets.HasTraits): """ Creates a shader from source @@ -252,7 +327,9 @@ class Shader(traitlets.HasTraits): source = traitlets.Any() shader_name = traitlets.CUnicode() info = traitlets.CUnicode() - shader_type = traitlets.CaselessStrEnum(("vertex", "fragment", "geometry")) + shader_type = traitlets.CaselessStrEnum( + ("vertex", "fragment", "geometry", "compute") + ) blend_func = traitlets.Tuple( GLValue(), GLValue(), default_value=("src alpha", "dst alpha") ) @@ -382,7 +459,7 @@ def _validate_shader(shader_type, value, allow_null=True): class ShaderTrait(traitlets.TraitType): default_value = None - info_text = "A shader (vertex, fragment or geometry)" + info_text = "A shader (vertex, fragment, geometry or compute)" def validate(self, obj, value): if isinstance(value, str): diff --git a/yt_idv/shaders/block_histogram.comp.glsl b/yt_idv/shaders/block_histogram.comp.glsl new file mode 100644 index 00000000..7c4cffeb --- /dev/null +++ b/yt_idv/shaders/block_histogram.comp.glsl @@ -0,0 +1,43 @@ +layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; +layout(binding = 0) uniform sampler3D texData; +layout(binding = 1) uniform sampler3D bitmapData; +layout(r32i, binding = 2) uniform iimage1D uHistogram; + +void main() { + // Get thread identifiers within the work group + uint gidX = gl_WorkGroupID.x; + uint gidY = gl_WorkGroupID.y; + uint gidZ = gl_WorkGroupID.z; + ivec3 dims = textureSize(texData, 0); + uint N = dims.x; + uint NxM = dims.x * dims.y; + uint NxMxL = dims.x * dims.y * dims.z; + + // Calculate global thread ID + uint globalIdx = gidZ * gl_NumWorkGroups.y * gl_NumWorkGroups.x + gidY * gl_NumWorkGroups.x + gidX; + // Check if global ID is within texture bounds + if (globalIdx >= NxMxL) { + return; + } + + // Calculate 3D texture coordinates from global ID + uint z = globalIdx / (NxM); + uint remainder = globalIdx % (NxM); + uint y = remainder / N; + uint x = remainder % N; + + // Sample the value from the 3D texture + vec4 texValue = texture(texData, vec3(x, y, z)); + + // Normalize the value to the range [0, 1] based on min and max + float normalizedValue = (texValue.r - min_val) / (max_val - min_val); + + // Clamp the normalized value to [0, 1] + normalizedValue = clamp(normalizedValue, 0.0, 1.0); + + // Calculate the bin index based on the number of bins + int binIndex = int(normalizedValue * (bins - 1)); + + imageAtomicAdd(uHistogram, binIndex, 1); + +} diff --git a/yt_idv/shaders/header.inc.glsl b/yt_idv/shaders/header.inc.glsl index 5ae7f438..fef66985 100644 --- a/yt_idv/shaders/header.inc.glsl +++ b/yt_idv/shaders/header.inc.glsl @@ -1 +1 @@ -#version 330 core +#version 430 core diff --git a/yt_idv/shaders/known_uniforms.inc.glsl b/yt_idv/shaders/known_uniforms.inc.glsl index 220d5d15..e7eb62d8 100644 --- a/yt_idv/shaders/known_uniforms.inc.glsl +++ b/yt_idv/shaders/known_uniforms.inc.glsl @@ -62,3 +62,8 @@ uniform int iso_num_layers; uniform float iso_layers[32]; uniform float iso_layer_tol[32]; uniform float iso_alphas[32]; + +// compute shader for histograms +uniform int bins; +uniform float min_val; +uniform float max_val; diff --git a/yt_idv/shaders/shaderlist.yaml b/yt_idv/shaders/shaderlist.yaml index 91a12820..7079016d 100644 --- a/yt_idv/shaders/shaderlist.yaml +++ b/yt_idv/shaders/shaderlist.yaml @@ -4,138 +4,146 @@ shader_definitions: info: A constant value applied source: constant.frag.glsl blend_func: - - one - - one + - one + - one blend_equation: func add constant_rgba: - info: A constant, specified RGBa value applied - source: constant_rgba.frag.glsl - blend_func: - - one - - one - blend_equation: func add + info: A constant, specified RGBa value applied + source: constant_rgba.frag.glsl + blend_func: + - one + - one + blend_equation: func add apply_colormap: - info: A second pass fragment shader used to apply a colormap to the result of + info: + A second pass fragment shader used to apply a colormap to the result of the first pass rendering source: apply_colormap.frag.glsl blend_func: - - src alpha - - dst alpha + - src alpha + - dst alpha blend_equation: func add expand_1d: info: This expands a 1D texture along the y dimension source: expand_1d.frag.glsl blend_func: - - one - - zero + - one + - zero blend_equation: func add draw_blocks: - info: A first pass fragment shader that performs ray casting using transfer function. + info: + A first pass fragment shader that performs ray casting using transfer function. See :ref:`volume-rendering-method` for more details. source: block_outline.frag.glsl blend_func: - - src alpha - - one minus src alpha + - src alpha + - one minus src alpha blend_equation: func add isocontour: info: A first pass fragment shader that renders isocontour layers. source: isocontour.frag.glsl blend_func: - - src alpha - - one minus src alpha + - src alpha + - one minus src alpha blend_equation: func add max_intensity: - info: A first pass fragment shader that computes Maximum Intensity Projection + info: + A first pass fragment shader that computes Maximum Intensity Projection of the data. See :ref:`projection-types` for more information. source: - - ray_tracing.frag.glsl - - max_intensity.frag.glsl + - ray_tracing.frag.glsl + - max_intensity.frag.glsl blend_func: - - one - - one + - one + - one blend_equation: max mesh: info: A vertex shader used for unstructured mesh rendering. source: mesh.frag.glsl depth_test: less blend_func: - - one - - zero + - one + - zero blend_equation: func add noop: - info: A second pass fragment shader that performs no operation. Usually used + 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 source: noop.frag.glsl passthrough: - info: A first pass fragment shader that performs no operation. Used for debug + info: + A first pass fragment shader that performs no operation. Used for debug puproses. It's distinct from NoOpFragmentShader, because of the number of uniforms source: passthrough.frag.glsl blend_func: - - src alpha - - dst alpha + - src alpha + - dst alpha blend_equation: func add draw_lines: info: A line drawing fragment shader source: draw_lines.frag.glsl blend_func: - - one - - zero + - one + - zero blend_equation: func add projection: - info: A first pass fragment shader that performs unweighted integration of the + info: + A first pass fragment shader that performs unweighted integration of the data along the line of sight. See :ref:`projection-types` for more information. source: - - ray_tracing.frag.glsl - - projection.frag.glsl + - ray_tracing.frag.glsl + - projection.frag.glsl blend_func: - - one - - one + - one + - one blend_equation: func add text_overlay: info: A simple text overlay shader source: textoverlay.frag.glsl blend_func: - - src alpha - - one minus src alpha + - src alpha + - one minus src alpha blend_equation: func add transfer_function: - info: A first pass fragment shader that performs ray casting using transfer function. + info: + A first pass fragment shader that performs ray casting using transfer function. See :ref:`volume-rendering-method` for more details. source: - - ray_tracing.frag.glsl - - transfer_function.frag.glsl + - ray_tracing.frag.glsl + - transfer_function.frag.glsl blend_func_separate: - - one minus dst alpha - - one - - one minus dst alpha - - one + - one minus dst alpha + - one + - one minus dst alpha + - one blend_equation_separate: - - func add - - func add + - func add + - func add sph_kernel: info: Sample pre-integrated SPH kernel source: sph_kernel.frag.glsl blend_func: - - one - - one + - one + - one blend_equation: func add field_value: info: Use field values as input source: field_value.frag.glsl blend_func: - - one - - one + - one + - one slice_sample: info: Slice through a block collection source: slice_sample.frag.glsl depth_test: less blend_func: - - one - - zero + - one + - zero blend_equation: func add vertex: default: - info: A first pass vertex shader that tranlates the location of vertices from + info: + A first pass vertex shader that tranlates the location of vertices from the world coordinates to the viewing plane coordinates source: default.vert.glsl mesh: @@ -167,23 +175,36 @@ shader_definitions: particle_expand: info: Expand particles from points to triangulated quads source: particle_expand.geom.glsl + compute: + block_histogram: + info: Compute a histogram of the data in a block + source: block_histogram.comp.glsl component_shaders: + block_histogram: + default_value: default + default: + description: Default + #first_vertex: passthrough + #first_fragment: passthrough + #second_vertex: passthrough + #second_fragment: passthrough + compute: block_histogram curve_rendering: - default_value: default - default: - description: Default - first_vertex: mesh - first_fragment: constant_rgba - second_vertex: passthrough - second_fragment: passthrough + default_value: default + default: + description: Default + first_vertex: mesh + first_fragment: constant_rgba + second_vertex: passthrough + second_fragment: passthrough multi_curve_rendering: - default_value: default - default: - description: Default - first_vertex: mesh - first_fragment: constant_rgba - second_vertex: passthrough - second_fragment: passthrough + default_value: default + default: + description: Default + first_vertex: mesh + first_fragment: constant_rgba + second_vertex: passthrough + second_fragment: passthrough block_rendering: default_value: max_intensity max_intensity: