├── .gitignore ├── README.md ├── SegmentAddon ├── __init__.py └── resources │ ├── segment.blend │ └── styles │ ├── classic.png │ ├── lcd.png │ ├── missing.png │ └── plain.png ├── documentation_czech.adoc ├── images ├── Oin86iZ0b7.gif ├── classic.png ├── classic_shader.png ├── clock_processor.png ├── create_digit.png ├── display_digit.png ├── display_generation.png ├── done.png ├── example_3d.png ├── example_classic_clock_2.png ├── example_crystal.png ├── example_crystal_2.png ├── example_lcd.png ├── example_plain_clock.png ├── example_plain_clock_2.png ├── example_plain_number.png ├── example_skew.png ├── examples.png ├── examples_low_res.png ├── lcd.png ├── lcd_shader.png ├── load_resources.png ├── overview.png ├── plain.png ├── plain_lcd_shader.png ├── segment.png ├── segment_base.png ├── segment_core.png ├── segment_cover.png ├── segment_material.png ├── setup_material.png └── timer_resolver.png ├── resources ├── segment.blend └── styles │ ├── classic.png │ ├── lcd.png │ ├── missing.png │ └── plain.png └── textures ├── segment_base_mask.png ├── segment_brightness_mask.png ├── segment_masks.psd └── segment_normal.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.blend1 2 | 3 | SegmentAddon.zip 4 | presets.txt 5 | 6 | DEMO7Segment.blend 7 | DEMO8Segment.blend 8 | TowerGenerator.py 9 | icon_view_example 10 | logger-house2.93.5.py 11 | rgblcdcell.py 12 | test.py 13 | test2.py 14 | test3addon.py 15 | tower_obj.blend 16 | garbage 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 7 Segment Display Addon 2 | **A Blender addon for generating animatable 7 segment displays** 3 | 4 | ## Features 5 | 6 | - Generate displays showing any number or time (hours, minutes, seconds and ms) 7 | - Displays can show static values or be animated by the addon 8 | - For example show the current animation frame or count up or down in some timeframe between set values 9 | - Customizable colors and styles 10 | - Powered by nodes but using real geometry 11 | 12 | ![](images/segment_cover.png) 13 | 14 | ## Installation 15 | 16 | > **NOTE**: The addon is made for Blender version 3.0.0+ 17 | 18 | Can be installed like usual by installing the addons _.zip_ archive through the Blender preferences window. 19 | The addon main panel is located in `3D View > Right sidebar > Segment display` 20 | 21 | ## Downloads 22 | 23 | Download `SegmentAddon.zip` from [releases](https://github.com/xDUDSSx/segment-display-blender-addon/releases). 24 | Alternatively you can zip the `SegmentAddon` folder yourself after cloning the repo. 25 | 26 | ## Documentation 27 | 28 | [User guide and technical explanation (**in czech**)](documentation_czech.adoc) 29 | 30 | ## Sources 31 | 32 | [Sharkigator segment display blogpost, original idea](https://sharkigator.wordpress.com/2016/01/15/7-segment-display-tutorial) 33 | 34 | [Video from Jonathan Kron, inspiration for the LCD shader](https://www.youtube.com/watch?v=fJ1WBx3kJaQ) 35 | -------------------------------------------------------------------------------- /SegmentAddon/__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import typing 3 | import copy 4 | import os 5 | import mathutils 6 | from bpy.types import Scene, WindowManager, Image, ShaderNodeTree, ShaderNodeGroup 7 | import bpy.utils.previews 8 | 9 | bl_info = { 10 | "name": "Segment Display Generator", 11 | "description": "Generates 7 segment displays in various formats and styles.", 12 | "author": "DUDSS", 13 | "version": (1, 0, 2), 14 | "blender": (4, 2, 1), 15 | "location": "3D View > Right sidebar > Segment display", 16 | "warning": "", 17 | "doc_url": "https://github.com/xDUDSSx/segment-display-blender-addon", 18 | "tracker_url": "https://github.com/xDUDSSx/segment-display-blender-addon", 19 | "category": "3D View" 20 | } 21 | 22 | ################################################################################ 23 | # DATA 24 | ################################################################################ 25 | 26 | class SegmentAddonData(bpy.types.PropertyGroup): 27 | # Display type 28 | ######################################################################## 29 | display_type: bpy.props.EnumProperty( 30 | name = "Display type", 31 | items = [ 32 | ("numeric", "Numeric", "A display showing decimal numbers, including decimal places."), 33 | ("clock", "Clock", "A display showing time in a specific format (hours, minutes, seconds, milliseconds)"), 34 | ] 35 | ) 36 | digits: bpy.props.IntProperty( 37 | name = "Digits", 38 | min = 1, max = 10, 39 | default = 3 40 | ) 41 | fraction_digits: bpy.props.IntProperty( 42 | name = "Decimal places", 43 | min = 0, max = 10, 44 | default = 2 45 | ) 46 | 47 | millisecond_digits: bpy.props.IntProperty( 48 | name = "Millisecond digits", 49 | min = 0, max = 3, 50 | default = 0 51 | ) 52 | second_digits: bpy.props.IntProperty( 53 | name = "Second digits", 54 | min = 0, max = 2, 55 | default = 2 56 | ) 57 | minute_digits: bpy.props.IntProperty( 58 | name = "Minute digits", 59 | min = 0, max = 2, 60 | default = 2 61 | ) 62 | hour_digits: bpy.props.IntProperty( 63 | name = "Hour digits", 64 | min = 0, max = 3, 65 | default = 0 66 | ) 67 | 68 | show_dot: bpy.props.BoolProperty( 69 | name = "Show dot", 70 | default = True, 71 | description="Show decimal or millisecond dot separator" 72 | ) 73 | show_colons: bpy.props.BoolProperty( 74 | name = "Show colons", 75 | default = True, 76 | description="Separate clock digits with colons" 77 | ) 78 | 79 | # Display value 80 | ######################################################################## 81 | # Display value numeric 82 | display_value_numeric: bpy.props.EnumProperty( 83 | name = "Numeric display value", 84 | items = [ 85 | ("number", "Number", "Show a specific decimal value"), 86 | ("frame", "Frame", "Show the current animation frame"), 87 | ("timer", "Timer", "Animate the display to go from one value to another"), 88 | ] 89 | ) 90 | number: bpy.props.FloatProperty( 91 | name = "Number", 92 | min = 0, 93 | default = 43.12 94 | ) 95 | frame_divisor: bpy.props.FloatProperty( 96 | name = "Divisor", 97 | min = 0, 98 | default = 1, 99 | description = "What number to divide the frame number with. Since frames are integers to get a changing fractional part use a divisor like 1.237" 100 | ) 101 | frame_offset: bpy.props.IntProperty( 102 | name = "Frame offset", 103 | default = 0, 104 | description = "Offsets the current frame number by an amount (Can be negative)" 105 | ) 106 | timer_number_from: bpy.props.FloatProperty( 107 | name = "From number", 108 | min = 0, 109 | default = 1 110 | ) 111 | timer_number_to: bpy.props.FloatProperty( 112 | name = "To number", 113 | min = 0, 114 | default = 50 115 | ) 116 | timer_frame_start: bpy.props.IntProperty( 117 | name = "Frame Start", 118 | min = 0, 119 | default = 50 120 | ) 121 | timer_frame_end: bpy.props.IntProperty( 122 | name = "End", 123 | min = 0, 124 | default = 250 125 | ) 126 | 127 | # Display value clock 128 | display_value_clock: bpy.props.EnumProperty( 129 | name = "Clock display value ", 130 | items = [ 131 | ("seconds", "Seconds", "Show a specific time for a number of seconds"), 132 | ("time", "Time", "Show a specific time"), 133 | ("frame", "Frame", "Show the current animation frame converted to time in seconds"), 134 | ("timer", "Timer", "Animate the display count down/up from one time to another"), 135 | ] 136 | ) 137 | hours: bpy.props.IntProperty( 138 | name = "Hours", 139 | min = 0, 140 | default = 12 141 | ) 142 | minutes: bpy.props.IntProperty( 143 | name = "Minutes", 144 | min = 0, max = 59, 145 | default = 32 146 | ) 147 | seconds: bpy.props.IntProperty( 148 | name = "Seconds", 149 | min = 0, max = 59, 150 | default = 5 151 | ) 152 | milliseconds: bpy.props.IntProperty( 153 | name = "Milliseconds", 154 | min = 0, 155 | default = 374 156 | ) 157 | number_of_seconds: bpy.props.FloatProperty( 158 | name = "Seconds", 159 | min = 0, 160 | default = 3666.143 161 | ) 162 | clock_frame_divisor: bpy.props.FloatProperty( 163 | name = "Divisor", 164 | min = 0, 165 | default = 1, 166 | description = "What number to divide the frame number with. This can be set to the current FPS to show realtime animation time." 167 | ) 168 | timer_time_from: bpy.props.FloatProperty( 169 | name = "From time", 170 | min = 0, 171 | default = 90, 172 | description = "The initial time in seconds" 173 | ) 174 | timer_time_to: bpy.props.FloatProperty( 175 | name = "To time", 176 | min = 0, 177 | default = 0, 178 | description = "The target time in seconds" 179 | ) 180 | 181 | # Appeareance 182 | ######################################################################## 183 | digit_foreground: bpy.props.FloatVectorProperty( 184 | name="Digit foreground", 185 | subtype='COLOR', 186 | default=(1.0, 0.043735, 0), 187 | min=0.0, max=1.0, 188 | description="Color of the lit digit segments" 189 | ) 190 | digit_background: bpy.props.FloatVectorProperty( 191 | name="Digit background", 192 | subtype='COLOR', 193 | default=(1.0, 1.0, 1.0), 194 | min=0.0, max=1.0, 195 | description="Color of the unlit digit segments" 196 | ) 197 | digit_background_override: bpy.props.BoolProperty( 198 | name = "Set manually", 199 | description = "Specify the digit background. Otherwise it is determined from the foreground color", 200 | default = False 201 | ) 202 | background: bpy.props.FloatVectorProperty( 203 | name="Background", 204 | subtype='COLOR', 205 | default=(0.0, 0.0, 0.0), 206 | min=0.0, max=1.0, 207 | description="Color of the display background" 208 | ) 209 | emission_strength: bpy.props.FloatProperty( 210 | name = "Emission strength", 211 | min = 0, 212 | default = 2.5 213 | ) 214 | normal_strength: bpy.props.FloatProperty( 215 | name = "Normal strength", 216 | min = 0, 217 | default = 0.5 218 | ) 219 | hide_background: bpy.props.BoolProperty( 220 | name = "Remove background", 221 | default = False, 222 | description="Deletes the faces making up the background, leaving just the digit segments" 223 | ) 224 | skew: bpy.props.FloatProperty( 225 | name = "Skew", 226 | default = 0, 227 | min = -1.5, max = 1.5, 228 | step = 0.05, 229 | description = "Skews the display" 230 | ) 231 | extrude: bpy.props.FloatProperty( 232 | name = "Extrude to 3D", 233 | default = 0, 234 | min = -20, max = 20, 235 | description = "Extrudes the display on the Z axis. This may cause issues/glitches with style shaders!" 236 | ) 237 | 238 | #style: bpy.props.EnumProperty - Set during register() 239 | 240 | # Classic style 241 | background_noise_strength: bpy.props.FloatProperty( 242 | name = "BG noise strength", 243 | min = 0, 244 | default = 1 245 | ) 246 | background_noise_scale: bpy.props.FloatProperty( 247 | name = "BG noise scale", 248 | default = 2 249 | ) 250 | 251 | # LCD style 252 | lcd_cell_width: bpy.props.FloatProperty( 253 | name = "Pixel width", 254 | description = "Pixel width factor", 255 | min = 0, 256 | default = 1 257 | ) 258 | lcd_cell_height: bpy.props.FloatProperty( 259 | name = "Pixel height", 260 | description = "Pixel height factor", 261 | min = 0, 262 | default = 1 263 | ) 264 | lcd_scale: bpy.props.FloatProperty( 265 | name = "Scale", 266 | default = 2 267 | ) 268 | lcd_unit_strength: bpy.props.FloatProperty( 269 | name = "Unlit strength", 270 | description = "How bright unlit lcd pixels should appear", 271 | default = 0.010 272 | ) 273 | lcd_cell_border_x_width: bpy.props.FloatProperty( 274 | name = "Pixel x gap", 275 | description = "Gap between pixels on the x axis", 276 | default = 0.1 277 | ) 278 | lcd_cell_border_y_width: bpy.props.FloatProperty( 279 | name = "Pixel y gap", 280 | description = "Gap between pixels on the y axis", 281 | default = 0.1 282 | ) 283 | lcd_cell_subpixel_border_width: bpy.props.FloatProperty( 284 | name = "Subpixel gap", 285 | description = "Gap between individual r,g,b subpixels", 286 | default = 0.02 287 | ) 288 | 289 | 290 | 291 | # Advanced 292 | ######################################################################## 293 | float_correction: bpy.props.FloatProperty( 294 | name = "Float correction", 295 | min = 0, 296 | default = 0.0001 297 | ) 298 | auto_background_dim_factor: bpy.props.FloatProperty( 299 | name = "Auto digit BG dim factor", 300 | description = "If digit background is set to auto, it is set to the foreground color multiplied by this factor.", 301 | default = 0.035 302 | ) 303 | object_scale: bpy.props.FloatProperty( 304 | name = "Object scale", 305 | default = 0.1 306 | ) 307 | join_display: bpy.props.BoolProperty( 308 | name = "Join display into a single object", 309 | default = True 310 | ) 311 | fuse_display: bpy.props.BoolProperty( 312 | name = "Physically merge neighbouring digit vertexes", 313 | default = True 314 | ) 315 | 316 | 317 | ################################################################################ 318 | # UI 319 | ################################################################################ 320 | 321 | class SegmentPanel(): 322 | bl_space_type = 'VIEW_3D' 323 | bl_region_type = 'UI' 324 | 325 | @classmethod 326 | def poll(cls, context): 327 | return True 328 | 329 | 330 | class MainPanel(SegmentPanel, bpy.types.Panel): 331 | bl_label = "Segment display" 332 | bl_idname = "SEGMENT_PT_main_panel" 333 | bl_category = "Segment display" 334 | bl_space_type = 'VIEW_3D' 335 | bl_region_type = 'UI' 336 | 337 | def draw(self, context): 338 | layout = self.layout 339 | scene = context.scene 340 | data = scene.segment_addon_data 341 | 342 | layout.label(text="Generate 7 segment displays", icon="INFO") 343 | 344 | class DisplayTypePanel(SegmentPanel, bpy.types.Panel): 345 | bl_label = "Display type / format" 346 | bl_idname = "SEGMENT_PT_display_type_panel" 347 | bl_parent_id = "SEGMENT_PT_main_panel" 348 | #bl_options = {'DEFAULT_CLOSED'} 349 | 350 | def draw(self, context): 351 | layout = self.layout 352 | scene = context.scene 353 | data = scene.segment_addon_data 354 | 355 | layout.prop(data, "display_type", expand=True) 356 | 357 | layout.use_property_split = True 358 | layout.use_property_decorate = False 359 | col = layout.column(align=True) 360 | 361 | if data.display_type == "numeric": 362 | col.prop(data, "digits") 363 | col.prop(data, "fraction_digits") 364 | col.separator() 365 | col.prop(data, "show_dot") 366 | elif data.display_type == "clock": 367 | col.prop(data, "hour_digits") 368 | col.prop(data, "minute_digits") 369 | col.prop(data, "second_digits") 370 | col.prop(data, "millisecond_digits") 371 | col.separator() 372 | col.prop(data, "show_dot") 373 | col.prop(data, "show_colons") 374 | 375 | 376 | class DisplayValuePanel(SegmentPanel, bpy.types.Panel): 377 | bl_label = "Display value" 378 | bl_idname = "SEGMENT_PT_display_value" 379 | bl_parent_id = "SEGMENT_PT_main_panel" 380 | #bl_options = {'DEFAULT_CLOSED'} 381 | 382 | def draw(self, context): 383 | layout = self.layout 384 | scene = context.scene 385 | data = scene.segment_addon_data 386 | 387 | if data.display_type == "numeric": 388 | layout.prop(data, "display_value_numeric", expand=True) 389 | 390 | layout.use_property_split = True 391 | layout.use_property_decorate = False 392 | col = layout.column(align=True) 393 | 394 | if data.display_value_numeric == "number": 395 | col.prop(data, "number") 396 | elif data.display_value_numeric == "frame": 397 | col.prop(data, "frame_divisor") 398 | col.prop(data, "frame_offset") 399 | elif data.display_value_numeric == "timer": 400 | col.prop(data, "timer_number_from") 401 | col.prop(data, "timer_number_to") 402 | col.separator() 403 | col.prop(data, "timer_frame_start") 404 | col.prop(data, "timer_frame_end") 405 | 406 | elif data.display_type == "clock": 407 | layout.prop(data, "display_value_clock", expand=True) 408 | 409 | if data.display_value_clock == "seconds": 410 | layout.use_property_split = True 411 | layout.use_property_decorate = False 412 | col = layout.column(align=True) 413 | col.prop(data, "number_of_seconds") 414 | elif data.display_value_clock == "time": 415 | row = layout.row(align=True) 416 | row.label(text="Time to show (H, M, S, Ms):", icon="TIME") 417 | row = layout.row(align=True) 418 | row.prop(data, "hours", text="") 419 | row.prop(data, "minutes", text="") 420 | row.prop(data, "seconds", text="") 421 | row.prop(data, "milliseconds", text="") 422 | elif data.display_value_clock == "frame": 423 | layout.use_property_split = True 424 | layout.use_property_decorate = False 425 | col = layout.column(align=True) 426 | col.prop(data, "clock_frame_divisor") 427 | col.prop(data, "frame_offset") 428 | elif data.display_value_clock == "timer": 429 | layout.use_property_split = True 430 | layout.use_property_decorate = False 431 | col = layout.column(align=True) 432 | col.prop(data, "timer_time_from") 433 | col.prop(data, "timer_time_to") 434 | col.separator() 435 | col.prop(data, "timer_frame_start") 436 | col.prop(data, "timer_frame_end") 437 | 438 | 439 | class DisplayAppearancePanel(SegmentPanel, bpy.types.Panel): 440 | bl_label = "Appeareance" 441 | bl_idname = "SEGMENT_PT_display_appearance" 442 | bl_parent_id = "SEGMENT_PT_main_panel" 443 | #bl_options = {'DEFAULT_CLOSED'} 444 | 445 | def draw(self, context): 446 | layout = self.layout 447 | scene = context.scene 448 | data = scene.segment_addon_data 449 | 450 | layout.use_property_split = True 451 | layout.use_property_decorate = False 452 | 453 | col = layout.column(align=True) 454 | col.prop(data, "digit_foreground") 455 | col2 = col.column(align=True) 456 | col2.enabled = data.digit_background_override 457 | col2.prop(data, "digit_background") 458 | col.prop(data, "digit_background_override") 459 | col.prop(data, "emission_strength") 460 | col.prop(data, "normal_strength") 461 | col.separator() 462 | col.prop(data, "background") 463 | col.prop(data, "hide_background") 464 | col.separator() 465 | col.prop(data, "skew") 466 | col.prop(data, "extrude") 467 | 468 | class DisplayStylePanel(SegmentPanel, bpy.types.Panel): 469 | bl_label = "Display style" 470 | bl_idname = "SEGMENT_PT_display_style" 471 | bl_parent_id = "SEGMENT_PT_display_appearance" 472 | 473 | def draw(self, context): 474 | layout = self.layout 475 | scene = context.scene 476 | data = scene.segment_addon_data 477 | 478 | layout.prop(data, "style", text="", icon="BLANK1") 479 | layout.template_icon_view(data, "style", show_labels=True) 480 | 481 | layout.use_property_split = True 482 | layout.use_property_decorate = False 483 | 484 | if data.style == 'classic': 485 | col = layout.column(align=True) 486 | col.prop(data, "background_noise_strength") 487 | col.prop(data, "background_noise_scale") 488 | elif data.style == 'lcd': 489 | col = layout.column(align=True) 490 | col.prop(data, "lcd_cell_width") 491 | col.prop(data, "lcd_cell_height") 492 | col.separator() 493 | col.prop(data, "lcd_scale") 494 | col.prop(data, "lcd_unit_strength") 495 | col.separator() 496 | col.prop(data, "lcd_cell_border_x_width") 497 | col.prop(data, "lcd_cell_border_y_width") 498 | col.prop(data, "lcd_cell_subpixel_border_width") 499 | 500 | class AdvancedPanel(SegmentPanel, bpy.types.Panel): 501 | bl_label = "Advanced" 502 | bl_idname = "SEGMENT_PT_advanced" 503 | bl_parent_id = "SEGMENT_PT_main_panel" 504 | bl_options = {'DEFAULT_CLOSED'} 505 | 506 | def draw(self, context): 507 | layout = self.layout 508 | scene = context.scene 509 | data = scene.segment_addon_data 510 | 511 | layout.use_property_split = True 512 | layout.use_property_decorate = False 513 | col = layout.column(align=True) 514 | col.prop(data, "object_scale") 515 | col.prop(data, "float_correction") 516 | col.prop(data, "auto_background_dim_factor") 517 | 518 | layout.use_property_split = False 519 | layout.prop(data, "join_display") 520 | layout.prop(data, "fuse_display") 521 | 522 | layout.operator("segment_addon.reset_to_defaults") 523 | 524 | 525 | class GeneratePanel(SegmentPanel, bpy.types.Panel): 526 | bl_label = "Generate display" 527 | bl_idname = "SEGMENT_PT_generate_display" 528 | bl_parent_id = "SEGMENT_PT_main_panel" 529 | 530 | def draw(self, context): 531 | layout = self.layout 532 | scene = context.scene 533 | data = scene.segment_addon_data 534 | 535 | layout.operator("segment_addon.create", icon="RESTRICT_VIEW_OFF") 536 | 537 | 538 | ################################################################################ 539 | # OPERATORS 540 | ################################################################################ 541 | 542 | class CreateDisplayOperator(bpy.types.Operator): 543 | bl_idname = "segment_addon.create" 544 | bl_label = "Create display" 545 | bl_description = "Create the segment display" 546 | bl_options = {'REGISTER', 'UNDO'} 547 | 548 | def execute(self, context): 549 | scene = context.scene 550 | data = scene.segment_addon_data 551 | 552 | print(f"Creating segment display [digits: {data.digits}]") 553 | print("Blend file path: " + SegmentAddon.addon_blend_path) 554 | 555 | # Load resources from the segment blend file 556 | link = False 557 | with bpy.data.libraries.load(SegmentAddon.addon_blend_path, link=link) as (data_src, data_dst): 558 | # Objects that only need to be loaded once 559 | objects_to_load = [SEGMENT_DIGIT, SEGMENT_EMPTY, SEGMENT_DOT, SEGMENT_COLON] 560 | for o in objects_to_load: 561 | if o not in bpy.data.objects: 562 | print("Appending prototype object: " + str(o)) 563 | data_dst.objects.append(o) 564 | # Avoid duplicates 565 | 566 | #data_dst.meshes = ['segment_digit_mesh'] 567 | #data_dst.objects = ['segment_digit', 'segment_empty', 'segment_dot', 'segment_colon'] 568 | data_dst.materials = ['.7SegmentDisplay', '.7SegmentDisplayBackground'] 569 | data_dst.node_groups = [ 570 | '.7SegmentDecimalProcessor', '.7SegmentClockProcessor', 571 | '.7SegmentTimerResolver', 572 | '.7SegmentClassicShader', '.7SegmentPlainShader', '.7SegmentPlainLCDShader' 573 | ] 574 | resource = data_dst 575 | 576 | segment_addon = SegmentAddon(context, data, resource) 577 | if not segment_addon.validate_data(): 578 | msg = "SegmentDisplayAddon: Invalid display type!" 579 | print(msg) 580 | self.report({'ERROR'}, msg) 581 | 582 | # Setup material 583 | segment_addon.setup_segment_material() 584 | 585 | # Generate digits 586 | digit_prototype = bpy.data.objects[SEGMENT_DIGIT] 587 | generated_objects = [] 588 | 589 | if data.display_type == "numeric": 590 | segment_addon.create_numeric_display(digit_prototype, generated_objects) 591 | elif data.display_type == "clock": 592 | segment_addon.create_clock_display(digit_prototype, generated_objects) 593 | 594 | # Join objects 595 | print(f"SegmentDisplayAddon: Generated {len(generated_objects)} objects!") 596 | bpy.ops.object.select_all(action='DESELECT') 597 | for o in generated_objects: 598 | o.select_set(True) 599 | 600 | if len(generated_objects) > 0: 601 | active_obj = generated_objects[0] 602 | bpy.context.view_layer.objects.active = active_obj 603 | 604 | # Joining 605 | if data.join_display: 606 | bpy.ops.object.join() 607 | # Rename joined object 608 | active_obj.name = "SegmentDisplay" + data.display_type.capitalize() + data.style.capitalize() 609 | else: 610 | # Name individual objects 611 | for i, o in enumerate(generated_objects, 1): 612 | o.name = "SegmentDisplay" + data.display_type.capitalize() + data.style.capitalize() + "_digit_" + str(i) 613 | 614 | # Remove doubles 615 | if data.fuse_display: 616 | bpy.ops.object.mode_set(mode='EDIT') 617 | bpy.ops.mesh.select_all(action='SELECT') 618 | bpy.ops.mesh.remove_doubles() 619 | bpy.ops.object.mode_set(mode='OBJECT') 620 | 621 | # Apply skew 622 | if data.skew > 0: 623 | skew_value = data.skew 624 | skew_value *= -1 625 | bpy.ops.object.mode_set(mode='EDIT') 626 | bpy.ops.mesh.select_all(action='SELECT') 627 | bpy.ops.transform.shear(value=skew_value, orient_axis='Z', orient_type='GLOBAL') 628 | bpy.ops.object.mode_set(mode='OBJECT') 629 | 630 | # Delete background 631 | if data.hide_background: 632 | bpy.ops.object.mode_set(mode='EDIT') 633 | bpy.ops.mesh.select_all(action='DESELECT') 634 | bpy.context.object.active_material_index = 1 635 | old_select_mode = SegmentAddon.get_select_mode() 636 | bpy.ops.mesh.select_mode(type="FACE") 637 | bpy.ops.object.material_slot_select() 638 | bpy.ops.mesh.select_all(action='INVERT') 639 | bpy.ops.mesh.delete(type='FACE') 640 | bpy.ops.mesh.select_mode(type=old_select_mode) 641 | bpy.ops.object.mode_set(mode='OBJECT') 642 | 643 | # Extrude 644 | if data.extrude != 0: 645 | bpy.ops.object.mode_set(mode='EDIT') 646 | bpy.ops.mesh.select_all(action='SELECT') 647 | extrude_value = data.extrude 648 | bpy.ops.mesh.extrude_region_move(MESH_OT_extrude_region={'use_normal_flip': False, 649 | 'use_dissolve_ortho_edges': False, 650 | 'mirror': False}, 651 | TRANSFORM_OT_translate={ 652 | 'value': (0, 0, extrude_value), 653 | 'orient_type': 'NORMAL', 654 | 'constraint_axis': (False, False, True), 655 | 'mirror': False, 656 | 'use_proportional_edit': False, 657 | 'proportional_edit_falloff': 'SMOOTH', 658 | 'proportional_size': 1, 659 | 'use_proportional_connected': False, 660 | 'use_proportional_projected': False, 661 | 'snap': False, 662 | 'snap_target': 'CLOSEST', 663 | 'snap_point': (0, 0, 0), 664 | 'snap_align': False, 665 | 'snap_normal': (0, 0, 0), 666 | 'gpencil_strokes': False, 667 | 'cursor_transform': False, 668 | 'texture_space': False, 669 | 'remove_on_cancel': False, 670 | 'view2d_edge_pan': False, 671 | 'release_confirm': False, 672 | 'use_accurate': False, 673 | 'use_automerge_and_split': False, 674 | }) 675 | bpy.ops.object.mode_set(mode='OBJECT') 676 | 677 | # Move to origin and scale 678 | cursor = bpy.context.scene.cursor.location 679 | # if not data.join_display: 680 | # # We have to move and scale all objects 681 | # for i, o in enumerate(generated_objects, 1): 682 | # o.name = "SegmentDisplay" + data.display_type.capitalize() + data.style.capitalize() + "_digit_" + str(i) 683 | # active_obj.name = "SegmentDisplay" + data.display_type.capitalize() + data.style.capitalize() 684 | # else: 685 | # # Just scale the active object 686 | 687 | bpy.ops.transform.translate( 688 | value=(cursor[0], cursor[1], cursor[2]), 689 | orient_type='GLOBAL', 690 | orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), 691 | orient_matrix_type='GLOBAL', 692 | mirror=False, 693 | use_proportional_edit=False, 694 | proportional_edit_falloff='SMOOTH', 695 | proportional_size=1, 696 | use_proportional_connected=False, 697 | use_proportional_projected=False, 698 | ) 699 | 700 | old_pivot_point = bpy.context.scene.tool_settings.transform_pivot_point 701 | bpy.context.scene.tool_settings.transform_pivot_point = 'ACTIVE_ELEMENT' 702 | scale_factor = data.object_scale 703 | bpy.ops.transform.resize( 704 | value=(scale_factor, scale_factor, scale_factor), 705 | orient_type='GLOBAL', 706 | orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), 707 | orient_matrix_type='GLOBAL', 708 | mirror=False, 709 | use_proportional_edit=False, 710 | proportional_edit_falloff='SMOOTH', 711 | proportional_size=1, 712 | use_proportional_connected=False, 713 | use_proportional_projected=False, 714 | ) 715 | bpy.context.scene.tool_settings.transform_pivot_point = old_pivot_point 716 | 717 | 718 | else: 719 | self.report({'WARNING'}, "SegmentDisplayAddon: No objects generated!") 720 | 721 | return {'FINISHED'} 722 | 723 | 724 | class ResetToDefaultsOperator(bpy.types.Operator): 725 | bl_idname = "segment_addon.reset_to_defaults" 726 | bl_label = "Reset settings to defaults" 727 | bl_description = "Resets all the current settings to their defaults" 728 | bl_options = {'REGISTER', 'UNDO'} 729 | 730 | def execute(self, context): 731 | scene = context.scene 732 | data = scene.segment_addon_data 733 | print(f"SegmentAddon: Resetting settings to defaults.") 734 | scene.property_unset("segment_addon_data") 735 | self.report({'INFO'}, "SegmentDisplayAddon: Reset settings to defaults.") 736 | return {'FINISHED'} 737 | 738 | 739 | ################################################################################ 740 | # CORE 741 | ################################################################################ 742 | 743 | SEGMENT_DIGIT = 'segment_digit' 744 | SEGMENT_EMPTY = 'segment_empty' 745 | SEGMENT_DOT = 'segment_dot' 746 | SEGMENT_COLON = 'segment_colon' 747 | 748 | class SegmentAddon: 749 | VC_STEP = 0.1 750 | VC_STEP_FAILSAFE = 0.01 751 | DIGIT_WIDTH = 12 752 | DIGIT_SEPARATOR_WIDTH = 3 753 | 754 | addon_directory_path = None 755 | addon_resources_dir = None 756 | addon_blend_path = None 757 | style_previews_dir = None 758 | 759 | previews = dict() 760 | style_previews = "styles_preview" 761 | 762 | def __init__(self, context, data: SegmentAddonData, resource): 763 | self.context = context 764 | self.data = data 765 | self.resource = resource 766 | 767 | def validate_data(self) -> bool: 768 | if self.data.display_type == "numeric": 769 | pass 770 | elif self.data.display_type == "clock": 771 | pass 772 | else: 773 | return False 774 | return True 775 | 776 | def create_numeric_display(self, digit_prototype, generated): 777 | offset_step = self.DIGIT_WIDTH 778 | offset = 0 779 | 780 | offset = self.create_digits(digit_prototype, offset, self.data.fraction_digits/10.0, self.data.fraction_digits, offset_step, generated) 781 | if self.data.fraction_digits > 0: 782 | self.create_dot(offset, generated) 783 | offset -= self.DIGIT_SEPARATOR_WIDTH 784 | offset = self.create_digits(digit_prototype, offset, 0, self.data.digits, offset_step, generated) 785 | 786 | def create_clock_display(self, digit_prototype, generated): 787 | offset_step = self.DIGIT_WIDTH 788 | offset = 0 789 | 790 | h = self.data.hour_digits 791 | m = self.data.minute_digits 792 | s = self.data.second_digits 793 | ms = self.data.millisecond_digits 794 | 795 | if ms > 0: 796 | offset = self.create_digits(digit_prototype, offset, 0, ms, offset_step, generated) 797 | if s > 0 or m > 0 or h > 0: 798 | self.create_dot(offset, generated) 799 | offset -= self.DIGIT_SEPARATOR_WIDTH 800 | if s > 0: 801 | offset = self.create_digits(digit_prototype, offset, 0.1, s, offset_step, generated) 802 | if m > 0 or h > 0: 803 | self.create_colon(offset, generated) 804 | offset -= self.DIGIT_SEPARATOR_WIDTH 805 | if m > 0: 806 | offset = self.create_digits(digit_prototype, offset, 0.2, m, offset_step, generated) 807 | if h > 0: 808 | self.create_colon(offset, generated) 809 | offset -= self.DIGIT_SEPARATOR_WIDTH 810 | if h > 0: 811 | offset = self.create_digits(digit_prototype, offset, 0.3, h, offset_step, generated) 812 | 813 | def setup_segment_material(self): 814 | mat = self.resource.materials[0] 815 | self.setup_segment_display_processor(mat) 816 | self.setup_display_value(mat) 817 | self.setup_display_shader(mat) 818 | 819 | bg_mat = self.resource.materials[1] 820 | self.setup_background_material(bg_mat) 821 | 822 | # Float correction 823 | mat.node_tree.nodes["segment_base"].inputs[2].default_value = self.data.float_correction 824 | 825 | def setup_background_material(self, mat): 826 | mat.node_tree.nodes['RGB'].outputs[0].default_value = (self.data.background.r, self.data.background.g, self.data.background.b, 1.0) 827 | 828 | def setup_display_shader(self, mat): 829 | """ 830 | Sets up the node group responsible for styling the segment base mask output. 831 | This is referred to as "display style" in the settings. 832 | """ 833 | # Create the appropriate shader depending on settings 834 | shader_node_group = self.create_display_style_shader(mat) 835 | Utils.move_node(shader_node_group, 1020, 671) 836 | shader_node_group.width = 212 837 | 838 | # Connect mask to the shader group 839 | mat.node_tree.links.new(mat.node_tree.nodes['segment_base'].outputs[0], shader_node_group.inputs[0]) 840 | 841 | # Set style specific settings 842 | if self.data.style == "plain": 843 | pass 844 | elif self.data.style == "classic": 845 | shader_node_group.inputs[5].default_value = self.data.background_noise_strength 846 | shader_node_group.inputs[6].default_value = self.data.background_noise_scale 847 | elif self.data.style == "lcd": 848 | shader_node_group.inputs[5].default_value = self.data.lcd_cell_width 849 | shader_node_group.inputs[6].default_value = self.data.lcd_cell_height 850 | shader_node_group.inputs[7].default_value = self.data.lcd_scale 851 | shader_node_group.inputs[8].default_value = self.data.lcd_unit_strength 852 | 853 | # Process and set the rgb cell border 854 | cell_x_ramp = shader_node_group.node_tree.nodes['segment_lcd_shader'].node_tree.nodes['cell_x_ramp'] 855 | cell_y_ramp = shader_node_group.node_tree.nodes['segment_lcd_shader'].node_tree.nodes['cell_y_ramp'] 856 | x_points = SegmentAddon.lcd_style_calculate_x_ramp(self.data.lcd_cell_border_x_width, self.data.lcd_cell_subpixel_border_width) 857 | y_points = SegmentAddon.lcd_style_calculate_y_ramp(self.data.lcd_cell_border_y_width) 858 | print(x_points) 859 | print(y_points) 860 | for i, p in enumerate(x_points): 861 | cell_x_ramp.color_ramp.elements[i+1].position = p 862 | for i, p in enumerate(y_points): 863 | cell_y_ramp.color_ramp.elements[i+1].position = p 864 | 865 | # Set common settings 866 | digit_foreground = self.color_property_to_rgba_tuple(self.data.digit_foreground) 867 | digit_background = self.color_property_to_rgba_tuple(self.data.digit_background) 868 | if not self.data.digit_background_override: 869 | if (self.data.style == 'lcd'): 870 | digit_background = (0., 0., 0., 1.) 871 | else: 872 | dim_factor = self.data.auto_background_dim_factor 873 | digit_background = self.rgba_tuple_multiply(digit_foreground, dim_factor) 874 | 875 | shader_node_group.inputs[1].default_value = digit_foreground 876 | shader_node_group.inputs[2].default_value = digit_background 877 | shader_node_group.inputs[3].default_value = self.data.emission_strength 878 | shader_node_group.inputs[4].default_value = self.data.normal_strength 879 | 880 | # Connect the shader group to the principled shader 881 | principled = mat.node_tree.nodes['segment_principled'] 882 | mat.node_tree.links.new(shader_node_group.outputs[0], principled.inputs['Base Color']) # Base 883 | mat.node_tree.links.new(shader_node_group.outputs[1], principled.inputs['Roughness']) # Roughness 884 | mat.node_tree.links.new(shader_node_group.outputs[2], principled.inputs['Emission Color']) # Emission 885 | mat.node_tree.links.new(shader_node_group.outputs[3], principled.inputs['Emission Strength']) # Emission strength 886 | mat.node_tree.links.new(shader_node_group.outputs[4], principled.inputs['Normal']) # Normal 887 | 888 | def create_display_style_shader(self, mat): 889 | node_tree = None 890 | 891 | if self.data.style == "plain": 892 | node_tree = self.resource.node_groups[4] 893 | elif self.data.style == "classic": 894 | node_tree = self.resource.node_groups[3] 895 | elif self.data.style == 'lcd': 896 | node_tree = self.resource.node_groups[5] 897 | 898 | # Rename to be unique 899 | 900 | 901 | node_group = mat.node_tree.nodes.new(type='ShaderNodeGroup') 902 | node_group.node_tree = node_tree 903 | node_group.name = node_tree.name 904 | return node_group 905 | 906 | def setup_display_value(self, mat): 907 | """ 908 | Sets up the 7SegmentBase number input to reflect display value settings. 909 | """ 910 | if self.data.display_type == "numeric": 911 | if self.data.display_value_numeric == "number": 912 | self.setup_numeric_number_display_value(mat, self.data.number) 913 | elif self.data.display_value_numeric == "frame": 914 | self.setup_numeric_frame_display_value(mat, self.data.frame_divisor) 915 | elif self.data.display_value_numeric == "timer": 916 | self.setup_numeric_timer_display_value(mat, self.data.timer_number_from, self.data.timer_number_to) 917 | elif self.data.display_type == 'clock': 918 | if self.data.display_value_clock == "seconds": 919 | self.setup_numeric_number_display_value(mat, self.data.number_of_seconds) 920 | elif self.data.display_value_clock == "time": 921 | self.setup_clock_time_display_value(mat) 922 | elif self.data.display_value_clock == "frame": 923 | self.setup_numeric_frame_display_value(mat, self.data.clock_frame_divisor) 924 | elif self.data.display_value_clock == "timer": 925 | self.setup_numeric_timer_display_value(mat, self.data.timer_time_from, self.data.timer_time_to) 926 | 927 | def setup_numeric_number_display_value(self, mat, number): 928 | """ 929 | Connects a simple value node to the number input. 930 | """ 931 | # Get number from the settings and set it as input for the segment base group 932 | value_node = mat.node_tree.nodes.new(type="ShaderNodeValue") 933 | segment_base_group = mat.node_tree.nodes['segment_base'] 934 | 935 | value_node.outputs[0].default_value = number 936 | mat.node_tree.links.new(value_node.outputs[0], segment_base_group.inputs[0]) 937 | Utils.move_node(value_node, 450, 300) 938 | 939 | # Set divisor to 1 940 | mat.node_tree.nodes["segment_base"].inputs[1].default_value = 1 941 | 942 | def setup_numeric_frame_display_value(self, mat, divisor): 943 | """ 944 | Connects an animated frame value node to the number input. 945 | """ 946 | frame_node = Utils.create_frame_value_node(mat.node_tree, self.data.frame_offset) 947 | segment_base_group = mat.node_tree.nodes['segment_base'] 948 | 949 | mat.node_tree.links.new(frame_node.outputs[0], segment_base_group.inputs[0]) 950 | mat.node_tree.nodes["segment_base"].inputs[1].default_value = divisor 951 | 952 | Utils.move_node(frame_node, 450, 300) 953 | 954 | def setup_numeric_timer_display_value(self, mat, from_value, to_value): 955 | """ 956 | Timer is realized by running the frame value through the 7SegmentTimerResolver node. 957 | """ 958 | # Create frame node 959 | frame_node = Utils.create_frame_value_node(mat.node_tree) 960 | segment_base_group = mat.node_tree.nodes['segment_base'] 961 | Utils.move_node(frame_node, 450-190, 300) 962 | 963 | # Create timer resolver group 964 | timer_resolver_tree = self.resource.node_groups[2] 965 | timer_resolver_group = mat.node_tree.nodes.new(type='ShaderNodeGroup') 966 | timer_resolver_group.node_tree = timer_resolver_tree 967 | timer_resolver_group.name = timer_resolver_tree.name 968 | Utils.move_node(timer_resolver_group, 463, 370) 969 | 970 | # Link nodes 971 | mat.node_tree.links.new(frame_node.outputs[0], timer_resolver_group.inputs[4]) 972 | mat.node_tree.links.new(timer_resolver_group.outputs[0], segment_base_group.inputs[0]) 973 | 974 | # Set divisor to 1 975 | mat.node_tree.nodes["segment_base"].inputs[1].default_value = 1 976 | 977 | # Set remaining timer resolver inputs according to settings 978 | timer_resolver_group.inputs[0].default_value = from_value 979 | timer_resolver_group.inputs[1].default_value = to_value 980 | timer_resolver_group.inputs[2].default_value = self.data.timer_frame_start 981 | timer_resolver_group.inputs[3].default_value = self.data.timer_frame_end 982 | 983 | def setup_clock_time_display_value(self, mat): 984 | hours = self.data.hours 985 | minutes = self.data.minutes; 986 | seconds = self.data.seconds 987 | milliseconds = self.data.milliseconds 988 | number = hours * 3600 + minutes * 60 + seconds + (milliseconds/1000) 989 | self.setup_numeric_number_display_value(mat, number) 990 | 991 | def setup_segment_display_processor(self, mat): 992 | """ 993 | Sets the node group responsible for adjusting the 7SegmentCore number input using the display vertex color. 994 | """ 995 | segment_base_group = mat.node_tree.nodes['segment_base'] 996 | number_node = segment_base_group.node_tree.nodes['number_adjusted'] 997 | display_node = segment_base_group.node_tree.nodes['display_converted'] 998 | 999 | processor_name = "" 1000 | processor_node_tree = None 1001 | if self.data.display_type == "numeric": 1002 | processor_node_tree = self.resource.node_groups[0] 1003 | processor_name = "Decimal" 1004 | elif self.data.display_type == "clock": 1005 | processor_node_tree = self.resource.node_groups[1] 1006 | processor_name = "Clock" 1007 | 1008 | processor_node_group = segment_base_group.node_tree.nodes.new(type='ShaderNodeGroup') 1009 | processor_node_group.node_tree = processor_node_tree 1010 | processor_node_group.name = f"7Segment{processor_name}Processor" 1011 | processor_node_group.location = number_node.location[0] + 280, number_node.location[1] + 70 1012 | 1013 | segment_core_group = segment_base_group.node_tree.nodes['segment_core'] 1014 | 1015 | segment_base_group.node_tree.links.new(number_node.outputs[0], processor_node_group.inputs[0]) 1016 | segment_base_group.node_tree.links.new(display_node.outputs[0], processor_node_group.inputs[1]) 1017 | segment_base_group.node_tree.links.new(processor_node_group.outputs[0], segment_core_group.inputs[0]) 1018 | 1019 | def create_digits(self, digit_prototype, offset, display, digit_count, offset_step, generated): 1020 | for i in range(0, digit_count): 1021 | self.create_digit(digit_prototype, offset, display, self.VC_STEP*i, generated) 1022 | offset -= offset_step 1023 | return offset 1024 | 1025 | def create_segment(self, prototype): 1026 | obj = Utils.copy_object(prototype) 1027 | obj.location = (0, 0, 0) 1028 | bpy.context.collection.objects.link(obj) 1029 | 1030 | bpy.ops.object.select_all(action='DESELECT') 1031 | bpy.context.view_layer.objects.active = obj 1032 | obj.select_set(True) 1033 | 1034 | self.assign_segment_materials() 1035 | return obj 1036 | 1037 | def create_digit(self, digit_prototype, offset, display, digit, generated): 1038 | """ 1039 | Creates a segment digit. 1040 | 1041 | Assumes: 1042 | No materials / slots 1043 | "Segment" vertex color map defining segments 1044 | "segments" boolean face attribute for active areas 1045 | """ 1046 | obj = Utils.copy_object(digit_prototype) 1047 | obj.location = (0, 0, 0) 1048 | bpy.context.collection.objects.link(obj) 1049 | 1050 | bpy.ops.object.select_all(action='DESELECT') 1051 | bpy.context.view_layer.objects.active = obj 1052 | obj.select_set(True) 1053 | 1054 | self.assign_segment_materials() 1055 | 1056 | mesh = obj.data 1057 | self.create_vertex_color_map(mesh, "Digit", digit) 1058 | self.create_vertex_color_map(mesh, "Display", display) 1059 | 1060 | # Move by offseta 1061 | obj.location.x += offset 1062 | 1063 | generated.append(obj) 1064 | 1065 | def create_aux(self, prototype, offset, generated): 1066 | """ 1067 | Creates an auxilliary segment like a dot or a colon. 1068 | The segment will be always on. 1069 | 1070 | Assumes: 1071 | No materials / slots 1072 | No vertex colors 1073 | "segments" boolean face attribute for active areas 1074 | """ 1075 | obj = self.create_segment(prototype) 1076 | 1077 | # Paint the segment mask override signal 1078 | # Makes SegmentBase always output "1" as a mask value 1079 | self.create_vertex_color_map_rgb(obj.data, "Segment", 1, 0, 1) 1080 | 1081 | # Move by offset 1082 | obj.location.x += offset 1083 | generated.append(obj) 1084 | 1085 | def create_dot(self, offset, generated): 1086 | if self.data.show_dot: 1087 | self.create_aux(bpy.data.objects[SEGMENT_DOT], offset, generated) 1088 | else: 1089 | self.create_aux(bpy.data.objects[SEGMENT_EMPTY], offset, generated) #Empty separator 1090 | 1091 | def create_colon(self, offset, generated): 1092 | if self.data.show_colons: 1093 | self.create_aux(bpy.data.objects[SEGMENT_COLON], offset, generated) 1094 | else: 1095 | self.create_aux(bpy.data.objects[SEGMENT_EMPTY], offset, generated) #Empty separator 1096 | 1097 | def assign_segment_materials(self): 1098 | """ 1099 | Assigns the segment foreground and background materials to the 1st and 2nd slots. 1100 | The segment foreground is assigned to faces based on the "segments" face map. 1101 | The segment background everywhere else. 1102 | 1103 | Uses operators on context.object 1104 | """ 1105 | bpy.ops.object.mode_set(mode='EDIT') 1106 | 1107 | # Select segments face map 1108 | bpy.ops.mesh.select_all(action = 'DESELECT') 1109 | bpy.ops.mesh.select_mode(type='FACE') 1110 | 1111 | mesh = bpy.context.object.data; 1112 | mesh.attributes.active = mesh.attributes['segments'] 1113 | bpy.ops.mesh.select_by_attribute() 1114 | 1115 | # Add materials 1116 | bpy.context.object.data.materials.append(self.resource.materials[1]) 1117 | bpy.context.object.data.materials.append(self.resource.materials[0]) 1118 | bpy.context.object.active_material_index = 1 1119 | bpy.ops.object.material_slot_assign() 1120 | 1121 | bpy.ops.mesh.select_mode(type='VERT') 1122 | bpy.ops.object.mode_set(mode='OBJECT') 1123 | 1124 | @staticmethod 1125 | def get_select_mode() -> str: 1126 | modes = bpy.context.tool_settings.mesh_select_mode[:] 1127 | if (modes[0] == True): 1128 | return 'VERT' 1129 | if (modes[1] == True): 1130 | return 'EDGE' 1131 | if (modes[2] == True): 1132 | return 'FACE' 1133 | print("SegmentAddon: Failed to detect select mode!") 1134 | return 'VERT' 1135 | 1136 | @staticmethod 1137 | def rgba_tuple_multiply(prop, mult): 1138 | return (prop[0] * mult, prop[1] * mult, prop[2] * mult, 1.0) 1139 | 1140 | @staticmethod 1141 | def color_property_to_rgba_tuple(prop): 1142 | return (prop.r, prop.g, prop.b, 1.0) 1143 | 1144 | @classmethod 1145 | def lcd_style_calculate_x_ramp(cls, bW, sbW) -> tuple: 1146 | bWH = bW/2 1147 | sbWH = sbW/2 1148 | 1149 | rgbW = (1 - bW) - sbW*2 1150 | 1151 | p1 = bWH 1152 | p6 = 1 - bWH 1153 | 1154 | rgb1 = bWH + sbWH + ((1/3)*rgbW) 1155 | p2 = rgb1 - sbWH 1156 | p3 = rgb1 + sbWH 1157 | 1158 | rgb2 = bWH + sbW + sbWH + ((2/3)*rgbW) 1159 | p4 = rgb2 - sbWH 1160 | p5 = rgb2 + sbWH 1161 | 1162 | print(f"bw: {bW} bWH: {bWH} sbW: {sbW} rgbW: {rgbW} rgb1: {rgb1} rgb2: {rgb2} p[]: {p1} {p2} {p3} {p4} {p5} {p6}") 1163 | return (p1, p2, p3, p4, p5, p6) 1164 | 1165 | @classmethod 1166 | def lcd_style_calculate_y_ramp(cls, bW) -> tuple: 1167 | bWH = bW/2 1168 | p1 = bWH 1169 | p2 = 1 - bWH 1170 | return (p1, p2) 1171 | 1172 | @classmethod 1173 | def vertex_paint_all_rgb(cls, mesh, color_map, r, g, b): 1174 | i = 0 1175 | for poly in mesh.polygons: 1176 | for idx in poly.loop_indices: 1177 | color_map.data[i].color = [r, g, b, 1] 1178 | i += 1 1179 | 1180 | @classmethod 1181 | def vertex_paint_all(cls, mesh, color_map, value): 1182 | cls.vertex_paint_all_rgb(mesh, color_map, value, value, value) 1183 | 1184 | @classmethod 1185 | def create_vertex_color_map_rgb(cls, mesh, name, r, g, b): 1186 | mesh.vertex_colors.new(name=name) 1187 | color_map = mesh.vertex_colors[name] 1188 | cls.vertex_paint_all_rgb(mesh, color_map, r, g, b) 1189 | 1190 | @classmethod 1191 | def create_vertex_color_map(cls, mesh, name, value, failsafe=None): 1192 | if failsafe is None: 1193 | failsafe = cls.VC_STEP_FAILSAFE 1194 | value = value + failsafe 1195 | cls.create_vertex_color_map_rgb(mesh, name, value, value, value) 1196 | 1197 | 1198 | class Utils: 1199 | @staticmethod 1200 | def move_node(node, dx, dy): 1201 | node.location = node.location[0] + dx, node.location[1] + dy 1202 | 1203 | @staticmethod 1204 | def create_frame_value_node(node_tree, offset=0): 1205 | value_node = node_tree.nodes.new('ShaderNodeValue') 1206 | Utils.create_expression_driver(value_node.outputs[0], 'default_value', 'frame' if offset == 0 else ('frame+'+str(offset))) 1207 | value_node.label = "frame" 1208 | return value_node 1209 | 1210 | @staticmethod 1211 | def create_expression_driver(target, prop, expression): 1212 | driver = target.driver_add(prop).driver 1213 | driver.expression = expression 1214 | 1215 | @staticmethod 1216 | def copy_object(obj): 1217 | new_obj = obj.copy() 1218 | new_obj.data = obj.data.copy() 1219 | return new_obj 1220 | 1221 | @staticmethod 1222 | def s2lin(x): 1223 | a = 0.055 1224 | if x <=0.04045 : 1225 | y = x * (1.0 / 12.92) 1226 | else: 1227 | y = pow( (x + a) * (1.0 / (1 + a)), 2.4) 1228 | return y 1229 | 1230 | @staticmethod 1231 | def lin2s(x): 1232 | a = 0.055 1233 | if x <=0.0031308: 1234 | y = x * 12.92 1235 | else: 1236 | y = (1 + a) * pow(x, 1 / 2.4) - a 1237 | return y 1238 | 1239 | 1240 | ################################################################################ 1241 | # ADDON 1242 | ################################################################################ 1243 | 1244 | classes = [MainPanel, DisplayTypePanel, DisplayValuePanel, DisplayAppearancePanel, DisplayStylePanel, AdvancedPanel, GeneratePanel, SegmentAddonData, CreateDisplayOperator, ResetToDefaultsOperator] 1245 | 1246 | def load_preview(pcoll, name, filepath, type): 1247 | if not name in pcoll.keys(): 1248 | return pcoll.load(name, filepath, type) 1249 | else: 1250 | print("Preview '" + name +"' already exists!") 1251 | return pcoll[name] 1252 | 1253 | 1254 | def generate_style_previews(): 1255 | directory = SegmentAddon.style_previews_dir 1256 | pcoll = SegmentAddon.previews[SegmentAddon.style_previews] 1257 | print("Loading style previews from directory: " + directory) 1258 | 1259 | items = [] 1260 | to_load = [ 1261 | ["plain", "Plain", "plain.png"], 1262 | ["classic", "Classic", "classic.png"], 1263 | ["lcd", "LCD", "lcd.png"], 1264 | ] 1265 | 1266 | for i, style in enumerate(to_load): 1267 | filepath = os.path.join(directory, style[2]) 1268 | if not filepath in pcoll.keys(): 1269 | thumb = None 1270 | if os.path.exists(filepath): 1271 | thumb = load_preview(pcoll, filepath, filepath, 'IMAGE') 1272 | else: 1273 | filepath = os.path.join(directory, "missing.png") 1274 | thumb = load_preview(pcoll, filepath, filepath, 'IMAGE') 1275 | items.append((style[0], style[1], "", thumb.icon_id, i)) 1276 | 1277 | return items 1278 | 1279 | 1280 | def register(): 1281 | print("SegmentAddon: Registering") 1282 | print("") 1283 | from bpy.utils import register_class 1284 | for cls in classes: 1285 | register_class(cls) 1286 | 1287 | SegmentAddon.addon_directory_path_full = os.path.dirname(os.path.realpath(__file__)) 1288 | if ".blend" in SegmentAddon.addon_directory_path_full: 1289 | # Run as a script 1290 | # NOTE: This does not work unless the blend file is saved. 1291 | SegmentAddon.addon_directory_path = os.path.split(SegmentAddon.addon_directory_path_full)[0] 1292 | else: 1293 | # Addon installed 1294 | SegmentAddon.addon_directory_path = SegmentAddon.addon_directory_path_full 1295 | 1296 | SegmentAddon.addon_resources_dir = os.path.join(SegmentAddon.addon_directory_path, 'resources') 1297 | SegmentAddon.addon_blend_path = os.path.join(SegmentAddon.addon_resources_dir, 'segment.blend') 1298 | SegmentAddon.style_previews_dir = os.path.join(SegmentAddon.addon_resources_dir, 'styles') 1299 | 1300 | print("Segment addon dir full: " + SegmentAddon.addon_directory_path_full) 1301 | print("Segment addon dir: " + SegmentAddon.addon_directory_path) 1302 | print("Segment resource dir: " + SegmentAddon.addon_resources_dir) 1303 | print("Segment blend path: " + SegmentAddon.addon_blend_path) 1304 | print("Segment style previews dir: " + SegmentAddon.style_previews_dir) 1305 | 1306 | Scene.segment_addon_data = bpy.props.PointerProperty(type=SegmentAddonData) 1307 | 1308 | # Create the display style enum 1309 | SegmentAddon.previews[SegmentAddon.style_previews] = bpy.utils.previews.new() 1310 | style_previews = generate_style_previews() 1311 | SegmentAddonData.style = bpy.props.EnumProperty( 1312 | name="Display style", 1313 | description="Sets in what style the segment display mask is processed to form the final display", 1314 | items=style_previews, 1315 | ) 1316 | 1317 | 1318 | def unregister(): 1319 | from bpy.utils import unregister_class 1320 | for cls in reversed(classes): 1321 | unregister_class(cls) 1322 | 1323 | for preview in SegmentAddon.previews.values(): 1324 | bpy.utils.previews.remove(preview) 1325 | SegmentAddon.previews.clear() 1326 | 1327 | del Scene.segment_addon_data 1328 | 1329 | 1330 | if __name__ == "__main__": 1331 | register() 1332 | -------------------------------------------------------------------------------- /SegmentAddon/resources/segment.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/SegmentAddon/resources/segment.blend -------------------------------------------------------------------------------- /SegmentAddon/resources/styles/classic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/SegmentAddon/resources/styles/classic.png -------------------------------------------------------------------------------- /SegmentAddon/resources/styles/lcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/SegmentAddon/resources/styles/lcd.png -------------------------------------------------------------------------------- /SegmentAddon/resources/styles/missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/SegmentAddon/resources/styles/missing.png -------------------------------------------------------------------------------- /SegmentAddon/resources/styles/plain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/SegmentAddon/resources/styles/plain.png -------------------------------------------------------------------------------- /documentation_czech.adoc: -------------------------------------------------------------------------------- 1 | :show-link-uri: 2 | = Blender Add-on pro generování 7 segmentových displejů 3 | :author: Dan Rakušan 4 | :email: rakusdan@fit.cvut.cz 5 | :doctype: book 6 | :sectnums: 7 | :sectanchors: 8 | :sectnumlevels: 4 9 | :toc: left 10 | :title-page: 11 | :toc-title: Obsah 12 | :toclevels: 5 13 | :outlinelevels: 6:0 14 | :icons: font 15 | :experimental: 16 | :description: Dokumentace 17 | :keywords: AsciiDoc 18 | :imagesdir: ./images 19 | 20 | == Úvod 21 | [.lead] 22 | Tento dokument je dokumentace semestrální práce předmětu BI-PGA na ČVUT FIT. + 23 | Předmětem práce je tvorba addonu/pluginu pro 3D program Blender. Téma jsem si vymyslel vlastní, a to addon, který umožnuje generování 7 segmentových displejů dle široké škály parametrů. Displeje lze také animovat a lze je použít pro zobrazení čísel / času např. při modelování nábytku, spotřebičů, hodinek apod. nebo opravdu kdekoli kde se segmentový displej hodí. 24 | 25 | [NOTE] 26 | O výběru tématu jsem nikoho neinformoval. Psal jsem email panu Chludilovi s návrhem ještě jiného tématu ale nikdy jsem nedostal odpověď. 27 | 28 | .Příklady generovaných displejů 29 | image::examples.png[width=100%] 30 | 31 | == Uživatelská příručka 32 | 33 | === Instalace 34 | [NOTE] 35 | Addon je vytvořen pro Blender verze 3.0.0+ 36 | 37 | Zazipovaný archiv `SegmentAddon.zip` standardně nainstalujte a zapněte skrz Blender preference. 38 | 39 | Hlavní panel addonu se nachází v pravém sidebaru (N-panelu) 3D View editoru pod záložkou "Segment display". 40 | 41 | === Použití 42 | 43 | Addon funguje velice jednoduše a dokáže jednorázově generovat nový displej dle daného nastavení. 44 | 45 | V panelu addonu stačí nastavení upravit a následně dole v panelu "Generate display" stisknout tlačítko s textem "Create display". 46 | 47 | Nový segmentový displej se vygeneruje na současné pozici kurzoru. V současné chvíli nelze displej dále addonem upravovat, displej se musí vygenerovat znovu v případě změny nastavení. Displej lze samozřejmě upravovat a nadále s ním pracovat přímo v 3D View nebo Shader editoru. 48 | 49 | === Stručný popis funkcí 50 | 51 | ==== Typ displeje 52 | .Hlavní panel addonu 53 | image::overview.png[float=right] 54 | 55 | Addon umí generovat 2 typy segmentových displejů. *Numerické* zobrazující decimální číslo a *časový*, zobrazující hodiny, minuty, sekundy a milisekundy. U každého typu lze nastavit počet cifer. 56 | 57 | ===== Zobrazovaná hodnota 58 | Numerický displej má módy zobrazení: 59 | 60 | - Číslo 61 | -- Zobrazí jedno statické číslo 62 | - Snímek (Animované) 63 | -- Zobrazí číslo současného snímku animace. Číslo snímku lze dále např. dělit nějakým čísem nebo posunout. 64 | - Časovač (Animované) 65 | -- Displej se postupně změní z jednoho čísla na druhé v nějakém časovém intervalu. Může počítat nahoru i dolu. 66 | 67 | Časový displej je podobný, ale pracuje v base-60: 68 | 69 | - Sekundy 70 | -- Zobrazí daný počet sekund 71 | - Čas 72 | -- Zobrazí daný čas 73 | - Snímek (Animované) 74 | -- Zobrazí číslo snímku převedeného na sekundy. Opět lze dělit a posouvat. Dělením počtem snímku za sekundu docílíme realtime zobrazení času. 75 | - Časovač (Animované) 76 | -- Počítá z nějakého času do času druhého. Nahoru i dolů. Tedy displej funguje jako stopky nebo klasický časovač. 77 | 78 | ==== Vzhled 79 | Displeji lze nastavit barvu svítícího a nesvítícího segmentu, popřípadě zhasnutá barva může být nastavena automaticky addonem. + 80 | Lze nastavit i sílu emission materiálu. 81 | Některým stylům lze i nastavit sílu případné normálové mapy. + 82 | Displej se také skládá z pozadí neboli oblasti mimo segmenty samotné. 83 | Pozadí se může také negenerovat a výsledný displej se bude skládat + pouze ze segmentů. 84 | Displej ještě může být rozšířen do 3. dimenze nebo/a nastavit míru šikmosti. 85 | 86 | ===== Styl displeje 87 | Poslední vzhledové nastavení je styl displeje. To je shader, který je aplikován na oblasti segmentů. 88 | 89 | - Prostý 90 | -- Segmenty jsou pouze zabarveny do nějaké barvy bez dalšího stínování. 91 | - Klasický 92 | -- Na segmenty je aplikovaná jasová textura, která zjasní prostředky segmentů a naopak ztemní jejich kraje 93 | - LCD 94 | -- Na segmenty je aplikován LCD shader, který jejich barvy přetvoří na pole různě zabarvených moderních LCD pixelů / subpixelů. 95 | 96 | {empty} + 97 | {empty} + 98 | {empty} + 99 | {empty} + 100 | {empty} + 101 | {empty} + 102 | {empty} + 103 | {empty} + 104 | {empty} + 105 | {empty} + 106 | {empty} + 107 | {empty} + 108 | {empty} + 109 | 110 | [.float-group] 111 | -- 112 | image:plain.png[width=32%,pdfwidth=32vw] 113 | image:classic.png[width=32%,pdfwidth=32vw] 114 | image:lcd.png[width=32%,pdfwidth=32vw] 115 | -- 116 | 117 | == Technická dokumentace 118 | V rámci technické dokumentace projdu klíčové části addonu a poskytnu vysvětlení jejich funkce. Do detailů ve smyslu komentáře ke každé lince/sekci kódu opravdu nezajdu. 119 | 120 | === Původ nápadu 121 | Na potřebu segmentového displeje v Blenderu jsem narazil při modelování obvodu pro časovanou nálož, kterou jsem modeloval v rámci jednoho hobby projektu. Blender zdánlivě žádný jednoduchý způsob, jak docílit animovaného segmentového displeje nemá, a tedy cílevědomý modelář začne hledat na webu. Tam je hned několik návodů, jak takového displeje docílit a jeden z prvních google výsledků je hned na již hotový 7 segmentový shader na blender marketu, který je velice pěkně udělaný ale bohužel se jedná o placený obsah. Zbývá tedy začít číst tutoriály a podobně nebo případně segmentový displej do blenderu dostat jako již hotové video z nějakého externího programu. 122 | 123 | Na jeden z těchto tutoriálu jsem narazil a jeho jednoduchý princip a public domain license mě zaujala. Specificky se jedná o https://sharkigator.wordpress.com/2016/01/15/7-segment-display-tutorial/[tento blogpost] od uživatele *sharkigator*. + 124 | Hlavní myšlenka addonu staví na vylepšeném node setupu právě z tohoto blogu. Původně jsem tento node setup pouze rozšířil, trochu s ním zjednodušil práci a pomocí něho vytvořil .blend soubor s mnoho manuálně vytvořenými displeji s různými počty cifer. + 125 | Na tento .blend soubor jsem si následně tento semestr vzpomněl a napadlo mě celý dost komplikovaný proces tvorby displeje zautomatizovat tvorbou addonu. 126 | 127 | === Princip fungování 128 | Addon sestavuje displej z modelu jedné číslice s různými parametry. Tento model má skutečnou geometrii reprezentující všech 7 segmentů číslice. Ploškám těchto segmentů je přiřazen materiál, který mění barvu jednotlivých segmentů, aby tvořili potřebné číslice. Vertexy tvořící jednotlivé segmenty jsou obarveny pomocí vertex paintingu na hodnoty, které identifikují, o jaký segment se jedná. Ostatní plošky modelu mají přirazen jiný jednoduchý materiál pozadí. + 129 | 130 | Materiál segmentu vytváří masku aktivních "rozsvícených" segmentů, na kterou lze dále aplikovat různé barvy a efekty dle nastavení addonu. 131 | 132 | Mezi modely číslic jsou případně vyloženy i užší modely rozdělovačů jako je tečka nebo dvojtečka. 133 | 134 | Tento přístup tedy je tak trochu hybridní, a tedy displej není zcela tvořen pouze shadery. Shader segmentů získává informace o segmentech z vertex barev, nikoli nějaké mapy generované pomocí obrázků nebo přímo matematicky v rámci shader editoru (i když to by také šlo). 135 | 136 | O tom zdali je čistý shaderový přístup lepší či ne tu asi nebudu diskutovat i když nejspíš je odpověď kladná. Tento hybridní přistup jsem zvolil, protože se mi dříve zmíněný blogpost zalíbil a práci jsem již měl rozdělanou. Hlavní výhoda tohoto přístupu je, že s displejem můžeme fyzicky manipulovat a tvoří ho reálné vertexy, což je tedy i hlavní nevýhoda. 137 | 138 | První vertex color atribut určuje tedy k jakému segmentu daný vertex patří. 139 | 140 | .Vertex barvy segmentu 141 | image::segment.png[align=center, width=30%] 142 | 143 | Dále existují ještě 2 vertex color atributy. Oba jsou přiřazeny všem vertexům číslice. Jeden co určuje který řád číslice zobrazuje (také součástí blogpostu) a další, který určuje jaký typ hodnoty číslice zobrazuje. V případě numerického displeje rozlišujeme mezi celočíselnou a zlomkovou částí a v případě hodin udává zdali zobrazujeme hodiny, minuty, sekundy nebo milisekundy. 144 | 145 | .Vertex barvy pro digit a display pro různé displeje 146 | image::display_digit.png[width=100%] 147 | 148 | ==== Základní node groupy 149 | ===== 7SegmentCore 150 | Segmentové jádro je node group z původního blogu od sharkigator. Jeho výstupem je maska reprezentující, které segmenty mají svítit pro dané číslo na dané číslici. + 151 | Na vstupu vezme číslo, pozici číslice a informace o vertex barvách segmentu. 152 | Nejdříve vydělením čísla řádem číslice a zahozením zlomkové části získáme číslici od 0 do 9 na pozici, o kterou máme zájem. V dolní části pak vezmeme vertex barvy segmentů a porovnáváním hodnot získáme mapy jednotlivých segmentů, které poté můžeme sečíst dohromady abychom složili mapy pro všech 10 číslic. + 153 | Výsledně máme 10 výstupu z nichž jeden nese hodnotu 1 pro identifikaci číslice, kterou chceme a 10 výstupů reprezentující jednotlivé číslice. Tyto výstupy (hodnoty 0 až 1) mezi sebou všechny vynásobíme (AND operace) a výsledek je maska pro zvolenou číslici. 154 | 155 | .7SegmentCore node group 156 | image::segment_core.png[width=100%] 157 | 158 | ===== 7SegmentBase 159 | Segmentové jádro je ovládáno segmentovou základnou. Tento node group získá hodnoty _segment_ a _digit_ vertex barev z _Attribute_ nodů a předá je jádru. Nejdříve však provede jistý preprocessing se zadaným číslem pomocí _display_ vertex barvy. Číslo podělí dělitelem zadaným vstupem a přidá malou hodnotu float correction, která může pomoci, když se začnou objevovat problémy s přesností floating-point čísel v shaderech. 160 | 161 | .7SegmentBase node group 162 | image::segment_base.png[width=100%] 163 | 164 | [NOTE] 165 | Možná si všimnete math _power_ nodů s exponentem 0.455. Tyto nody převádějí vertex barvu z lineární interpretace do interpretace sRGB. Když v vertex paint editoru nastavíme vertexu barvu (0.5, 0.5, 0.5), ve skutečnosti se do vertexu uloží hodnota 0.216, kterou pak __Attribute_ nody vrací. Umocnění na 0.455 zhruba zpátky sRGB aproximuje. Toto nejde nijak změnit a je zdrojem velké frustrace a v Blender dev trackeru už dlouhou dobu visí ticket, který by toto chování měl změnit. Konverzi lze udělat přesněji ale toto stačí. V kódu addonu jsou metody, které tuto konverzi provádí přesně a lze je "přepsat" do nodů, ale zatím není potřeba. 166 | 167 | Poté je číslo společně s _display_ barvou předáno do takzvané "processor" node groupy, kterou addon dynamicky volí podle typu displeje. Pro numerické displeje je `7SegmentDecimalProcessor` a pro hodiny `7SegmentClockProcessor`. Tyto node groupy provedou potřebné úpravy čísla dle typu displeje. 168 | 169 | ====== 7SegmentDecimalProcessor 170 | 171 | Tento procesor jednoduše číslo podělí řádem zlomkového displeje. Pokud chceme zobrazovat číslo s přesností na tisíciny, vertex color _display_ bude nastaveno na 0.3, z 0.3 uděláme 10^(0.3*10) = 10^3 a a číslo vynásobíme 1000. Segmentové jádro potom zobrazí první 3 číslice tohoto velkého čísla, což je přesně zlomková část čísla původního 172 | 173 | ====== 7SegmentClockProcessor 174 | 175 | Tento procesor číslo chápe jako počet sekund k zobrazení a barvu _display_ jako indikátor, zdali chceme hodiny=0.3, minuty=0.2, sekundy=0.1 nebo milisekundy=0.0. 176 | 177 | Počet sekund převedeme na počet hodin, minut, sekund nebo milisekund a podle hodnoty _display_ pomocí multiply a add math nodů vybereme tu variantu, kterou chceme. 178 | 179 | 180 | .7SegmentClockProcessor node group 181 | image::clock_processor.png[width=100%] 182 | 183 | ==== Materiály 184 | Displeje mají 2 materiály. Materiál segmentů a materiál pozadí (pouze barva). 185 | 186 | V materiálu segmentů se odehrává všechno důležité. Je přiřazen ploškám tvořícím jednotlivé segmenty. 187 | Skládá se z 2 částí, segmentové základy a segmentového shaderu. 188 | Segmentová základna vytváří masku pro dané číslo, tuto masku následně vezme segment shader a pokusí se z ní udělat něco pěkného. Výstupem segmentového shaderu jsou vstupy pro klasický Principled BSDF shader, který je výstup materiálu. 189 | 190 | image::segment_material.png[width=100%] 191 | 192 | ===== Segmentová základna 193 | Addon zprostředkovává číselný vstup a upravuje ho podle požadované zobrazované hodnoty. 194 | To může být pouze value node s číslem, animovaný value node dle #frame driveru nebo node group `7SegmentTimerResolver`, který číslo současného snímku přetvoří dle nastavení časovače. 195 | 196 | ====== 7SegmentTimerResolver 197 | Timer resolver má 5 vstupů. 198 | Nějakou hodnotu, která dává časovač do pohybu, tu nastavuje addon na driver #frame (číslo současného snímku animace). Dále máme inputy From a To. To je počáteční a konečná hodnota mezi, kterou časovač interpoluje. 199 | Interpolace je provedena v nějakém časovém intervalu, a to mezi Start a Stop, což jsou čísla snímků začátku a konce interpolace. 200 | 201 | Node setup vypadá složitě ale je to jen základní matematika. Ze současného snímku zjistíme, zdali je mezi snímky Start a Stop. Pokud ne tak nastavíme výstup jako buď hodnotu From nebo To. Pokud v intervalu interpolace jsme, zjistíme, jak daleko je současný snímek od začátku interpolace. Tu informaci získáme jako faktor od 0 do 1, kterým můžeme vynásobit rozdíl mezi From a To a přičíst k From abychom získali aktualní hodnotu pro daný snímek. 202 | 203 | Tento proces může fungovat i opačně když From je větší než To, stačí si pouze ohlídat záporné hodnoty, a nakonec od From odečíst místo přičíst. 204 | 205 | ===== Segmentový shader 206 | Addon také vybírá vhodný segmentový shader dle zvoleného stylu displeje. Tento shader také addon upravuje dle nastavení, většinou se jedná jen o menší úpravy parametrů v případech, kde se hodí logika pythonu. 207 | 208 | 209 | image::timer_resolver.png[width=100%] 210 | 211 | ====== 7SegmentPlainShader 212 | Plain shader pouze zabarvuje masku dle nastavení a výstup jsou tedy pouze základní barvy. 213 | 214 | ====== 7SegmentClassicShader 215 | Classic shader používá texturu mapy jasnosti a texturu normal mapy. 216 | Podle mapy jasnosti zjasňuje či ztemňuje části segmentů, specificky zatemní kraje a zjasní prostředky, aby docílil realističtějšího vzhledu. Normálová mapa pouze dodává segmentům tvar. 217 | 218 | image::classic_shader.png[width=100%] 219 | 220 | ====== 7SegmentPlainLCDShader 221 | PlainLCD shader používá plain shader v kombinaci s node groupou LCD efektu napodobují vzhled LCD panelů moderních monitorů. 222 | LCD efektu je docíleno generací mnoha opakujících se pixelových buňek, každá obsahující subpixel pouze červené/zelené/modré barvy. Obrazový vstup je pak vynásoben tímto polem čehož docílíme různé míry "rozsvícení" jednotlivých subpixelů. Pokud se na výsledný materiál podíváme z jisté dálky, barvy splynou a vytvoří cílovou barvu, stejně jako monitory. Je pěkné že tento efekt funguje dobře i v Blenderu. 223 | 224 | Addon je schopen LCD node groupu upravovat a změnit proporce samotného LCD pixelu. 225 | 226 | .Plain LCD shader 227 | image::plain_lcd_shader.png[width=100%] 228 | 229 | .LCD node group 230 | image::lcd_shader.png[width=100%] 231 | 232 | === Addon 233 | Teď trochu o addonu samotném. Addon funguje pouze jako zprostředkovatel / sestavovač všech komponentů výše zmíněných. Jak už jsem zmínil, nehodlám zacházet do detailu kódu samotného a pouze projdu obecnou strukturu a životo-cyklus kódu. 234 | 235 | ==== Obecné 236 | Addon si do scény ukládá jednu velkou instanci datové _PropertyGroup_ s názvem _SegmentAddonData_. Tato skupina vlastností obsahuje kompletní nastavení pro jeden displej. Toto nastavení je prezentováno v jednom hlavním panelu uživateli s možností spuštění operátoru _CreateDisplayOperator_, který na základě současného stavu nastavení vygeneruje jeden displej na aktuální pozici kurzoru. 237 | 238 | 239 | ==== UI 240 | UI je relatvině jednoduché, vše se nachází v panelu _MainPanel_, který zobrazuje všechny vlastnosti _SegmentAddonData_ aktivní scény. Hlavní panel je rozdělen na mnoho subpanelů dle potřeb seskupování možností. 241 | 242 | Za zmínku možná stojí zobrazení enumu displejových stylů pomocí `template_icon_view`. Styly jsou zobrazeny pomocí velkých ikon načtených pomocí `bpy.utils.previews` ze složky /resources/styles. O toto načtení se stará metoda `generate_style_previews()`. 243 | 244 | ==== Operátor generace 245 | Tento operátor se spustí po stisknutí tlačítka "Create display" v UI. 246 | 247 | ===== Načtení zdrojů 248 | Nejdříve se načtou potřebné objekty, materiály a node groupy z přibaleného `segment.blend` souboru v adresáři addonu. Načtení těchto data bloků je provedeno pomocí `bpy.data.libraries.load`. 249 | 250 | image::load_resources.png[width=100%] 251 | 252 | ===== Materiály 253 | Načtený vzor segmentového materiálu je dle nastavení doupraven. + 254 | Tedy je vybrán správný procesor čísla v node groupě segmentové základny. 255 | Segmentové základně je nastaveno náležité vstupní číslo, v případě časovače je toto číslo nejdříve prohnáno náležitě nastaveným resolverem. + 256 | Segmentová základna je následně napojena na vybraný stylový shader. Tento shader je také podle jeho typu upraven. 257 | 258 | image::setup_material.png[width=100%] 259 | 260 | ===== Generace displeje 261 | Dle typu displeje se začnou generovat číslice displeje. 262 | Ze `segment.blend` souboru je načten objekt číslice s předpřipravenými _segment_ vertex barvami. Tento objekt je pro každou číslici zduplikován a přidán do scény. + 263 | Tento objekt má také uloženou face mapu plošek, které tvoří segmenty. Těmto ploškám je přiřazen segmentový materiál a ostatním je přiřazený materiál pozadí. + 264 | 265 | Číslice se generují zprava doleva. Vždy po vytvoření číslice se další číslice posune o šířku předchozí generované číslice. Mezi číslice se případně generuje objekt rozdělovače, kterému jsou také přiřazeny připravené materiály. 266 | 267 | .Sestavování numerického a hodinového displeje 268 | image::display_generation.png[width=100%] 269 | 270 | Každé číslici jsou vytvořeny a obarveny vertex color mapy pro _digit_ a _display_ dle pozice/typu generované číslice. Obarveny jsou plošně všechny vertexy číslice. 271 | 272 | .Metoda create_digit() 273 | image::create_digit.png[width=100%] 274 | 275 | Po dokončení generace se dle nastavení všechny objekty číslic a rozdělovačů spojí pomocí klasických operátorů do jednoho objektu, objekt je pojmenován a případně je na něm spuštěn `remove_doubles()` operátor ke spojení "sdílených" vertexů sousedních číslic/rozdělovačů. + 276 | Na objekt je možné také aplikovat `shear` operátor dle hodnoty _skew_ nastavení nebo operátor `extrude` po ose Z pro vytvoření 3D displeje. + 277 | Také lze smazat všechny vertexy, které netvoří samotné segmenty a tím se zbavit pozadí číslic. 278 | 279 | Nakonec se vygenerovaný objekt (nebo objekty) posune na aktuální pozici kurzoru a také je škálován dle nastavení _object_scale_. 280 | 281 | .Vygenerovaný displej 282 | image::done.png[width=100%] 283 | 284 | == Limitace 285 | 286 | Jsou tu jisté limitace, které současný návrh má. + 287 | Určitě by bylo dobré mít možnost displeje dynamicky upravovat, tedy aby displej reagoval na změny v UI. Tuto funkci jsem zatím vynechal, ale určitě je možná. 288 | 289 | Další limitace plynou z použití vertex barev. Max. počet cifer jednoho typu je omezen na 10. Tedy numerický displej může mít až 20 cifer. Je dost a číslo by šlo i zvětšit pokud bych použil přesnější konverzi mezi lineárním a sRGB prostorem. 290 | 291 | Víc cifer však potřeba moc není, protože jedno z největších omezení je floating-point přesnost čísel v shader editoru. V tuto chvíli se zdá, že displej je schopen ukázat čísla na zhruba 8 desetinných míst. To zhruba odpovídá tomu co jsem si o tomto tématu v rámci Blenderu četl, ale nejsem si jistý. 292 | 293 | Z použití geometrie a vertex barev, tedy tohoto "hybridního" přístupu, který nepoužívá pouze shader, plyne omezení, že vytvořená segmentová maska nemá přesně dané okraje. Respektive její okraje jsou přímo dané geometrií a přirazeným materiálem. + 294 | To znamená, že shadery nemají jasnou informaci o tom, kde je hranice segmentu. 295 | Není známá ani vzdálenost k nejbližšímu okraji tak k zatemnění krajů segmentů musím využívat texturu. Shadery, které využívají pixelizace jako např správně udělaný LCD shader nebo dot matrix shader (přeložení obrázku na různě zabarvené kolečka) nemůžou fungovat zcela správně. Např. některé pixely součásného LCD shaderu jsou "rozřízlé" napůl, tak skutečný displej nefunguje. 296 | 297 | Tento problém by nejspíš v budoucích iteracích addonu šel obejít dodáním textury, která indikuje jak daleko je daný bod segmentu od nejbližší hrany. 298 | Taková textura by šla i generovat dynamicky podle geometrie segmentů, i samotná geometrie segmentů by šla generovat a upravovat dynamicky. 299 | Addonu by šly i dodávat různé modely číslic, které samy o sobě mají nějaký styl. Tedy jiné tvary segmentů a podobně. 300 | 301 | == Závěr 302 | 303 | Obecně jsem s addonem spokojený. Dělá co má a zdá se mi lehký na použití. 304 | Je také lehké ho dál rozšiřovat vzhledem k tomu, že sestavování materiálů je z velké části modulární díky node groupům. + 305 | 306 | https://github.com/xDUDSSx/segment-display-blender-addon[GitHub repo link] 307 | 308 | == Zdroje 309 | 310 | https://sharkigator.wordpress.com/2016/01/15/7-segment-display-tutorial/[Sharkigator segment display blogpost] 311 | 312 | https://www.youtube.com/watch?v=fJ1WBx3kJaQ[YouTube video od Jonathan Kron - Inspirace pro LCD shader] 313 | -------------------------------------------------------------------------------- /images/Oin86iZ0b7.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/Oin86iZ0b7.gif -------------------------------------------------------------------------------- /images/classic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/classic.png -------------------------------------------------------------------------------- /images/classic_shader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/classic_shader.png -------------------------------------------------------------------------------- /images/clock_processor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/clock_processor.png -------------------------------------------------------------------------------- /images/create_digit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/create_digit.png -------------------------------------------------------------------------------- /images/display_digit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/display_digit.png -------------------------------------------------------------------------------- /images/display_generation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/display_generation.png -------------------------------------------------------------------------------- /images/done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/done.png -------------------------------------------------------------------------------- /images/example_3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/example_3d.png -------------------------------------------------------------------------------- /images/example_classic_clock_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/example_classic_clock_2.png -------------------------------------------------------------------------------- /images/example_crystal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/example_crystal.png -------------------------------------------------------------------------------- /images/example_crystal_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/example_crystal_2.png -------------------------------------------------------------------------------- /images/example_lcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/example_lcd.png -------------------------------------------------------------------------------- /images/example_plain_clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/example_plain_clock.png -------------------------------------------------------------------------------- /images/example_plain_clock_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/example_plain_clock_2.png -------------------------------------------------------------------------------- /images/example_plain_number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/example_plain_number.png -------------------------------------------------------------------------------- /images/example_skew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/example_skew.png -------------------------------------------------------------------------------- /images/examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/examples.png -------------------------------------------------------------------------------- /images/examples_low_res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/examples_low_res.png -------------------------------------------------------------------------------- /images/lcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/lcd.png -------------------------------------------------------------------------------- /images/lcd_shader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/lcd_shader.png -------------------------------------------------------------------------------- /images/load_resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/load_resources.png -------------------------------------------------------------------------------- /images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/overview.png -------------------------------------------------------------------------------- /images/plain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/plain.png -------------------------------------------------------------------------------- /images/plain_lcd_shader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/plain_lcd_shader.png -------------------------------------------------------------------------------- /images/segment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/segment.png -------------------------------------------------------------------------------- /images/segment_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/segment_base.png -------------------------------------------------------------------------------- /images/segment_core.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/segment_core.png -------------------------------------------------------------------------------- /images/segment_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/segment_cover.png -------------------------------------------------------------------------------- /images/segment_material.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/segment_material.png -------------------------------------------------------------------------------- /images/setup_material.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/setup_material.png -------------------------------------------------------------------------------- /images/timer_resolver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/images/timer_resolver.png -------------------------------------------------------------------------------- /resources/segment.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/resources/segment.blend -------------------------------------------------------------------------------- /resources/styles/classic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/resources/styles/classic.png -------------------------------------------------------------------------------- /resources/styles/lcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/resources/styles/lcd.png -------------------------------------------------------------------------------- /resources/styles/missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/resources/styles/missing.png -------------------------------------------------------------------------------- /resources/styles/plain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/resources/styles/plain.png -------------------------------------------------------------------------------- /textures/segment_base_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/textures/segment_base_mask.png -------------------------------------------------------------------------------- /textures/segment_brightness_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/textures/segment_brightness_mask.png -------------------------------------------------------------------------------- /textures/segment_masks.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/textures/segment_masks.psd -------------------------------------------------------------------------------- /textures/segment_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xDUDSSx/segment-display-blender-addon/9bf389361cdb666426d05c696f15feefa211c379/textures/segment_normal.png --------------------------------------------------------------------------------