NovaBryan/EVE_Dump
Log | Files | 📦 下载最新源码 (.zip) | Refs | README

paperDollPortrait.py (26074B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\paperDoll\paperDollPortrait.py
      2 from __future__ import print_function
      3 import math
      4 import datetime
      5 import os
      6 import geo2
      7 import trinity
      8 import blue
      9 import yaml
     10 import telemetry
     11 import legacy_r_drive
     12 import eve.common.script.paperDoll.paperDollDefinitions as pdDef
     13 import eve.common.script.paperDoll.paperDollDataManagement as pdDm
     14 import eve.client.script.paperDoll.commonClientFunctions as pdCcf
     15 import eve.common.script.paperDoll.paperDollCommonFunctions as pdCf
     16 from eve.client.script.paperDoll.SkinLightmapRenderer import SkinLightmapRenderer
     17 from itertools import chain
     18 
     19 def ApplySuffix(basePath, suffix):
     20     path = '{0}{1}'.format(basePath, suffix)
     21     return path
     22 
     23 
     24 _FACE_SETUP = {}
     25 _FACE_TWEAK = None
     26 
     27 def _GetFaceSetup(gender):
     28     if gender in _FACE_SETUP:
     29         return _FACE_SETUP[gender]
     30     rootFolder = 'R:/'
     31     if not legacy_r_drive.loadFromContent:
     32         rootFolder = 'res:/'
     33     faceSetupPath = rootFolder + 'Graphics/Character/Global/FaceSetup/BasicFace.face'
     34     if gender != pdDef.GENDER.FEMALE:
     35         faceSetupPath = rootFolder + 'Graphics/Character/Global/FaceSetup/BasicFaceMale.face'
     36     if blue.paths.exists(faceSetupPath):
     37         data = pdDm.LoadYamlFileNicely(faceSetupPath)
     38     else:
     39         data = None
     40     _FACE_SETUP[gender] = data
     41 
     42 
     43 def _GetFaceTweakData():
     44     global _FACE_TWEAK
     45     if _FACE_TWEAK:
     46         return _FACE_TWEAK
     47     rootFolder = 'R:/'
     48     if not legacy_r_drive.loadFromContent:
     49         rootFolder = 'res:/'
     50     faceTweaksPath = rootFolder + 'Graphics/Character/Global/FaceSetup/FaceTweakSettings.yaml'
     51     _FACE_TWEAK = pdDm.LoadYamlFileNicely(faceTweaksPath)
     52     return _FACE_TWEAK
     53 
     54 
     55 class PortraitTools(object):
     56     __metaclass__ = telemetry.ZONE_PER_METHOD
     57 
     58     @staticmethod
     59     def BindHeroShader(effect, isOptimized, suffix):
     60         if not isOptimized:
     61             effect.effectFilePath = 'res:/Graphics/Effect/Managed/Interior/Avatar/skinnedavatarbrdf_detailed' + suffix
     62         else:
     63             effect.effectFilePath = 'res:/Graphics/Effect/Managed/Interior/Avatar/avatarbrdfcombined_detailed' + suffix
     64         while not effect.effectResource.isPrepared:
     65             pdCf.Yield()
     66 
     67         if not effect.effectResource.isGood:
     68             raise ValueError('Paperdoll::Effect resource failed to load.')
     69         effect.PopulateParameters()
     70         for res in effect.resources:
     71             if res.name == 'NormalDetailMap':
     72                 res.resourcePath = 'res:/Texture/Global/NormalNoise_N.dds'
     73                 break
     74 
     75         for par in effect.parameters:
     76             if par.name == 'NormalDetailStrength':
     77                 par.value = 0.25
     78             if par.name == 'NormalDetailTiling':
     79                 par.value = 50.0
     80 
     81     def SetupWrinkleMapControls(self, avatar, effects, pdDoll):
     82         if avatar is None:
     83             return
     84         set = trinity.TriCurveSet()
     85         set.name = 'Wrinkles'
     86         for s in avatar.curveSets:
     87             if s.name == 'Wrinkles':
     88                 for bind in s.bindings:
     89                     if hasattr(bind, 'copyValueCallable'):
     90                         bind.copyValueCallable = None
     91 
     92                 avatar.curveSets.remove(s)
     93                 break
     94 
     95         avatar.curveSets.append(set)
     96         weakAvatar = blue.BluePythonWeakRef(avatar)
     97         pdDoll.LoadBindPose()
     98 
     99         def CreateBoneDriver(set, bone1, bone2, zone, min, max, oldMin, oldMax, curveDictionary, name, tweakData):
    100             if bone1 in pdDoll.bindPose and bone1 in pdDoll.boneOffsets and bone2 in pdDoll.bindPose and bone2 in pdDoll.boneOffsets:
    101                 bindPosePos1 = pdDoll.bindPose[bone1]['translation']
    102                 offset1 = pdDoll.boneOffsets[bone1]['translation']
    103                 bindPosePos2 = pdDoll.bindPose[bone2]['translation']
    104                 offset2 = pdDoll.boneOffsets[bone2]['translation']
    105                 realPos1 = (bindPosePos1[0] / 100.0 + offset1[0], bindPosePos1[1] / 100.0 + offset1[1], bindPosePos1[2] / 100.0 + offset1[2])
    106                 realPos2 = (bindPosePos2[0] / 100.0 + offset2[0], bindPosePos2[1] / 100.0 + offset2[1], bindPosePos2[2] / 100.0 + offset2[2])
    107                 bindPoseDist = geo2.Vec3Distance(bindPosePos1, bindPosePos2) / 100.0
    108                 dist = geo2.Vec3Distance(realPos1, realPos2)
    109                 multiplier = dist / bindPoseDist
    110                 oldMin = oldMin * multiplier
    111                 oldMax = oldMax * multiplier
    112 
    113             def GetOrMakeCurve(bone, set, curveDictionary, name):
    114                 curve = curveDictionary.get((weakAvatar, bone), None)
    115                 if curve is not None:
    116                     return curve
    117                 curve = trinity.Tr2BoneMatrixCurve()
    118                 curve.bone = bone
    119                 curve.name = name
    120                 curveDictionary[weakAvatar, bone] = curve
    121                 set.curves.append(curve)
    122                 return curve
    123 
    124             curve1 = GetOrMakeCurve(bone1, set, curveDictionary, 'bone1')
    125             curve2 = GetOrMakeCurve(bone2, set, curveDictionary, 'bone2')
    126             bind = trinity.TriValueBinding()
    127             set.bindings.append(bind)
    128 
    129             def distanceBinding(curve1, curve2, min, max, parameters, index, gamma):
    130                 avatar = weakAvatar.object
    131                 if avatar is None:
    132                     return
    133                 curve1.skinnedObject = avatar
    134                 curve2.skinnedObject = avatar
    135                 if curve1.startValue == curve1.currentValue or curve2.startValue == curve2.currentValue:
    136                     curve1.skinnedObject = None
    137                     curve2.skinnedObject = None
    138                     return
    139                 p1 = curve1.currentValue[3]
    140                 p2 = curve2.currentValue[3]
    141                 dist = geo2.Vec3Distance(p1, p2)
    142                 d = 1.0 - (dist - min) * 1 / (max - min)
    143                 if d < 0:
    144                     d = 0
    145                 elif d > 1:
    146                     d = 1
    147                 if gamma != 1.0:
    148                     d = math.pow(d, gamma)
    149                 if False:
    150                     trinity.GetDebugRenderer().DrawLine((p1[0], p1[1], p1[2]), 4294967295L, (p2[0], p2[1], p2[2]), 4294967295L)
    151                     trinity.GetDebugRenderer().Print3D((p1[0], p1[1], p1[2]), 4294967295L, str(dist))
    152                     trinity.GetDebugRenderer().Print3D((p2[0], p2[1], p2[2]), 4294967295L, str(d))
    153                 for p_ in parameters:
    154                     p = p_.object
    155                     if p is None:
    156                         continue
    157                     if index == 1:
    158                         p.v1 = d
    159                     if index == 2:
    160                         p.v2 = d
    161                     if index == 3:
    162                         p.v3 = d
    163                     if index == 4:
    164                         p.v4 = d
    165 
    166                 curve1.skinnedObject = None
    167                 curve2.skinnedObject = None
    168 
    169             zone = zone - 1
    170             paramIndex = zone / 4 + 1
    171             index = zone % 4 + 1
    172             param = 'WrinkleNormalStrength' + str(paramIndex)
    173             parameters = []
    174             for fx in effects:
    175                 for p in fx.parameters:
    176                     if p.name == param:
    177                         parameters.append(blue.BluePythonWeakRef(p))
    178 
    179             gammaCurves = tweakData.get('gammaCurves', {})
    180             gamma = gammaCurves.get(name, gammaCurves.get('default', 1.0))
    181             bind.copyValueCallable = lambda source, dest: distanceBinding(curve1, curve2, oldMin, oldMax, parameters, index, gamma)
    182 
    183         def CreateAxisDriver(set, jointName, zone, axis, maxValue, name, tweakData, pdDoll):
    184             avatar = weakAvatar.object
    185             if avatar is None or not hasattr(avatar, 'animationUpdater') or avatar.animationUpdater is None or not hasattr(avatar.animationUpdater, 'network') or avatar.animationUpdater.network is None:
    186                 return
    187             bind = trinity.TriValueBinding()
    188             set.bindings.append(bind)
    189             if not hasattr(avatar.animationUpdater, 'network'):
    190                 return
    191             jointIndex = avatar.animationUpdater.network.GetBoneIndex(jointName)
    192             if jointIndex == -1:
    193                 print("Error: Can't find bone for axis control: " + jointName)
    194                 return
    195             if not type(jointIndex) == type(1):
    196                 print('Non-int returned for bone in axis: ' + str(jointIndex) + ', name:' + jointName)
    197                 return
    198             bindPosePos = pdDoll.bindPose[jointName]
    199             if jointName in pdDoll.boneOffsets:
    200                 offset = pdDoll.boneOffsets[jointName]
    201             else:
    202                 offset = {'translation': [0, 0, 0]}
    203             startPos = [0,
    204              0,
    205              0,
    206              0,
    207              0,
    208              0,
    209              0,
    210              0,
    211              0,
    212              (bindPosePos['translation'][0] + offset['translation'][0]) / 100.0,
    213              (bindPosePos['translation'][1] + offset['translation'][1]) / 100.0,
    214              (bindPosePos['translation'][2] + offset['translation'][2]) / 100.0]
    215 
    216             def AxisBinding(startPos, jointIndex, axis, maxValue, parameters, index, gamma):
    217                 avatar = weakAvatar.object
    218                 if avatar is None:
    219                     return
    220                 if not hasattr(avatar.animationUpdater, 'network') or avatar.animationUpdater.network is None:
    221                     return
    222                 newPos = avatar.animationUpdater.network.GetRawTrackData(jointIndex)
    223                 diff = (newPos[9] - startPos[9], newPos[10] - startPos[10], newPos[11] - startPos[11])
    224                 value = 0
    225                 if axis == 'x':
    226                     value = diff[0]
    227                 elif axis == 'y':
    228                     value = diff[1]
    229                 elif axis == 'z':
    230                     value = diff[2]
    231                 finalValue = 0
    232                 max = maxValue
    233                 if max < 0:
    234                     max = -max
    235                     value = -value
    236                 if value > max:
    237                     finalValue = 1.0
    238                 elif value < 0:
    239                     finalValue = 0.0
    240                 else:
    241                     finalValue = value / max
    242                 if gamma != 1.0:
    243                     finalValue = math.pow(finalValue, gamma)
    244                 for p_ in parameters:
    245                     p = p_.object
    246                     if p is None:
    247                         continue
    248                     if index == 1:
    249                         p.v1 = finalValue
    250                     if index == 2:
    251                         p.v2 = finalValue
    252                     if index == 3:
    253                         p.v3 = finalValue
    254                     if index == 4:
    255                         p.v4 = finalValue
    256 
    257             zone = zone - 1
    258             paramIndex = zone / 4 + 1
    259             index = zone % 4 + 1
    260             param = 'WrinkleNormalStrength' + str(paramIndex)
    261             parameters = []
    262             for fx in effects:
    263                 for p in fx.parameters:
    264                     if p.name == param:
    265                         parameters.append(blue.BluePythonWeakRef(p))
    266 
    267             gammaCurves = tweakData.get('gammaCurves', {})
    268             gamma = gammaCurves.get(name, gammaCurves.get('default', 1.0))
    269             bind.name = 'AxisBind'
    270             bind.copyValueCallable = lambda source, dest: AxisBinding(startPos, jointIndex, axis, maxValue, parameters, index, gamma)
    271 
    272         data = _GetFaceSetup(pdDoll.gender)
    273         if not data:
    274             return
    275         tweakData = _GetFaceTweakData()
    276         curveDictionary = {}
    277         for d in data:
    278             entry = data[d]
    279             if len(entry) == 7:
    280                 CreateBoneDriver(set, entry[0], entry[1], entry[2], entry[3] / 100.0, entry[4] / 100.0, entry[5] / 100.0, entry[6] / 100.0, curveDictionary, d, tweakData)
    281             elif len(entry) == 4:
    282                 CreateAxisDriver(set, entry[0], entry[1], entry[2], entry[3] / 100.0, d, tweakData, pdDoll)
    283 
    284         wrinkleMultiplier = tweakData.get('wrinkleMultiplier', 1.0)
    285         correctionMultiplier = tweakData.get('correctionMultiplier', 1.0)
    286         for fx in effects:
    287             for p in fx.parameters:
    288                 if p.name == 'WrinkleParams':
    289                     p.value = (wrinkleMultiplier,
    290                      correctionMultiplier,
    291                      0.0,
    292                      0.0)
    293 
    294         set.Play()
    295         return set
    296 
    297     @staticmethod
    298     def BindLinearAvatarBRDF(effect, suffix):
    299         path = effect.effectFilePath.lower()
    300         pathSansSuffix = path[:path.rfind('.')]
    301         if pathSansSuffix.endswith('skinnedavatarbrdf') or pathSansSuffix.endswith('skinnedavatarbrdfdouble') or pathSansSuffix.endswith('clothavatar'):
    302             effect.effectFilePath = ApplySuffix(pathSansSuffix + 'linear', suffix)
    303         elif pathSansSuffix.endswith('skinnedavatarbrdflinear') or pathSansSuffix.endswith('skinnedavatarbrdfdoublelinear') or pathSansSuffix.endswith('clothavatarlinear'):
    304             effect.effectFilePath = ApplySuffix(pathSansSuffix, suffix)
    305 
    306     @staticmethod
    307     def TweakDiffuseSampler(effect):
    308         for s in effect.samplerOverrides:
    309             if s[0] == 'DiffuseMapSampler':
    310                 return
    311 
    312         sampler = list(effect.samplerOverrides.GetDefaultValue())
    313         sampler[0] = 'DiffuseMapSampler'
    314         sampler[1] = 3
    315         sampler[2] = 3
    316         effect.samplerOverrides.append(tuple(sampler))
    317 
    318     @staticmethod
    319     def BindSkinShader(effect, wrinkleFx, scattering, buildDataManager, gender, use_png, fxSuffix):
    320         name = effect.name.lower()
    321         path = effect.effectFilePath.lower()
    322         if name.startswith('c_skin'):
    323             wasDouble = 'double' in path
    324             fxPath = pdDef.SKINNED_AVATAR_SINGLEPASS_DOUBLE_PATH if wasDouble else pdDef.SKINNED_AVATAR_SINGLEPASS_SINGLE_PATH
    325             effect.effectFilePath = ApplySuffix(fxPath[:-3], fxSuffix)
    326             while not effect.effectResource.isPrepared:
    327                 pdCf.Yield()
    328 
    329             if not effect.effectResource.isGood:
    330                 raise ValueError('Paperdoll::Effect resource failed to load')
    331             effect.PopulateParameters()
    332             pdCcf.SetOrAddMap(effect, 'BeckmannLookup', 'res:/Texture/Global/beckmannSpecular.dds')
    333             PortraitTools.TweakDiffuseSampler(effect)
    334             suffix = 'tga'
    335             if use_png and not legacy_r_drive.loadFromContent:
    336                 suffix = 'png'
    337             headMods = buildDataManager.GetModifiersByCategory(pdDef.DOLL_PARTS.HEAD)
    338             if pdDef.GENDER_ROOT:
    339                 if gender == pdDef.GENDER.FEMALE:
    340                     headFolderGeneric = 'res:/Graphics/Character/Female/Paperdoll/head/Head_Generic'
    341                 else:
    342                     headFolderGeneric = 'res:/Graphics/Character/Male/Paperdoll/head/Head_Generic'
    343             elif gender == pdDef.GENDER.FEMALE:
    344                 headFolderGeneric = 'res:/Graphics/Character/Modular/Female/head/Head_Generic'
    345             else:
    346                 headFolderGeneric = 'res:/Graphics/Character/Modular/Male/head/Head_Generic'
    347             headFolder = headFolderGeneric
    348             for hm in headMods:
    349                 headFolder = os.path.dirname(hm.redfile)
    350 
    351             if scattering:
    352                 if headFolder != '' and blue.paths.exists(headFolder + '/SkinMap.' + suffix):
    353                     pdCcf.SetOrAddMap(effect, 'SkinMap', headFolder + '/SkinMap.' + suffix)
    354                 else:
    355                     pdCcf.SetOrAddMap(effect, 'SkinMap', headFolderGeneric + '/SkinMap.' + suffix)
    356             if wrinkleFx is not None:
    357                 pdCcf.SetOrAddMap(effect, 'WrinkleZoneMap', pdDef.FEMALE_WRINKLE_FACEZONE_PREFIX + suffix)
    358                 if headFolder != '' and blue.paths.exists(headFolder + '/WrinkleNormal.' + suffix):
    359                     pdCcf.SetOrAddMap(effect, 'WrinkleNormalMap', headFolder + '/WrinkleNormal.' + suffix)
    360                 else:
    361                     pdCcf.SetOrAddMap(effect, 'WrinkleNormalMap', headFolderGeneric + '/WrinkleNormal.' + suffix)
    362                 if headFolder != '' and blue.paths.exists(headFolder + '/WrinkleNormalCorrection.' + suffix):
    363                     pdCcf.SetOrAddMap(effect, 'WrinkleNormalCorrectionMap', headFolder + '/WrinkleNormalCorrection.' + suffix)
    364                 else:
    365                     pdCcf.SetOrAddMap(effect, 'WrinkleNormalCorrectionMap', headFolderGeneric + '/WrinkleNormalCorrection.' + suffix)
    366                 if headFolder != '' and blue.paths.exists(headFolder + '/WrinkleDiffuse.' + suffix):
    367                     pdCcf.SetOrAddMap(effect, 'WrinkleDiffuseMap', headFolder + '/WrinkleDiffuse.' + suffix)
    368                 else:
    369                     pdCcf.SetOrAddMap(effect, 'WrinkleDiffuseMap', headFolderGeneric + '/WrinkleDiffuse.' + suffix)
    370                 wrinkleFx.append(effect)
    371         elif name.startswith('c_standard'):
    372 
    373             def AddClothingCube(effect):
    374                 for res in effect.resources:
    375                     if res.name == 'ClothingReflectionCube':
    376                         return
    377 
    378                 res = trinity.TriTextureParameter()
    379                 res.name = 'ClothingReflectionCube'
    380                 res.resourcePath = 'res:/Texture/Global/GenericReflection_cube.dds'
    381                 effect.resources.append(res)
    382 
    383             AddClothingCube(effect)
    384         elif name.startswith('c_eyewetness'):
    385             effect.effectFilePath = ApplySuffix(pdDef.SKINNED_AVATAR_EYEWETNESS_SHADER[:-3], fxSuffix)
    386         elif name.startswith('c_tongue'):
    387             effect.effectFilePath = ApplySuffix(pdDef.SKINNED_AVATAR_TONGUE_SHADER[:-3], fxSuffix)
    388         elif name.startswith('c_teeth'):
    389             effect.effectFilePath = ApplySuffix(pdDef.SKINNED_AVATAR_TEETH_SHADER[:-3], fxSuffix)
    390         elif name.startswith('c_eyes'):
    391             effect.effectFilePath = ApplySuffix(pdDef.SKINNED_AVATAR_EYE_SHADER[:-3], fxSuffix)
    392 
    393             def AddEyeCube(effect):
    394                 for res in effect.resources:
    395                     if res.name == 'EyeReflectionCube':
    396                         return
    397 
    398                 res = trinity.TriTextureParameter()
    399                 res.name = 'EyeReflectionCube'
    400                 res.resourcePath = pdDef.EYE_SHADER_REFLECTION_CUBE_PATH
    401                 effect.resources.append(res)
    402 
    403             AddEyeCube(effect)
    404 
    405     def BindCustomShaders(effect, useFastShaders, doll):
    406         path = effect.effectFilePath.lower()
    407         name = effect.name.lower()
    408         if 'glassshader' in path:
    409             for res in effect.resources:
    410                 if res.name == 'GlassReflectionCube':
    411                     return
    412 
    413             res = trinity.TriTextureParameter()
    414             res.name = 'GlassReflectionCube'
    415             res.resourcePath = pdDef.GLASS_SHADER_REFLECTION_CUBE_PATH
    416             effect.resources.append(res)
    417         elif name.startswith('c_s2'):
    418             for res in effect.resources:
    419                 if res.name == 'ClothingReflectionCube':
    420                     return
    421 
    422             res = trinity.TriTextureParameter()
    423             res.name = 'ClothingReflectionCube'
    424             res.resourcePath = 'res:/Texture/Global/GenericReflection_cube.dds'
    425             effect.resources.append(res)
    426             for name in ['CellReflectionMap', 'CellReflection2ndMap']:
    427                 res = trinity.TriTextureParameter()
    428                 res.name = name
    429                 effect.resources.append(res)
    430 
    431         fastFx = '_fast.fx'
    432         if useFastShaders and fastFx not in path and path in pdDef.SHADERS_THAT_CAN_SWITCH_TO_FAST_SHADER_MODE:
    433             effect.effectFilePath = ApplySuffix(effect.effectFilePath[:-3], fastFx)
    434         elif doll.useDXT5N:
    435             effect.effectFilePath = ApplySuffix(effect.effectFilePath[:-3], '_dxt5n.fx')
    436 
    437     @staticmethod
    438     def BindHeroHairShader(effect, suffix):
    439         path = effect.effectFilePath.lower()
    440         if path.find('hair') == -1:
    441             return False
    442         if path.find('clothavatarhair') != -1:
    443             effect.effectFilePath = ApplySuffix('res:/Graphics/Effect/Managed/Interior/Avatar/clothavatarhair_detailed', suffix)
    444         else:
    445             effect.effectFilePath = ApplySuffix('res:/Graphics/Effect/Managed/Interior/Avatar/skinnedavatarhair_detailed', suffix)
    446         while not effect.effectResource.isPrepared:
    447             pdCf.Yield()
    448 
    449         if not effect.effectResource.isGood:
    450             raise ValueError('Paperdoll::Effect resource failed to load')
    451         name = effect.name.lower()
    452         effect.PopulateParameters()
    453         pdCcf.SetOrAddMap(effect, 'HairNoise', 'res:/Texture/Global/noise1d.dds')
    454         pdCcf.SetOrAddMap(effect, 'TangentMap', 'res:/Texture/Global/50gray.dds' if not name.startswith('c_hair_head') else 'res:/Texture/Global/HeadTangents.dds')
    455         PortraitTools.TweakDiffuseSampler(effect)
    456         return True
    457 
    458     @staticmethod
    459     def BindHeroClothShader(effect, useDXT5n):
    460         path = effect.effectFilePath.lower()
    461         if 'clothavatar' not in path or 'clothavatarhair' in path:
    462             return
    463         suffix = '_dxt5n.fx' if useDXT5n else '.fx'
    464         effect.effectFilePath = ApplySuffix('res:/Graphics/Effect/Managed/Interior/Avatar/ClothAvatarLinear', suffix)
    465         while not effect.effectResource.isPrepared:
    466             pdCf.Yield()
    467 
    468         if not effect.effectResource.isGood:
    469             raise ValueError('Paperdoll::Effect resource failed to load.')
    470         effect.PopulateParameters()
    471         PortraitTools.TweakDiffuseSampler(effect)
    472 
    473     @staticmethod
    474     def SetupStubble(meshes, name, stubblePath, adding = True):
    475         ret = []
    476         stubbleName = '{0}Stubble'.format(name)
    477         stubbleMeshes = (mesh for mesh in meshes if mesh.name.startswith(pdDef.DOLL_PARTS.HEAD))
    478         for mesh in stubbleMeshes:
    479             if adding:
    480                 if any(map(lambda x: 'stubble' in x.name.lower(), mesh.decalAreas)):
    481                     ret.append(mesh)
    482                     continue
    483                 c_skin_opaque_areas = (area for area in mesh.opaqueAreas if area.effect.name.lower().startswith('c_skin_'))
    484                 c_skin_decal_areas = (area for area in mesh.decalAreas if area.effect.name.lower().startswith('c_skin_'))
    485                 c_skin_areas = chain(c_skin_opaque_areas, c_skin_decal_areas)
    486                 for area in c_skin_areas:
    487                     stubbleArea = trinity.Tr2MeshArea()
    488                     stubbleArea.name = stubbleName
    489                     stubbleArea.index = area.index
    490                     stubbleArea.count = area.count
    491                     blue.paths.GetFileContentsWithYield(stubblePath)
    492                     stubbleArea.effect = trinity.Load(stubblePath)
    493                     for resource in stubbleArea.effect.resources:
    494                         if resource.name == 'LengthTexture':
    495                             starttime = datetime.datetime.now()
    496                             timeout = starttime + datetime.timedelta(seconds=10)
    497                             while not (resource.resource.isPrepared and resource.resource.isGood) and datetime.datetime.now() < timeout:
    498                                 pdCf.Yield()
    499 
    500                             if not resource.resource.IsGood():
    501                                 raise ValueError('Paperdoll::Stubble texture failed to load.')
    502 
    503                     mesh.decalAreas.append(stubbleArea)
    504                     ret.append(mesh)
    505                     for instance in SkinLightmapRenderer.instances:
    506                         if instance():
    507                             instance().BindLightmapShader(mesh)
    508 
    509             else:
    510                 targetAreasToRemove = [ area for area in mesh.decalAreas if area.name == stubbleName ]
    511                 for ta in targetAreasToRemove:
    512                     mesh.decalAreas.remove(ta)
    513 
    514         return ret
    515 
    516     @staticmethod
    517     def HandleRemovedStubble(removedModifiers, buildDataManager):
    518         stubbleMods = (mod for mod in removedModifiers if mod.stubblePath)
    519         for mod in stubbleMods:
    520             stubbleMeshes = buildDataManager.GetMeshes(pdDef.DOLL_PARTS.HEAD)
    521             PortraitTools.SetupStubble(stubbleMeshes, mod.name, mod.stubblePath, False)
    522 
    523     @staticmethod
    524     def GetStubbleHash(addedAndChangedModifiers):
    525         for mod in iter(addedAndChangedModifiers):
    526             if mod.stubblePath:
    527                 return 'active'
    528 
    529         return ''
    530 
    531     @staticmethod
    532     def HandleUpdatedStubble(addedAndChangedModifiers, buildDataManager):
    533         stubbleMods = (mod for mod in addedAndChangedModifiers if mod.stubblePath)
    534         stubbleMeshes = buildDataManager.GetMeshes(pdDef.DOLL_PARTS.HEAD)
    535         for mod in stubbleMods:
    536             if mod.weight > 0.001:
    537                 meshes = PortraitTools.SetupStubble(stubbleMeshes[:], mod.name, mod.stubblePath, True)
    538                 mod.meshes = meshes
    539             for mesh in iter(mod.meshes):
    540                 for area in mesh.decalAreas:
    541                     fx = area.effect
    542                     wt = mod.weight
    543                     length = 0.003 + wt * 0.0027
    544                     alpha = 0.25 + wt * 2.25
    545                     for p in fx.parameters:
    546                         if p.name == 'MaterialDiffuseColor':
    547                             col = mod.colorizeData[0]
    548                             p.value = (col[0],
    549                              col[1],
    550                              col[2],
    551                              1.0)
    552                         if p.name == 'FurLength':
    553                             p.value = length
    554                         if p.name == 'AlphaMultiplier':
    555                             p.value = alpha
    556 
    557     @staticmethod
    558     def RebindDXT5ShadersForSM2(meshes):
    559         if meshes is None:
    560             return
    561         for mesh in meshes:
    562             effects = pdCcf.GetEffectsFromMesh(mesh)
    563             for effect in effects:
    564                 path = effect.effectFilePath.lower()
    565                 if '_dxt5n.fx' not in path:
    566                     continue
    567                 path = path[:-9] + '.fx'
    568                 if path not in pdDef.SHADERS_THAT_CAN_SWITCH_TO_FAST_SHADER_MODE:
    569                     continue
    570                 path = path[:-3] + '_fast_dxt5n.fx'
    571                 effect.effectFilePath = path