admin管理员组

文章数量:1616416

用了photoshop那么久,从来没仔细想过它到底用了哪些算法。想一想就觉得倒抽一口凉气。

传闻photoshop的创始人,和wps创始人一样,就一个程序员写好了这第一版的成品。同样是做人,咋就差距这么大呢?

千古疑问
  1. 所以这个.psd文件的格式是什么,它和png、jpg、gif,甚至json文件的格式上有什么区别?
  2. .psd文件是怎么做到分层的,分层的存储方式又是啥?
  3. 如何把.psd文件的单层图另外存储起来成为多个sprite?(没错就是Unity中的sprite)以及如何对其进行轮廓检测呢?(unity是如何做到的呢?)
在危险的ps插件边缘试探

作为一个见得不够多脑子也不够灵活的菜鸡,我决定 “旁征博引” “凿壁偷光”,先康康有哪些源码可以让我学习学习~这要从psd文件导入unity的一个插件说起!

我们把一个很神秘的文件放进了photoshop的这个generator路径下,那么这个文件里面都有啥呢?


(JavaScript) main.jsx

(function() {
    "use strict";

    // *** PACKAGES ***
    var fs                   = require("fs");
    var path                 = require("path");
    var pluginPackage        = require("./package.json");

    // *** CONSTANTS ***
    var MENU_ID              = pluginPackage.name;        // our plugin's menu id
    var MENU_STRING          = "Ps2D Map";                // our plugin's menu text
    var PLUGIN_VERSION       = pluginPackage.version;     // our plugin version
    var PLUGIN_NAME          = pluginPackage.name;        // plugin name
    var MAP_EXTENSION        = ".ps2dmap.json";           // map's extension

    // *** STATE VARIABLES ***
    var _generator           = null;  // the generator framework
    var _currentDocumentId   = null;  // the document's id
    var _currentDocumentFile = null;  // the document's filename

    // *** PLUGIN FUNCTIONALITY

    // kick it off!
    function run() {

        // request the document from photoshop
        _generator.getDocumentInfo(_currentDocumentId)

            .then(function(document) {
                _currentDocumentFile = document.file;
                var layoutDocument   = createLayoutDocument(document);
                saveMapFile(layoutDocument);
                popup("The Ps2D map file has been created.  Hurray!");
            })

            .done();

    }

    // Get the full filename for the layout file we create.
    // It will be written in the same directory as the PSD file.
    function layoutFilename() {
        var myExtension = path.extname(_currentDocumentFile);
        var myBasename  = path.basename(_currentDocumentFile, myExtension);
        var myPath      = path.dirname(_currentDocumentFile);

        return myPath + path.sep + myBasename + MAP_EXTENSION;
    }

    // save the layout to the file system
    function saveMapFile(layoutDocument) {

        // get the filename
        var filename = layoutFilename();

        // serialize our layoutDocument to a string
        var contents = JSON.stringify(layoutDocument);

        try
        {
            // a synchronous node.js call?  finally, sanity!!!  just kidding.  mostly.
            fs.writeFileSync(filename, contents);
        } catch (err)
        {
            // TODO: It'd be nice to show people a message instead of just logging
            log("unable to save map file to " + filename);
        }
    }

    // convert the photoshop document to our own layout document
    function createLayoutDocument(document) {
        // create our own document
        var layoutDocument = {};

        layoutDocument.pluginVersion = PLUGIN_VERSION;
        layoutDocument.bounds        = document.bounds;
        layoutDocument.mask          = document.mask;
        layoutDocument.sprites       = convertLayersToSprites(document.layers);

        return layoutDocument;
    }

    // convert a list of layers into a list of sprites
    function convertLayersToSprites(layers) {
        // sanity
        if (layers === null || layers === undefined || layers.length === 0)
            return null;

        // sprites go here
        var sprites = [];

        // go through each layer
        for (var i = 0, z = layers.length; i < z; i++)
        {
            var layer = layers[i];

            // convert it
            var sprite = convertLayerToSprite(layer);

            // add it to the list if it's legit
            if (sprite !== null) {
                sprites.push(sprite);
            }
        }

        return sprites;

    }

    // convert a ps layer to a sprite
    function convertLayerToSprite(layer) {

        // safety
        if (layer === null || layer === undefined) return null;

        // grab what we need for our map
        var sprite = {};
        sprite.id      = layer.id;
        sprite.name    = layer.name;
        sprite.bounds  = layer.bounds;
        sprite.mask    = layer.mask;
        sprite.visible = layer.visible;
        sprite.sprites = convertLayersToSprites(layer.layers);

        return sprite;
    }

    // *** EVENT HANDLERS ***

    // fires when we've installed successfully
    function handleMenuItemInstallSuccess() {}

    // fires when PS can't install our menu item
    function handleMenuItemInstallFailed() {
        log("Unable to create Ps2D menu item.");
    }

    // the user has clicked our menu item
    function handleGeneratorMenuClicked(e) {
        // which menu item caused this event?
        var menu = e.generatorMenuChanged;

        // if it wasn't us, jet.
        if (!menu || menu.name !== MENU_ID) return;

        // ok, it's go time!
        run();
    }

    // the photoshop document has changed
    function handleCurrentDocumentChanged(id) {
        _currentDocumentId = id;
    }

    // *** UTILITIES ***

    // write a log message
    function log(s) {
        console.log("[" + PLUGIN_NAME + "] " + s);
    }

    // show a popup message
    function popup(msg) {
        var js = "alert('" + msg + "');"
        sendJavascript(js);
    }

    // run some javascript
    function sendJavascript(str){
        _generator.evaluateJSXString(str).then(
            function(result){
                console.log(result);
            },
            function(err){
                console.log(err);
            });
    }

    // *** INITIALIZATION ***

    // main entry point for the plugin
    function init(generator) {

        // remember this generator in our scope
        _generator = generator;

        // install the menu item
        _generator
            .addMenuItem(MENU_ID, MENU_STRING, true, false)
            .then(handleMenuItemInstallSuccess, handleMenuItemInstallFailed);

        // subscribe to PS events
        _generator.onPhotoshopEvent("generatorMenuChanged", handleGeneratorMenuClicked);
    }

    // make our entry point available
    exports.init = init;

}());

