Mesh Masking Tool for Blender – Generate Illuminated Mesh Maps for Roblox Using Neon Material

:hammer_and_wrench: [Blender Plugin] Mesh Masking Tool for Neon Illumination in Roblox

Hey developers!
I’m sharing a Blender plugin designed specifically to help Roblox devs generate custom illuminated mesh segments using the Neon material in Roblox Studio.

This tool makes it easy to split up parts of your mesh based on a black-and-white texture mask, letting you isolate glowing areas for cool effects like light strips, outlines, emissive surfaces, sci-fi panels, and more.

This took me hours to figure out so hopefully will be useful to somebody.

:magnifying_glass_tilted_left: What does it do?

The plugin analyzes an image mask and:

  • Creates a vertex group for vertices that fall under black pixels in a texture.
  • Lets you subdivide the mesh automatically for better sampling accuracy.
  • Adds a Mask Modifier in Blender to separate geometry for export.
  • Helps you prep those masked sections to be assigned the Neon material in Roblox Studio.

This is especially useful if you’re creating stylized lighting effects without using lights or SurfaceGuis — just geometry and materials.


:gear: Features

  • :framed_picture: Supports any mask image (black = masked area).
  • :brick: Generates a vertex group automatically.
  • :carpentry_saw: Auto-subdivision for better precision on lower-poly meshes.
  • :control_knobs: Control everything via the Blender 3D Viewport > N-Panel > Mesh Mask.
  • :light_bulb: Compatible with Blender 4.3+.

:test_tube: How to use

  1. Select a mesh in Blender, and separate the faces with the glowing parts ready for subdivision and select them.
  2. Assign a black & white image mask.
  3. Set the black threshold and subdivision level.
  4. Click “Create Vertex Group”.
  5. Now you should manually add the modifier and clone that part.
  6. set one part to inverse one to normal.
  7. clean up geometry of the normal and inverse mask with a modifier of clean-up geometry of like 0.03 depends how much you subdivided, in my situation I had to subdivide a lot because the glowing parts were really small textures on a big face so therefore had to do bigger clean-up.
  8. Merge the normal with the original mesh, and u left with the inverse of the mask which will be the glowing parts of the mesh.
  9. Export the masked piece and import into Roblox.
  10. In Studio, assign Neon material to the masked mesh part.

That’s it! The result is a Roblox mesh with clean glowing areas – no lights or tricks required.


:file_folder: Installation

  • :link: copy the code underneath the post and create up .py anywhere on pc.
  • add the addon in preferences tab in blender.
  • you will find the new gui in n-panel of blender.

:pushpin: Notes

  • This plugin does not export the mesh for you — use Blender’s FBX or OBJ export for that.
  • Make sure your masked mesh and main mesh are exported separately if you want to apply different materials in Studio.
  • Works great for sci-fi panels, glowing armor, UI indicators, and more.

:man_technologist: Source Code

bl_info = {
    "name": "Mesh Masking Tool",
    "author": "sylwek1100",
    "version": (1, 0),
    "blender": (4, 3, 2),
    "location": "3D Viewport > N-Panel > Mesh Mask",
    "description": "Create vertex groups based on black pixels in textures for mesh masking",
    "category": "Mesh",
}

import bpy
import bmesh
import mathutils
from bpy.props import PointerProperty, FloatProperty, StringProperty, IntProperty, BoolProperty
from bpy.types import Panel, Operator, PropertyGroup

class MeshMaskProperties(PropertyGroup):
    """Properties for the mesh masking tool"""
    
    target_mesh: PointerProperty(
        name="Target Mesh",
        description="Select the mesh object to apply masking to",
        type=bpy.types.Object,
        poll=lambda self, obj: obj.type == 'MESH'
    )
    
    mask_image: PointerProperty(
        name="Mask Image",
        description="Select the image to use as mask (black pixels = remove)",
        type=bpy.types.Image
    )
    
    threshold: FloatProperty(
        name="Black Threshold",
        description="Threshold for considering pixels as black (0.0 = pure black, 1.0 = white)",
        default=0.1,
        min=0.0,
        max=1.0,
        step=0.01,
        precision=3
    )
    
    vertex_group_name: StringProperty(
        name="Vertex Group Name",
        description="Name for the created vertex group",
        default="MaskGroup"
    )
    
    subdivision_levels: IntProperty(
        name="Subdivision Levels",
        description="Number of subdivision levels to add for better sampling resolution",
        default=2,
        min=0,
        max=100
    )
    
    apply_subdivision: BoolProperty(
        name="Auto Subdivide",
        description="Automatically subdivide mesh for better sampling resolution",
        default=True
    )

