o3d_shapes (转)

这一章介绍怎么用顶点数组创建一个3D 模型,如何创建一个shape 对象,缓冲(buffers) ,域(fields) 等等。

 


    由于要定义这个3D 模型的每个顶点,然后存入顶点数组,所以这章不会画出一个比较复杂的3D 模型,我们只是画一个立方体来说明如何创建一个3D 模型,如果对于一个复杂的3D 模型还是一个个顶点画的话,只能说太牛逼了,那时候就要用到3dmax ,maya 等软件来建模了,经过传换成一定格式的文件后再在程序中导入。

 


    上一章中直接用了一个createSphere ()函数创建了一个球体,这个函数是o3d 自带的,其实也有自带的创建立方体的函数,当然这章是不会用的,否则就没东西可以讲了。我们要做的就是自己写一个。

 

 

    function createCube(material, size){

   

    这里的两个参数,前一个是这个立方体要用到的材质,材质这块下一章会讲到。Size 顾名思义就是这个立方体的大小了。

 


    在o3d 中首先要做的便是创建一个shape 对象,一个图元(primitive )对象和一个streamBank 对象,

 


var cubeShape = g_pack . createObject ( 'Shape' );
    var cubePrimitive = g_pack . createObject ( 'Primitive' );
    var streamBank = g_pack . createObject ( 'StreamBank' );

 

cubePrimitive . material = material ;      // 定义这个形状的材质
    cubePrimitive . owner = cubeShape ;            // 定义shape 和图元的关系
    cubePrimitive . streamBank = streamBank ; ·· // 定义这个shape 的streamBank

 

    其中shape 是许多图元的集合,而一个图元则是一个点或者多个点通过一定规律连接在一起组成的图形 ( 不像纸上画画最重要的是把每条线画出来,在O3d 或者说是传统的openGL 和D3D 中画一个模型是把这个模型的每个顶点的坐标定义好,然后系统便会自己把这些点连接起来) 其中点与点之间的连接可以有多种规律,比如说比较常用的每三个点通过连线组成三角形,多个点连成多个连续的三角形,每四个点组成的四边形,多个点连成的多个连续的四边形等等。

   

             下面就要定义一下图元中各个点之间连接的规律了

cubePrimitive . primitiveType = g_o3d . Primitive . TRIANGLELIST ; // 三角形绘制

cubePrimitive . numberPrimitives = 12 ;         // 这个图元中有12 个三角形
    cubePrimitive . numberVertices = 8 ;        // 有8 个顶点

 

    创建一个绘制元素

cubePrimitive . createDrawElement ( g_pack , null );

 

  接下来填充这8 个顶点的数据,在js 中这些数据都是存放在一个一维数组中的:

var positionArray = [
    - size/2 , - size/2 ,   size/2 ,   // 顶点0
      size/2 , - size/2 ,   size/2 ,   // 顶点1
    - size/2 ,   size/2 ,   size/2 ,   // 顶点2
      size/2 ,   size/2 ,   size/2 ,   // 顶点3
    - size/2 ,   size/2 , - size/2 ,   // 顶点4
      size/2 ,   size/2 , - size/2 ,   // 顶点5
    - size/2 , - size/2 , - size/2 ,   // 顶点6
      size/2 , - size/2 , - size/2   // 顶点7
  ];

 

 这么大串size/2 (还记得size 是这个函数传进来的参数吧)都是每个顶点的坐标。


 

现在就有个问题,就是size 要多大才能够在屏幕当中显示一个身材合适的立方体呢,一开始可能会假象这个size 取成10 吧,10 这个数字不大不小蛮好的,可是你会发现屏幕中除了一片灰色就什么都没有了,因为10 对于现在这个情况来说太大了,连摄像机都在那个立方体里面了(当然你不会看到立方体里面那个面,因为一般情况下o3d 是只绘制一个面的)。

 

在o3d 中到底用的是什么单位导致10 这个在现实世界中这么小的一个数字能够变得这么大。咳咳,其实貌似在这样的3D 世界中是没有单位之说的( 至少我还不知道) ,因为这个物体的大小是相对于镜头的位置的,如果镜头离物体近了,size 就算很小那物体还是庞然大物,因此想要调整物体大小,可以调整size 的大小,也可以把镜头拉远(还记得第一章中设置照相机的位置吧),也可以把物体放远点,这些都是跟真实世界中差不多的。

 

O3d 中这些顶点的坐标数据都会先放入一个缓冲(buffers) 中,缓冲是一个对象(object) ,用来暂时存储顶点的各种数据的,像位置坐标,色彩,贴图坐标。下面就先创建缓冲对象:

    // 创建顶点缓冲来存放顶点数据

    var positionsBuffer = g_pack.createObject('VertexBuffer');

   


    然后创建域(field) ,域是一小块buffer 。

var positionsField = positionsBuffer . createField ( 'FloatField' , 3 );

positionsBuffer . set ( positionArray );     // 设置该缓冲所用的数组

   


    在一开始有说到要创建一个streamBank 对象,但是没解释为什么,这个真是说来话长啊,因为o3d 使用的是可编程渲染管线(即shader ),而非固定函数渲染管线(fixed function pipeline) ,shader (又可以叫做着色器)可以分为vertex shader (顶点着色器)和pixel shader (像素着色器)。有了pixel shader 之后,你能决定里面的每个像素该怎么填充。因为GPU 浮点运算能力比CPU 比显卡强得多,所以把这些计算交给显卡做再合适不过了。

   


    而这次绘制正方形,每个顶点也是通过vertex shader 来渲染的,是不是有点大材小用了,其实我也这么觉得,不过我们还好暂时还不用去写这个shader ,可以让Js 的接口来做这件事,这就是这个streamBank 做的了,streamBank 把域(field) 中的数据作为vertex shader 的输入。可以是js 中的顶点数据和底层shader 交互的通道。下面这段代码就是把域(field )和vertex shader 联系起来。

streamBank . setVertexStream (
    g_o3d . Stream . POSITION , // semantic: This stream stores vertex positions
    0 ,                     // semantic index: First (and only) position stream
    positionsField ,         // field: the field this stream uses.
    0 );                     // start_index: How many elements to skip in the field.

( 注释的英文我就不翻译了- -)

   

          最后你可以创建一个index buffer ,这个是用来重排上面vertex buffer 里各个点的得排列顺序的。你可以不加这个,不过显示出来的是不是一个立方体就不一定了- - 。

var indicesArray = [
      0 , 1 , 2 ,   // face 1
      2 , 1 , 3 ,
      2 , 3 , 4 ,   // face 2
      4 , 3 , 5 ,
      4 , 5 , 6 ,   // face 3
      6 , 5 , 7 ,
      6 , 7 , 0 ,   // face 4
      0 , 7 , 1 ,
      1 , 7 , 3 ,   // face 5
      3 , 7 , 5 ,
      6 , 0 , 4 ,   // face 6
      4 , 0 , 2
    ];

var indexBuffer = g_pack . createObject ( 'IndexBuffer' );

indexBuffer . set ( indicesArray );

cubePrimitive . indexBuffer = indexBuffer ;

Index buffer 和vertex buffer 的创建方式和存储方式是差不多的. 是两个差不多的Object ,都是用来存储数据的。

   

          函数最后就是把这个已经创建好的shape 返回了。

return cubeShape;

}

        

          函数写好了,得试下先,把前一章用来创建一个球体的语句替换掉,替换成

     var shape = createSphere(material,0.5);

   

         然后就会发现原来那个很丑的圆就变成一个很丑的正方形了。为了体现出这是一个立方体,而不是一个正方形,我们还要做透视投影变换,这个得用shading language 了,

