[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.
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.
Features
Supports any mask image (black = masked area).
Generates a vertex group automatically.
Auto-subdivision for better precision on lower-poly meshes.
Control everything via the Blender 3D Viewport > N-Panel > Mesh Mask.
Compatible with Blender 4.3+.
How to use
- Select a mesh in Blender, and separate the faces with the glowing parts ready for subdivision and select them.
- Assign a black & white image mask.
- Set the black threshold and subdivision level.
- Click “Create Vertex Group”.
- Now you should manually add the modifier and clone that part.
- set one part to inverse one to normal.
- 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.
- 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.
- Export the masked piece and import into Roblox.
- 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.
Installation
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.
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.
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()