class MESH_OT_create_mask_vertex_group(Operator):
    """Create vertex group based on black pixels in texture"""
    bl_idname = "mesh.create_mask_vertex_group"
    bl_label = "Create Mask Vertex Group"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.mesh_mask_props
        
        # Validate inputs
        if not props.target_mesh:
            self.report({'ERROR'}, "Please select a target mesh")
            return {'CANCELLED'}
            
        if not props.mask_image:
            self.report({'ERROR'}, "Please select a mask image")
            return {'CANCELLED'}
            
        if props.target_mesh.type != 'MESH':
            self.report({'ERROR'}, "Target object must be a mesh")
            return {'CANCELLED'}
            
        # Get image data
        image = props.mask_image
        if not image.pixels:
            self.report({'ERROR'}, "Image has no pixel data")
            return {'CANCELLED'}
            
        # Get mesh object
        mesh_obj = props.target_mesh
        mesh = mesh_obj.data
        
        # Ensure we're in object mode for vertex group operations
        context.view_layer.objects.active = mesh_obj
        bpy.ops.object.mode_set(mode='OBJECT')
        
        # Create or get vertex group
        vertex_group_name = props.vertex_group_name or "MaskGroup"
        if vertex_group_name in mesh_obj.vertex_groups:
            vertex_group = mesh_obj.vertex_groups[vertex_group_name]
        else:
            vertex_group = mesh_obj.vertex_groups.new(name=vertex_group_name)
        
        # Clear existing weights
        if len(mesh.vertices) > 0:
            try:
                vertex_group.remove(range(len(mesh.vertices)))
            except:
                pass  # Group might be empty
        
        # Enter edit mode to work with bmesh
        bpy.ops.object.mode_set(mode='EDIT')
        
        # Create bmesh from mesh
        bm = bmesh.from_edit_mesh(mesh)
        
        # Apply subdivision if requested
        if props.apply_subdivision and props.subdivision_levels > 0:
            bmesh.ops.subdivide_edges(
                bm,
                edges=bm.edges[:],
                cuts=props.subdivision_levels,
                use_grid_fill=True
            )
            # Update the mesh with subdivisions
            bmesh.update_edit_mesh(mesh)
        
        # Ensure face indices are valid after potential subdivision
        bm.faces.ensure_lookup_table()
        bm.verts.ensure_lookup_table()
        
        # Check if mesh has UV coordinates
        if not bm.loops.layers.uv:
            self.report({'ERROR'}, "Mesh has no UV coordinates")
            bpy.ops.object.mode_set(mode='OBJECT')
            return {'CANCELLED'}
            
        uv_layer = bm.loops.layers.uv.active
        
        # Get image dimensions
        width, height = image.size
        pixels = list(image.pixels)
        
        # Process each vertex
        vertices_to_remove = set()
        
        for face in bm.faces:
            for loop in face.loops:
                vertex = loop.vert
                uv = loop[uv_layer].uv
                
                # Convert UV to pixel coordinates
                x = int(uv.x * width) % width
                y = int(uv.y * height) % height
                
                # Get pixel index (RGBA format)
                pixel_index = (y * width + x) * 4
                
                if pixel_index < len(pixels) - 3:
                    # Get RGB values (ignore alpha)
                    r = pixels[pixel_index]
                    g = pixels[pixel_index + 1]
                    b = pixels[pixel_index + 2]
                    
                    # Calculate brightness (grayscale)
                    brightness = (r + g + b) / 3.0
                    
                    # If pixel is below threshold (dark), mark vertex for removal
                    if brightness <= props.threshold:
                        vertices_to_remove.add(vertex.index)
        
        # Update mesh from bmesh
        bmesh.update_edit_mesh(mesh)
        # Don't free bmesh when using from_edit_mesh
        
        # Switch to object mode
        bpy.ops.object.mode_set(mode='OBJECT')
        
        # Add vertices to vertex group (vertices to be masked out)
        if vertices_to_remove:
            vertex_group.add(list(vertices_to_remove), 1.0, 'REPLACE')
            total_verts = len(mesh.vertices)
            percentage = (len(vertices_to_remove) / total_verts) * 100
            self.report({'INFO'}, 
                f"Added {len(vertices_to_remove)} of {total_verts} vertices ({percentage:.1f}%) to group '{vertex_group_name}'")
        else:
            self.report({'WARNING'}, "No vertices found matching the mask criteria. Try lowering the threshold or enabling subdivision.")
        
        return {'FINISHED'}