(json package.json)

{
    "name": "Ps2D",
    "main": "main.jsx",
    "version": "1.0.0",
    "generator-core-version": ">=2.0.2",
    "author": "Steve Kellock"
}

读者看懂了没有,作为一个业余js但是有着“深厚”c++功底的吹牛逼选手,我认为以上这个js代码要表达的思想很简单:

ps给了layers接口,这边负责用一个sprite类接住了layer类的一系列成员变量,包括id、name、bounds、mask、visible,然后把这些东西存成一个json格式的文件~就酱。好,那么我们做个试验,看看这个存成xx.ps2dmap的文件内容都有点啥:

xx.ps2dmap

{
    "pluginVersion":"1.0.0",
    "bounds":{
        "top":0,
        "left":0,
        "bottom":1820,
        "right":788
    },
    "sprites":[
        {
            "id":12,
            "name":"head",
            "bounds":{
                "top":55,
                "left":242,
                "bottom":357,
                "right":478
            },
            "visible":true,
            "sprites":null
        },
        {
            "id":15,
            "name":"neck",
            "bounds":{
                "top":352,
                "left":280,
                "bottom":414,
                "right":436
            },
            "visible":true,
            "sprites":null
        },
        {
            "id":16,
            "name":"right-arm",
            "bounds":{
                "top":401,
                "left":274,
                "bottom":1017,
                "right":422
            },
            "visible":true,
            "sprites":null
        },
        {
            "id":20,
            "name":"torso-add",
            "bounds":{
                "top":392,
                "left":272,
                "bottom":910,
                "right":520
            },
            "visible":true,
            "sprites":null
        },
        {
            "id":22,
            "name":"right-leg",
            "bounds":{
                "top":880,
                "left":153,
                "bottom":1729,
                "right":525
            },
            "visible":true,
            "sprites":null
        },
        {
            "id":24,
            "name":"left-leg-add",
            "bounds":{
                "top":873,
                "left":339,
                "bottom":1703,
                "right":598
            },
            "visible":true,
            "sprites":null
        }
    ]
}

