PyGame 能做 3d 吗?
- 2025-03-04 08:24:00
- admin 原创
- 88
问题描述:
我似乎在任何地方都找不到这个问题的答案。我知道你必须使用 PyOpenGL 或类似的东西来做 OpenGL 的事情,但我想知道是否有可能在不依赖任何其他依赖项的情况下完成非常基本的 3D 图形。
解决方案 1:
不,Pygame 是 SDL 的包装器,SDL 是一个 2D API。Pygame 不提供任何 3D 功能,而且可能永远不会提供。
Python 的 3D 库包括Panda3D和DirectPython,尽管它们使用起来可能相当复杂,尤其是后者。
解决方案 2:
好吧,如果你能做 2d,你就能做 3d。3d 实际上是倾斜的二维表面,给人一种你在看有深度的东西的感觉。真正的问题是它能不能做好,你甚至想这样做吗。浏览 pyGame 文档一段时间后,它看起来只是一个 SDL 包装器。SDL 不适用于 3d 编程,所以真正问题的答案是,不,我甚至不会尝试。
解决方案 3:
在没有其他依赖项帮助的情况下,Pygame 中的 3D 渲染很难实现,并且性能不佳。Pygame 不提供任何绘制 3D 形状、网格甚至透视和照明的功能。
如果您想使用 Pygame 绘制 3D 场景,则需要使用矢量算法计算顶点并使用多边形将几何图形拼接在一起。Pygame绕轴旋转立方体
的答案示例:
这种方式性能并不理想,仅具有学习价值。3D场景是借助GPU生成的。单纯使用CPU的方式无法达到所需的性能。
尽管如此,使用 2.5-D 方法可以取得不错的效果。请参阅如何修复射线投射器中的墙体扭曲?的答案:
import pygame
import math
pygame.init()
tile_size, map_size = 50, 8
board = [
'########',
'# # #',
'# # ##',
'# ## #',
'# #',
'### ###',
'# #',
'########']
def cast_rays(sx, sy, angle):
rx = math.cos(angle)
ry = math.sin(angle)
map_x = sx // tile_size
map_y = sy // tile_size
t_max_x = sx/tile_size - map_x
if rx > 0:
t_max_x = 1 - t_max_x
t_max_y = sy/tile_size - map_y
if ry > 0:
t_max_y = 1 - t_max_y
while True:
if ry == 0 or t_max_x < t_max_y * abs(rx / ry):
side = 'x'
map_x += 1 if rx > 0 else -1
t_max_x += 1
if map_x < 0 or map_x >= map_size:
break
else:
side = 'y'
map_y += 1 if ry > 0 else -1
t_max_y += 1
if map_x < 0 or map_y >= map_size:
break
if board[int(map_y)][int(map_x)] == "#":
break
if side == 'x':
x = (map_x + (1 if rx < 0 else 0)) * tile_size
y = player_y + (x - player_x) * ry / rx
direction = 'r' if x >= player_x else 'l'
else:
y = (map_y + (1 if ry < 0 else 0)) * tile_size
x = player_x + (y - player_y) * rx / ry
direction = 'd' if y >= player_y else 'u'
return (x, y), math.hypot(x - sx, y - sy), direction
def cast_fov(sx, sy, angle, fov, no_ofrays):
max_d = math.tan(math.radians(fov/2))
step = max_d * 2 / no_ofrays
rays = []
for i in range(no_ofrays):
d = -max_d + (i + 0.5) * step
ray_angle = math.atan2(d, 1)
pos, dist, direction = cast_rays(sx, sy, angle + ray_angle)
rays.append((pos, dist, dist * math.cos(ray_angle), direction))
return rays
area_width = tile_size * map_size
window = pygame.display.set_mode((area_width*2, area_width))
clock = pygame.time.Clock()
board_surf = pygame.Surface((area_width, area_width))
for row in range(8):
for col in range(8):
color = (192, 192, 192) if board[row][col] == '#' else (96, 96, 96)
pygame.draw.rect(board_surf, color, (col * tile_size, row * tile_size, tile_size - 2, tile_size - 2))
player_x, player_y = round(tile_size * 4.5) + 0.5, round(tile_size * 4.5) + 0.5
player_angle = 0
max_speed = 3
colors = {'r' : (196, 128, 64), 'l' : (128, 128, 64), 'd' : (128, 196, 64), 'u' : (64, 196, 64)}
run = True
while run:
clock.tick(30)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
keys = pygame.key.get_pressed()
hit_pos_front, dist_front, side_front = cast_rays(player_x, player_y, player_angle)
hit_pos_back, dist_back, side_back = cast_rays(player_x, player_y, player_angle + math.pi)
player_angle += (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * 0.1
speed = ((0 if dist_front <= max_speed else keys[pygame.K_UP]) - (0 if dist_back <= max_speed else keys[pygame.K_DOWN])) * max_speed
player_x += math.cos(player_angle) * speed
player_y += math.sin(player_angle) * speed
rays = cast_fov(player_x, player_y, player_angle, 60, 40)
window.blit(board_surf, (0, 0))
for ray in rays:
pygame.draw.line(window, (0, 255, 0), (player_x, player_y), ray[0])
pygame.draw.line(window, (255, 0, 0), (player_x, player_y), hit_pos_front)
pygame.draw.circle(window, (255, 0, 0), (player_x, player_y), 8)
pygame.draw.rect(window, (128, 128, 255), (400, 0, 400, 200))
pygame.draw.rect(window, (128, 128, 128), (400, 200, 400, 200))
for i, ray in enumerate(rays):
height = round(10000 / ray[2])
width = area_width // len(rays)
color = pygame.Color((0, 0, 0)).lerp(colors[ray[3]], min(height/256, 1))
rect = pygame.Rect(area_width + i*width, area_width//2-height//2, width, height)
pygame.draw.rect(window, color, rect)
pygame.display.flip()
pygame.quit()
exit()
另请参阅PyGameExamplesAndAnswers - Raycasting
我知道你问的是“...但我想知道是否有可能在不依赖任何其他依赖项的情况下制作非常基本的 3D 图形。”。无论如何,我将为你提供一些具有其他依赖项的附加选项。
在 Python 中使 3D 场景更强大的一种方法是使用基于 OpenGL 的库,如 pyglet或ModernGL。
但是,您可以使用 Pygame 窗口创建OpenGL 上下文pygame.OPENGL
。您需要在创建显示表面时设置标志(请参阅pygame.display.set_mode
):
window = pg.display.set_mode((width, height), pygame.OPENGL | pygame.DOUBLEBUF)
现代 OpenGL PyGame/PyOpenGL 示例:
import pygame
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GL.shaders import *
import ctypes
import glm
glsl_vert = """
#version 330 core
layout (location = 0) in vec3 a_pos;
layout (location = 1) in vec4 a_col;
out vec4 v_color;
uniform mat4 u_proj;
uniform mat4 u_view;
uniform mat4 u_model;
void main()
{
v_color = a_col;
gl_Position = u_proj * u_view * u_model * vec4(a_pos.xyz, 1.0);
}
"""
glsl_frag = """
#version 330 core
out vec4 frag_color;
in vec4 v_color;
void main()
{
frag_color = v_color;
}
"""
class Cube:
def __init__(self):
v = [(-1,-1,-1), ( 1,-1,-1), ( 1, 1,-1), (-1, 1,-1), (-1,-1, 1), ( 1,-1, 1), ( 1, 1, 1), (-1, 1, 1)]
edges = [(0,1), (1,2), (2,3), (3,0), (4,5), (5,6), (6,7), (7,4), (0,4), (1,5), (2,6), (3,7)]
surfaces = [(0,1,2,3), (5,4,7,6), (4,0,3,7),(1,5,6,2), (4,5,1,0), (3,2,6,7)]
colors = [(1,0,0), (0,1,0), (0,0,1), (1,1,0), (1,0,1), (1,0.5,0)]
line_color = [0, 0, 0]
edge_attributes = []
for e in edges:
edge_attributes += v[e[0]]
edge_attributes += line_color
edge_attributes += v[e[1]]
edge_attributes += line_color
face_attributes = []
for i, quad in enumerate(surfaces):
for iv in quad:
face_attributes += v[iv]
face_attributes += colors[i]
self.edge_vbo = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, self.edge_vbo)
glBufferData(GL_ARRAY_BUFFER, (GLfloat * len(edge_attributes))(*edge_attributes), GL_STATIC_DRAW)
self.edge_vao = glGenVertexArrays(1)
glBindVertexArray(self.edge_vao)
glVertexAttribPointer(0, 3, GL_FLOAT, False, 6*ctypes.sizeof(GLfloat), ctypes.c_void_p(0))
glEnableVertexAttribArray(0)
glVertexAttribPointer(1, 3, GL_FLOAT, False, 6*ctypes.sizeof(GLfloat), ctypes.c_void_p(3*ctypes.sizeof(GLfloat)))
glEnableVertexAttribArray(1)
self.face_vbos = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, self.face_vbos)
glBufferData(GL_ARRAY_BUFFER, (GLfloat * len(face_attributes))(*face_attributes), GL_STATIC_DRAW)
self.face_vao = glGenVertexArrays(1)
glBindVertexArray(self.face_vao)
glVertexAttribPointer(0, 3, GL_FLOAT, False, 6*ctypes.sizeof(GLfloat), ctypes.c_void_p(0))
glEnableVertexAttribArray(0)
glVertexAttribPointer(1, 3, GL_FLOAT, False, 6*ctypes.sizeof(GLfloat), ctypes.c_void_p(3*ctypes.sizeof(GLfloat)))
glEnableVertexAttribArray(1)
def draw(self):
glEnable(GL_DEPTH_TEST)
glLineWidth(5)
glBindVertexArray(self.edge_vao)
glDrawArrays(GL_LINES, 0, 12*2)
glBindVertexArray(0)
glEnable(GL_POLYGON_OFFSET_FILL)
glPolygonOffset( 1.0, 1.0 )
glBindVertexArray(self.face_vao)
glDrawArrays(GL_QUADS, 0, 6*4)
glBindVertexArray(0)
glDisable(GL_POLYGON_OFFSET_FILL)
def set_projection(w, h):
return glm.perspective(glm.radians(45), w / h, 0.1, 50.0)
pygame.init()
window = pygame.display.set_mode((400, 300), pygame.DOUBLEBUF | pygame.OPENGL | pygame.RESIZABLE)
clock = pygame.time.Clock()
proj = set_projection(*window.get_size())
view = glm.lookAt(glm.vec3(0, 0, 5), glm.vec3(0, 0, 0), glm.vec3(0, 1, 0))
model = glm.mat4(1)
cube = Cube()
angle_x, angle_y = 0, 0
program = compileProgram(
compileShader(glsl_vert, GL_VERTEX_SHADER),
compileShader(glsl_frag, GL_FRAGMENT_SHADER))
attrib = { a : glGetAttribLocation(program, a) for a in ['a_pos', 'a_col'] }
print(attrib)
uniform = { u : glGetUniformLocation(program, u) for u in ['u_model', 'u_view', 'u_proj'] }
print(uniform)
glUseProgram(program)
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
elif event.type == pygame.VIDEORESIZE:
glViewport(0, 0, event.w, event.h)
proj = set_projection(event.w, event.h)
model = glm.mat4(1)
model = glm.rotate(model, glm.radians(angle_y), glm.vec3(0, 1, 0))
model = glm.rotate(model, glm.radians(angle_x), glm.vec3(1, 0, 0))
glUniformMatrix4fv(uniform['u_proj'], 1, GL_FALSE, glm.value_ptr(proj))
glUniformMatrix4fv(uniform['u_view'], 1, GL_FALSE, glm.value_ptr(view))
glUniformMatrix4fv(uniform['u_model'], 1, GL_FALSE, glm.value_ptr(model))
angle_x += 1
angle_y += 0.4
glClearColor(0.5, 0.5, 0.5, 1)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
cube.draw()
pygame.display.flip()
pygame.quit()
exit()
传统 OpenGL PyGame/PyOpenGL 示例:
import pygame
from OpenGL.GL import *
from OpenGL.GLU import *
class Cube:
def __init__(self):
self.v = [(-1,-1,-1), ( 1,-1,-1), ( 1, 1,-1), (-1, 1,-1), (-1,-1, 1), ( 1,-1, 1), ( 1, 1, 1), (-1, 1, 1)]
self.edges = [(0,1), (1,2), (2,3), (3,0), (4,5), (5,6), (6,7), (7,4), (0,4), (1,5), (2,6), (3,7)]
self.surfaces = [(0,1,2,3), (5,4,7,6), (4,0,3,7),(1,5,6,2), (4,5,1,0), (3,2,6,7)]
self.colors = [(1,0,0), (0,1,0), (0,0,1), (1,1,0), (1,0,1), (1,0.5,0)]
def draw(self):
glEnable(GL_DEPTH_TEST)
glLineWidth(5)
glColor3fv((0, 0, 0))
glBegin(GL_LINES)
for e in self.edges:
glVertex3fv(self.v[e[0]])
glVertex3fv(self.v[e[1]])
glEnd()
glEnable(GL_POLYGON_OFFSET_FILL)
glPolygonOffset( 1.0, 1.0 )
glBegin(GL_QUADS)
for i, quad in enumerate(self.surfaces):
glColor3fv(self.colors[i])
for iv in quad:
glVertex3fv(self.v[iv])
glEnd()
glDisable(GL_POLYGON_OFFSET_FILL)
def set_projection(w, h):
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(45, w / h, 0.1, 50.0)
glMatrixMode(GL_MODELVIEW)
def screenshot(display_surface, filename):
size = display_surface.get_size()
buffer = glReadPixels(0, 0, *size, GL_RGBA, GL_UNSIGNED_BYTE)
screen_surf = pygame.image.fromstring(buffer, size, "RGBA")
pygame.image.save(screen_surf, filename)
pygame.init()
window = pygame.display.set_mode((400, 300), pygame.DOUBLEBUF | pygame.OPENGL | pygame.RESIZABLE)
clock = pygame.time.Clock()
set_projection(*window.get_size())
cube = Cube()
angle_x, angle_y = 0, 0
run = True
while run:
clock.tick(60)
take_screenshot = False
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
elif event.type == pygame.VIDEORESIZE:
glViewport(0, 0, event.w, event.h)
set_projection(event.w, event.h)
elif event.type == pygame.KEYDOWN:
take_screenshot = True
glLoadIdentity()
glTranslatef(0, 0, -5)
glRotatef(angle_y, 0, 1, 0)
glRotatef(angle_x, 1, 0, 0)
angle_x += 1
angle_y += 0.4
glClearColor(0.5, 0.5, 0.5, 1)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
cube.draw()
if take_screenshot:
screenshot(window, "cube.png")
pygame.display.flip()
pygame.quit()
exit()
解决方案 4:
你可以只用 pygame 来做伪 3d 游戏(比如“毁灭战士”):
http://code.google.com/p/gh0stenstein/
如果你浏览 pygame.org 网站,你可能会发现更多用 python 和 pygame 完成的“3d”游戏。
然而,如果您真的想进入 3d 编程,您应该研究 OpenGl、Blender 或任何其他真正的 3d 库。
解决方案 5:
Pygame 最初并不是用来做 3D 的,但有一种方法可以用任何 2D 图形库来做 3D。您所需要的只是以下函数,它将 3D 点转换为 2D 点,这样您只需在屏幕上画线就可以制作任何 3D 形状。
def convert_to_2d(point=[0,0,0]):
return [point[0]*(point[2]*.3),point[1]*(point[2]*.3)]
这称为伪 3d,或 2.5d。这可以实现,但速度可能很慢,而且极其困难,因此建议您使用专为 3d 设计的库。
解决方案 6:
它不支持,但结合 PyOpenGL 你可以利用两者的强大功能,这是一个完整的示例
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
import random
vertices = ((1, -1, -1),(1, 1, -1),(-1, 1, -1),(-1, -1, -1),(1, -1, 1),(1, 1, 1),(-1, -1, 1),(-1, 1, 1))
edges = ((0,1),(0,3),(0,4),(2,1),(2,3),(2,7),(6,3),(6,4),(6,7),(5,1),(5,4),(5,7))
surfaces = ((0,1,2,3),(3,2,7,6),(6,7,5,4),(4,5,1,0),(1,5,7,2),(4,0,3,6))
colors = ((1,0,0),(0,1,0),(0,0,1),(0,1,0),(1,1,1),(0,1,1),(1,0,0),(0,1,0),(0,0,1),(1,0,0),(1,1,1),(0,1,1),)
def set_vertices(max_distance, min_distance = -20):
x_value_change = random.randrange(-10,10)
y_value_change = random.randrange(-10,10)
z_value_change = random.randrange(-1*max_distance,min_distance)
new_vertices = []
for vert in vertices:
new_vert = []
new_x = vert[0] + x_value_change
new_y = vert[1] + y_value_change
new_z = vert[2] + z_value_change
new_vert.append(new_x)
new_vert.append(new_y)
new_vert.append(new_z)
new_vertices.append(new_vert)
return new_vertices
def Cube(vertices):
glBegin(GL_QUADS)
for surface in surfaces:
x = 0
for vertex in surface:
x+=1
glColor3fv(colors[x])
glVertex3fv(vertices[vertex])
glEnd()
glBegin(GL_LINES)
for edge in edges:
for vertex in edge:
glVertex3fv(vertices[vertex])
glEnd()
def main():
pygame.init()
display = (800,600)
pygame.display.set_mode(display, DOUBLEBUF|OPENGL)
max_distance = 100
gluPerspective(45, (display[0]/display[1]), 0.1, max_distance)
glTranslatef(random.randrange(-5,5),random.randrange(-5,5), -40)
#object_passed = False
x_move = 0
y_move = 0
cube_dict = {}
for x in range(50):
cube_dict[x] =set_vertices(max_distance)
#glRotatef(25, 2, 1, 0)
x = glGetDoublev(GL_MODELVIEW_MATRIX)
camera_x = x[3][0]
camera_y = x[3][1]
camera_z = x[3][2]
button_down = False
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
if event.type == pygame.MOUSEMOTION:
if button_down == True:
print(pygame.mouse.get_pressed())
glRotatef(event.rel[1], 1, 0, 0)
glRotatef(event.rel[0], 0, 1, 0)
for event in pygame.mouse.get_pressed():
# print(pygame.mouse.get_pressed())
if pygame.mouse.get_pressed()[0] == 1:
button_down = True
elif pygame.mouse.get_pressed()[0] == 0:
button_down = False
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
for each_cube in cube_dict:
Cube(cube_dict[each_cube])
pygame.display.flip()
pygame.time.wait(10)
main()
pygame.quit()
quit()
解决方案 7:
你看到的 3D 实际上是 2D 游戏。毕竟,你正在观看的是你的屏幕,而屏幕(通常 ;) 是 2D 的。虚拟世界(3D 的)被投射到平面上,然后显示在你的屏幕上。然后我们的大脑将 2D 图像转换为 3D 图像(就像它们对我们眼睛的图像所做的那样),使其看起来像 3D 的。
因此,制作 3D 游戏相对容易:只需使用多维矩阵创建一个虚拟世界,然后将其每个循环投影到 2D 平面上,然后将其显示在屏幕上。
这是一个可以引导您使用 3D 程序(使用 pygame)的教程。
解决方案 8:
你可以这样做:
def convert_2d(x, y, z, horizon):
d = 1 - (z/horizon)
return x*d, y*d
def draw_list_of_points(lst):
'''Assume that lst is a list of 3 dimentionnal points like [(0, 0, 0), (1, 6, 2)...
Let's take 200 for the horizon, it can give us a pretty clean 3D'''
for x, y, z in lst:
pygame.draw.circle(screen, color, convert_2d(x, y, z, 200), 1)
但是速度不是很快。如果你想要更快,可以尝试用 C++/SDL2 或 C 来实现。Pygame 不太适合 3D 图形。
解决方案 9:
为 PyGame 制作 3D 驱动程序很容易。PyGame 有一些用于 3D 游戏开发的资产。我现在正在使用 PyGame 开发 Py3D 驱动程序。完成后,我会向您显示下载 Py3D 的链接。我尝试使用 PyGame 制作 3D 游戏,但我只需要 PyGame 的小插件。您认为必须使用 SDL、PyOpenGL、OpenGL、PyQt5、Tkinter 的想法是错误的。它们都不适合制作 3D 游戏。OpenGL 和 PyOpenGL 或 Panda3D 非常难学。我用这些驱动程序制作的所有游戏都很糟糕。PyQt5 和 Tkinter 不是用于制作游戏的驱动程序,但它们有插件。不要尝试在这些驱动程序上制作任何游戏。所有需要使用数学模块的驱动程序都很难。您可以轻松地为它们制作小插件,我认为每个人都可以在 1-2 周内为 PyGame 制作驱动程序。
解决方案 10:
如果您想在制作游戏时坚持使用类似 Python 的语言,Godot 是一个不错的选择,它同时支持 2D 和 3D,拥有庞大的社区和大量教程。它的自定义脚本语言 (gdscript) 有一些细微的差别,但总体而言基本相同。它还支持 c# 和 c++,在游戏开发方面具有更多功能。
解决方案 11:
Pygame 只是一个用于更改像素颜色的库(以及一些用于编程游戏的其他有用的东西)。您可以通过将图像传输到屏幕上或直接设置像素颜色来实现这一点。
因此,使用 pygame 编写 2D 游戏很容易,因为上述内容就是您真正需要的。但 3D 游戏只是一些 3D 对象“压缩”(渲染)成 2D,以便可以显示在屏幕上。因此,要仅使用 pygame 制作 3D 游戏,您必须自己处理此渲染,包括所有必要的复杂矩阵数学。
由于涉及巨大的处理能力,这不仅会运行缓慢,而且还需要您编写一个庞大的 3D 渲染/光栅化引擎。而且由于需要解释 Python,它会更慢。正确的方法是使用 (Py)opengl 在 GPU 上运行此过程。
所以,从技术上讲,仅使用 pygame 实现 3D 是可行的,但绝对不推荐。我建议你学习 Panda3D 或类似的 3D 引擎。
解决方案 12:
很简单:只需绘制一堆多边形,如下所示:
import pygame
screen = pygame.display.set_mode((100, 100))
While True:
screen.fill((0, 0, 0))
Pos = [(10, 10), (20, 10), (20, 20), (10, 20)]
# first side (the front) in red
pygame.draw.polygon(screen, (225, 0, 0), Pos)
# outline in white
pygame.draw.lines(screen, (225, 225, 225), Pos)
# Second side (the back) in blue
Pos2 = [(Pos[0[0]] + 2.5, Pos[0[1]] + 2.5), (Pos2[0[0]] + 5, Pos2[0[1]]), (Pos2[1[0]], Pos2[1[1]] + 5), (Pos2[0[0]], Pos2[0[1]] + 5)]
pygame.draw.polygon(screen, (0, 0, 225), Pos2)
pygame.draw.lines(screen, (225, 225, 225), Pos2)
# Third side (the left but just 2 lines(not really)) in green
Pos3 = [Pos[0], Pos2[0], Pos2[3], Pos[3]]
pygame.draw.polygon(screen, (0, 225, 0), Pos3)
pygame.draw.lines(screen, (225, 225, 225), Pos3)
# Fourth side (the right) in purple
Pos4 = [Pos[1], Pos2[1], Pos2[2], Pos[2]]
pygame.draw.polygon(screen, (225, 0, 225), Pos4)
pygame.draw.lines(screen, (225, 225, 225), Pos4)
pygame.display.flip()
&有一个简单的立方体&我很快会提供完整代码的链接,以便能够旋转立方体并调整其大小
这应该能给你与使用 OpenGL 获得的结果相当的结果
解决方案 13:
这是我仅使用 Pygame 和 Numpy 而没有使用 OpenGL 和一些基本的阴影所完成的。
您可以在 PyGame 中实现 3D,但可能不是最高效和最快的。
扫码咨询,免费领取项目管理大礼包!