From f753d6de4a3cc786bd6589b00e23dab6f02a7f1b Mon Sep 17 00:00:00 2001 From: Matthieu Bouron Date: Fri, 1 Dec 2023 08:56:26 +0100 Subject: [PATCH] nodes: add RadialBlur node --- CHANGELOG.md | 1 + libnopegl/meson.build | 2 + libnopegl/nodes.specs | 33 ++ libnopegl/src/glsl/blur_radial_zoom.frag | 68 ++++ libnopegl/src/glsl/helper_misc_utils.glsl | 1 + libnopegl/src/node_rblur.c | 366 ++++++++++++++++++++++ libnopegl/src/nodes_register.h | 1 + libnopegl/src/nopegl.h.in | 1 + tests/blur.py | 23 ++ tests/meson.build | 1 + tests/refs/blur_radial_zoom.ref | 10 + 11 files changed, 507 insertions(+) create mode 100644 libnopegl/src/glsl/blur_radial_zoom.frag create mode 100644 libnopegl/src/node_rblur.c create mode 100644 tests/refs/blur_radial_zoom.ref diff --git a/CHANGELOG.md b/CHANGELOG.md index ae553f649..75a494674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Versioning](https://semver.org/spec/v2.0.0.html) for `libnopegl`. - `ngl_get_viewport()` to get the viewport currently in use by the rendering context - `FontFace.index` parameter to select a different face in the font file +- `RadialBlur` node to apply a post processing radial blur effect to a scene ### Changed - `Text.font_files` text-based parameter is replaced with `Text.font_faces` node diff --git a/libnopegl/meson.build b/libnopegl/meson.build index c69f5745a..e7e84650d 100644 --- a/libnopegl/meson.build +++ b/libnopegl/meson.build @@ -217,6 +217,7 @@ lib_src = files( 'src/node_pathkey.c', 'src/node_program.c', 'src/node_quad.c', + 'src/node_rblur.c', 'src/node_resourceprops.c', 'src/node_rotate.c', 'src/node_rotatequat.c', @@ -675,6 +676,7 @@ shaders = { 'blur_downsample.frag': 'blur_downsample_frag.h', 'blur_upsample.frag': 'blur_upsample_frag.h', 'blur_interpolate.frag': 'blur_interpolate_frag.h', + 'blur_radial_zoom.frag': 'blur_radial_zoom_frag.h', 'colorstats_init.comp': 'colorstats_init_comp.h', 'colorstats_sumscale.comp': 'colorstats_sumscale_comp.h', 'colorstats_waveform.comp': 'colorstats_waveform_comp.h', diff --git a/libnopegl/nodes.specs b/libnopegl/nodes.specs index e6d0d7137..3965c252d 100644 --- a/libnopegl/nodes.specs +++ b/libnopegl/nodes.specs @@ -3360,6 +3360,39 @@ } ] }, + "RadialBlur": { + "file": "src/node_rblur.c", + "params": [ + { + "name": "source", + "type": "node", + "node_types": ["Texture2D"], + "flags": ["nonull"], + "desc": "source to use for the blur" + }, + { + "name": "destination", + "type": "node", + "node_types": ["Texture2D"], + "flags": ["nonull"], + "desc": "destination to use for the blur" + }, + { + "name": "amount", + "type": "f32", + "default": 0.000000, + "flags": ["node"], + "desc": "amount of bluriness in the range [-1,1]" + }, + { + "name": "center", + "type": "vec2", + "default": [0.000000,0.000000], + "flags": ["node"], + "desc": "center of the radial blur" + } + ] + }, "RenderToTexture": { "file": "src/node_rtt.c", "params": [ diff --git a/libnopegl/src/glsl/blur_radial_zoom.frag b/libnopegl/src/glsl/blur_radial_zoom.frag new file mode 100644 index 000000000..6b5d2fcf6 --- /dev/null +++ b/libnopegl/src/glsl/blur_radial_zoom.frag @@ -0,0 +1,68 @@ +/* + * Copyright 2023-2024 Matthieu Bouron + * Copyright 2023-2024 Nope Forge + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include helper_srgb.glsl + +void main() +{ + vec2 size = vec2(textureSize(tex, 0)); + vec2 direction = (tex_coord - center) * size; + vec2 step = normalize(direction) / size; + float radius = length(direction) * amount; + int radius_i = int(ceil(radius)); + + /* + * Use the 3-sigma rule to approximate sigma, see: + * - https://en.wikipedia.org/wiki/Talk%3AGaussian_blur#Radius_again + * - https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule + */ + float sigma = radius / 3.0; + + /* + * Fast approximation of the gaussian function using the incremental gaussian + * algorithm adapted from GPU Gem 3. + */ + float g0 = 1.0; + float g1 = sigma > 1e-4 ? exp(-1.0 / (2.0 * sigma * sigma)) : 0.0; + float g2 = g1 * g1; + + vec4 color = vec4(0.0); + float total_weight = 0.0; + + int nb_samples = max(radius_i, 2); + for (int i = 0; i < nb_samples; i = i + 2) { + /* Take advantage of hw filtering to reduce the number of texture fetches */ + float w1 = g0; + g0 *= g1; + g1 *= g2; + float w2 = g0; + g0 *= g1; + g1 *= g2; + float w = w1 + w1; + vec2 offset = ((float(i) * w1 + (float(i) + 1.0) * w2) / w) * step; + vec4 value = texture(tex, tex_coord - offset); + color += vec4(ngli_srgb2linear(value.rgb), value.a) * w; + total_weight += w; + } + color /= total_weight; + ngl_out_color = vec4(ngli_linear2srgb(color.rgb), color.a); +} diff --git a/libnopegl/src/glsl/helper_misc_utils.glsl b/libnopegl/src/glsl/helper_misc_utils.glsl index 7ecdf9c9d..a488cb26c 100644 --- a/libnopegl/src/glsl/helper_misc_utils.glsl +++ b/libnopegl/src/glsl/helper_misc_utils.glsl @@ -30,3 +30,4 @@ float ngli_aa(float x) { return ngli_sat(x / fwidth(x) + 0.5); } const vec3 ngli_luma_weights = vec3(.2126, .7152, .0722); // BT.709 const float ngli_pi = 3.14159265358979323846; const float ngli_tau = 6.28318530717958647692; +const float ngli_inv_sqrt_tau = 0.3989422804014327; diff --git a/libnopegl/src/node_rblur.c b/libnopegl/src/node_rblur.c new file mode 100644 index 000000000..6449dda23 --- /dev/null +++ b/libnopegl/src/node_rblur.c @@ -0,0 +1,366 @@ +/* + * Copyright 2023-2024 Matthieu Bouron + * Copyright 2023-2024 Nope Forge + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include +#include +#include +#include + +#include "gpu_block.h" +#include "gpu_ctx.h" +#include "graphics_state.h" +#include "internal.h" +#include "log.h" +#include "math_utils.h" +#include "nopegl.h" +#include "rendertarget.h" +#include "rtt.h" +#include "topology.h" +#include "utils.h" + +/* GLSL shaders */ +#include "blur_common_vert.h" +#include "blur_radial_zoom_frag.h" + +struct params_block { + float amount; + float center[2]; +}; + +struct rblur_opts { + struct ngl_node *source; + struct ngl_node *destination; + struct ngl_node *amount_node; + float amount; + struct ngl_node *center_node; + float center[2]; +}; + +struct rblur_priv { + int32_t width; + int32_t height; + + struct image *image; + size_t image_rev; + + int dst_is_resizeable; + struct rendertarget_layout dst_layout; + struct rtt_ctx *dst_rtt_ctx; + + struct gpu_block blur_params; + struct pgcraft *crafter; + struct pipeline_compat *pl_blur_r; +}; + +#define OFFSET(x) offsetof(struct rblur_opts, x) +static const struct node_param rblur_params[] = { + {"source", NGLI_PARAM_TYPE_NODE, OFFSET(source), + .node_types=(const uint32_t[]){NGL_NODE_TEXTURE2D, NGLI_NODE_NONE}, + .flags=NGLI_PARAM_FLAG_NON_NULL | NGLI_PARAM_FLAG_DOT_DISPLAY_FIELDNAME, + .desc=NGLI_DOCSTRING("source to use for the blur")}, + {"destination", NGLI_PARAM_TYPE_NODE, OFFSET(destination), + .node_types=(const uint32_t[]){NGL_NODE_TEXTURE2D, NGLI_NODE_NONE}, + .flags=NGLI_PARAM_FLAG_NON_NULL | NGLI_PARAM_FLAG_DOT_DISPLAY_FIELDNAME, + .desc=NGLI_DOCSTRING("destination to use for the blur")}, + {"amount", NGLI_PARAM_TYPE_F32, OFFSET(amount_node), + .flags=NGLI_PARAM_FLAG_ALLOW_NODE, + .desc=NGLI_DOCSTRING("amount of bluriness in the range [-1,1]")}, + {"center", NGLI_PARAM_TYPE_VEC2, OFFSET(center_node), + .flags=NGLI_PARAM_FLAG_ALLOW_NODE, + .desc=NGLI_DOCSTRING("center of the radial blur")}, + {NULL} +}; + +static int rblur_init(struct ngl_node *node) +{ + struct ngl_ctx *ctx = node->ctx; + struct gpu_ctx *gpu_ctx = ctx->gpu_ctx; + struct rblur_priv *s = node->priv_data; + struct rblur_opts *o = node->opts; + + struct texture_priv *src_priv = o->source->priv_data; + s->image = &src_priv->image; + s->image_rev = SIZE_MAX; + + /* Disable direct rendering */ + src_priv->supported_image_layouts = 1U << NGLI_IMAGE_LAYOUT_DEFAULT; + + /* Override texture params */ + src_priv->params.min_filter = NGLI_FILTER_LINEAR; + src_priv->params.min_filter = NGLI_FILTER_LINEAR; + src_priv->params.mipmap_filter = NGLI_MIPMAP_FILTER_LINEAR; + + struct texture_priv *dst_priv = o->destination->priv_data; + dst_priv->params.usage |= NGLI_TEXTURE_USAGE_COLOR_ATTACHMENT_BIT; + + s->dst_is_resizeable = (dst_priv->params.width == 0 && dst_priv->params.height == 0); + s->dst_layout.colors[0].format = dst_priv->params.format; + s->dst_layout.nb_colors = 1; + + const struct gpu_block_field params_fields[] = { + NGLI_GPU_BLOCK_FIELD(struct params_block, amount, NGLI_TYPE_F32, 0), + NGLI_GPU_BLOCK_FIELD(struct params_block, center, NGLI_TYPE_VEC2, 0), + }; + const struct gpu_block_params blur_params = { + .count = 1, + .fields = params_fields, + .nb_fields = NGLI_ARRAY_NB(params_fields), + }; + + int ret = ngli_gpu_block_init(gpu_ctx, &s->blur_params, &blur_params); + if (ret < 0) + return ret; + + const struct pgcraft_iovar vert_out_vars[] = { + {.name = "tex_coord", .type = NGLI_TYPE_VEC2}, + }; + + const struct pgcraft_texture textures[] = { + { + .name = "tex", + .type = NGLI_PGCRAFT_SHADER_TEX_TYPE_2D, + .precision = NGLI_PRECISION_HIGH, + .stage = NGLI_PROGRAM_SHADER_FRAG + }, + }; + + const struct pgcraft_block crafter_blocks[] = { + { + .name = "blur_params", + .instance_name = "", + .type = NGLI_TYPE_UNIFORM_BUFFER, + .stage = NGLI_PROGRAM_SHADER_FRAG, + .block = &s->blur_params.block, + .buffer = { + .buffer = s->blur_params.buffer, + .size = s->blur_params.block_size, + }, + }, + }; + + const struct pgcraft_params crafter_params = { + .program_label = "nopegl/radial-blur", + .vert_base = blur_common_vert, + .frag_base = blur_radial_zoom_frag, + .textures = textures, + .nb_textures = NGLI_ARRAY_NB(textures), + .blocks = crafter_blocks, + .nb_blocks = NGLI_ARRAY_NB(crafter_blocks), + .vert_out_vars = vert_out_vars, + .nb_vert_out_vars = NGLI_ARRAY_NB(vert_out_vars), + }; + + s->crafter = ngli_pgcraft_create(ctx); + if (!s->crafter) + return NGL_ERROR_MEMORY; + + ret = ngli_pgcraft_craft(s->crafter, &crafter_params); + if (ret < 0) + return ret; + + s->pl_blur_r = ngli_pipeline_compat_create(gpu_ctx); + if (!s->pl_blur_r) + return NGL_ERROR_MEMORY; + + const struct pipeline_compat_params params = { + .type = NGLI_PIPELINE_TYPE_GRAPHICS, + .graphics = { + .topology = NGLI_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, + .state = NGLI_GRAPHICS_STATE_DEFAULTS, + .rt_layout = s->dst_layout, + .vertex_state = ngli_pgcraft_get_vertex_state(s->crafter), + }, + .program = ngli_pgcraft_get_program(s->crafter), + .layout = ngli_pgcraft_get_pipeline_layout(s->crafter), + .resources = ngli_pgcraft_get_pipeline_resources(s->crafter), + .compat_info = ngli_pgcraft_get_compat_info(s->crafter), + }; + + ret = ngli_pipeline_compat_init(s->pl_blur_r, ¶ms); + if (ret < 0) + return ret; + + const int32_t index = ngli_pgcraft_get_uniform_index(s->crafter, "tex_coord_matrix", NGLI_PROGRAM_SHADER_VERT); + ngli_assert(index >= 0); + + NGLI_ALIGNED_MAT(tmp_coord_matrix) = NGLI_MAT4_IDENTITY; + ngli_pipeline_compat_update_uniform(s->pl_blur_r, index, tmp_coord_matrix); + + return 0; +} + +static int resize(struct ngl_node *node) +{ + int ret = 0; + struct ngl_ctx *ctx = node->ctx; + struct rblur_priv *s = node->priv_data; + struct rblur_opts *o = node->opts; + + ngli_node_draw(o->source); + + struct texture_priv *src_priv = o->source->priv_data; + const int32_t width = src_priv->image.params.width; + const int32_t height = src_priv->image.params.height; + if (s->width == width && s->height == height) + return 0; + + /* Assert that the destination texture format does not change */ + struct texture_priv *dst_priv = o->destination->priv_data; + ngli_assert(dst_priv->params.format == s->dst_layout.colors[0].format); + + struct texture *dst = NULL; + struct rtt_ctx *dst_rtt_ctx = NULL; + + dst = dst_priv->texture; + if (s->dst_is_resizeable) { + dst = ngli_texture_create(ctx->gpu_ctx); + if (!dst) { + ret = NGL_ERROR_MEMORY; + goto fail; + } + + struct texture_params params = dst_priv->params; + params.width = width; + params.height = height; + ret = ngli_texture_init(dst, ¶ms); + if (ret < 0) + goto fail; + } + + if (s->dst_is_resizeable) { + ngli_texture_freep(&dst_priv->texture); + dst_priv->texture = dst; + dst_priv->image.params.width = dst->params.width; + dst_priv->image.params.height = dst->params.height; + dst_priv->image.planes[0] = dst; + dst_priv->image.rev = dst_priv->image_rev++; + } + + dst_rtt_ctx = ngli_rtt_create(ctx); + if (!dst_rtt_ctx) { + ret = NGL_ERROR_MEMORY; + goto fail; + } + + const struct rtt_params rtt_params = (struct rtt_params) { + .width = dst->params.width, + .height = dst->params.height, + .nb_colors = 1, + .colors[0] = { + .attachment = dst, + .load_op = NGLI_LOAD_OP_CLEAR, + .store_op = NGLI_STORE_OP_STORE, + }, + }; + + ret = ngli_rtt_init(dst_rtt_ctx, &rtt_params); + if (ret < 0) + goto fail; + + ngli_rtt_freep(&s->dst_rtt_ctx); + s->dst_rtt_ctx = dst_rtt_ctx; + + s->width = width; + s->height = height; + + return 0; + +fail: + ngli_rtt_freep(&dst_rtt_ctx); + if (s->dst_is_resizeable) + ngli_texture_freep(&dst); + + LOG(ERROR, "failed to resize blur: %dx%d", width, height); + return ret; +} + +static void rblur_draw(struct ngl_node *node) +{ + struct ngl_ctx *ctx = node->ctx; + struct gpu_ctx *gpu_ctx = ctx->gpu_ctx; + struct rblur_priv *s = node->priv_data; + struct rblur_opts *o = node->opts; + + int ret = resize(node); + if (ret < 0) + return; + + const float amount_raw = *(float *)ngli_node_get_data_ptr(o->amount_node, &o->amount); + float amount = NGLI_CLAMP(amount_raw, -1.f, 1.f); + + const float *center = (float *)ngli_node_get_data_ptr(o->center_node, &o->center); + + ngli_gpu_block_update(&s->blur_params, 0, &(struct params_block) { + .amount = amount, + .center = {center[0], center[1]}, + }); + + ngli_rtt_begin(s->dst_rtt_ctx); + ngli_gpu_ctx_begin_render_pass(gpu_ctx, ctx->current_rendertarget); + ctx->render_pass_started = 1; + if (s->image_rev != s->image->rev) { + ngli_pipeline_compat_update_image(s->pl_blur_r, 0, s->image); + s->image_rev = s->image->rev; + } + ngli_pipeline_compat_draw(s->pl_blur_r, 3, 1); + ngli_rtt_end(s->dst_rtt_ctx); + + /* + * The blur render pass does not deal with the texture coordinates at all, + * thus we need to forward the source coordinates matrix to the + * destination. + */ + struct texture_priv *dst_priv = (struct texture_priv *)o->destination->priv_data; + struct image *dst_image = &dst_priv->image; + memcpy(dst_image->coordinates_matrix, s->image->coordinates_matrix, sizeof(s->image->coordinates_matrix)); +} + +static void rblur_release(struct ngl_node *node) +{ + struct rblur_priv *s = node->priv_data; + + ngli_rtt_freep(&s->dst_rtt_ctx); +} + +static void rblur_uninit(struct ngl_node *node) +{ + struct rblur_priv *s = node->priv_data; + + ngli_gpu_block_reset(&s->blur_params); + ngli_pipeline_compat_freep(&s->pl_blur_r); + ngli_pgcraft_freep(&s->crafter); +} + +const struct node_class ngli_rblur_class = { + .id = NGL_NODE_RADIALBLUR, + .name = "RadialBlur", + .init = rblur_init, + .prepare = ngli_node_prepare_children, + .update = ngli_node_update_children, + .draw = rblur_draw, + .release = rblur_release, + .uninit = rblur_uninit, + .opts_size = sizeof(struct rblur_opts), + .priv_size = sizeof(struct rblur_priv), + .params = rblur_params, + .file = __FILE__, +}; diff --git a/libnopegl/src/nodes_register.h b/libnopegl/src/nodes_register.h index 532d03e12..0c503cc7b 100644 --- a/libnopegl/src/nodes_register.h +++ b/libnopegl/src/nodes_register.h @@ -141,6 +141,7 @@ action(NGL_NODE_PATHKEYMOVE, ngli_pathkeymove_class) \ action(NGL_NODE_PROGRAM, ngli_program_class) \ action(NGL_NODE_QUAD, ngli_quad_class) \ + action(NGL_NODE_RADIALBLUR, ngli_rblur_class) \ action(NGL_NODE_RENDERTOTEXTURE, ngli_rtt_class) \ action(NGL_NODE_RESOURCEPROPS, ngli_resourceprops_class) \ action(NGL_NODE_ROTATE, ngli_rotate_class) \ diff --git a/libnopegl/src/nopegl.h.in b/libnopegl/src/nopegl.h.in index 372d89d86..cb62b8e2f 100644 --- a/libnopegl/src/nopegl.h.in +++ b/libnopegl/src/nopegl.h.in @@ -374,6 +374,7 @@ NGL_API void ngl_scene_unrefp(struct ngl_scene **sp); #define NGL_NODE_PATHKEYMOVE NGLI_FOURCC('P','h','K','0') #define NGL_NODE_PROGRAM NGLI_FOURCC('P','r','g','m') #define NGL_NODE_QUAD NGLI_FOURCC('Q','u','a','d') +#define NGL_NODE_RADIALBLUR NGLI_FOURCC('R','B','l','r') #define NGL_NODE_RENDERTOTEXTURE NGLI_FOURCC('R','T','T',' ') #define NGL_NODE_RESOURCEPROPS NGLI_FOURCC('R','e','s','P') #define NGL_NODE_ROTATE NGLI_FOURCC('T','R','o','t') diff --git a/tests/blur.py b/tests/blur.py index a0577c175..bc937ddd1 100644 --- a/tests/blur.py +++ b/tests/blur.py @@ -66,3 +66,26 @@ def blur_fast_gaussian(cfg: ngl.SceneCfg): ), ) return ngl.Group(children=(blur, ngl.DrawTexture(blurred_texture))) + + +@test_fingerprint(width=256, height=256, keyframes=10, tolerance=1) +@ngl.scene() +def blur_radial_zoom(cfg: ngl.SceneCfg): + cfg.aspect_ratio = (1, 1) + cfg.duration = 10 + + noise = ngl.DrawNoise(type="blocky", octaves=3, scale=(9, 9)) + noise_texture = ngl.Texture2D(data_src=noise) + blurred_texture = ngl.Texture2D() + blur = ngl.RadialBlur( + source=noise_texture, + destination=blurred_texture, + amount=ngl.AnimatedFloat( + [ + ngl.AnimKeyFrameFloat(0, 0), + ngl.AnimKeyFrameFloat(cfg.duration, 1), + ] + ), + center=ngl.UniformVec2(value=(0.5, 0.5), live_id="center"), + ) + return ngl.Group(children=(blur, ngl.DrawTexture(blurred_texture))) diff --git a/tests/meson.build b/tests/meson.build index 5840af7b1..051969fdc 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -92,6 +92,7 @@ foreach backend : backends tests_blur = [ 'gaussian', 'fast_gaussian', + 'radial_zoom', ] tests_color = [ diff --git a/tests/refs/blur_radial_zoom.ref b/tests/refs/blur_radial_zoom.ref new file mode 100644 index 000000000..4a2ffc9ed --- /dev/null +++ b/tests/refs/blur_radial_zoom.ref @@ -0,0 +1,10 @@ +D1F7C0A0777038F3FA181885868C977F D1F7C0A0777038F3FA181885868C977F D1F7C0A0777038F3FA181885868C977F 00000000000000000000000000000000 +C1F780A0777038F3FA181895868C977F C1F780A0777038F3FA181895868C977F C1F780A0777038F3FA181895868C977F 00000000000000000000000000000000 +81F784A0F77038F3FA1C1895028C97FF 81F784A0F77038F3FA1C1895028C97FF 81F784A0F77038F3FA1C1895028C97FF 00000000000000000000000000000000 +81E584A8F77038F7FA1C1895028C97FF 81E584A8F77038F7FA1C1895028C97FF 81E584A8F77038F7FA1C1895028C97FF 00000000000000000000000000000000 +80E584A8F57038F7FA1C0895028D97EB 80E584A8F57038F7FA1C0895028D97EB 80E584A8F57038F7FA1C0895028D97EB 00000000000000000000000000000000 +80E584A8F570B8F7EA1C0895028D97EB 80E584A8F570B8F7EA1C0895028D97EB 80E584A8F570B8F7EA1C0895028D97EB 00000000000000000000000000000000 +80E184A8F5F0B8D7EA1E0895028D17EB 80E184A8F5F0B8D7EA1E0895028D17EB 80E184A8F5F0B8D7EA1E0895028D17EB 00000000000000000000000000000000 +90E184A8E5F0B8D7EA1E0895008D17A3 90E184A8E5F0B8D7EA1E0895008D17A3 90E184A8E5F0B8D7EA1E0895008D17A3 00000000000000000000000000000000 +90E18428E5F0B857EA1E0895008517A3 90E18428E5F0B857EA1E0895008517A3 90E18428E5F0B857EA1E0895008517A3 00000000000000000000000000000000 +90EB8428E5F0F857EA1E0895008517A3 90EB8428E5F0F857EA1E0895008517A3 90EB8428E5F0F857EA1E0895008517A3 00000000000000000000000000000000