铁汁们,看懂了点啥?原来对于每个图层来说,ps都已经把bounds算好了,这个bounds就是对这个图层中的图像求的包围盒诶!所以PhotoShop用的包围盒算法又是什么捏,这里就不多说了。但是注意包围盒不是轮廓线。

在危险的unity插件边缘试探

作为一款吊炸天的ps2d插件,我真的不知道为啥用户这么少,但是不妨碍我们研究研究这款插件的源码。感谢这个插件的程序员,只有这些cs文件~


当我们把上面生成xx.psd2dmap和xx.psd文件放在unity的文件目录下时,打开插件,进行一些列操作会发生什么呢。
我上个

大致的类图:

总结起来很简单:就是根据ps插件生成的xx.psd2dmap这个json文件,找到ps导出的对应的分层png图片,转化为Unity自带的SerializedSprite类,然后根据json文件中的layerorder、pixelbound等参数计算出这些分层sprite的中心点位置、根据pixelToUnit等转化到场景中,得到这些sprite在Scene场景中的position。这样在Unity中展示的就是一个有着和photoshop中一样拓扑结构的角色(物体)。

在另一款危险的unity插件边缘试探

试探过一次危险边缘之后,虽然对代码并不能说完全搞透彻,但是也大体知道都做了什么了。再次感慨为什么天才程序员这么多。。这么牛。。。让我这种垃圾基因怎么活。。

回到开头提出来的最后一个问题:

以及如何对其进行轮廓检测呢?(unity是如何做到的呢?

看起来PS2D插件只是读取并分析psd文件然后无缝融合到Unity,生成分层的拓扑物体到场景中。但是对于这些sprite而言,他们的轮廓提取和切分具体又用了什么样的原理和算法呢?(Unity自带的slice搞不动的)

让我们梦回Anima2D这个插件(诶,我之前没有写过这个插件的哈!)那,让我们初探Anima2D这个插件~(unity store免费)。这一切要从一个叫做SpriteMesh的自定义类说起

重点类图:

可以看出来,当程序把这个png转化为object 然后转化为Sprite的时候,Unity内部就已经自动算好了轮廓点(网格点),包围盒等一系列参数…后续的三角剖分及展示在界面上这些东西,程序员可以自己定义,程序员还可以通过人机交互,自定义网格顶点等信息。

鉴于Unity不开源它的源代码,个人猜测它是用了类似OpenCV中的findCountours()这个函数的内部算法,先把图像二值化,然后边缘检测,再对边缘线求离散点,作为网格顶点,继续调整~

既然我们通过Anima2D插件接到了unity自带的类Sprite类给出的网格顶点,那么后续的三角剖分,以及调整网格,就完全可以学习Anima2D的源代码来剖析了。

当我们在Unity的Asset/xxx目录下看到一个png图片,我们右键,create->Anima2d->spritemesh,程序会走入这样一个流程:

public static void CreateSpriteMesh(Texture2D texture)
		{
			if(texture)
			{
				Object[] objects = AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(texture));
				
				for (int i = 0; i < objects.Length; i++)
				{
					Object o = objects [i];
					Sprite sprite = o as Sprite;
					if (sprite) {
						EditorUtility.DisplayProgressBar ("Processing " + texture.name, sprite.name, (i+1) / (float)objects.Length);
						CreateSpriteMesh(sprite);
					}
				}
				
				EditorUtility.ClearProgressBar();
			}
		}

这里有一点想说一下,看第一条语句,通过断点监视objects变量,我们可以看到一个png的texture(在unity面板上已经改成了sprite),经过这么一个getAsset就会变成两个object,一个负责是texture,一个负责是sprite,说明Unity在内部(或者内存中)已经存在这个对应的sprite文件(信息),只是没显示出来而已。
基于好奇,我们去路径下看了看这个png对应的meta文件,果然是有相关信息的,至于LoadAllAssetAtPath()这个函数到底怎么读的文件,我们就暂不深究了,谁让unity爸爸没有公开源码,我又菜到抠脚呢:

