用了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这个字段。很明显就是和引用有关啦。

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

  1. 室内高品质海报框架模型模板(Photoshop PSD)

    海报框架模型模板(Photoshop PSD) 优雅的位置,这些海报/绘画/相框模型模板将提升您的下一次演示!这些模型是完全可定制的, 你可以从5个PSD文件中挑选,非常容易编辑.最终,你会得到美丽的 ...

  2. linux lds,Linux LDS 文件格式详细分析.pdf

    Linux LDS 文件格式详细分析.pdf LDS 文件格式分析 连接脚本的格式 ==================== 连接脚本是文本文件. 你写了一系列的命令作为一个连接脚本. 每一个命令是一 ...

  3. C# 读写 Photoshop PSD文件 操作类

    使用方法 显示PSD OpenFileDialog _Dialog = new OpenFileDialog();_Dialog.Filter = "*.psd|*.psd";if ...

  4. matlab表面形貌,采用Photoshop与MATLAB软件分析壁画表面形貌变化的方法与流程

    本发明涉及一种壁画表面形貌变化的分析方法,尤其涉及一种采用Photoshop与MATLAB软件分析壁画表面形貌变化的方法,属于壁画表面形貌变化的分析技术领域. 背景技术: 壁画是墙壁上的艺术,即直接画 ...

  5. JPEG文件格式简单分析

    本文选自 http://www.blogjava.net/wilsonny/archive/2005/07/01/7000.aspx 摘要: 这篇文章大体上介绍了JPEG文件的结构信息以及它的压缩算法 ...

  6. 腾讯游戏学院专家分析:Unity在移动设备的GPU内存机制

    导语CPU和GPU是共享一份内存的吗?腾讯游戏学院专家Donald将在本文尝试以一张贴图纹理的虚拟内存占用为例,解答一些内存方面的问题.本篇主要分析iOS系统,后续会更新安卓篇. 开发手机游戏时,常听 ...

  7. 1709 ltsb 内存占用_腾讯游戏学院专家分析:Unity在移动设备的GPU内存机制

    导语 CPU和GPU是共享一份内存的吗?腾讯游戏学院专家Donald将在本文尝试以一张贴图纹理的虚拟内存占用为例,解答一些内存方面的问题.本篇主要分析iOS系统,后续会更新安卓篇. 开发手机游戏时,常 ...

  8. python 导入数据对不齐_[Python] 大文件数据读取分析

    首先我们可以确定的是不能用read()与readlines()函数: 因为如果将这两个函数均将数据全部读入内存,会造成内存不足的情况. 针对数据按行划分的文件 以计算行数为例,首先针对几种不同的方法来 ...

  9. MySQL笔记-ibd文件格式初步分析(仅数据块笔记)

    在MySQL建立表后,会在对应的库文件夹下创建2个文件. 一个是frm,一个是ibd,目前这个博文为简单分析下这个文件格式. 这里首先要知道一些预备知识: 查看InnoDB块的大小,一般是16k sh ...

  10. 海报框架模型Photoshop PSD样机模板

    如果你有观众喜欢的精美艺术品,不要仅仅满足于通过印刷品来展示. 相反,使用模型,向他们展示你的作品在他们的公寓里会是什么样子. 这就是海报框架样机模型的魔力所在. 由艺术家和图形设计师使用,模型打开了 ...

最新文章

  1. 百度重新定义「智能屏」,瞄准10后
  2. myeclipse 2019中文版
  3. 日常生活中的法语积累2
  4. leetcode 593. Valid Square | 593. 有效的正方形(Java)
  5. 栈解析html文件,利用栈将html源码解析为节点树
  6. Win32ASM学习[6]: PTR、OFFSET、ADDR、THIS
  7. 如何在Ubuntu 16.04上使用MySQL 5.6配置Galera集群
  8. IXMLDOMDocument 成員
  9. 【c++】简单的string类的几个基本函数
  10. Data Minig --- Decision Tree ID3 C4.5 Gini Index
  11. CentOS 关闭蜂鸣器声音
  12. P1090 合并果子
  13. 设计模式(九): 从醋溜土豆丝和清炒苦瓜中来学习模板方法模式(Template Method Pattern)...
  14. OpenCV-图像处理(21、霍夫圆变换)
  15. iphone163邮件服务器设置,怎样在iphone上设置网易免费企业邮箱收发邮件
  16. 共享计算机桌面,DeskTopShare桌面屏幕共享软件
  17. 微信小程序-服务通知的订阅与下发(基于云调用)
  18. 微信公众号之订阅号(已认证)实现oauth2授权登录详细步骤介绍
  19. 网站域名假墙处理方法 内含cloudflare API自动更换IP的php脚本
  20. HOJ 2786 Convert Kilometers to Miles

热门文章

  1. 全局快门与卷帘式快门
  2. 精灵图,雪碧图的应用
  3. 前沿重器[26] | 预训练模型的领域适配问题
  4. 功能测试怎么做?常用功能测试方法总结
  5. HttpServletRequest获取中文参数乱码问题
  6. 概念数据模型(CDM)、逻辑数据模型(LDM)、物理数据模型(PDM)区别以及哪些适合需求分析阶段的数据建模
  7. Element 表格序号问题
  8. Linux查看最近开关机记录
  9. V神站台--黑马BZZ究竟如何?和FIL 有什么区别?
  10. “华为杯”山东理工大学第十一届ACM程序设计竞赛(正式赛)网络同步赛