<textarea id="effect">

  // World View Projection matrix that will transform the input vertices

  // to screen space.

  float4x4 worldViewProjection : WorldViewProjection;

 

  // input parameters for our vertex shader

  struct VertexShaderInput {

    float4 position : POSITION;

  };

 

  // input parameters for our pixel shader

  struct PixelShaderInput {

    float4 position : POSITION;

  };

 

  /**

   * The vertex shader simply transforms the input vertices to screen space.

   */

  PixelShaderInput vertexShaderFunction(VertexShaderInput input) {

    PixelShaderInput output;

 

    // Multiply the vertex positions by the worldViewProjection matrix to

    // transform them to screen space.

    output.position = mul(input.position, worldViewProjection);

    return output;

  }

 

  /**

   * This pixel shader just returns the color red.

   */

  float4 pixelShaderFunction(PixelShaderInput input): COLOR {

    return float4(1, 0, 0, 1);  // Red.

  }

 

  // Here we tell our effect file *which* functions are

  // our vertex and pixel shaders.

 

  // #o3d VertexShaderEntryPoint vertexShaderFunction

  // #o3d PixelShaderEntryPoint pixelShaderFunction

  // #o3d MatrixLoadOrder RowMajor

</textarea>

 

    这段就是传说中的shading language 了,它放在一个textarea 中方便js 程序获取,可以看到里面有两个函数,vertexShaderFunction 和pixelShaderFunction ,分别为vertex shader 和pixelShader 的入口,这个怎么工作,通俗点讲vertexShaderFunction 就是把每个顶点的数据传进去后经过一定的计算后再返回进行绘制。pixelShaderFunction 也是这样,在这里vertexShaderFunction 就一句代码:output.position = mul(input.position, worldViewProjection); 就是进行了一次投影变换. 而pixelShaderFunction 就只是把这个立方体的颜色变成了红色。最后三句不要看成是注释啊,前两句申明了vertex shader 的入口函数是vertexShaderFunction , pixelShader 的入口函数是pixelShaderFunction 。

   

          接下来就要用这段代码作为这个立方体的shader string, 还记不记得在前面创建过一个effect ,在后面加上下面两句代码

    var shaderString = document.getElementById('effect').value;

  effect.loadFromFXString(shaderString);               // 载入shading language

   

            然后就会发现这个图形已经有立方体的样子了(我们已经能够脑淫出这是个立方体了- - )。

   

            换个视角看会更像,还记得上一章设置摄像机参数吧。