ok,我们继续往下:
我们可以看到Unity已经初步把这个sprite的顶点、uv坐标、网格三角形坐标先给确定了,这一套就相当于UV映射的初步,纹理和网格已经有了简单的connection

接下来就是生成相关的asset资源:

public static SpriteMesh CreateSpriteMesh(Sprite sprite)
		{
			SpriteMesh spriteMesh = SpriteMeshPostprocessor.GetSpriteMeshFromSprite(sprite);
			SpriteMeshData spriteMeshData = null;
			
			if(!spriteMesh && sprite)
			{
				string spritePath = AssetDatabase.GetAssetPath(sprite);
				string directory = Path.GetDirectoryName(spritePath);
				string assetPath = AssetDatabase.GenerateUniqueAssetPath(directory + Path.DirectorySeparatorChar + sprite.name + ".asset");
				
				spriteMesh = ScriptableObject.CreateInstance<SpriteMesh>();
				InitFromSprite(spriteMesh,sprite);
				AssetDatabase.CreateAsset(spriteMesh,assetPath);
				
				spriteMeshData = ScriptableObject.CreateInstance<SpriteMeshData>();
				spriteMeshData.name = spriteMesh.name + "_Data";
				spriteMeshData.hideFlags = HideFlags.HideInHierarchy;
				InitFromSprite(spriteMeshData,sprite);
				AssetDatabase.AddObjectToAsset(spriteMeshData,assetPath);
				
				UpdateAssets(spriteMesh,spriteMeshData);
				
				AssetDatabase.SaveAssets();
				AssetDatabase.ImportAsset(assetPath);
				
				Selection.activeObject = spriteMesh;
			}
			
			return spriteMesh;
		}

虽然我们在之前的博客讲过ScriptableObject和SerializedObject的区别,但是这里我还是要深入一下。看附录。

然后我们就生成了SpriteMesh和SpriteMeshData这个asset资源,并合并存放在了本地文件夹下,这个东西到时候在游戏运行时就会被疯狂引用。

程序运行到这里,我们的断点单步执行,又走到了如下这里(涉及观察者模式,请看我的github设计模式里有讲解:)

SpriteMeshEditorWindow类用来控制相关SpriteMesh和SpriteMeshData的asset资源在unity Window面板上的显示(类似Scene场景的离线渲染)。

然后有一个sliceEditor控制德劳内三角剖分和细化(用的是微软维护的Triangle C# 开源库),SpriteMeshInstance负责作为一个组件挂载到物体上,控制到Scene中的渲染。

如下是SpriteMeshUtils.cs中的部分代码,控制三角剖分最底层算法的调用,我曾经用Fade库写过三角剖分,基本用法没啥大差别

public static void Triangulate(List<Vector2> vertices, List<IndexedEdge> edges, List<Hole> holes,ref List<int> indices)
		{
			indices.Clear();
			
			if(vertices.Count >= 3)
			{
				InputGeometry inputGeometry = new InputGeometry(vertices.Count);
				
				for(int i = 0; i < vertices.Count; ++i)
				{
					Vector2 position = vertices[i];
					inputGeometry.AddPoint(position.x,position.y);
				}
				
				for(int i = 0; i < edges.Count; ++i)
				{
					IndexedEdge edge = edges[i];
					inputGeometry.AddSegment(edge.index1,edge.index2);
				}
				
				for(int i = 0; i < holes.Count; ++i)
				{
					Vector2 hole = holes[i].vertex;
					inputGeometry.AddHole(hole.x,hole.y);
				}
				
				TriangleNet.Mesh triangleMesh = new TriangleNet.Mesh();

				triangleMesh.Triangulate(inputGeometry);
				
				foreach (TriangleNet.Data.Triangle triangle in triangleMesh.Triangles)
				{
					if(triangle.P0 >= 0 && triangle.P0 < vertices.Count &&
					   triangle.P0 >= 0 && triangle.P1 < vertices.Count &&
					   triangle.P0 >= 0 && triangle.P2 < vertices.Count)
					{
						indices.Add(triangle.P0);
						indices.Add(triangle.P2);
						indices.Add(triangle.P1);
					}
				}
			}
		}
		
		public static void Tessellate(List<Vector2> vertices, List<IndexedEdge> indexedEdges, List<Hole> holes, List<int> indices, float tessellationAmount)
		{
			if(tessellationAmount <= 0f)
			{
				return;
			}
			
			indices.Clear();
			
			if(vertices.Count >= 3)
			{
				InputGeometry inputGeometry = new InputGeometry(vertices.Count);
				
				for(int i = 0; i < vertices.Count; ++i)
				{
					Vector2 vertex = vertices[i];
					inputGeometry.AddPoint(vertex.x,vertex.y);
				}
				
				for(int i = 0; i < indexedEdges.Count; ++i)
				{
					IndexedEdge edge = indexedEdges[i];
					inputGeometry.AddSegment(edge.index1,edge.index2);
				}
				
				for(int i = 0; i < holes.Count; ++i)
				{
					Vector2 hole = holes[i].vertex;
					inputGeometry.AddHole(hole.x,hole.y);
				}
				
				TriangleNet.Mesh triangleMesh = new TriangleNet.Mesh();
				TriangleNet.Tools.Statistic statistic = new TriangleNet.Tools.Statistic();
				
				triangleMesh.Triangulate(inputGeometry);
				
				triangleMesh.Behavior.MinAngle = 20.0;
				triangleMesh.Behavior.SteinerPoints = -1;
				triangleMesh.Refine(true);
				
				statistic.Update(triangleMesh,1);
				
				triangleMesh.Refine(statistic.LargestArea / tessellationAmount);
				triangleMesh.Renumber();
				
				vertices.Clear();
				indexedEdges.Clear();
				
				foreach(TriangleNet.Data.Vertex vertex in triangleMesh.Vertices)
				{
					vertices.Add(new Vector2((float)vertex.X,(float)vertex.Y));
				}
				
				foreach(TriangleNet.Data.Segment segment in triangleMesh.Segments)
				{
					indexedEdges.Add(new IndexedEdge(segment.P0,segment.P1));
				}
				
				foreach (TriangleNet.Data.Triangle triangle in triangleMesh.Triangles)
				{
					if(triangle.P0 >= 0 && triangle.P0 < vertices.Count &&
					   triangle.P0 >= 0 && triangle.P1 < vertices.Count &&
					   triangle.P0 >= 0 && triangle.P2 < vertices.Count)
					{
						indices.Add(triangle.P0);
						indices.Add(triangle.P2);
						indices.Add(triangle.P1);
					}
				}
			}
		}

基本网格剖分和场景渲染这里已经搞的差不多了,看如下的类图,我来大刀阔斧总结一下(记得关注序号代表流程顺序,比较方便理清):

但是,进行到这里远远没有结束。为什么,因为我们的目的是动画,接下来就是骨骼的权重绑定方面的理解,ok往下走啦!

骨骼这里使用的流程很简单,骨骼是一种Object,所以在Hierarchy面板上右键create->2d object->bone,然后把骨骼拖到SpriteMeshInstance组件上,(注意让骨骼的位置和要绑定的纹理层不要间隔太远)再从SpriteMeshEditor编辑网格绑定骨骼就可以啦。有个细节需要注意,当处理好骨骼和图层的绑定之后,点击Apply,这时候在场景中的某个图层,就会从SpriteMeshInstance组件+MeshFilter组件+MeshRender组件变成SpriteMeshInstance组件+Skinned Mesh Renderer组件。

这里用到的重点类图如下所示:

public void BindBone(Bone2D bone)
		{
			if(spriteMeshInstance && bone)
			{
				BindInfo bindInfo = new BindInfo();
				bindInfo.bindPose = bone.transform.worldToLocalMatrix * spriteMeshInstance.transform.localToWorldMatrix;
				bindInfo.boneLength = bone.localLength;
				bindInfo.path = BoneUtils.GetBonePath (bone);
				bindInfo.name = bone.name;
				bindInfo.color = ColorRing.GetColor(bindPoses.Count);
				
				if(!bindPoses.Contains(bindInfo))
				{
					bindPoses.Add (bindInfo);
					
					isDirty = true;
				}
			}
		}
public void CalculateAutomaticWeights(List<Node> targetNodes)
		{
			float pixelsPerUnit = SpriteMeshUtils.GetSpritePixelsPerUnit(spriteMesh.sprite);
			
			if(nodes.Count <= 0)
			{
				Debug.Log("Cannot calculate automatic weights from a SpriteMesh with no vertices.");
				return;
			}
			
			if(bindPoses.Count <= 0)
			{
				Debug.Log("Cannot calculate automatic weights. Specify bones to the SpriteMeshInstance.");
				return;
			}
			
			if(!spriteMesh)
				return;

			List<Vector2> controlPoints = new List<Vector2>();
			List<IndexedEdge> controlPointEdges = new List<IndexedEdge>();
			List<int> pins = new List<int>();
			
			foreach(BindInfo bindInfo in bindPoses)
			{
				Vector2 tip = SpriteMeshUtils.VertexToTexCoord(spriteMesh,pivotPoint,bindInfo.position,pixelsPerUnit);
				Vector2 tail = SpriteMeshUtils.VertexToTexCoord(spriteMesh,pivotPoint,bindInfo.endPoint,pixelsPerUnit);
				
				if(bindInfo.boneLength <= 0f)
				{
					int index = controlPoints.Count;
					controlPoints.Add(tip);
					pins.Add(index);

					continue;
				}

				int index1 = -1;
				
				if(!ContainsVector(tip,controlPoints,0.01f, out index1))
				{
					index1 = controlPoints.Count;
					controlPoints.Add(tip);
				}
				
				int index2 = -1;
				
				if(!ContainsVector(tail,controlPoints,0.01f, out index2))
				{
					index2 = controlPoints.Count;
					controlPoints.Add(tail);
				}
				
				IndexedEdge edge = new IndexedEdge(index1, index2);
				controlPointEdges.Add(edge);
				
			}
			
			UnityEngine.BoneWeight[] boneWeights = BbwPlugin.CalculateBbw(m_TexVertices.ToArray(), indexedEdges.ToArray(), controlPoints.ToArray(), controlPointEdges.ToArray(), pins.ToArray());
			
			foreach(Node node in targetNodes)
			{
				UnityEngine.BoneWeight unityBoneWeight = boneWeights[node.index];
				
				SetBoneWeight(node,CreateBoneWeightFromUnityBoneWeight(unityBoneWeight));
			}
			
			isDirty = true;
		}

这里我竟然发现,难道继承自MonoBehavior的类都是可以序列化的?因为我发现这样一行代码:

public static void UpdateRenderer(SpriteMeshInstance spriteMeshInstance, bool undo = true)
		{
			if(!spriteMeshInstance)
			{
				return;
			}
			
			SerializedObject spriteMeshInstaceSO = new SerializedObject(spriteMeshInstance);
			
			SpriteMesh spriteMesh = spriteMeshInstaceSO.FindProperty("m_SpriteMesh").objectReferenceValue as SpriteMesh;
			
			if(spriteMesh)
			{
				Mesh sharedMesh = spriteMesh.sharedMesh;
				
				if(sharedMesh.bindposes.Length > 0 && spriteMeshInstance.bones.Count > sharedMesh.bindposes.Length)
				{
					spriteMeshInstance.bones = spriteMeshInstance.bones.GetRange(0,sharedMesh.bindposes.Length);
				}
				
				if(CanEnableSkinning(spriteMeshInstance))
				{
					MeshFilter meshFilter = spriteMeshInstance.cachedMeshFilter;
					MeshRenderer meshRenderer = spriteMeshInstance.cachedRenderer as MeshRenderer;
					
					if(meshFilter)
					{
						if(undo)
						{
							Undo.DestroyObjectImmediate(meshFilter);
						}else{
							GameObject.DestroyImmediate(meshFilter);
						}
					}
					if(meshRenderer)
					{
						if(undo)
						{
							Undo.DestroyObjectImmediate(meshRenderer);
						}else{
							GameObject.DestroyImmediate(meshRenderer);
						}
					}
					
					SkinnedMeshRenderer skinnedMeshRenderer = spriteMeshInstance.cachedSkinnedRenderer;
					
					if(!skinnedMeshRenderer)
					{
						if(undo)
						{
							skinnedMeshRenderer = Undo.AddComponent<SkinnedMeshRenderer>(spriteMeshInstance.gameObject);
						}else{
							skinnedMeshRenderer = spriteMeshInstance.gameObject.AddComponent<SkinnedMeshRenderer>();
						}
					}
					
					skinnedMeshRenderer.bones = spriteMeshInstance.bones.ConvertAll( bone => bone.transform ).ToArray();
					
					if(spriteMeshInstance.bones.Count > 0)
					{
						skinnedMeshRenderer.rootBone = spriteMeshInstance.bones[0].transform;
					}

					EditorUtility.SetDirty(skinnedMeshRenderer);
				}else{
					SkinnedMeshRenderer skinnedMeshRenderer = spriteMeshInstance.cachedSkinnedRenderer;
					MeshFilter meshFilter = spriteMeshInstance.cachedMeshFilter;
					MeshRenderer meshRenderer = spriteMeshInstance.cachedRenderer as MeshRenderer;
					
					if(skinnedMeshRenderer)
					{
						if(undo)
						{
							Undo.DestroyObjectImmediate(skinnedMeshRenderer);
						}else{
							GameObject.DestroyImmediate(skinnedMeshRenderer);
						}
					}
					
					if(!meshFilter)
					{
						if(undo)
						{
							meshFilter = Undo.AddComponent<MeshFilter>(spriteMeshInstance.gameObject);
						}else{
							meshFilter = spriteMeshInstance.gameObject.AddComponent<MeshFilter>();
						}

						EditorUtility.SetDirty(meshFilter);
					}
					
					if(!meshRenderer)
					{
						if(undo)
						{
							meshRenderer = Undo.AddComponent<MeshRenderer>(spriteMeshInstance.gameObject);
						}else{
							meshRenderer = spriteMeshInstance.gameObject.AddComponent<MeshRenderer>();
						}
						
						EditorUtility.SetDirty(meshRenderer);
					}
				}
			}
		}

可以看到在绑定完骨骼以后,对应的SpriteMesh和SpriteMeshData资源是有跟新的,至于跟新了什么,我们结合代码和文件来看看:
vscode对比文件:


至于骨骼怎么根据这些矩阵,一步步的带动网格变形这个,我们这个博客就不讲了。下个博客再讲。

怎么得到一个简单的Animaton呢,这里用到了Unity自带的Animation。可以选中角色,在Animation的面板上,new animation cilp,然后这个角色就会自动添加Animator组件,对应的是xx.controller.

让Animator记录的应该是Bone2D这个对应的Object所用的position和rotation属性。只记录那个SpriteMeshInstance对应的Object没有用…虽然我也没太搞清楚是为什么。。。

截至目前,基本已经把有用的代码研究的差不多了。但其实还有很多黑盒子看不到,感觉很难受。

比如:
1、Animation Clip的录制程序流程,当我拖动Scene中的骨骼时,走的哪里的代码,让它绑定的纹理网格也跟着动了?哪个模块负责的渲染,SpriteMeshInstance吗?
2、blenshape到底干啥的啊??
3、IK,CCD底层算法还来不及研究…

但是这篇blog我就暂时先写到这里了,后续有啥新的收获再更新~~

课堂小知识

ScriptableObject & SerializedObject

ScriptableObject :https://blog.csdn/qq_36383623/article/details/99649941

看代码理解一下,SpriteMeshScriptableObject类型,我们声明了一个SerializedObject对象来通过FindProperty()等函数获取和这个ScriptableObject对象的一些属性~然后们还看到了objectReferenceValue这个字段。很明显就是和引用有关啦。

本文标签: 文件格式photoshoppsdUnity