199 lines
5.7 KiB
Python
199 lines
5.7 KiB
Python
# SPDX-FileCopyrightText: 2009 Campbell Barton
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
def ensure_active_color_attribute(me):
|
|
if me.attributes.active_color:
|
|
return me.attributes.active_color
|
|
return me.color_attributes.new("Color", 'BYTE_COLOR', 'CORNER')
|
|
|
|
|
|
def applyVertexDirt(me, blur_iterations, blur_strength, clamp_dirt, clamp_clean, dirt_only, normalize):
|
|
from mathutils import Vector
|
|
from math import acos
|
|
import array
|
|
|
|
# We simulate the accumulation of dirt in the creases of geometric surfaces
|
|
# by comparing the vertex normal to the average direction of all vertices
|
|
# connected to that vertex. We can also simulate surfaces being buffed or
|
|
# worn by testing protruding surfaces.
|
|
#
|
|
# So if the angle between the normal and geometric direction is:
|
|
# < 90 - dirt has accumulated in the crease
|
|
# > 90 - surface has been worn or buffed
|
|
# ~ 90 - surface is flat and is generally unworn and clean
|
|
#
|
|
# This method is limited by the complexity or lack there of in the geometry.
|
|
#
|
|
# Original code and method by Keith "Wahooney" Boshoff.
|
|
|
|
vert_tone = array.array("f", [0.0]) * len(me.vertices)
|
|
|
|
# create lookup table for each vertex's connected vertices (via edges)
|
|
con = [[] for i in range(len(me.vertices))]
|
|
|
|
# add connected verts
|
|
for e in me.edges:
|
|
con[e.vertices[0]].append(e.vertices[1])
|
|
con[e.vertices[1]].append(e.vertices[0])
|
|
|
|
for i, v in enumerate(me.vertices):
|
|
vec = Vector()
|
|
no = v.normal
|
|
co = v.co
|
|
|
|
# get the direction of the vectors between the vertex and it's connected vertices
|
|
for c in con[i]:
|
|
vec += (me.vertices[c].co - co).normalized()
|
|
|
|
# average the vector by dividing by the number of connected verts
|
|
tot_con = len(con[i])
|
|
|
|
if tot_con == 0:
|
|
ang = pi / 2.0 # assume 90°, i. e. flat
|
|
else:
|
|
vec /= tot_con
|
|
|
|
# angle is the acos() of the dot product between normal and connected verts.
|
|
# > 90 degrees: convex
|
|
# < 90 degrees: concave
|
|
ang = acos(no.dot(vec))
|
|
|
|
# enforce min/max
|
|
ang = max(clamp_dirt, ang)
|
|
|
|
if not dirt_only:
|
|
ang = min(clamp_clean, ang)
|
|
|
|
vert_tone[i] = ang
|
|
|
|
# blur tones
|
|
for i in range(blur_iterations):
|
|
# backup the original tones
|
|
orig_vert_tone = vert_tone[:]
|
|
|
|
# use connected verts look up for blurring
|
|
for j, c in enumerate(con):
|
|
for v in c:
|
|
vert_tone[j] += blur_strength * orig_vert_tone[v]
|
|
|
|
vert_tone[j] /= len(c) * blur_strength + 1
|
|
del orig_vert_tone
|
|
|
|
if normalize:
|
|
min_tone = min(vert_tone)
|
|
max_tone = max(vert_tone)
|
|
else:
|
|
min_tone = clamp_dirt
|
|
max_tone = clamp_clean
|
|
|
|
tone_range = max_tone - min_tone
|
|
|
|
if tone_range < 0.0001:
|
|
# weak, don't cancel, see #43345
|
|
tone_range = 0.0
|
|
else:
|
|
tone_range = 1.0 / tone_range
|
|
|
|
active_color_attribute = ensure_active_color_attribute(me)
|
|
if not active_color_attribute:
|
|
return {'CANCELLED'}
|
|
|
|
point_domain = active_color_attribute.domain == 'POINT'
|
|
|
|
attribute_data = active_color_attribute.data
|
|
|
|
use_paint_mask = me.use_paint_mask
|
|
for i, p in enumerate(me.polygons):
|
|
if not use_paint_mask or p.select:
|
|
for loop_index in p.loop_indices:
|
|
loop = me.loops[loop_index]
|
|
v = loop.vertex_index
|
|
col = attribute_data[v if point_domain else loop_index].color
|
|
tone = vert_tone[v]
|
|
tone = (tone - min_tone) * tone_range
|
|
|
|
if dirt_only:
|
|
tone = min(tone, 0.5) * 2.0
|
|
|
|
col[0] = tone * col[0]
|
|
col[1] = tone * col[1]
|
|
col[2] = tone * col[2]
|
|
me.update()
|
|
return {'FINISHED'}
|
|
|
|
|
|
from bpy.types import Operator
|
|
from bpy.props import FloatProperty, IntProperty, BoolProperty
|
|
from math import pi
|
|
|
|
|
|
class VertexPaintDirt(Operator):
|
|
"""Generate a dirt map gradient based on cavity"""
|
|
bl_idname = "paint.vertex_color_dirt"
|
|
bl_label = "Dirty Vertex Colors"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
blur_strength: FloatProperty(
|
|
name="Blur Strength",
|
|
description="Blur strength per iteration",
|
|
min=0.01, max=1.0,
|
|
default=1.0,
|
|
)
|
|
blur_iterations: IntProperty(
|
|
name="Blur Iterations",
|
|
description="Number of times to blur the colors (higher blurs more)",
|
|
min=0, max=40,
|
|
default=1,
|
|
)
|
|
clean_angle: FloatProperty(
|
|
name="Highlight Angle",
|
|
description="Less than 90 limits the angle used in the tonal range",
|
|
min=0.0, max=pi,
|
|
default=pi,
|
|
unit='ROTATION',
|
|
)
|
|
dirt_angle: FloatProperty(
|
|
name="Dirt Angle",
|
|
description="Less than 90 limits the angle used in the tonal range",
|
|
min=0.0, max=pi,
|
|
default=0.0,
|
|
unit='ROTATION',
|
|
)
|
|
dirt_only: BoolProperty(
|
|
name="Dirt Only",
|
|
description="Don't calculate cleans for convex areas",
|
|
default=False,
|
|
)
|
|
normalize: BoolProperty(
|
|
name="Normalize",
|
|
description="Normalize the colors, increasing the contrast",
|
|
default=True,
|
|
)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
obj = context.object
|
|
return (obj and obj.type == 'MESH')
|
|
|
|
def execute(self, context):
|
|
obj = context.object
|
|
mesh = obj.data
|
|
|
|
ret = applyVertexDirt(
|
|
mesh,
|
|
self.blur_iterations,
|
|
self.blur_strength,
|
|
self.dirt_angle,
|
|
self.clean_angle,
|
|
self.dirt_only,
|
|
self.normalize,
|
|
)
|
|
|
|
return ret
|
|
|
|
|
|
classes = (
|
|
VertexPaintDirt,
|
|
)
|