EDIT: Ok heres the final script updated. Basically what it does is combine multiple animated models into one object (one drawcall) while still preserving separate movement and animation. There are only two steps to get the script to work. 1. Add this script to an object and add any skinned mesh renderers to the array that you want to combine. 2. On the Animation component of each of the objects your combining you have to set the Culling Type to Always Animate. And you should be set to go.
private var vertCount : int = 0;
private var normCount : int = 0;
private var triCount : int = 0;
private var uvCount : int = 0;
private var boneCount : int = 0;
private var bpCount : int = 0;
private var bwCount : int = 0;
var SMRs : SkinnedMeshRenderer[];
var material : Material;
private var bones : Transform[];
private var bindPoses : Matrix4x4[];
private var weights : BoneWeight[];
private var verts : Vector3[];
private var norms : Vector3[];
private var tris : int[];
private var uvs : Vector2[];
private var vertOffset : int = 0;
private var normOffset : int = 0;
private var triOffset : int = 0;
private var uvOffset : int = 0;
private var meshOffset : int = 0;
private var smr : SkinnedMeshRenderer;
private var newSMR : SkinnedMeshRenderer;
private var splitPoint : int[];
private var boneSplit : int = 0;
private var bNum : int = 0;
private var bCount : int[];
function Start(){
for(smr in SMRs){
vertCount += smr.sharedMesh.vertices.Length;
normCount += smr.sharedMesh.normals.Length;
triCount += smr.sharedMesh.triangles.Length;
uvCount += smr.sharedMesh.uv.Length;
boneCount += smr.bones.Length;
bpCount += smr.sharedMesh.bindposes.Length;
bwCount += smr.sharedMesh.boneWeights.Length;
bNum++;
}
bCount = new int[3];
bones = new Transform[boneCount];
weights = new BoneWeight[bwCount];
bindPoses = new Matrix4x4[bpCount];
mCount = 0;
for(smr in SMRs){
for(var b1 : int = 0; b1 < smr.bones.Length; b1++){
bones[bCount[0]] = smr.bones[b1];
bCount[0]++;
}
for(var b2 : int = 0; b2 < smr.sharedMesh.boneWeights.Length; b2++){
weights[bCount[1]] = smr.sharedMesh.boneWeights[b2];
weights[bCount[1]].boneIndex0 += boneSplit;
weights[bCount[1]].weight0 = 1;
bCount[1]++;
}
for(var b3 : int = 0; b3 < smr.sharedMesh.bindposes.Length; b3++){
bindPoses[bCount[2]] = smr.sharedMesh.bindposes[b3];
bCount[2]++;
}
boneSplit += smr.bones.Length;
Debug.Log("BoneSplit = " + boneSplit);
}
verts = new Vector3[vertCount];
norms = new Vector3[normCount];
tris = new int[triCount];
uvs = new Vector2[uvCount];
Combine();
}
function Combine() {
for(smr in SMRs){
for(var i in smr.sharedMesh.triangles){
tris[triOffset++] = i + vertOffset;
}
for(var v in smr.sharedMesh.vertices){
verts[vertOffset++] = v;
}
for(var n in smr.sharedMesh.normals){
norms[normOffset++] = n;
}
for(var uv in smr.sharedMesh.uv){
uvs[uvOffset++] = uv;
}
meshOffset++;
smr.enabled = false;
}
//New Mesh
var me : Mesh = new Mesh();
me.name = gameObject.name;
me.vertices = verts;
me.normals = norms;
me.boneWeights = weights;
me.uv = uvs;
me.triangles = tris;
me.bindposes = bindPoses;
newSMR = gameObject.AddComponent(typeof(SkinnedMeshRenderer)) as SkinnedMeshRenderer;
newSMR.sharedMesh = me;
newSMR.bones = bones;
newSMR.updateWhenOffscreen = true;
renderer.material = material;
}
I managed to modify it to work with SkinnedMeshRenderers but I haven’t been able to get animations working. Heres the current script, maybe someone can help.
Mesh Merger Script
// Copyright 2009, Russ Menapace
// http://humanpoweredgames.com
// Summary:
// This script allows you to draw a large number of meshes with a single
// draw call. This is particularly useful for iPhone games.
// License:
// Free to use as you see fit, but I would appreciate one of the following:
// * A credit for Human Powered Games, or even a link to humanpoweredgames.com
// in whatever you make with this
// * Hire me to make games or simulations
// * A donation to the PayPal account at russ@databar.com. I'm very poor, so
// even a small donation would be greatly appreciated!
// * A thank you note to russ@databar.com
// * Suggestions on how the script could be improved mailed to russ@databar.com
// Warranty:
// This software carries no warranty, and I don't guarantee anything about it.
// If it burns down your house or gets your cat pregnant, don't look at me.
// Acnowledgements:
// This was pieced together out of code I found onthe UnifyCommunity wiki, and
// the Unity forum. I did not keep track of names, but I do recall gaining
// a lot of insight from the posts of PirateNinjaAlliance.
// Thanks to anybody that may have been involved.
// Requirements:
// All the meshes you want to use must use the same material.
// This material may be a texture atlas and the meshes UV to portions of the atlas.
// The texture atlas technique works particularly well for GUI stuff.
// Usage:
// There are two ways to use this script:
// Implicit:
// Simply drop the script into a GameObject that has a number of
// child objects containing mesh filters.
// Explicit:
// Populate the meshFilter array with the meshes you want merged
// Optionally, set the material to be used. If no material is selected,
// The script will apply the first material it encounters to all subsequent
// meshes
// To see if it's working:
// Move the camera so you can see several of your objects in the Game pane
// Note the number of draw calls
// Hit play. You should see the number of draw calls for those meshes reduced to one
using UnityEngine;
using System;
//==============================================================================
public class MeshMergerV2 : MonoBehaviour
{
public SkinnedMeshRenderer[] meshFilters;
public Material material;
//----------------------------------------------------------------------------
public void Start()
{
// if not specified, go find meshes
if(meshFilters.Length == 0)
{
// find all the mesh filters
Component[] comps = GetComponentsInChildren(typeof(SkinnedMeshRenderer));
meshFilters = new SkinnedMeshRenderer[comps.Length];
int mfi = 0;
foreach(Component comp in comps)
meshFilters[mfi++] = (SkinnedMeshRenderer) comp;
}
// figure out array sizes
int vertCount = 0;
int normCount = 0;
int triCount = 0;
int uvCount = 0;
foreach(SkinnedMeshRenderer mf in meshFilters)
{
vertCount += mf.sharedMesh.vertices.Length;
normCount += mf.sharedMesh.normals.Length;
triCount += mf.sharedMesh.triangles.Length;
uvCount += mf.sharedMesh.uv.Length;
if(material == null)
material = mf.gameObject.renderer.material;
}
// allocate arrays
Vector3[] verts = new Vector3[vertCount];
Vector3[] norms = new Vector3[normCount];
Transform[] aBones = new Transform[meshFilters.Length * 3];
Matrix4x4[] bindPoses = new Matrix4x4[meshFilters.Length * 3];
BoneWeight[] weights = new BoneWeight[vertCount];
int[] tris = new int[triCount];
Vector2[] uvs = new Vector2[uvCount];
int vertOffset = 0;
int normOffset = 0;
int triOffset = 0;
int uvOffset = 0;
int meshOffset = 0;
// merge the meshes and set up bones
foreach(SkinnedMeshRenderer mf in meshFilters)
{
foreach(int i in mf.sharedMesh.triangles)
tris[triOffset++] = i + vertOffset;
//aBones[meshOffset] = mf.transform;
//bindPoses[meshOffset] = Matrix4x4.identity;
foreach(Vector3 v in mf.sharedMesh.vertices)
{
weights[vertOffset].weight0 = 1.0f;
weights[vertOffset].boneIndex0 = meshOffset;
verts[vertOffset++] = v;
}
foreach(Vector3 n in mf.sharedMesh.normals)
norms[normOffset++] = n;
foreach(Vector2 uv in mf.sharedMesh.uv)
uvs[uvOffset++] = uv;
meshOffset++;
for(var a = 0; a < 3; a++){
aBones[a*meshOffset] = mf.bones[a];
bindPoses[a*meshOffset] = Matrix4x4.identity;
}
if(mf)
mf.enabled = false;
}
// hook up the mesh
Mesh me = new Mesh();
me.name = gameObject.name;
me.vertices = verts;
me.normals = norms;
me.boneWeights = weights;
me.uv = uvs;
me.triangles = tris;
me.bindposes = bindPoses;
// hook up the mesh renderer
SkinnedMeshRenderer smr =
gameObject.AddComponent(typeof(SkinnedMeshRenderer))
as SkinnedMeshRenderer;
smr.sharedMesh = me;
smr.bones = aBones;
renderer.material = material;
}
}
EDIT: Ok before I spend even more time on this, I’d just like to know if this is even possible? Or if its even worth it? Would combining multiple skinned meshes together to reduce drawcalls even increase performance? Is that why they don’t dynamically batch? Thanks.
mesh filter and renderer are two very separate things.
if youre trying to combine renderers, thats basically the same as combining materials via your atlas map. there might be a script floating around that combines materials, but it is probably not going to do what you’re lookin for
you said in another thread that using the same exact material is not creating ANY dynamic batching… which doesnt seem right, because reducing draw calls is one of the primary reasons for using a texture map. are you absolutely positive all your models are using the exact same material, as opposed to just the same texture?
also, you shouldn’t expect any magical results from optimization scripts . my point is, don’t put all your eggs into that basket… there might be some better ways of handling all those objects… do you really need so many on the screen at once?
No I got it to combine them all into one drawcall just fine, the problem is is that animations no longer work (I can move them each separately but the bones no longer affect the mesh understandably). I’m still working on seeing if I can get the bones to work, I just needed I point in the right direction, or if its more work than its worth than I would just look into something else. I wasn’t expecting magical results I just wanted to optimize as best as possible. And yes I positively made sure that they all used the same texture and material (although I don’t think skinned mesh renderers dynamically batch).
Ok I made progress, I got the bones for one mesh to control the next mesh, the only problem is that when I try two objects it stops rendering the second mesh, and I can’t figure out why. Any ideas?
private var vertCount = 0;
private var normCount = 0;
private var triCount = 0;
private var uvCount = 0;
var SMRs : SkinnedMeshRenderer[];
var material : Material;
var bones : Transform[];
var bindPoses : Matrix4x4[];
var weights : BoneWeight[];
private var verts : Vector3[];
private var norms : Vector3[];
private var tris : int[];
private var uvs : Vector2[];
private var vertOffset : int = 0;
private var normOffset : int = 0;
private var triOffset : int = 0;
private var uvOffset : int = 0;
private var meshOffset : int = 0;
private var smr : SkinnedMeshRenderer;
private var newSMR : SkinnedMeshRenderer;
function Start(){
weights = new BoneWeight[bones.length];
for(smr in SMRs){
vertCount += smr.sharedMesh.vertices.Length;
normCount += smr.sharedMesh.normals.Length;
triCount += smr.sharedMesh.triangles.Length;
uvCount += smr.sharedMesh.uv.Length;
weights += smr.sharedMesh.boneWeights;
bindPoses += smr.sharedMesh.bindposes;
bones += smr.bones;
}
verts = new Vector3[vertCount];
norms = new Vector3[normCount];
tris = new int[triCount];
uvs = new Vector2[uvCount];
Combine();
}
function Combine() {
for(smr in SMRs){
for(var i in smr.sharedMesh.triangles){
tris[triOffset++] = i + vertOffset;
}
for(var v in smr.sharedMesh.vertices){
verts[vertOffset++] = v;
}
for(var n in smr.sharedMesh.normals){
norms[normOffset++] = n;
}
for(var uv in smr.sharedMesh.uv){
uvs[uvOffset++] = uv;
}
meshOffset++;
smr.enabled = false;
}
//New Mesh
var me : Mesh = new Mesh();
me.name = gameObject.name;
me.vertices = verts;
me.normals = norms;
me.boneWeights = weights;
me.uv = uvs;
me.triangles = tris;
me.bindposes = bindPoses;
newSMR = gameObject.AddComponent(typeof(SkinnedMeshRenderer)) as SkinnedMeshRenderer;
newSMR.sharedMesh = me;
newSMR.bones = bones;
renderer.material = material;
}
EDIT: Ok I narrowed it down, it seems that only the object with the first three bones renderers, the other one somehow doesn’t render in the mesh, does anyone know if theres a way to assign certain bones to certain vertexes? Like the first three bones would affect the first object vertices and the next three would be set to the next set of vertexes (the next object).
EDIT2: I’ve got another problem, after I added another object to the mix just to see what would happen I’m now getting an error with this chuck of code
The problem is, is that it gives off a null reference exception for all three of the arrays bones, weights, and bindPoses. If I make each of those arrays have a length of 1 to start it works fine adding in all the rest but that messing stuff up because theres one empty slot at the beginning of each array. Is it just a bug? If so I guess I’ll just have to do it the long way.
EDIT3: Well I took the long way and it now works except that all of the bones affect all the different meshes. So each set of bones affect each part of the mesh meaning that its all over the place. Suggestions?
HAHA YES!!! I finally got it, this script combines animated objects while still allowing them to move and animate separately. (still needs a little work and I gotta add comments explaining it, but it works!). Here’s the code.
private var vertCount : int = 0;
private var normCount : int = 0;
private var triCount : int = 0;
private var uvCount : int = 0;
private var boneCount : int = 0;
private var bpCount : int = 0;
private var bwCount : int = 0;
var SMRs : SkinnedMeshRenderer[];
var material : Material;
private var bones : Transform[];
private var bindPoses : Matrix4x4[];
private var weights : BoneWeight[];
private var verts : Vector3[];
private var norms : Vector3[];
private var tris : int[];
private var uvs : Vector2[];
private var vertOffset : int = 0;
private var normOffset : int = 0;
private var triOffset : int = 0;
private var uvOffset : int = 0;
private var meshOffset : int = 0;
private var mCount : int = 0;
private var smr : SkinnedMeshRenderer;
private var newSMR : SkinnedMeshRenderer;
var splitPoint : int;
private var bCount : int[];
function Start(){
splitPoint = SMRs[0].sharedMesh.vertices.Length;
for(smr in SMRs){
vertCount += smr.sharedMesh.vertices.Length;
normCount += smr.sharedMesh.normals.Length;
triCount += smr.sharedMesh.triangles.Length;
uvCount += smr.sharedMesh.uv.Length;
boneCount += smr.bones.Length;
bpCount += smr.sharedMesh.bindposes.Length;
bwCount += smr.sharedMesh.boneWeights.Length;
}
bCount = new int[3];
bones = new Transform[boneCount];
weights = new BoneWeight[bwCount];
bindPoses = new Matrix4x4[bpCount];
for(smr in SMRs){
for(var b1 : int = 0; b1 < smr.bones.Length; b1++){
bones[bCount[0]] = smr.bones[b1];
Debug.Log("Bones = " + bones[bCount[0]]);
Debug.Log(bCount[0]);
bCount[0]++;
}
for(var b2 : int = 0; b2 < smr.sharedMesh.boneWeights.Length; b2++){
weights[bCount[1]] = smr.sharedMesh.boneWeights[b2];
bCount[1]++;
}
for(var b3 : int = 0; b3 < smr.sharedMesh.bindposes.Length; b3++){
bindPoses[bCount[2]] = smr.sharedMesh.bindposes[b3];
bCount[2]++;
}
for(var b4 : int = 0; b4 < weights.length; b4++){
if(b4 < splitPoint){
}
else{
weights[b4].boneIndex0 += 3;
weights[b4].weight0 = 1;
}
}
}
verts = new Vector3[vertCount];
norms = new Vector3[normCount];
tris = new int[triCount];
uvs = new Vector2[uvCount];
Combine();
}
function Combine() {
for(smr in SMRs){
for(var i in smr.sharedMesh.triangles){
tris[triOffset++] = i + vertOffset;
}
for(var v in smr.sharedMesh.vertices){
verts[vertOffset++] = v;
}
for(var n in smr.sharedMesh.normals){
norms[normOffset++] = n;
}
for(var uv in smr.sharedMesh.uv){
uvs[uvOffset++] = uv;
}
meshOffset++;
smr.enabled = false;
}
//New Mesh
var me : Mesh = new Mesh();
me.name = gameObject.name;
me.vertices = verts;
me.normals = norms;
me.boneWeights = weights;
me.uv = uvs;
me.triangles = tris;
me.bindposes = bindPoses;
newSMR = gameObject.AddComponent(typeof(SkinnedMeshRenderer)) as SkinnedMeshRenderer;
newSMR.sharedMesh = me;
newSMR.bones = bones;
renderer.material = material;
}
Thanks! In the editor there was a performance increase not huge but there was one, have yet to test it on the device though as I’ve been working on a new spawn system to integrate into my game. Also the script above had a few problems, one being that it only worked if everything was the same model and had the same bone number. All that is fixed and working great now (works with any group of animated models, doesn’t matter the vertex or bone count). Anyway heres the latest version, I’ll add some comments into it later.
There are only two things you have to do to get it working.
drag and drop the skinned mesh renderers you want to combine into the array
in order to get animations working you have to go to each objects animation component and set the Culling Type to always animate
And you should be good to go, I’m going to continue working on it, making it automatically do the second step, and include an option to auto search for the skinned mesh renderers.
EDIT: Small bug broke part of the script, I fixed it and updated the first post with the final code and instructions.
Ok Wow! I wanted to see how much this actually increased performance so I did a test of 36 of my simple enemies (in editor) all running animations and ai. It went down and stayed around 260 fps, with this script enabled and it all set up performance went up to around 500 fps. I really didn’t think I would get that much more performance (gave me an 240 fps boost with 36 animated enemies on screen), but the more enemies you add the better it seems to run.
EDIT: The only downside seems to be that the only way to hide enemies is to deactivate them and then move them out of camera view as you can’t stop them individually from rendering. But I’ll take that for nearly doubling performance.
I was doing some fps tests on both device and editor and I got some kinda strange results. I used the fps counter script on the wiki to get the fps on the iphone, but I wanted to know how it worked so I also had the stats window up. I tested using 60 enemies with AI and animations. Here are the stats I got,
Editor:
Combined:
fps (wiki counter) 125
fps (stats window) 300
drawcalls 5
Un-Combined:
fps (wiki counter) 95
fps (stats window) 165
drawcalls 64
iPhone:
Combined:
fps (wiki counter) 14.5
Un-Combined:
fps (wiki counter) 12.5
My first question is why is the wiki fps counter and the stats fps so different? Second, why is there so little difference on the iphone compared to the editor (30 fps difference editor, 2 fps difference iPhone)?
I used your script but I got a rather strange result :-S
Material got a whitish color and animations do not work. Also a guy got drunk and stuck to another guy!
Here is the script for reference reasons:
// Mesh Merger Script
// Copyright 2009, Russ Menapace
// http://humanpoweredgames.com
// Summary:
// This script allows you to draw a large number of meshes with a single
// draw call. This is particularly useful for iPhone games.
// License:
// Free to use as you see fit, but I would appreciate one of the following:
// * A credit for Human Powered Games, or even a link to humanpoweredgames.com
// in whatever you make with this
// * Hire me to make games or simulations
// * A donation to the PayPal account at russ@databar.com. I'm very poor, so
// even a small donation would be greatly appreciated!
// * A thank you note to russ@databar.com
// * Suggestions on how the script could be improved mailed to russ@databar.com
// Warranty:
// This software carries no warranty, and I don't guarantee anything about it.
// If it burns down your house or gets your cat pregnant, don't look at me.
// Acnowledgements:
// This was pieced together out of code I found onthe UnifyCommunity wiki, and
// the Unity forum. I did not keep track of names, but I do recall gaining
// a lot of insight from the posts of PirateNinjaAlliance.
// Thanks to anybody that may have been involved.
// Requirements:
// All the meshes you want to use must use the same material.
// This material may be a texture atlas and the meshes UV to portions of the atlas.
// The texture atlas technique works particularly well for GUI stuff.
// Usage:
// There are two ways to use this script:
// Implicit:
// Simply drop the script into a GameObject that has a number of
// child objects containing mesh filters.
// Explicit:
// Populate the meshFilter array with the meshes you want merged
// Optionally, set the material to be used. If no material is selected,
// The script will apply the first material it encounters to all subsequent
// meshes
// To see if it's working:
// Move the camera so you can see several of your objects in the Game pane
// Note the number of draw calls
// Hit play. You should see the number of draw calls for those meshes reduced to one
using UnityEngine;
using System;
//==============================================================================
public class MeshMergerV2 : MonoBehaviour
{
public SkinnedMeshRenderer[] meshFilters;
public Material material;
//----------------------------------------------------------------------------
public void Start()
{
// if not specified, go find meshes
if (meshFilters.Length == 0)
{
// find all the mesh filters
Component[] comps = GetComponentsInChildren(typeof(SkinnedMeshRenderer));
meshFilters = new SkinnedMeshRenderer[comps.Length];
int mfi = 0;
foreach (Component comp in comps)
meshFilters[mfi++] = (SkinnedMeshRenderer)comp;
}
// figure out array sizes
int vertCount = 0;
int normCount = 0;
int triCount = 0;
int uvCount = 0;
foreach (SkinnedMeshRenderer mf in meshFilters)
{
vertCount += mf.sharedMesh.vertices.Length;
normCount += mf.sharedMesh.normals.Length;
triCount += mf.sharedMesh.triangles.Length;
uvCount += mf.sharedMesh.uv.Length;
if (material == null)
material = mf.gameObject.renderer.material;
}
// allocate arrays
Vector3[] verts = new Vector3[vertCount];
Vector3[] norms = new Vector3[normCount];
Transform[] aBones = new Transform[meshFilters.Length * 3];
Matrix4x4[] bindPoses = new Matrix4x4[meshFilters.Length * 3];
BoneWeight[] weights = new BoneWeight[vertCount];
int[] tris = new int[triCount];
Vector2[] uvs = new Vector2[uvCount];
int vertOffset = 0;
int normOffset = 0;
int triOffset = 0;
int uvOffset = 0;
int meshOffset = 0;
// merge the meshes and set up bones
foreach (SkinnedMeshRenderer mf in meshFilters)
{
foreach (int i in mf.sharedMesh.triangles)
tris[triOffset++] = i + vertOffset;
aBones[meshOffset] = mf.transform;
bindPoses[meshOffset] = Matrix4x4.identity;
foreach (Vector3 v in mf.sharedMesh.vertices)
{
weights[vertOffset].weight0 = 1.0f;
weights[vertOffset].boneIndex0 = meshOffset;
verts[vertOffset++] = v;
}
foreach (Vector3 n in mf.sharedMesh.normals)
norms[normOffset++] = n;
foreach (Vector2 uv in mf.sharedMesh.uv)
uvs[uvOffset++] = uv;
meshOffset++;
for (var a = 0; a < 3; a++)
{
aBones[a * meshOffset] = mf.bones[a];
bindPoses[a * meshOffset] = Matrix4x4.identity;
}
if (mf)
mf.enabled = false;
}
// hook up the mesh
Mesh me = new Mesh();
me.name = gameObject.name;
me.vertices = verts;
me.normals = norms;
me.boneWeights = weights;
me.uv = uvs;
me.triangles = tris;
me.bindposes = bindPoses;
// hook up the mesh renderer
SkinnedMeshRenderer smr =
gameObject.AddComponent(typeof(SkinnedMeshRenderer))
as SkinnedMeshRenderer;
smr.sharedMesh = me;
smr.bones = aBones;
renderer.material = material;
}
}