PhotoShop .psd文件格式读取分析(结合unity)

news/2024/11/28 6:39:40/

用了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 idvar MENU_STRING          = "Ps2D Map";                // our plugin's menu textvar PLUGIN_VERSION       = pluginPackage.version;     // our plugin versionvar PLUGIN_NAME          = pluginPackage.name;        // plugin namevar MAP_EXTENSION        = ".ps2dmap.json";           // map's extension// *** STATE VARIABLES ***var _generator           = null;  // the generator frameworkvar _currentDocumentId   = null;  // the document's idvar _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 systemfunction saveMapFile(layoutDocument) {// get the filenamevar filename = layoutFilename();// serialize our layoutDocument to a stringvar 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 logginglog("unable to save map file to " + filename);}}// convert the photoshop document to our own layout documentfunction createLayoutDocument(document) {// create our own documentvar 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 spritesfunction convertLayersToSprites(layers) {// sanityif (layers === null || layers === undefined || layers.length === 0)return null;// sprites go herevar sprites = [];// go through each layerfor (var i = 0, z = layers.length; i < z; i++){var layer = layers[i];// convert itvar sprite = convertLayerToSprite(layer);// add it to the list if it's legitif (sprite !== null) {sprites.push(sprite);}}return sprites;}// convert a ps layer to a spritefunction convertLayerToSprite(layer) {// safetyif (layer === null || layer === undefined) return null;// grab what we need for our mapvar 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 successfullyfunction handleMenuItemInstallSuccess() {}// fires when PS can't install our menu itemfunction handleMenuItemInstallFailed() {log("Unable to create Ps2D menu item.");}// the user has clicked our menu itemfunction 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 changedfunction handleCurrentDocumentChanged(id) {_currentDocumentId = id;}// *** UTILITIES ***// write a log messagefunction log(s) {console.log("[" + PLUGIN_NAME + "] " + s);}// show a popup messagefunction popup(msg) {var js = "alert('" + msg + "');"sendJavascript(js);}// run some javascriptfunction sendJavascript(str){_generator.evaluateJSXString(str).then(function(result){console.log(result);},function(err){console.log(err);});}// *** INITIALIZATION ***// main entry point for the pluginfunction 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 availableexports.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.net/qq_36383623/article/details/99649941
在这里插入图片描述
看代码理解一下,SpriteMeshScriptableObject类型,我们声明了一个SerializedObject对象来通过FindProperty()等函数获取和这个ScriptableObject对象的一些属性~然后们还看到了objectReferenceValue这个字段。很明显就是和引用有关啦。
在这里插入图片描述


http://www.ppmy.cn/news/338466.html

相关文章

史上最简单的 Nginx 教程,没有之一!

作者&#xff1a;哆啦A梦的猜想 链接&#xff1a;https://juejin.im/post/5d81906c518825300a3ec7ca 安装 安装依赖 安装 nginx 之前&#xff0c;确保系统已经安装 gcc、openssl-devel、pcre-devel 和 zlib-devel 软件库 gcc 可以通过光盘直接选择安装openssl-devel、zlib-dev…

一个合格的中级前端工程师需要掌握的技能笔记(下)

Github来源&#xff1a;一个合格的中级前端工程师需要掌握的技能 | 求星星 ✨ | 给个❤️关注&#xff0c;❤️点赞&#xff0c;❤️鼓励一下作者 大家好&#xff0c;我是魔王哪吒&#xff0c;很高兴认识你~~ 哪吒人生信条&#xff1a;如果你所学的东西 处于喜欢 才会有强大的动…

miranda- core src tree

1、小子&#xff0c;敢泡我马子&#xff01;你说吧&#xff0c;是单挑还是群殴&#xff1f;群殴&#xff0c;我们一帮殴你一个&#xff1b;单挑&#xff0c;你挑我们一帮&#xff01;       2、常函数和指数函数e的x次方走在街上&#xff0c;远远看到微分算子&#xff0c; …

python 怎么得到图像深度图 软件_如何用 Python 和 fast.ai 做图像深度迁移学习?...

本文带你认识一个优秀的新深度学习框架&#xff0c;了解深度学习中最重要的3件事。 框架 看到这个题目&#xff0c;你可能会疑惑&#xff1a;老师&#xff0c;你不是讲过如何用深度学习做图像分类了吗&#xff1f;迁移学习好像也讲过了啊&#xff01; 说得对&#xff01;我要感…

python深度神经网络文本二分类代码_如何用Python和深度神经网络识别图像?

只需要10几行Python代码&#xff0c;你就能自己构建机器视觉模型&#xff0c;对图片做出准确辨识和分类。快来试试吧&#xff01; 视觉 进化的作用&#xff0c;让人类对图像的处理非常高效。 这里&#xff0c;我给你展示一张照片。 如果我这样问你&#xff1a; 你能否分辨出图片…

使用graphhopper(map-matching)进行地图匹配

因为在做国创项目的时候需要进行地图匹配&#xff0c;在地图匹配的过程中也遇见了很多问题&#xff0c;在此记录&#xff0c;希望我的经历可以帮助大家规避一些问题&#xff0c;节约时间 在github上面有关地图匹配的开源项目有很多&#xff0c;我在此分享一下&#xff1a; bm…

handler图片自动切换+listview+GridView+侧滑菜单+fragment

//效果图如下 //首先把所需要的依赖包导入 gson jar包、imageLoader jar包、design jar包&#xff0c;&#xff0c;&#xff0c;然后导入library&#xff0c;新建项目&#xff0c;把library导入项目中 //添加权限 <uses-permission android:name"android.permission…

cute-cnblogs 自定义博客园样式美化二期来啦~

cute-cnblogs 自定义博客园样式美化二期来啦~ 说明 cute-cnblogs 可爱的博客园样式美化、自定义博客园样式 二期样式已经编写完毕了&#xff0c;如果说 一期样式 给人的感觉是简洁清爽的小婴儿的话&#xff0c;那么 二期样式 就是一个有自己小个性&#xff08;花样&#xff09;…