Created
January 28, 2026 15:52
-
-
Save aobond2/144859b00503f4862314d2424d575503 to your computer and use it in GitHub Desktop.
Blender plugin to make vertex colour gradient on selected objects, from top to bottom. Work on multiple objects.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import bpy | |
| import mathutils | |
| class VCH_OT_VertexColourByHeight(bpy.types.Operator): | |
| """Vertex colour based on world space Z position""" | |
| bl_idname = "paint.colour_by_height" | |
| bl_label = "Vertex colour by world height" | |
| bl_options = {'REGISTER', 'UNDO'} | |
| @classmethod | |
| def poll(cls, context): | |
| return context.selected_objects and any(obj.type == 'MESH' for obj in context.selected_objects) | |
| def execute(self, context): | |
| # Get the meshes | |
| selectedMeshes = [obj for obj in context.selected_objects if obj.type == 'MESH'] | |
| if not selectedMeshes: | |
| self.report({'ERROR'}, "Select mesh to work on") | |
| return {'CANCELLED'} | |
| # Get bounds of selected objects | |
| # Initialize min max | |
| globalMinZ = float('inf') | |
| globalMaxZ = float('-inf') | |
| hasVertices = False | |
| # Cache vertex Z coords per object, like this {objName: zCoord_0, zCoord_1, ...} | |
| objZCache = {} | |
| for obj in selectedMeshes: | |
| mesh = obj.data | |
| if len(mesh.vertices) == 0: | |
| continue | |
| hasVertices = True | |
| worldMatrix = obj.matrix_world | |
| # Calculate all z coords of vertices in the object | |
| # Do obj.matrix_world multiplication to convert from local space to world space | |
| zValues = [(worldMatrix @ v.co).z for v in mesh.vertices] | |
| # Update Z min max | |
| currentMin = min(zValues) | |
| currentMax = max(zValues) | |
| if currentMin < globalMinZ: | |
| globalMinZ = currentMin | |
| if currentMax > globalMaxZ: | |
| globalMaxZ = currentMax | |
| # filling the set | |
| objZCache[obj.name] = zValues | |
| if not hasVertices: | |
| self.report({'WARNING'}, "Selected meshes have no vertices") | |
| return {'CANCELLED'} | |
| zRange = globalMaxZ - globalMinZ | |
| # Setting this value to not be 0 (if it's 0), so later we don't divide by zero. | |
| # If object is thin and flat this value can be 0 | |
| if zRange == 0: | |
| zRange = 1.0 | |
| # Step 2 colouring the vertices | |
| # Iterate on each object | |
| for obj in selectedMeshes: | |
| if obj.name not in objZCache: | |
| continue | |
| mesh = obj.data | |
| zValues = objZCache[obj.name] | |
| # Make or get vertex colour | |
| if not mesh.vertex_colors: | |
| VCLayer = mesh.vertex_colors.new(name="ZHeightColours") | |
| else: | |
| VCLayer = mesh.vertex_colors.active | |
| data = VCLayer.data | |
| # Iterate on loops of the mesh where Blender store VC data(corner of triangle, that can be shared by vertex) | |
| for i, loop in enumerate(mesh.loops): | |
| vIndex = loop.vertex_index | |
| z = zValues[vIndex] | |
| # Get difference/distance of current Z with overall Min Z, then normalize it to get ratio. | |
| # Then clamp again just in case. | |
| ratio = (z - globalMinZ) / zRange | |
| ratio = max(0.0, min(1.0, ratio)) | |
| # Colouring Red(low) Blue(high) | |
| r = 1.0 - ratio | |
| g = 0.0 | |
| b = ratio | |
| a = 1.0 | |
| data[i].color = (r, g, b, a) | |
| return {'FINISHED'} | |
| # GUI | |
| class VCH_PT_VertexColourByHeight(bpy.types.Panel): | |
| bl_label = "Vertex Colour Tools" | |
| bl_idname = "VIEW3D_PT_colour_by_height" | |
| bl_space_type = 'VIEW_3D' | |
| bl_region_type = 'UI' | |
| bl_category = 'Vertex Colour' | |
| def draw(self, context): | |
| layout = self.layout | |
| row = layout.row() | |
| row.scale_y = 1.5 | |
| row.operator("paint.colour_by_height", icon='BRUSH_DATA') | |
| # Class registration | |
| classes = ( | |
| VCH_OT_VertexColourByHeight, | |
| VCH_PT_VertexColourByHeight | |
| ) | |
| def register(): | |
| for cls in classes: | |
| bpy.utils.register_class(cls) | |
| def unregister(): | |
| for cls in reversed(classes): | |
| bpy.utils.unregister_class(cls) | |
| if __name__ == "__main__": | |
| register() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment