unity 导出 stl

Unity导出stl格式

stl是常用的3D打印格式,目前有不少文章介绍stl的,这里不多介绍。
导出stl分为ascii形式和二进制形式,区别在于ascii可以直接用文本文件打开查看,而二进制直接打开是乱码,但是二进制形式读写速度较快,生成的文件也比ascii要小很多。

开发环境

unity:2018.2.16 2019.3.15
模型查看工具:Meshlab2020.09CAD Assistant
测试模型:assetstore 上 的 office building

核心模块

  • ASCII形式 将单个mesh写出为stl
		/// <summary>
        /// 将单个mesh数据使用StreamWrite写出为stl
        /// </summary>
        /// <param name="mesh">待导出的mesh</param>
        /// <param name="sw">输出流</param>
        /// <param name="trans">mesh对应的transform,用于将顶点法线转到世界空间</param>
        /// <param name="exchangeCoordinate">是否需要变换坐标手系,unity是左手坐标系,默认变换到右手坐标系</param>
        private static void ExportMeshToStl(Mesh mesh, StreamWriter sw, Transform trans, bool exchangeCoordinate = true)
        {
            for (int j = 0; j < mesh.subMeshCount; j++)
            {
                int[] tris;
                if (mesh.subMeshCount == 1)
                {
                    sw.Write("\nsolid " + mesh.name + "\n");
                    tris = mesh.triangles;
                }
                else
                {
                    sw.Write("\nsolid " + mesh.name + "_" + j + "\n");
                    tris = mesh.GetIndices(j);
                }
                Vector3[] vertices = mesh.vertices;
                Vector3[] normals = mesh.normals;
                for (int i = 0; i < tris.Length / 3; i++)
                {
                	//法线变换到世界空间
                    Vector3 nor1 = trans.TransformDirection(normals[tris[i * 3]]);
                    Vector3 nor2 = trans.TransformDirection(normals[tris[i * 3 + 1]]);
                    Vector3 nor3 = trans.TransformDirection(normals[tris[i * 3 + 2]]);
					//顶点变换到世界空间
                    Vector3 worldPos1 = trans.TransformPoint(vertices[tris[i * 3]]);
                    Vector3 worldPos2 = trans.TransformPoint(vertices[tris[i * 3 + 1]]);
                    Vector3 worldPos3 = trans.TransformPoint(vertices[tris[i * 3 + 2]]);
					//如果需要从左手系变换到右手系
                    if (exchangeCoordinate)
                    {
                        nor1.x *= -1;
                        nor2.x *= -1;
                        nor3.x *= -1;
                        worldPos1.x *= -1;
                        worldPos2.x *= -1;
                        worldPos3.x *= -1;
                    }

                    Vector3 normal = (nor1 + nor2 + nor3) / 3;
                    sw.Write("\tfacet normal " + normal.x + " " + normal.y + " " + normal.z);
                    sw.Write("\n\t\touter loop\n");

                    sw.Write("\t\t\tvertex " + worldPos1.x + " " + worldPos1.y + " " + worldPos1.z + "\n");
                    if (exchangeCoordinate)
                    {
                        sw.Write("\t\t\tvertex " + worldPos3.x + " " + worldPos3.y + " " + worldPos3.z + "\n");
                        sw.Write("\t\t\tvertex " + worldPos2.x + " " + worldPos2.y + " " + worldPos2.z + "\n");
                    }
                    else
                    {
                        sw.Write("\t\t\tvertex " + worldPos2.x + " " + worldPos2.y + " " + worldPos2.z + "\n");
                        sw.Write("\t\t\tvertex " + worldPos3.x + " " + worldPos3.y + " " + worldPos3.z + "\n");
                    }
                    sw.Write("\t\tendloop\n");
                    sw.Write("\tendfacet\n");
                }
                if (mesh.subMeshCount == 1)
                {
                    sw.Write("endsolid " + mesh.name);
                }
                else
                {
                    sw.Write("endsolid " + mesh.name + "_" + j);
                }
            }
        }
  • 二进制形式 将单个mesh写出为stl
		/// <summary>
        /// 将单个mesh数据使用StreamWrite写出为stl,二进制格式
        /// </summary>
        /// <param name="mesh">待导出的mesh</param>
        /// <param name="bw">BinaryWriter 写出二进制数据的类</param>
        /// <param name="trans">mesh对应的transform,用于将顶点法线转到世界空间</param>
        /// <param name="exchangeCoordinate">是否需要变换坐标手系,unity是左手坐标系,默认变换到右手坐标系</param>
        private static void ExportMeshToStl(Mesh mesh, BinaryWriter bw, Transform trans, bool exchangeCoordinate = true)
        {

            Vector3[] vertices = mesh.vertices;
            Vector3[] normals = mesh.normals;
            int[] tris = mesh.triangles;
            //每个三角面片固定占用50个字节
            for (int i = 0; i < tris.Length / 3; i++)
            {
                Vector3 nor1 = trans.TransformDirection(normals[tris[i * 3]]);
                Vector3 nor2 = trans.TransformDirection(normals[tris[i * 3 + 1]]);
                Vector3 nor3 = trans.TransformDirection(normals[tris[i * 3 + 2]]);

                Vector3 worldPos1 = trans.TransformPoint(vertices[tris[i * 3]]);
                Vector3 worldPos2 = trans.TransformPoint(vertices[tris[i * 3 + 1]]);
                Vector3 worldPos3 = trans.TransformPoint(vertices[tris[i * 3 + 2]]);

                if (exchangeCoordinate)
                {
                    nor1.x *= -1;
                    nor2.x *= -1;
                    nor3.x *= -1;
                    worldPos1.x *= -1;
                    worldPos2.x *= -1;
                    worldPos3.x *= -1;
                }

                Vector3 normal = (nor1 + nor2 + nor3) / 3;
                bw.Write(normal.x);
                bw.Write(normal.y);
                bw.Write(normal.z);
                bw.Write(worldPos1.x);
                bw.Write(worldPos1.y);
                bw.Write(worldPos1.z);
                if (exchangeCoordinate)
                {
                    bw.Write(worldPos3.x);
                    bw.Write(worldPos3.y);
                    bw.Write(worldPos3.z);
                    bw.Write(worldPos2.x);
                    bw.Write(worldPos2.y);
                    bw.Write(worldPos2.z);
                }
                else
                {
                    bw.Write(worldPos2.x);
                    bw.Write(worldPos2.y);
                    bw.Write(worldPos2.z);
                    bw.Write(worldPos3.x);
                    bw.Write(worldPos3.y);
                    bw.Write(worldPos3.z);
                }
                
                //填充两个字节  三角面片的最后2个字节用来描述三角面片的属性信息(包括颜色属性等)暂时没有用
                bw.Seek(2, SeekOrigin.Current);
            }
        }

完整代码

/****************************************************
    文件:Exporter.cs
	作者:TKB
    邮箱: 544726237@qq.com
    日期:2021/7/24 23:9:12
	功能:导出stl
*****************************************************/

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;

namespace TLib
{
    public class Exporter
    {
     	#region 导出stl
        /// <summary>
        /// 导出Transfrom及其子mesh为单个stl
        /// </summary>
        /// <param name="trans">待导出的transfrom</param>
        /// <param name="outputPath">导出的stl完整路径,如D:/TKB/output.stl</param>
        /// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系,默认为true</param>
        /// <param name="isBinary">是否以格式导出</param>
        public static void ExportStl(Transform trans, string outputPath, bool exchangeCoordinate = true, bool isBinary = true)
        {
            ExportStl(trans.gameObject, outputPath, exchangeCoordinate, isBinary);
        }

        /// <summary>
        /// 导出GameObject及其子mesh为单个stl
        /// </summary>
        /// <param name="go">待导出的GameObject</param>
        /// <param name="outputPath">导出的stl完整路径,如D:/TKB/output.stl</param>
        /// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系,默认为true</param>
        /// <param name="isBinary">是否以格式导出</param>
        public static void ExportStl(GameObject go, string outputPath, bool exchangeCoordinate = true, bool isBinary = true)
        {
            if (!go) return;
            if (!Directory.Exists(Path.GetDirectoryName(outputPath)))
            {
                Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
            }
            if (File.Exists(outputPath))
            {
                try
                {
                    File.Delete(outputPath);
                    Debug.LogWarning("该路径已存在同名文件,已删除!" + outputPath);
                }
                catch (Exception e)
                {
                    Debug.LogError(e + "该路径已存在同名文件并且删除失败!" + outputPath);
                    return;
                }
            }

            MeshFilter[] meshFilters = go.GetComponentsInChildren<MeshFilter>();
            SkinnedMeshRenderer[] skinnedMeshRenderers = go.GetComponentsInChildren<SkinnedMeshRenderer>();
            int meshCount = meshFilters.Length + skinnedMeshRenderers.Length;
            try
            {
                FileStream meshFS = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); ;
                StreamWriter meshSW = null;
                BinaryWriter meshBW = null;
                if (!isBinary)
                {
                    meshSW = new StreamWriter(meshFS, Encoding.UTF8);
                }
                else
                {
                    meshBW = new BinaryWriter(meshFS, Encoding.UTF8);
                    //文件的起始80字节是文件头存储零件名,可以放入任何文字信息
                    meshBW.Write(go.name);
                    meshBW.Seek(80, SeekOrigin.Begin);
                    //紧随着用4个字节的整数来描述实体的三角面片个数
                    int count = 0;
                    for (int i = 0; i < meshFilters.Length; i++)
                    {
                        Mesh mesh;
#if UNITY_EDITOR
                        mesh = meshFilters[i].sharedMesh;
                        
#else
                        mesh = meshFilters[i].mesh;
#endif
                        count += mesh.triangles.Length;
                    }
                    for (int i = 0; i < skinnedMeshRenderers.Length; i++)
                    {
                        Mesh mesh;
#if UNITY_EDITOR
                        mesh = skinnedMeshRenderers[i].sharedMesh;
                        
#else
                        mesh = meshFilters[i].mesh;
#endif
                        count += mesh.triangles.Length;
                    }
                    meshBW.Write(count/3);
                }

                for (int i = 0; i < meshFilters.Length; i++)
                {
                    Mesh mesh;
#if UNITY_EDITOR
                    mesh = meshFilters[i].sharedMesh;
                    UnityEditor.EditorUtility.DisplayProgressBar("导出Stl", mesh.name + ":" + i + "/" + meshCount, i * 1.0f / meshCount);
#else
                    mesh = meshFilters[i].mesh;
#endif
                    if (!isBinary)
                        ExportMeshToStl(mesh, meshSW, meshFilters[i].transform, exchangeCoordinate);
                    else
                        ExportMeshToStl(mesh, meshBW, meshFilters[i].transform, exchangeCoordinate);
                }
                for (int i = 0; i < skinnedMeshRenderers.Length; i++)
                {
                    Mesh mesh;
#if UNITY_EDITOR
                    mesh = skinnedMeshRenderers[i].sharedMesh;
                    UnityEditor.EditorUtility.DisplayProgressBar("导出Stl", mesh.name + ":" + (i + meshFilters.Length) + "/" + meshCount, i * 1.0f / meshCount);
#else
                    mesh = meshFilters[i].mesh;
#endif
                    if (!isBinary)
                        ExportMeshToStl(mesh, meshSW, skinnedMeshRenderers[i].transform, exchangeCoordinate);
                    else
                        ExportMeshToStl(mesh, meshBW, skinnedMeshRenderers[i].transform, exchangeCoordinate);
                }
                if (!isBinary)
                {
                    meshSW.Close();
                }
                else
                {
                    meshBW.Close();
                }
                meshFS.Close();
            }
            catch (Exception e)
            {
                Debug.LogError(e);
            }
            finally
            {
                UnityEditor.EditorUtility.ClearProgressBar();
            }
           
        }

        /// <summary>
        /// 将Transform及其子对象导出为多个stl,每个mesh对应一个
        /// </summary>
        /// <param name="trans">待导出的Transform</param>
        /// <param name="outputDir">导出的文件夹路径,stl存放的位置</param>
        /// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系,默认为true</param>
        ///  <param name="isBinary">是否以格式导出</param>
        public static void ExportStls(Transform trans, string outputDir, bool exchangeCoordinate = true, bool isBinary = true)
        {
            ExportStls(trans.gameObject, outputDir, exchangeCoordinate, isBinary);
        }

        /// <summary>
        ///  将GameObject及其子对象导出为多个stl,每个mesh对应一个
        /// </summary>
        /// <param name="go">待导出的GameObject</param>
        /// <param name="outputDir">导出的文件夹路径,stl存放的位置</param>
        /// <param name="exchangeCoordinate">是否要变换坐标系,从左手坐标系(unity)变换到右手坐标系,默认为true</param>
        ///  <param name="isBinary">是否以格式导出</param>
        public static void ExportStls(GameObject go, string outputDir, bool exchangeCoordinate = true, bool isBinary = true)
        {
            if (!Directory.Exists(outputDir)) Directory.CreateDirectory(outputDir);
            MeshFilter[] meshFilters = go.GetComponentsInChildren<MeshFilter>();
            SkinnedMeshRenderer[] skinnedMeshRenderers = go.GetComponentsInChildren<SkinnedMeshRenderer>();
            Dictionary<string, int> meshNameDic = new Dictionary<string, int>();
            int meshCount = meshFilters.Length + skinnedMeshRenderers.Length;
            for (int i = 0; i < meshFilters.Length; i++)
            {
                try
                {
                    string name = meshFilters[i].gameObject.name;
                    if (meshNameDic.ContainsKey(name))
                    {
                        meshNameDic[name]++;
                        name += meshNameDic[name];
                    }
                    else meshNameDic.Add(name, 0);
                    string stlPath = Path.Combine(outputDir, name + ".stl");
                    FileStream meshFS = new FileStream(stlPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
                    StreamWriter meshSW = null;
                    BinaryWriter meshBW = null;

                    Mesh mesh;
#if UNITY_EDITOR
                    mesh = meshFilters[i].sharedMesh;
                    UnityEditor.EditorUtility.DisplayProgressBar("导出Stl", mesh.name + ":" + i + "/" + meshCount, i * 1.0f / meshCount);
#else
                    mesh = meshFilters[i].mesh;
#endif
                    if (!isBinary)
                    {
                        meshSW = new StreamWriter(meshFS, Encoding.UTF8);
                        ExportMeshToStl(mesh, meshSW, meshFilters[i].transform, exchangeCoordinate);
                        meshSW.Close();
                    }
                    else
                    {
                        meshBW = new BinaryWriter(meshFS, Encoding.UTF8);
                        //文件的起始80字节是文件头存储零件名,可以放入任何文字信息
                        meshBW.Write(name);
                        meshBW.Seek(80, SeekOrigin.Begin);
                        meshBW.Write(mesh.triangles.Length / 3);
                        ExportMeshToStl(mesh, meshBW, meshFilters[i].transform, exchangeCoordinate);
                        meshBW.Close();
                    }
                    meshFS.Close();
                }
                catch (Exception e)
                {
                    Debug.LogError(e);
                }
            }
            for (int i = 0; i < skinnedMeshRenderers.Length; i++)
            {
                try
                {
                    string name = skinnedMeshRenderers[i].gameObject.name;
                    if (meshNameDic.ContainsKey(name))
                    {
                        name += meshNameDic[name];
                        meshNameDic[name]++;
                    }
                    else meshNameDic.Add(name, 1);
                    string stlPath = Path.Combine(outputDir, name + ".stl");
                    FileStream meshFS = new FileStream(stlPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
                    StreamWriter meshSW = null;
                    BinaryWriter meshBW = null;

                    Mesh mesh;
#if UNITY_EDITOR
                    mesh = skinnedMeshRenderers[i].sharedMesh;
                    UnityEditor.EditorUtility.DisplayProgressBar("导出Stl", mesh.name + ":" + (i + meshFilters.Length) + "/" + meshCount, i * 1.0f / meshCount);
#else
                    mesh = meshFilters[i].mesh;
materials;
#endif
                    if (!isBinary)
                    {
                        meshSW = new StreamWriter(meshFS, Encoding.UTF8);
                        ExportMeshToStl(mesh, meshSW, skinnedMeshRenderers[i].transform, exchangeCoordinate);
                        meshSW.Close();
                    }
                    else
                    {
                        meshBW = new BinaryWriter(meshFS, Encoding.UTF8);
                        //文件的起始80字节是文件头存储零件名,可以放入任何文字信息
                        meshBW.Write(name);
                        meshBW.Seek(80, SeekOrigin.Begin);
                        meshBW.Write(mesh.triangles.Length / 3);
                        ExportMeshToStl(mesh, meshBW, skinnedMeshRenderers[i].transform, exchangeCoordinate);
                        meshBW.Close();
                    }
                    meshFS.Close();
                }
                catch (Exception e)
                {
                    Debug.LogError(e);
                }
            }
            UnityEditor.EditorUtility.ClearProgressBar();
        }
        /// <summary>
        /// 将单个mesh数据使用StreamWrite写出为stl
        /// </summary>
        /// <param name="mesh">待导出的mesh</param>
        /// <param name="sw">输出流</param>
        /// <param name="trans">mesh对应的transform,用于将顶点法线转到世界空间</param>
        /// <param name="exchangeCoordinate">是否需要变换坐标手系,unity是左手坐标系,默认变换到右手坐标系</param>
        private static void ExportMeshToStl(Mesh mesh, StreamWriter sw, Transform trans, bool exchangeCoordinate = true)
        {
            for (int j = 0; j < mesh.subMeshCount; j++)
            {
                int[] tris;
                if (mesh.subMeshCount == 1)
                {
                    sw.Write("\nsolid " + mesh.name + "\n");
                    tris = mesh.triangles;
                }
                else
                {
                    sw.Write("\nsolid " + mesh.name + "_" + j + "\n");
                    tris = mesh.GetIndices(j);
                }
                Vector3[] vertices = mesh.vertices;
                Vector3[] normals = mesh.normals;
                for (int i = 0; i < tris.Length / 3; i++)
                {
                    Vector3 nor1 = trans.TransformDirection(normals[tris[i * 3]]);
                    Vector3 nor2 = trans.TransformDirection(normals[tris[i * 3 + 1]]);
                    Vector3 nor3 = trans.TransformDirection(normals[tris[i * 3 + 2]]);

                    Vector3 worldPos1 = trans.TransformPoint(vertices[tris[i * 3]]);
                    Vector3 worldPos2 = trans.TransformPoint(vertices[tris[i * 3 + 1]]);
                    Vector3 worldPos3 = trans.TransformPoint(vertices[tris[i * 3 + 2]]);

                    if (exchangeCoordinate)
                    {
                        nor1.x *= -1;
                        nor2.x *= -1;
                        nor3.x *= -1;
                        worldPos1.x *= -1;
                        worldPos2.x *= -1;
                        worldPos3.x *= -1;
                    }

                    Vector3 normal = (nor1 + nor2 + nor3) / 3;
                    sw.Write("\tfacet normal " + normal.x + " " + normal.y + " " + normal.z);
                    sw.Write("\n\t\touter loop\n");

                    sw.Write("\t\t\tvertex " + worldPos1.x + " " + worldPos1.y + " " + worldPos1.z + "\n");
                    if (exchangeCoordinate)
                    {
                        sw.Write("\t\t\tvertex " + worldPos3.x + " " + worldPos3.y + " " + worldPos3.z + "\n");
                        sw.Write("\t\t\tvertex " + worldPos2.x + " " + worldPos2.y + " " + worldPos2.z + "\n");
                    }
                    else
                    {
                        sw.Write("\t\t\tvertex " + worldPos2.x + " " + worldPos2.y + " " + worldPos2.z + "\n");
                        sw.Write("\t\t\tvertex " + worldPos3.x + " " + worldPos3.y + " " + worldPos3.z + "\n");
                    }
                    sw.Write("\t\tendloop\n");
                    sw.Write("\tendfacet\n");
                }
                if (mesh.subMeshCount == 1)
                {
                    sw.Write("endsolid " + mesh.name);
                }
                else
                {
                    sw.Write("endsolid " + mesh.name + "_" + j);
                }
            }
        }

        /// <summary>
        /// 将单个mesh数据使用StreamWrite写出为stl,二进制格式
        /// </summary>
        /// <param name="mesh">待导出的mesh</param>
        /// <param name="bw">BinaryWriter 写出二进制数据的类</param>
        /// <param name="trans">mesh对应的transform,用于将顶点法线转到世界空间</param>
        /// <param name="exchangeCoordinate">是否需要变换坐标手系,unity是左手坐标系,默认变换到右手坐标系</param>
        private static void ExportMeshToStl(Mesh mesh, BinaryWriter bw, Transform trans, bool exchangeCoordinate = true)
        {

            Vector3[] vertices = mesh.vertices;
            Vector3[] normals = mesh.normals;
            int[] tris = mesh.triangles;
            //每个三角面片固定占用50个字节
            for (int i = 0; i < tris.Length / 3; i++)
            {
                Vector3 nor1 = trans.TransformDirection(normals[tris[i * 3]]);
                Vector3 nor2 = trans.TransformDirection(normals[tris[i * 3 + 1]]);
                Vector3 nor3 = trans.TransformDirection(normals[tris[i * 3 + 2]]);

                Vector3 worldPos1 = trans.TransformPoint(vertices[tris[i * 3]]);
                Vector3 worldPos2 = trans.TransformPoint(vertices[tris[i * 3 + 1]]);
                Vector3 worldPos3 = trans.TransformPoint(vertices[tris[i * 3 + 2]]);

                if (exchangeCoordinate)
                {
                    nor1.x *= -1;
                    nor2.x *= -1;
                    nor3.x *= -1;
                    worldPos1.x *= -1;
                    worldPos2.x *= -1;
                    worldPos3.x *= -1;
                }

                Vector3 normal = (nor1 + nor2 + nor3) / 3;
                bw.Write(normal.x);
                bw.Write(normal.y);
                bw.Write(normal.z);
                bw.Write(worldPos1.x);
                bw.Write(worldPos1.y);
                bw.Write(worldPos1.z);
                if (exchangeCoordinate)
                {
                    bw.Write(worldPos3.x);
                    bw.Write(worldPos3.y);
                    bw.Write(worldPos3.z);
                    bw.Write(worldPos2.x);
                    bw.Write(worldPos2.y);
                    bw.Write(worldPos2.z);
                }
                else
                {
                    bw.Write(worldPos2.x);
                    bw.Write(worldPos2.y);
                    bw.Write(worldPos2.z);
                    bw.Write(worldPos3.x);
                    bw.Write(worldPos3.y);
                    bw.Write(worldPos3.z);
                }
                
                //填充两个字节  三角面片的最后2个字节用来描述三角面片的属性信息(包括颜色属性等)暂时没有用
                bw.Seek(2, SeekOrigin.Current);
            }
        }
    }
    #endregion
}

测试脚本

/****************************************************
    文件:ExportStlExample.cs
	作者:TKB
    邮箱: 544726237@qq.com
    日期:2021/7/26 22:19:56
	功能:编辑器环境测试将选中的物体导出stl   
*****************************************************/

using UnityEngine;

namespace TLib
{
    public class ExportStlExample
    {
#if UNITY_EDITOR
		//将当前选中的物体下所有mesh导出到一个stl中
        [UnityEditor.MenuItem("Tools/导出stl", false)]
        private static void OnClickExportObj()
        {
            GameObject go = UnityEditor.Selection.activeObject as GameObject;
            Exporter.ExportStl(go.transform, Application.dataPath +"/"+ go.name+".stl",true,false);
            UnityEditor.AssetDatabase.Refresh();
        }
        //将当前选中的物体下的mesh分别导出为stl
        [UnityEditor.MenuItem("Tools/导出stls", false)]
        private static void OnClickExportObj1()
        {
            GameObject go = UnityEditor.Selection.activeObject as GameObject;
            Exporter.ExportStls(go, Application.dataPath + "/Exports");
            UnityEditor.AssetDatabase.Refresh();
        }
#endif
    }
}

效果

unity中的模型截图:
unity中的模型
选择导出为stl 在meshlab中的截图:
stl外部
内部细节
选择导出为stls时导出了3000+的模型:
请添加图片描述

注意

这个测试模型导出二进制格式时使用meshlab打开报错,但是用CAD Assistant可以打开,原因还不清楚

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值