class MESH_OT_apply_mask_modifier(Operator):
    """Apply a Mask modifier using the created vertex group"""
    bl_idname = "mesh.apply_mask_modifier"
    bl_label = "Add Mask Modifier"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.mesh_mask_props
        
        if not props.target_mesh:
            self.report({'ERROR'}, "Please select a target mesh")
            return {'CANCELLED'}
            
        mesh_obj = props.target_mesh
        vertex_group_name = props.vertex_group_name or "MaskGroup"
        
        # Check if vertex group exists
        if vertex_group_name not in mesh_obj.vertex_groups:
            self.report({'ERROR'}, f"Vertex group '{vertex_group_name}' not found. Create it first.")
            return {'CANCELLED'}
        
        # Add mask modifier
        modifier = mesh_obj.modifiers.new(name="MaskModifier", type='MASK')
        modifier.vertex_group = vertex_group_name
        modifier.invert_vertex_group = True  # Invert so black pixels are removed
        
        self.report({'INFO'}, f"Added Mask modifier using vertex group '{vertex_group_name}'")
        return {'FINISHED'}

class VIEW3D_PT_mesh_mask_panel(Panel):
    """N-Panel for mesh masking tool"""
    bl_label = "Mesh Mask Tool"
    bl_idname = "VIEW3D_PT_mesh_mask"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Mesh Mask"
    
    def draw(self, context):
        layout = self.layout
        props = context.scene.mesh_mask_props
        
        # Input section
        layout.label(text="Input Selection:")
        box = layout.box()
        box.prop(props, "target_mesh")
        box.prop(props, "mask_image")
        
        # Settings section
        layout.label(text="Settings:")
        box = layout.box()
        box.prop(props, "vertex_group_name")
        box.prop(props, "threshold")
        
        # Subdivision section
        box.separator()
        box.prop(props, "apply_subdivision")
        if props.apply_subdivision:
            box.prop(props, "subdivision_levels")
        
        # Help text
        layout.separator()
        box = layout.box()
        box.label(text="Instructions:", icon='INFO')
        box.label(text="1. Select mesh object")
        box.label(text="2. Select mask image")
        box.label(text="3. Enable subdivision for fine details")
        box.label(text="4. Adjust black threshold")
        box.label(text="5. Create vertex group")
        box.label(text="6. Add mask modifier")
        
        # Buttons section
        layout.separator()
        col = layout.column(align=True)
        col.operator("mesh.create_mask_vertex_group", text="Create Vertex Group", icon='GROUP_VERTEX')
        col.operator("mesh.apply_mask_modifier", text="Add Mask Modifier", icon='MOD_MASK')
        
        # Status info
        if props.target_mesh and props.mask_image:
            layout.separator()
            box = layout.box()
            box.label(text="Ready to process", icon='CHECKMARK')
            if props.target_mesh:
                box.label(text=f"Mesh: {props.target_mesh.name}")
            if props.mask_image:
                box.label(text=f"Image: {props.mask_image.name} ({props.mask_image.size[0]}x{props.mask_image.size[1]})")

# Registration
classes = [
    MeshMaskProperties,
    MESH_OT_create_mask_vertex_group,
    MESH_OT_apply_mask_modifier,
    VIEW3D_PT_mesh_mask_panel,
]

def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    
    bpy.types.Scene.mesh_mask_props = PointerProperty(type=MeshMaskProperties)

def unregister():
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
    
    del bpy.types.Scene.mesh_mask_props

if __name__ == "__main__":
    register()
3 Likes

Could you provide some pictures? Can’t think off the top of my head how this would look

2 Likes


Where the holes are before it was a unified mesh with the glowing parts marked with a texture now instead they separated into two objects

2 Likes


here is the effect, it allows for realistic models, on the right the mesh is exported as a one (roblox don’t support illuminated textures so it looks boring). The left one has been processed by my plugin and looks a lot better

2 Likes