Changing Avatar Appearance With Material Scripting
From Multiverse
| Client Scripting |
|
Overview • Coordinated Effects • Fireball Example • Terrain Decals • Compositors • About Shadows • Client Animation System • Creating Your Own Animations • Scripting Avatar Appearance |
| Reference |
This document presents an example of how to change avatar appearance dynamically during character creation using the Client Scripting API material objects. The art assets (meshes and textures) are not currently available, so this is not a running example; but you can adopt similar techniques for your world.
The source code for the scripts are included here and are also available as a downloadable zip file.
Contents |
Overview
This example illustrates:
- Using Client scripting to give the user the ability to customize the avatar during character creation, providing options for facial appearance, skin color, hair style, hair color, tattoo, upper body clothing, and lower body clothing.
- Giving the avatar the ability to "ride" a skateboard, which involves turning on the skateboard submesh, and playing different idle and movement animations (the logic for playing the animations is in a separate avatar animation script).
- Support for normal and specular maps.
- Support for an emissive map that can be turned on and off with a script. This was designed to support "black light" style lighting effects in a nightclub.
- Ability to gracefully fall back to more limited functionality on lower-end hardware, including graphics cards that do not support programmable shaders.
Client API classes
Use the following classes in the Client scripting API for scripting materials:
Theory of operation
The script works by watching for changes to various object properties and changing the appearance of the avatar based on the values of those properties. The first property of interest is AppearanceOverride. If this property is set to the value avatar, then the script will take control of the appearance of the object, otherwise the script will ignore the object.
When the AppearanceOverride property is set to avatar, the script registers for PropertyChange events on the object. Every time one of the properties that controls appearance is changed, the script will update the avatar's appearance. In the case of our avatar, updating the appearance involves selecting which submesh to display for each body part, and which textures to apply to the materials for those submeshes.
We have chosen to use a set of properties that describe the items of clothing the avatar is wearing, and then in the script we map the clothing item to a set of assets (submeshes and textures). The server just knows that the avatar is wearing a red striped shirt, and doesn't need to worry about what textures and submeshes are used to display that red striped shirt. This gives us the flexibility to change the details of how the art is constructed without having to rewrite server side game logic.
The following table describes the object properties that determine the avatar's appearance:
| Property | Description | Possible Values |
|---|---|---|
| SkinColor | Color of the avatar's skin. | caucasian, asian, african_american |
| HeadShape | Head model to use. | caucasian_01, asian_01, african_american_01
|
| HairStyle | Shape of the hair. | pony, bob, layers, bob2 |
| HairColor | Color of the hair. | blonde, red, brown, black |
| ClothesTorso | Upper clothing item. | shirt1_white, shirt1_purple, shirt1_blue, shirt2_purple, shirt2_red, shirt2_brown, shirt3_blue, shirt3_red, shirt3_green |
| ClothesLegs | Lower torso clothing item. | pants1_black, pants1_brown, pants1_blue, pants2_leopard, pants2_red |
| Tattoo | Tattoo currently applied. | tattoo_butterfly_01_chest, tattoo_butterfly_01_arm, tattoo_butterfly_01_back, tattoo_butterfly_02_chest, tattoo_butterfly_02_arm, tattoo_butterfly_02_back, tattoo_butterfly_03_chest, tattoo_butterfly_03_arm, tattoo_butterfly_03_back, tattoo_butterfly_04_chest, tattoo_butterfly_04_arm, tattoo_butterfly_04_back, tattoo_butterfly_05_chest, tattoo_butterfly_05_arm, tattoo_butterfly_05_back, tattoo_roses_01_chest, tattoo_roses_01_arm, tattoo_roses_01_back, tattoo_roses_02_chest, tattoo_roses_02_arm, tattoo_roses_02_back, tattoo_roses_03_chest, tattoo_roses_03_arm, tattoo_roses_03_back, tattoo_roses_04_chest, tattoo_roses_04_arm, tattoo_roses_04_back, tattoo_roses_05_chest, tattoo_roses_05_arm, tattoo_roses_05_back, tattoo_roses_06_chest, tattoo_roses_06_arm, tattoo_roses_06_back, tattoo_sunburst_01_chest, tattoo_sunburst_01_arm, tattoo_sunburst_01_back, tattoo_sunburst_02_chest, tattoo_sunburst_02_arm, tattoo_sunburst_02_back, tattoo_sunburst_03_chest, tattoo_sunburst_03_arm, tattoo_sunburst_03_back, tattoo_sunburst_04_chest, tattoo_sunburst_04_arm, tattoo_sunburst_04_back, tattoo_sunburst_05_chest, tattoo_sunburst_05_arm, tattoo_sunburst_05_back |
| OnSkateboard | Whether the avatar has the skateboard equipped. | True, False |
| AppearanceOverride | Triggers the client side dynamic appearance script. | avatar |
Avatar model
Note: the model for this example is not available to the public at this time. It is described here to give some context for the scripts below.
The avatar model is broken up into four parts: head, hair, torso, legs. The model contains multiple submeshes for each part to allow for customized geometry based on the appearance and clothing selections. The model has a second set of UV coordinates on the torso submeshes to support tattoo rendering. See the section on Tattoos below.
The following table describes the model's submeshes.
| Body Part | Available Submeshes |
|---|---|
| Head |
|
| Hair |
|
| Torso |
|
| Legs |
|
Tattoos
It would be inefficient to use the same texture coordinates for the tattoos as the other textures (skin, clothes, and so on). For example, the area of the torso covered by an arm tattoo is small compared to the entire area of the torso. A tattoo texture mapped over the whole torso would only have a small percentage of its pixel area devoted to the actual tattoo, and the rest of the texture would be blank, resulting in lots of wasted texture memory and poor-looking tattoos.
To solve this problem the avatar has a second set of texture UV coordinates to use when loading pixels from the tattoo texture. There are three tattoo locations on the avatar, chest, arms, and back. All use V texture coordinate values from 0 to 1. However, the values of the U coordinate varies:
- chest: 0 to 1
- arm: 2 to 3
- back: 4 to 5
All other parts of the avatar have UV coordinates outside those ranges to prevent the tattoo from "leaking" into other areas.
When fetching pixels from the tattoo texture, we use the "border" texture addressing mode, with an RGBA border color of (0,0,0,0). This ensures that the tattoo will be both blank and transparent for any area with UV coordinates outside the 0 to 1 range. This results in the tattoo being visible in the chest area, and not visible anywhere else on the model. To move the tattoo to the arm, we add an offset to the UV coordinates before fetching pixels from the texture. An offset of -2 to the U coordinate adjusts the texture coordinates so that the chest is in the range -2 to -1, the arm is in the range 0 to 1, and the back is in the range 2 to 3. This results in the tattoo becoming visible on the arm, and invisible in the other locations. Changing the offset to -4 results in the back tattoo becoming visible.
The AvatarMaterial script described below includes the method SetTattooSite() that the appearance scripts uses to change the material to move the tattoo to various locations.
The material
For each submesh of the model, the appearance script makes a copy of the material, edits it based on the appearance properties, and sets it as the material for the particular submesh.
This material implements three layered diffuse textures for avatars:
- Skin
- Tattoo
- Clothes.
Alpha-blending these three textures then produces the diffuse color for the material. All techniques, passes and texture units have explicit names, which enables Python scripts to easily access them.
The script includes two techniques:
- The "Shader20" technique uses shader model 2.0 vertex and fragment programs, and targets DirectX 9 compatible graphics cards. It does skeletal animation in hardware, blending of skin, tattoo and clothing texture layers, and lighting with emissive, specular and normal mapping.
- The fallback technique, "Fixed," uses the fixed function pipeline. It works on older graphics cards that do not support pixel and vertex shaders but that support more than the minimum number of simultaneous textures and texture blend stages. It does skeletal animation in software, blending of skin, tattoo and clothing texture layers, and lighting with emissive mapping but does not support specular and normal mapping.
The base avatar material file is:
material LayeredAvatar
{
// Technique that uses shader model 2.0
technique Shader20
{
scheme Default
pass Single
{
// Vertex program reference
vertex_program_ref LayeredAvatarVP20
{
}
shadow_caster_vertex_program_ref LayeredAvatarVP_Shadow
{
}
shadow_receiver_vertex_program_ref LayeredAvatarVP_ShadowReceiver
{
}
fragment_program_ref LayeredAvatarFP20
{
}
texture_unit SkinMap
{
}
texture_unit TattooMap
{
texture blank_tattoo.dds
// use a black transparent border for tattoo texture
tex_address_mode border
tex_border_colour 0.0 0.0 0.0 0.0
}
texture_unit ClothesMap
{
texture blank_clothes.dds
}
texture_unit SpecularMap
{
texture blank_specular.dds
}
texture_unit EmissiveMap
{
texture blank_emissive.dds
}
texture_unit NormalMap
{
texture blank_normal.dds
}
}
}
// Technique that uses fixed function graphics pipeline.
technique Fixed
{
scheme Fixed
pass Single
{
shading phong
ambient 1.00000 1.00000 1.00000 1.00000
diffuse 1.00000 1.00000 1.00000 1.00000
specular 0.00000 0.00000 0.00000 1.00000 1.00000
emissive 0.00000 0.00000 0.00000 1.00000
texture_unit SkinMap
{
color_op_ex source1 src_texture src_texture
tex_coord_set 0
}
texture_unit TattooMap
{
// use alpha blend to combine tattoo with skin
color_op alpha_blend
alpha_op_ex source1 src_current src_current
// texture scroll is used to select which tattoo site to use (back, arm, chest)
scroll 0 0
// tattoos have their own texture coordinate set
tex_coord_set 1
// use a black transparent border for tattoo texture
tex_address_mode border
tex_border_colour 0.0 0.0 0.0 0.0
}
texture_unit ClothesMap
{
// use alpha blend to combine clothes with tattoo and skin
color_op alpha_blend
alpha_op_ex source1 src_current src_current
tex_coord_set 0
}
texture_unit EmissiveMap
{
tex_coord_set 0
// default is to modulate by light
color_op_ex modulate src_current src_diffuse
// for emissive, use the following, which modulates by the emissive texture instead
// the python script AvatarMaterial.py will set the color_op_ex based on the
// whether emissive is turned on or off.
//color_op_ex modulate src_current src_texture
alpha_op_ex source1 src_current src_current
}
}
}
}
The material script
The material script provides an interface for higher level gameplay scripts to edit the material. It is not required, but it is good practice to have a separate script file that contains the functions for editing a particular material. This lets you encapsulate all knowledge of the internals of the material in one place, so that it is easy to update the script when the material is changed, and you can avoid cluttering your game logic with details of how the assets are constructed.
The material script for this example is shown below. It contains three public functions for changing state in the material.
SetEmissive
The SetEmissive() function enables and disables the emissive lighting effect. The shader and fixed function techniques implement the emissive effect differently, so there is separate logic to set the correct state for each technique.
SetHair
The material for hair submeshes needs to be slightly different from the materials for the other submeshes of the avatar. It needs to disable backface culling and enable alpha rejection. This function sets these modes for both techniques of the material.
SetTattooSite
This function configures which tattoo location the tattoo will be drawn on. The shader and fixed function techniques implement the tattoo offset differently, so there is separate logic to set the correct state for each technique. See the Tattoos section above for a description of how this works.
import ClientAPI
#
# This function turns the emissive effect on and off in the avatar material.
# It uses material scripting to edit values in both the Shader 2.0 technique
# and the fixed function technique.
#
def SetEmissive(material, value):
# Get the material technique for shader model 2.0
tech = material.GetTechnique('Shader20')
if tech.IsSupported:
# Get the Pass from the technique
matpass = tech.GetPass('Single')
# Use white as the multiplier color to enable emissive. Use black to
# disable emissive.
if value:
color = ClientAPI.ColorEx.White
else:
color = ClientAPI.ColorEx.Black
# Set the two emissive multiplier shader parameters
matpass.SetGPUParam(ClientAPI.GPUProgramType.Fragment, 'EmissiveMult', color)
matpass.SetGPUParam(ClientAPI.GPUProgramType.Fragment, 'TattooEmissiveMult', color)
# Get the material technique that supports the fixed function rendering pipeline.
tech = material.GetTechnique('Fixed')
if tech.IsSupported:
# Get the pass from the technique
matpass = tech.GetPass('Single')
# Set up the fixed function color operation to use the emissive texture if
# emissive is enabled, or just use the diffuse lighting color if emissive
# is disabled.
if value:
source2 = ClientAPI.LayerBlendSource.Texture
else:
source2 = ClientAPI.LayerBlendSource.Diffuse
tu = matpass.GetTextureUnit('EmissiveMap')
tu.SetColorOperationEx(ClientAPI.LayerBlendOperationEx.Modulate, ClientAPI.LayerBlendSource.Current, source2)
#
# Set up a material pass for hair rendering by disabling backface culling
# and enabling alpha rejection.
#
def SetHairPass(hairPass):
hairPass.HardwareCullMode = ClientAPI.CullingMode.None
hairPass.SoftwareCullMode = ClientAPI.SoftwareCullingMode.None
hairPass.AlphaRejectFunction = ClientAPI.CompareFunction.Greater
hairPass.AlphaRejectValue = 128
#
# Set up a material for hair rendering by configuring both possible techniques
#
def SetHair(mat):
hairPass = mat.GetTechnique('Shader20').GetPass('Single')
SetHairPass(hairPass)
hairPass = mat.GetTechnique('Fixed').GetPass('Single')
SetHairPass(hairPass)
#
# Sets the tattoo location. Site numbers are:
# 0 - chest
# 1 - arm
# 2 - back
#
# The model has a separate set of UV coordinates for tattoos. The three
# different tattoo sites are mapped into different areas of the UV space.
# All three tattoo sites are in the 0..1 range for the V coordinate. The
# chest site is mapped from 0..1 in the U coordinate. The arm tattoo
# site uses U coordinates 2..3, and the back tattoo site uses U
# coordinates 4..5.
#
def SetTattooSite(material, siteNum):
offset = siteNum * 2
tech = material.GetTechnique('Shader20')
if tech.IsSupported:
# ClientAPI.Write("Setting Shader20 technique tattoo offset to " + str(offset))
matpass = tech.GetPass('Single')
# for the shader technique, the tattoo offset is set as a shader parameter
value = ClientAPI.Vector4(offset, 0, 0, 0)
matpass.SetGPUParam(ClientAPI.GPUProgramType.Fragment, 'tattooOffset', value)
tech = material.GetTechnique('Fixed')
if tech.IsSupported:
# ClientAPI.Write("Setting Fixed technique tattoo offset to " + str(offset))
matpass = tech.GetPass('Single')
tu = matpass.GetTextureUnit('TattooMap')
# for the fixed function technique, the tattoo offset is set using the
# texture scrolling feature of the material
tu.TextureScrollU = -offset
The appearance script
The appearance script is shown below. The UpdateAvatar() function does the bulk of the work: it gets the appearance property values, looks up the submesh and texture names,
and then configures the object with the correct submeshes and materials. This function uses several pre-initialized dictionaries to map from property values to asset names (submesh and texture file names).
import ClientAPI
import AvatarMaterial
import MarsCommand
_debug = False
_materials = {}
# Map skin color and body part to the correct texture name
skinMap = {
'caucasian' : {
'head' : 'avatar_head_cauc_01_diffuse.dds',
'torso' : 'avatar_torso_cauc_01_diffuse.dds',
'legs' : 'avatar_legs_cauc_01_diffuse.dds'
},
'asian' : {
'head' : 'avatar_head_asian_01_diffuse.dds',
'torso' : 'avatar_torso_asian_01_diffuse.dds',
'legs' : 'avatar_legs_asian_01_diffuse.dds'
},
'african_american' : {
'head' : 'avatar_head_africanamerican_01_diffuse.dds',
'torso' : 'avatar_torso_africanamerican_01_diffuse.dds',
'legs' : 'avatar_legs_africanamerican_01_diffuse.dds'
}
}
# Map head shape to which submesh to enable
headMeshMap = {
'caucasian_01' : 'head_cauc_01-mesh.0',
'asian_01' : 'head_asian_01-mesh.0',
'african_american_01' : 'head_aa_01-mesh.0'
}
# Map hair style to submesh
hairMeshMap = {
'pony' : 'hair_pony-mesh.0',
'bob' : 'hair_bob-mesh.0',
'layers' : 'hair_layers-mesh.0',
'bob2' : 'hair_bob2-mesh.0'
}
# Map hair color texture
hairColorMap = {
'pony' : {
'blonde' : 'avatar_hair_pony_blonde_diffuse.dds',
'red' : 'avatar_hair_pony_red_diffuse.dds',
'brown' : 'avatar_hair_pony_brown_diffuse.dds',
'black' : 'avatar_hair_pony_black_diffuse.dds'
},
'bob' : {
'blonde' : 'avatar_hair_bob_blonde_diffuse.dds',
'red' : 'avatar_hair_bob_red_diffuse.dds',
'brown' : 'avatar_hair_bob_brown_diffuse.dds',
'black' : 'avatar_hair_bob_black_diffuse.dds'
},
'layers' : {
'blonde' : 'avatar_hair_layers_blonde_diffuse.dds',
'red' : 'avatar_hair_layers_red_diffuse.dds',
'brown' : 'avatar_hair_layers_brown_diffuse.dds',
'black' : 'avatar_hair_layers_black_diffuse.dds'
},
'bob2' : {
'blonde' : 'avatar_hair_bob2_blonde_diffuse.dds',
'red' : 'avatar_hair_bob2_red_diffuse.dds',
'brown' : 'avatar_hair_bob2_brown_diffuse.dds',
'black' : 'avatar_hair_bob2_black_diffuse.dds'
}
}
# Map torso clothing to submesh
torsoMeshMap = {
'shirt1_white' : 'torso_shirt1-mesh.0',
'shirt1_purple' : 'torso_shirt1-mesh.0',
'shirt1_blue' : 'torso_shirt1-mesh.0',
'shirt2_brown' : 'torso_shirt2-mesh.0',
'shirt2_purple' : 'torso_shirt2-mesh.0',
'shirt2_red' : 'torso_shirt2-mesh.0',
'shirt3_blue' : 'torso_shirt3-mesh.0',
'shirt3_red' : 'torso_shirt3-mesh.0',
'shirt3_green': 'torso_shirt3-mesh.0'
}
# Map torso clothing to diffuse texture
torsoClothesTextureMap = {
'shirt1_white' : 'avatar_torso_shirt1_white_diffuse.dds',
'shirt1_purple' : 'avatar_torso_shirt1_purple_diffuse.dds',
'shirt1_blue' : 'avatar_torso_shirt1_blue_diffuse.dds',
'shirt2_brown' : 'avatar_torso_shirt2_brown_diffuse.dds',
'shirt2_purple' : 'avatar_torso_shirt2_purple_diffuse.dds',
'shirt2_red' : 'avatar_torso_shirt2_red_diffuse.dds',
'shirt3_blue' : 'avatar_torso_shirt3_blue_diffuse.dds',
'shirt3_red' : 'avatar_torso_shirt3_red_diffuse.dds',
'shirt3_green': 'avatar_torso_shirt3_green_diffuse.dds'
}
# Map torso clothing to emissive texture
torsoEmissiveTextureMap = {
'shirt1_white' : 'avatar_torso_shirt1_white_emissive.dds',
'shirt1_purple' : 'avatar_torso_shirt1_purple_emissive.dds',
'shirt1_blue' : 'avatar_torso_shirt1_blue_emissive.dds',
'shirt2_brown' : 'avatar_torso_shirt2_brown_emissive.dds',
'shirt2_purple' : 'avatar_torso_shirt2_purple_emissive.dds',
'shirt2_red' : 'avatar_torso_shirt2_red_emissive.dds',
'shirt3_blue' : 'avatar_torso_shirt3_blue_emissive.dds',
'shirt3_red' : 'avatar_torso_shirt3_red_emissive.dds',
'shirt3_green': 'avatar_torso_shirt3_green_emissive.dds'
}
# Map legs clothing to submesh
legsMeshMap = {
'pants1_black' : 'legs_pants1-mesh.0',
'pants1_brown' : 'legs_pants1-mesh.0',
'pants1_blue' : 'legs_pants1-mesh.0',
'pants2_leopard' : 'legs_pants2-mesh.0',
'pants2_red' : 'legs_pants2-mesh.0',
}
# Map legs clothing to diffuse texture
legsClothesTextureMap = {
'pants1_black' : 'avatar_legs_pants1_black_diffuse.dds',
'pants1_brown' : 'avatar_legs_pants1_brown_diffuse.dds',
'pants1_blue' : 'avatar_legs_pants1_blue_diffuse.dds',
'pants2_leopard' : 'avatar_legs_pants2_leopard_diffuse.dds',
'pants2_red' : 'avatar_legs_pants2_red_diffuse.dds',
}
# Map legs clothing to emissive texture
legsEmissiveTextureMap = {
'pants1_black' : 'avatar_legs_pants1_black_emissive.dds',
'pants1_brown' : 'avatar_legs_pants1_brown_emissive.dds',
'pants1_blue' : 'avatar_legs_pants1_blue_emissive.dds',
'pants2_leopard' : 'avatar_legs_pants2_leopard_emissive.dds',
'pants2_red' : 'avatar_legs_pants2_red_emissive.dds',
}
# These are default property values that will be used if a given property does not
# exist for the avatar.
defaultPropertyValues = {
'Gender' : 'female',
'SkinColor' : 'caucasian',
'HeadShape' : 'caucasian_01',
'HeadDetail' : ,
'HairStyle' : 'bob',
'HairColor' : 'blonde',
'ClothesTorso' : 'shirt1_purple',
'ClothesLegs' : 'pants1_black',
'Tattoo' : ,
'OnSkateboard' : False,
'Emissive' : False
}
# map the tattoo property values to tattoo texture and body position offset
tattooTextureAndOffset = {
: ( 'blank_tattoo.dds', 0 ),
'tattoo_butterfly_01_chest' : ( 'avatar_torso_tattoo_butterfly_01.dds', 0 ),
'tattoo_butterfly_01_arm' : ( 'avatar_torso_tattoo_butterfly_01.dds', 1 ),
'tattoo_butterfly_01_back' : ( 'avatar_torso_tattoo_butterfly_01.dds', 2 ),
'tattoo_butterfly_02_chest' : ( 'avatar_torso_tattoo_butterfly_02.dds', 0 ),
'tattoo_butterfly_02_arm' : ( 'avatar_torso_tattoo_butterfly_02.dds', 1 ),
'tattoo_butterfly_02_back' : ( 'avatar_torso_tattoo_butterfly_02.dds', 2 ),
'tattoo_butterfly_03_chest' : ( 'avatar_torso_tattoo_butterfly_03.dds', 0 ),
'tattoo_butterfly_03_arm' : ( 'avatar_torso_tattoo_butterfly_03.dds', 1 ),
'tattoo_butterfly_03_back' : ( 'avatar_torso_tattoo_butterfly_03.dds', 2 ),
'tattoo_butterfly_04_chest' : ( 'avatar_torso_tattoo_butterfly_04.dds', 0 ),
'tattoo_butterfly_04_arm' : ( 'avatar_torso_tattoo_butterfly_04.dds', 1 ),
'tattoo_butterfly_04_back' : ( 'avatar_torso_tattoo_butterfly_04.dds', 2 ),
'tattoo_butterfly_05_chest' : ( 'avatar_torso_tattoo_butterfly_05.dds', 0 ),
'tattoo_butterfly_05_arm' : ( 'avatar_torso_tattoo_butterfly_05.dds', 1 ),
'tattoo_butterfly_05_back' : ( 'avatar_torso_tattoo_butterfly_05.dds', 2 ),
'tattoo_roses_01_chest' : ( 'avatar_torso_tattoo_roses_01.dds', 0 ),
'tattoo_roses_01_arm' : ( 'avatar_torso_tattoo_roses_01.dds', 1 ),
'tattoo_roses_01_back' : ( 'avatar_torso_tattoo_roses_01.dds', 2 ),
'tattoo_roses_02_chest' : ( 'avatar_torso_tattoo_roses_02.dds', 0 ),
'tattoo_roses_02_arm' : ( 'avatar_torso_tattoo_roses_02.dds', 1 ),
'tattoo_roses_02_back' : ( 'avatar_torso_tattoo_roses_02.dds', 2 ),
'tattoo_roses_03_chest' : ( 'avatar_torso_tattoo_roses_03.dds', 0 ),
'tattoo_roses_03_arm' : ( 'avatar_torso_tattoo_roses_03.dds', 1 ),
'tattoo_roses_03_back' : ( 'avatar_torso_tattoo_roses_03.dds', 2 ),
'tattoo_roses_04_chest' : ( 'avatar_torso_tattoo_roses_04.dds', 0 ),
'tattoo_roses_04_arm' : ( 'avatar_torso_tattoo_roses_04.dds', 1 ),
'tattoo_roses_04_back' : ( 'avatar_torso_tattoo_roses_04.dds', 2 ),
'tattoo_roses_05_chest' : ( 'avatar_torso_tattoo_roses_05.dds', 0 ),
'tattoo_roses_05_arm' : ( 'avatar_torso_tattoo_roses_05.dds', 1 ),
'tattoo_roses_05_back' : ( 'avatar_torso_tattoo_roses_05.dds', 2 ),
'tattoo_roses_06_chest' : ( 'avatar_torso_tattoo_roses_06.dds', 0 ),
'tattoo_roses_06_arm' : ( 'avatar_torso_tattoo_roses_06.dds', 1 ),
'tattoo_roses_06_back' : ( 'avatar_torso_tattoo_roses_06.dds', 2 ),
'tattoo_sunburst_01_chest' : ( 'avatar_torso_tattoo_sunburst_01.dds', 0 ),
'tattoo_sunburst_01_arm' : ( 'avatar_torso_tattoo_sunburst_01.dds', 1 ),
'tattoo_sunburst_01_back' : ( 'avatar_torso_tattoo_sunburst_01.dds', 2 ),
'tattoo_sunburst_02_chest' : ( 'avatar_torso_tattoo_sunburst_02.dds', 0 ),
'tattoo_sunburst_02_arm' : ( 'avatar_torso_tattoo_sunburst_02.dds', 1 ),
'tattoo_sunburst_02_back' : ( 'avatar_torso_tattoo_sunburst_02.dds', 2 ),
'tattoo_sunburst_03_chest' : ( 'avatar_torso_tattoo_sunburst_03.dds', 0 ),
'tattoo_sunburst_03_arm' : ( 'avatar_torso_tattoo_sunburst_03.dds', 1 ),
'tattoo_sunburst_03_back' : ( 'avatar_torso_tattoo_sunburst_03.dds', 2 ),
'tattoo_sunburst_04_chest' : ( 'avatar_torso_tattoo_sunburst_04.dds', 0 ),
'tattoo_sunburst_04_arm' : ( 'avatar_torso_tattoo_sunburst_04.dds', 1 ),
'tattoo_sunburst_04_back' : ( 'avatar_torso_tattoo_sunburst_04.dds', 2 ),
'tattoo_sunburst_05_chest' : ( 'avatar_torso_tattoo_sunburst_05.dds', 0 ),
'tattoo_sunburst_05_arm' : ( 'avatar_torso_tattoo_sunburst_05.dds', 1 ),
'tattoo_sunburst_05_back' : ( 'avatar_torso_tattoo_sunburst_05.dds', 2 )
}
# A list containing the names of the appearance properties. The UpdateAvatar() method
# will be called if any of these properties change value.
appearancePropList = ['SkinColor', 'HeadShape', 'HeadDetail', 'HairStyle', 'HairColor',
'ClothesTorso', 'ClothesLegs', 'Tattoo', 'OnSkateboard', 'Emissive']
# Set all the textures for a particular submesh on the avatar. Use default values if not all are provided.
def SetTextures(material, skinMap, clothesMap='blank_clothes.dds', tattooMap='blank_tattoo.dds',
normalMap='blank_normal.dds', specularMap='blank_specular.dds', emissiveMap='blank_emissive.dds'):
material.ApplyTextureAlias("SkinMap", skinMap)
material.ApplyTextureAlias("ClothesMap", clothesMap)
material.ApplyTextureAlias("TattooMap", tattooMap)
material.ApplyTextureAlias("NormalMap", normalMap)
material.ApplyTextureAlias("SpecularMap", specularMap)
material.ApplyTextureAlias("EmissiveMap", emissiveMap)
# Get the value of a property if it exists, otherwise return a default value
def GetProp(worldObj, propName):
if worldObj.PropertyExists(propName):
value = worldObj.GetProperty(propName)
else:
value = defaultPropertyValues[propName]
return value
# Update the appearance of the avatar based on the values of the appearance properties.
def UpdateAvatar(worldObj):
model = worldObj.Model
# start by hiding all submeshes
for submesh in model.SubMeshNames:
model.HideSubMesh(submesh)
# set variables from the object
SkinColor = GetProp(worldObj, "SkinColor")
HeadShape = GetProp(worldObj, "HeadShape")
HeadDetail = GetProp(worldObj, "HeadDetail")
HairStyle = GetProp(worldObj, "HairStyle")
HairColor = GetProp(worldObj, "HairColor")
ClothesTorso = GetProp(worldObj, "ClothesTorso")
ClothesLegs = GetProp(worldObj, "ClothesLegs")
Tattoo = GetProp(worldObj, "Tattoo")
OnSkateboard = GetProp(worldObj, "OnSkateboard")
Emissive = GetProp(worldObj, "Emissive")
# dump property values for debugging
if _debug:
ClientAPI.Write("SkinColor: " + SkinColor)
ClientAPI.Write("HeadShape: " + HeadShape)
ClientAPI.Write("HeadDetail: " + HeadDetail)
ClientAPI.Write("HairStyle: " + HairStyle)
ClientAPI.Write("HairColor: " + HairColor)
ClientAPI.Write("ClothesTorso: " + ClothesTorso)
ClientAPI.Write("ClothesLegs: " + ClothesLegs)
ClientAPI.Write("Tattoo: " + Tattoo)
ClientAPI.Write("OnSkateboard: " + str(OnSkateboard))
ClientAPI.Write("Emissive: " + str(Emissive))
# select head assets
headMesh = headMeshMap[HeadShape]
headSkin = skinMap[SkinColor]["head"]
# select hair assets
hairMesh = hairMeshMap[HairStyle]
hairTexture = hairColorMap[HairStyle][HairColor]
# select torso assets
torsoMesh = torsoMeshMap[ClothesTorso]
torsoSkin = skinMap[SkinColor]["torso"]
torsoClothesTexture = torsoClothesTextureMap[ClothesTorso]
torsoEmissiveTexture = torsoEmissiveTextureMap[ClothesTorso]
torsoTattooTexture, torsoTattooOffset = tattooTextureAndOffset[Tattoo]
# select legs assets
legsMesh = legsMeshMap[ClothesLegs]
legsSkin = skinMap[SkinColor]["legs"]
legsClothesTexture = legsClothesTextureMap[ClothesLegs]
legsEmissiveTexture = legsEmissiveTextureMap[ClothesLegs]
# dump meshes and textures for debugging
if _debug:
ClientAPI.Write("Head Mesh: " + headMesh)
ClientAPI.Write("Head Skin: " + headSkin)
ClientAPI.Write("Hair Mesh: " + hairMesh)
ClientAPI.Write("Hair Texture: " + hairTexture)
ClientAPI.Write("Torso Mesh: " + torsoMesh)
ClientAPI.Write("Torso Skin: " + torsoSkin)
ClientAPI.Write("Torso Tattoo: " + torsoTattooTexture)
ClientAPI.Write("Torso Clothes Texture: " + torsoClothesTexture)
ClientAPI.Write("Legs Mesh: " + legsMesh)
ClientAPI.Write("Legs Skin: " + legsSkin)
ClientAPI.Write("Legs Clothes Texture: " + legsClothesTexture)
# configure meshes
model.ShowSubMesh(headMesh)
model.ShowSubMesh(hairMesh)
model.ShowSubMesh(torsoMesh)
model.ShowSubMesh(legsMesh)
# show the skateboard. It doesn't need any material whacking.
if OnSkateboard == True:
model.ShowSubMesh("skateboard-mesh.0")
# fetch or create materials
if worldObj.OID in _materials :
headMaterial, hairMaterial, torsoMaterial, legsMaterial = _materials[worldObj.OID]
else :
# Since we don't have existing materials for this object, create one for each body
# part by cloning the base avatar material.
baseMaterial = ClientAPI.GetMaterial("LayeredAvatar")
headMaterial = baseMaterial.Clone("avatar-head." + str(worldObj.OID))
hairMaterial = baseMaterial.Clone("avatar-hair." + str(worldObj.OID))
torsoMaterial = baseMaterial.Clone("avatar-torso." + str(worldObj.OID))
legsMaterial = baseMaterial.Clone("avatar-legs." + str(worldObj.OID))
# Save the materials in a dictionary indexed by OID, so that we can reuse them when the
# appearance changes, rather than having to create new materials.
_materials[worldObj.OID] = (headMaterial, hairMaterial, torsoMaterial, legsMaterial)
# We need to Load() these two materials because if we don't, the calls to
# AvatarMaterial.SetHair() and AvatarMaterial.SetTattooSite() below will
# fail due to the material not being fully initialized.
hairMaterial.Load()
torsoMaterial.Load()
# Update textures in materials
SetTextures(headMaterial, headSkin)
SetTextures(hairMaterial, hairTexture)
SetTextures(torsoMaterial, torsoSkin, torsoClothesTexture, torsoTattooTexture, emissiveMap=torsoEmissiveTexture)
SetTextures(legsMaterial, legsSkin, legsClothesTexture, emissiveMap=legsEmissiveTexture)
# Set special modes for hair material
AvatarMaterial.SetHair(hairMaterial)
# Set offset for tattoo site
AvatarMaterial.SetTattooSite(torsoMaterial, torsoTattooOffset)
# configure materials
model.SetSubMeshMaterial(headMesh, headMaterial.Name)
model.SetSubMeshMaterial(hairMesh, hairMaterial.Name)
model.SetSubMeshMaterial(torsoMesh, torsoMaterial.Name)
model.SetSubMeshMaterial(legsMesh, legsMaterial.Name)
# Turn on or off the emissive effect in the material for legs and torso
if Emissive == True or Emissive == "True":
AvatarMaterial.SetEmissive(torsoMaterial, True)
AvatarMaterial.SetEmissive(legsMaterial, True)
else:
AvatarMaterial.SetEmissive(torsoMaterial, False)
AvatarMaterial.SetEmissive(legsMaterial, False)
# clean up materials since the object is being removed from the scene
def disposeHandler(worldObj):
if worldObj.OID in _materials :
headMaterial, hairMaterial, torsoMaterial, legsMaterial = _materials[worldObj.OID]
headMaterial.Dispose()
hairMaterial.Dispose()
torsoMaterial.Dispose()
legsMaterial.Dispose()
del _materials[worldObj.OID]
# if one of the appearance properties changes, then update the avatar appearance
def propertyChangeHandler(worldObj, propName):
if propName in appearancePropList:
UpdateAvatar(worldObj)
# When the AppearanceOverride property is set to a value of "avatar", this script
# will take control of the appearance of that object by registering for
# property change events and keeping the appearance updated as the
# appearance related properties change value.
def appearanceOverrideHandler(sender, args):
propName = args.PropName
worldObj = ClientAPI.World.GetObjectByOID(args.Oid)
overrideProp = worldObj.GetProperty("AppearanceOverride")
if overrideProp == "avatar":
worldObj.RegisterEventHandler("Disposed", disposeHandler)
worldObj.RegisterEventHandler("PropertyChange", propertyChangeHandler)
UpdateAvatar(worldObj)
ClientAPI.World.RegisterObjectPropertyChangeHandler("AppearanceOverride", appearanceOverrideHandler)