viewInfo.drawContext.view = g_math.matrix4.lookAt([2, 2, 2],  // 将摄像机放置在(2 ,2 ,2 )位置上

                                                      [0, 0, 0],  // 目标仍是立方体

                                                      [0, 1, 0]); // up

   

 

import numpy as np import open3d as o3d import matplotlib.pyplot as plt import os from matplotlib.gridspec import GridSpec # 设置中文字体为宋体 plt.rcParams["font.family"] = ["SimSun"] plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题 class SimplifiedPointCloudProcessor: def __init__(self, random_seed=42, output_dir="output_models"): """初始化点云处理器,使用固定参数计算法向量""" self.random_seed = random_seed self.output_dir = output_dir # 输出目录 np.random.seed(random_seed) # 创建输出目录(如果不存在) os.makedirs(self.output_dir, exist_ok=True) # 初始化数据结构 self.data = None self.points = None self.labels = None self.unique_labels = {} # 可视化几何体存储 self.geometries = [] self.geometries_pcd = [] # 处理参数 self.cluster_index = 0 self.min_points_threshold = 20 self.small_cluster_threshold = 10 self.outlier_std_ratio = 1.2 # 法线计算参数 - 使用固定KNN参数 self.knn_param = 30 # 固定KNN参数 self.consistent_knn = 12 # 法线一致性检查的邻点数 # 可视化参数 self.visualization_pause_time = 0 # 可视化暂停时间,0表示无限等待 self.coord_offset_ratio = 0.3 # 坐标系偏移比例 self.coord_position_choice = 0 # 0:左下, 1:右下, 2:左上, 3:右上 # 三视图参数 self.viewpoint_size = 100 # 三视图点大小 self.save_views = True # 是否保存三视图 def load_data(self, file_path): """加载点云数据""" try: self.data = np.genfromtxt( file_path, delimiter=',', skip_header=1 ) self.points = self.data[:, :3] self.labels = self.data[:, 3].astype(int) for label in np.unique(self.labels): self.unique_labels[label] = self.points[self.labels == label] # 计算点云整体边界框,用于放置坐标系 self._calculate_overall_bounding_box() print(f"成功加载数据,共 {len(self.points)} 个点,{len(self.unique_labels)} 个类簇") return True except Exception as e: print(f"数据加载失败: {str(e)}") return False def _calculate_overall_bounding_box(self): """计算整个点云的边界框,用于确定坐标系位置""" if self.points is not None and len(self.points) > 0: self.overall_min = np.min(self.points, axis=0) self.overall_max = np.max(self.points, axis=0) self.overall_center = np.mean(self.points, axis=0) self.overall_size = self.overall_max - self.overall_min else: self.overall_min = np.array([0, 0, 0]) self.overall_max = np.array([100, 100, 100]) self.overall_center = np.array([50, 50, 50]) self.overall_size = np.array([100, 100, 100]) def _filter_outliers(self, pcd): """过滤离群点""" cl, ind = pcd.remove_statistical_outlier( nb_neighbors=15, std_ratio=self.outlier_std_ratio ) return cl, ind def _orient_normals_externally(self, pcd): """将法线方向统一向外""" points = np.asarray(pcd.points) normals = np.asarray(pcd.normals) # 使用包围盒中心方法 bbox = pcd.get_axis_aligned_bounding_box() center = bbox.get_center() to_center = points - center dot_products = np.sum(normals * to_center, axis=1) flip_mask = dot_products > 0 # 指向中心的需要翻 normals[flip_mask] *= -1 pcd.normals = o3d.utility.Vector3dVector(normals) return pcd def _create_colored_coordinate_frame(self, size=1.0): """创建自定义颜色的坐标系:x轴蓝色,y轴绿色,z轴红色""" frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=size) # 设置坐标轴颜色 frame.vertex_colors[0] = [0, 0, 1] # x轴顶点1 - 蓝色 frame.vertex_colors[1] = [0, 0, 1] # x轴顶点2 - 蓝色 frame.vertex_colors[2] = [0, 1, 0] # y轴顶点1 - 绿色 frame.vertex_colors[3] = [0, 1, 0] # y轴顶点2 - 绿色 frame.vertex_colors[4] = [1, 0, 0] # z轴顶点1 - 红色 frame.vertex_colors[5] = [1, 0, 0] # z轴顶点2 - 红色 # 根据选择计算不同的坐标系位置 offset = self.overall_size * self.coord_offset_ratio if self.coord_position_choice == 0: # 左下 coord_position = self.overall_min - offset elif self.coord_position_choice == 1: # 右下 coord_position = np.array([self.overall_max[0] + offset[0], self.overall_min[1] - offset[1], self.overall_min[2] - offset[2]]) elif self.coord_position_choice == 2: # 左上 coord_position = np.array([self.overall_min[0] - offset[0], self.overall_max[1] + offset[1], self.overall_min[2] - offset[2]]) else: # 右上 coord_position = self.overall_max + offset # 平移坐标系到计算位置 frame.translate(coord_position) return frame def _save_3d_model(self, mesh, cluster_index, label): """保存三维模型为多种格式""" if not mesh: return # 构建文件名(包含类簇信息) base_filename = f"Ore_cluster_{label}_sub_{cluster_index}" # 保存为PLY格式(包含颜色信息) ply_path = os.path.join(self.output_dir, f"{base_filename}.ply") o3d.io.write_triangle_mesh(ply_path, mesh) print(f"已保存PLY模型: {ply_path}") # 保存为STL格式(广泛用于3D打印) stl_path = os.path.join(self.output_dir, f"{base_filename}.stl") o3d.io.write_triangle_mesh(stl_path, mesh) print(f"已保存STL模型: {stl_path}") # 保存为OBJ格式(在许多3D软件中兼容) obj_path = os.path.join(self.output_dir, f"{base_filename}.obj") o3d.io.write_triangle_mesh(obj_path, mesh) print(f"已保存OBJ模型: {obj_path}") def _process_sub_cluster(self, sub_cluster, cluster_index, label): """处理单个子簇""" # 添加微小噪声打破共面性 noise = np.random.normal(0, 1e-6, sub_cluster.shape) sub_cluster += noise # 创建点云对象 pcd = o3d.geometry.PointCloud() pcd.points = o3d.utility.Vector3dVector(sub_cluster) # 过滤点数太少的子簇 if len(pcd.points) < self.small_cluster_threshold: print(f"子簇点数不足 {self.small_cluster_threshold},跳过处理") return None, None # 不过滤离群点 filtered_pcd = pcd # 过滤离群点 # filtered_pcd, inliers = self._filter_outliers(pcd) # if len(filtered_pcd.points) < 0.5 * len(pcd.points): # print("过滤后点太少,使用原始点云") # filtered_pcd = pcd # else: # print(f"过滤离群点: {len(pcd.points)} -> {len(filtered_pcd.points)}") print(f"使用KNN参数计算法向量: {self.knn_param}个最近邻") # 计算法向量 - 使用固定KNN参数 filtered_pcd.estimate_normals( o3d.geometry.KDTreeSearchParamKNN(knn=self.knn_param), fast_normal_computation=False # 禁用快速计算,提高精度 ) # 法线方向一致化 filtered_pcd.orient_normals_consistent_tangent_plane(k=self.consistent_knn) # 设置颜色 color = plt.get_cmap("Accent")(cluster_index % 8)[:3] filtered_pcd.paint_uniform_color(color) # 泊松曲面重建 try: with o3d.utility.VerbosityContextManager( o3d.utility.VerbosityLevel.Debug ) as cm: mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson( filtered_pcd, depth=8, width=5, scale=1.1, linear_fit=True ) # 后处理网格 mesh.compute_vertex_normals() mesh.paint_uniform_color(color) # 保存三维模型 self._save_3d_model(mesh, cluster_index, label) return filtered_pcd, mesh except Exception as e: print(f"泊松重建失败: {str(e)}") return filtered_pcd, None def process_clusters(self): """处理所有类簇""" if not self.unique_labels: print("没有可处理的类簇数据") return # 遍历每个类簇 for label, cluster_points in self.unique_labels.items(): if label == -1: # 跳过噪声点 continue if len(cluster_points) < self.min_points_threshold: print(f"类簇 {label} 点数不足 {self.min_points_threshold},跳过处理") continue print(f"处理类簇 {label},包含 {len(cluster_points)} 个点") current_clusters = self._get_sub_clusters(label, cluster_points) for sub_cluster in current_clusters: # 传递label参数用于保存文件 pcd, mesh = self._process_sub_cluster(sub_cluster, self.cluster_index, label) if pcd: self.geometries_pcd.append(pcd) if mesh: self.geometries.append(mesh) self.cluster_index += 1 print(f"共处理 {self.cluster_index} 个有效类簇") def _get_sub_clusters(self, label, cluster_points): """获取子簇,直接返回原始类簇""" return [cluster_points] def visualize_orthographic_views(self): """生成并显示三视图(正视图、侧视图、俯视图)""" if self.points is None or len(self.points) == 0: print("没有点云数据可生成三视图") return # 创建图形和子图 fig = plt.figure(figsize=(15, 10)) gs = GridSpec(2, 2, figure=fig) # 正视图 (X-Y平面) ax1 = fig.add_subplot(gs[0, 0]) # 侧视图 (Y-Z平面) ax2 = fig.add_subplot(gs[0, 1]) # 俯视图 (X-Z平面) ax3 = fig.add_subplot(gs[1, 0]) # 设置标题 ax1.set_title('正视图 (X-Y平面)') ax2.set_title('侧视图 (Y-Z平面)') ax3.set_title('俯视图 (X-Z平面)') # 设置坐标轴标签 ax1.set_xlabel('X') ax1.set_ylabel('Y') ax2.set_xlabel('Y') ax2.set_ylabel('Z') ax3.set_xlabel('X') ax3.set_ylabel('Z') # 确保各视图比例一致 ax1.set_aspect('equal') ax2.set_aspect('equal') ax3.set_aspect('equal') # 获取颜色映射 cmap = plt.get_cmap("Accent") # 按类别绘制点 for i, (label, points) in enumerate(self.unique_labels.items()): if label == -1: # 跳过噪声点 continue color = cmap(i % 8)[:3] # 正视图 (X-Y) ax1.scatter(points[:, 0], points[:, 1], c=[color], s=self.viewpoint_size, alpha=0.6, label=f'类别 {label}') # 侧视图 (Y-Z) ax2.scatter(points[:, 1], points[:, 2], c=[color], s=self.viewpoint_size, alpha=0.6) # 俯视图 (X-Z) ax3.scatter(points[:, 0], points[:, 2], c=[color], s=self.viewpoint_size, alpha=0.6) # 添加图例 ax1.legend(bbox_to_anchor=(1.05, 1), loc='upper left') # 调整布局 plt.tight_layout() # 保存三视图(如果启用) if self.save_views: views_path = os.path.join(self.output_dir, "point_cloud_orthographic_views.png") plt.savefig(views_path, dpi=300, bbox_inches='tight') print(f"已保存三视图: {views_path}") # 显示三视图 plt.show() def visualize_results(self): """可视化处理结果,包括3D视图和三视图""" # 先显示三视图 self.visualize_orthographic_views() if not self.geometries_pcd and not self.geometries: print("没有可可视化的3D结果") return # 创建自定义颜色的坐标系 coord_frame_size = np.max(self.overall_size) * 0.1 colored_coord_frame = self._create_colored_coordinate_frame(size=coord_frame_size) # 可视化点云(显示法线) if self.geometries_pcd: print("显示点云与法线方向 (按Q键关闭窗口)") vis = o3d.visualization.Visualizer() vis.create_window(window_name="点云与法线方向", width=1024, height=768) # 添加自定义坐标系 vis.add_geometry(colored_coord_frame) # 添加点云 for pcd in self.geometries_pcd: vis.add_geometry(pcd) # 设置渲染参数 opt = vis.get_render_option() opt.point_size = 3 opt.line_width = 1 opt.show_coordinate_frame = False # 运行可视化 vis.run() vis.destroy_window() # 可视化点云和重建网格 if self.geometries: print("显示点云和泊松重建网格 (按Q键关闭窗口)") vis = o3d.visualization.Visualizer() vis.create_window(window_name="点云和泊松重建网格", width=1024, height=768) # 添加自定义坐标系 vis.add_geometry(colored_coord_frame) # 添加几何体 for geom in self.geometries + self.geometries_pcd: vis.add_geometry(geom) opt = vis.get_render_option() opt.mesh_show_back_face = True opt.point_size = 2 opt.show_coordinate_frame = False vis.run() vis.destroy_window() if __name__ == "__main__": # 创建处理器实例,指定输出目录 processor = SimplifiedPointCloudProcessor(output_dir="3d_models_output") # 加载数据(请替换为实际文件路径) file_path = 'midpoints_with_cluster_index_SandStone_filtered_Test_20_2404080.csv' if processor.load_data(file_path): # 处理类簇(处理过程中会自动保存模型) processor.process_clusters() # 可视化结果(现在包括三视图) processor.visualize_results() else: print("无法继续处理,数据加载失败") 确保保存的体模型为闭合模型
09-06
(1)普通用户端(全平台) 音乐播放核心体验: 个性化首页:基于 “听歌历史 + 收藏偏好” 展示 “推荐歌单(每日 30 首)、新歌速递、相似曲风推荐”,支持按 “场景(通勤 / 学习 / 运动)” 切换推荐维度。 播放页功能:支持 “无损音质切换、倍速播放(0.5x-2.0x)、定时关闭、歌词逐句滚动”,提供 “沉浸式全屏模式”(隐藏冗余控件,突出歌词与专辑封面)。 多端同步:自动同步 “播放进度、收藏列表、歌单” 至所有登录设备(如手机暂停后,电脑端打开可继续播放)。 音乐发现与管理: 智能搜索:支持 “歌曲名 / 歌手 / 歌词片段” 搜索,提供 “模糊匹配(如输入‘晴天’联想‘周杰伦 - 晴天’)、热门搜索词推荐”,结果按 “热度 / 匹配度” 排序。 歌单管理:创建 “公开 / 私有 / 加密” 歌单,支持 “批量添加歌曲、拖拽排序、一键分享到社交平台”,系统自动生成 “歌单封面(基于歌曲风格配色)”。 音乐分类浏览:按 “曲风(流行 / 摇滚 / 古典)、语言(国语 / 英语 / 日语)、年代(80 后经典 / 2023 新歌)” 分层浏览,每个分类页展示 “TOP50 榜单”。 社交互动功能: 动态广场:查看 “关注的用户 / 音乐人发布的动态(如‘分享新歌感受’)、好友正在听的歌曲”,支持 “点赞 / 评论 / 发”,可直接点击动态中的歌曲播放。 听歌排行:个人页展示 “本周听歌 TOP10、累计听歌时长”,平台定期生成 “全球 / 好友榜”(如 “好友中你本周听歌时长排名第 3”)。 音乐圈:加入 “特定曲风圈子(如‘古典音乐爱好者’)”,参与 “话题讨论(如‘你心中最经典的钢琴曲’)、线上歌单共创”。 (2)音乐人端(创作者中心) 作品管理: 音乐上传:支持 “无损音频(FLAC/WAV)+ 歌词文件(LRC)+ 专辑封面” 上传,填写 “歌曲信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值