openGL兔兔大作業-面的滑鼠拾取
好久沒寫過GL了,正好來發發教程
要求
給出兔兔的頂點座標與三角面索引,需要實現:
- 繪製模型,馮氏光照
- 模型及視角移動
- 滑鼠點選高亮某三角面
總體思路 & 坑點
For 可以自己實現OpenGL編寫的朋友
-
匯入模型 :給出的資料為頂點座標,沒有法線資訊,需要求解法線,在我的實現中對每一三角面根據三點座標求解了法線,沒有進行法線插值,這會使得兔兔表面不夠圓潤,並且需要 NumFace * 3 大小的VBO,較浪費空間。按道理可以對每一點求解法線,使用該點與鄰接點的向量進行加權平均,具體可以參考連結 Weighted Vertex Normals
-
光照模型:沒什麼特別的,Blinn-Phong或者Phong的Shader
-
模型及視角移動:也沒什麼特別的,取下幀間滑鼠Δ值和鍵盤按鍵變model view矩陣就行了
-
點選高亮:這個還蠻有趣的,想了個辦法,應該不是最優解,用一個drawcall繪製一張每個面顏色都是該面索引值 / 總面數的RT,然後readBack一下滑鼠位置的顏色,拿到高亮面的頂點,這裡我直接再加了一個drawcall畫這三角,應該是多餘了。
實現細節
For 不怎麼熟悉OpenGL的朋友
配置OpenGL環境
- 使用GLFW初始化視窗
GLFW is a lightweight utility library for use with OpenGL. GLFW stands for Graphics Library Framework. It provides programmers with the ability to create and manage windows and OpenGL contexts, as well as handle joystick, keyboard and mouse input.
一般使用GLFW作為跨平臺的視窗工具,在本例中就作為建立OpenGL繪製視窗,處理滑鼠鍵盤事件的API。
- 使用GLAD連結OpenGL API
可以簡單認為,雖然各個平臺都支援OpenGL繪製,但一般都僅僅是提供了按OpenGL標準所實現的二進位制連結庫,如WIndows下預設靜態連結庫會有opengl32.lib,但仍然需要一個第三方庫去在執行時載入這一dll,提供一個符合標準的c++標頭檔案,並且將二進位制庫中的實現載入到對應的函式API上。
void Application::Run()
{
//Initialize GLFW Window
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
window = glfwCreateWindow(800, 600, "Bunny", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
}
glfwMakeContextCurrent(window);
//Initialize GLAD
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
}
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
Init(); //Read Data From File & Initialize VAOs FBOs
while (!glfwWindowShouldClose(window))
{
Render(); //Main Render Function
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
}
處理輸入
- 將所有點與索引輸入,按面順序排序,每個點有7個float的長度:
- 位置:vec3
- 法線:vec3
- 面編號(歸一化):float
void Application::Init() {
//Read From File
std::ifstream fin("bunny_iH.ply2");
if (!fin) {
assert(false);
}
numVertices = 0;
fin >> numVertices;
fin >> numFaces;
vertices = new float[(size_t)numVertices * 3];
sortedVertices = new float[(size_t)numFaces * 3 * 7];
for (int i = 0; i < numVertices; i++) {
fin >> vertices[i * 3] >> vertices[i * 3 + 1] >> vertices[i * 3 + 2];
}
for (int i = 0; i < numFaces; i++) {
int a, b, c, d;
fin >> a >> b >> c >> d; // a always equals to 3
int v1 = i * 3;
int v2 = i * 3 + 1;
int v3 = i * 3 + 2;
// position : vec3 [v*7, v*7+2]
// normal : vec3 [v*7+3, v*7+5]
// faceIndex : float [v*7+6, v*7+6]
sortedVertices[v1 * 7] = vertices[b * 3];
sortedVertices[v1 * 7 + 1] = vertices[b * 3 + 1];
sortedVertices[v1 * 7 + 2] = vertices[b * 3 + 2];
sortedVertices[v2 * 7] = vertices[c * 3];
sortedVertices[v2 * 7 + 1] = vertices[c * 3 + 1];
sortedVertices[v2 * 7 + 2] = vertices[c * 3 + 2];
sortedVertices[v3 * 7] = vertices[d * 3];
sortedVertices[v3 * 7 + 1] = vertices[d * 3 + 1];
sortedVertices[v3 * 7 + 2] = vertices[d * 3 + 2];
float* normal = new float[3];
calcNormal(&sortedVertices[v1 * 7], &sortedVertices[v2 * 7], &sortedVertices[v3 * 7], normal);
sortedVertices[v1 * 7 + 3] = normal[0];
sortedVertices[v1 * 7 + 4] = normal[1];
sortedVertices[v1 * 7 + 5] = normal[2];
sortedVertices[v1 * 7 + 6] = (float)i / numFaces;
sortedVertices[v2 * 7 + 3] = normal[0];
sortedVertices[v2 * 7 + 4] = normal[1];
sortedVertices[v2 * 7 + 5] = normal[2];
sortedVertices[v2 * 7 + 6] = (float)i / numFaces;
sortedVertices[v3 * 7 + 3] = normal[0];
sortedVertices[v3 * 7 + 4] = normal[1];
sortedVertices[v3 * 7 + 5] = normal[2];
sortedVertices[v3 * 7 + 6] = (float)i / numFaces;
}
fin.close();
- 求面法線:求兩向量叉積,實現為化簡過後的過程。
void calcNormal(float* v1, float* v2, float* v3, float* nor) {
float na = (v2[1] - v1[1]) * (v3[2] - v1[2]) - (v2[2] - v1[2]) * (v3[1] - v1[1]);
float nb = (v2[2] - v1[2]) * (v3[0] - v1[0]) - (v2[0] - v1[0]) * (v3[2] - v1[2]);
float nc = (v2[0] - v1[0]) * (v3[1] - v1[1]) - (v2[1] - v1[1]) * (v3[0] - v1[0]);
nor[0] = na;
nor[1] = nb;
nor[2] = nc;
}
初始化Shader
-
本實現中使用了三個Shader,分別用於繪製面索引,光照模型,高亮三角
void Application::InitShader() { mShaders["BlinnPhong"] = new Shader("BlinnPhong"); mShaders["FaceIndex"] = new Shader("FaceIndex"); mShaders["postprocess"] = new Shader("postprocess"); }
-
從檔案讀入Shader程式碼(這裡有個bug,在檔案尾會讀入幾個亂碼字元,這裡在shader最後手動加換行暫時刪掉了亂碼)
Shader::Shader(const std::string& shaderName) { const std::string vertShaderName = shaderName + ".vert"; const std::string fragShaderName = shaderName + ".frag"; std::ifstream vertFile(vertShaderName); std::ifstream fragFile(fragShaderName); if (!vertFile || !fragFile) { assert(false); } vertFile.seekg(0, std::ios::end); int length = vertFile.tellg(); GLchar* vertexShaderCode = new GLchar[length]; vertFile.seekg(0, std::ios::beg); vertFile.read(vertexShaderCode, length); while (vertexShaderCode[length - 1] != '\n') { //TODO vertexShaderCode[length - 1] = 0; length--; } fragFile.seekg(0, std::ios::end); length = fragFile.tellg(); GLchar* fragmentShaderCode = new GLchar[length]; fragFile.seekg(0, std::ios::beg); fragFile.read(fragmentShaderCode, length); while (fragmentShaderCode[length - 1] != '\n') { //TODO fragmentShaderCode[length - 1] = 0; length--; }
-
編譯Shader程式碼
GLuint VertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(VertexShader, 1, &vertexShaderCode, NULL);
glCompileShader(VertexShader);
GLuint FragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(FragmentShader, 1, &fragmentShaderCode, NULL);
glCompileShader(FragmentShader);
mProgramID = glCreateProgram();
glAttachShader(mProgramID, VertexShader);
glAttachShader(mProgramID, FragmentShader);
glLinkProgram(mProgramID);
glUseProgram(mProgramID);
GLint success;
glGetProgramiv(mProgramID, GL_LINK_STATUS, &success);
if (!success) {
GLchar infoLog[1024];
glGetProgramInfoLog(mProgramID, 512, NULL, infoLog);
std::cout << infoLog << std::endl;
}
glDeleteShader(VertexShader);
glDeleteShader(FragmentShader);
- 傳入uniform 以及 繫結program
// Check location
#define CHECK_LOC(loc) \
if(loc == -1){ \
assert(false);\
}
void Shader::SetMat(const std::string& name, const glm::mat4& mat)
{
GLint loc = glGetUniformLocation(mProgramID, name.c_str());
CHECK_LOC(loc);
glUniformMatrix4fv(loc, 1, GL_FALSE, &mat[0][0]);
}
//setVec.......
//setFloat.....
//setInt.......
void Shader::Bind()
{
glUseProgram(mProgramID);
}
初始化頂點緩衝
- 主頂點緩衝,包含所有頂點資訊
InitShader();
glGenVertexArrays(1, &vao); //Vertex Array Buffer
glBindVertexArray(vao);
GLuint vbo;
glGenBuffers(1, &vbo); //Vertex Buffer Object
glBindBuffer(GL_ARRAY_BUFFER, vbo);
//Total size : Face Number * 3 * sizeof(Per Vertex)
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * numFaces * 3 * 7, sortedVertices, GL_STATIC_DRAW);
//Set Per Vertex Data Format
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)0); //position
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(3 * sizeof(float))); //normal
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(6 * sizeof(float))); //FaceIndex
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
- 高亮三角緩衝,僅三個點,使用Dynamic Draw宣告該資料會實時變化,令GPU對資料儲存進行優化
glGenVertexArrays(1, &CoveredVAO);
glBindVertexArray(CoveredVAO);
glGenBuffers(1, &CoveredVBO);
glBindBuffer(GL_ARRAY_BUFFER, CoveredVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 9, nullptr, GL_DYNAMIC_DRAW); // A hint for GPU to optimize VBO
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
初始化FrameBuffers
- 一個FrameBuffer表示一組Render Target(可以理解為繪製到的一張視窗大小的圖片)的集合,包含若干color attachment以及一個depth-stencil attachment,本例中為了繪製FaceIndex使用一個R32F的紋理作為RT繫結。
glGenFramebuffers(1, &FaceIndexFBO); // generate frame buffer
glBindFramebuffer(GL_FRAMEBUFFER, FaceIndexFBO); // bind frame buffer
glGenTextures(1, &FaceIndexTex); // generate the texture to storage FaceIndex
glBindTexture(GL_TEXTURE_2D, FaceIndexTex); // bind texture
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, 800, 600, 0, GL_RED, GL_FLOAT, NULL); // Set texture Format to R32F
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, FaceIndexTex, 0); // Bind as Color Attachment 0 of the FrameBuffer
主繪製流程
- 處理滑鼠輸入
void Application::HandleMouse() {
glfwGetCursorPos(window, &MouseX, &MouseY); // Use GLFW API to get current Mouse Postion
auto MouseDelta = glm::vec2(0, 0); // Delta Vector between last and current
if (LastMouseX >= 0 && LastMouseY >= 0) { // if last pos is legal (in the window)
MouseDelta = glm::vec2(MouseX - LastMouseX, MouseY - LastMouseY);
}
auto state = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT); // Get current left button state
if (state == GLFW_PRESS) { // if left buttton is pressed
// rotate viewDir around vertical axis (Up)
viewDir = glm::mat3(glm::rotate(MouseDelta.x * 0.0015f, viewUp)) * viewDir;
// rotate viewDir around horizontal axis (ViewUp x ViewDir)
viewDir = glm::mat3(glm::rotate(-MouseDelta.y * 0.003f, glm::cross(viewUp, viewDir))) * viewDir;
LastMouseX = MouseX;
LastMouseY = MouseY;
}
else {
LastMouseX = -1.0f;
LastMouseX = -1.0f;
}
}
-
繪製頂點索引
void Application::Render() { HandleMouse(); glEnable(GL_CULL_FACE); // NOTICE : Must Enable Cull Face to ensure NOT rendering back face of the BUNNY !!! glEnable(GL_DEPTH_TEST); glBindVertexArray(vao); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glm::mat4 projection = glm::perspective((float)glm::radians(45.0), 800.0f / 600.0f, 0.1f, 100.0f); glm::mat4 model = glm::scale(glm::mat4(1.0f), glm::vec3(5.0f)); glm::mat4 view = glm::lookAt(viewPos, viewPos + viewDir, viewUp); mShaders["FaceIndex"]->Bind(); // set Uniforms mShaders["FaceIndex"]->SetMat("projection", projection); mShaders["FaceIndex"]->SetMat("model", model); mShaders["FaceIndex"]->SetMat("view", view); glBindFramebuffer(GL_FRAMEBUFFER, FaceIndexFBO); glDrawArrays(GL_TRIANGLES, 0, numFaces * 3);
-
頂點索引 Shader 程式碼
//FaceIndex.vert #version 330 core layout(location = 0) in vec3 in_position; layout(location = 1) in vec3 in_normal; layout(location = 2) in float in_index; uniform mat4 model; uniform mat4 view; uniform mat4 projection; out vec3 thePosition; out vec3 theNormal; flat out float theIndex; // NOTICE: Index should NOT be interpolated void main() { vec4 v = vec4(in_position,1.0); gl_Position = projection * view * model * v; thePosition = vec3(model * v); theNormal = normalize(vec3(model * vec4(in_normal,0))); theIndex = in_index; }
//FaceIndex.frag #version 430 out vec4 daColor; in vec3 thePosition; in vec3 theNormal; flat in float theIndex; void main() { daColor = vec4(theIndex,0,0,0); }
-
-
回讀滑鼠位置的顏色(索引值)
float res[4]; GLint viewport[4]; glGetIntegerv(GL_VIEWPORT, viewport); glReadPixels((GLint)MouseX, viewport[3] - MouseY, 1, 1, GL_RED, GL_FLOAT, &res); // y is flipped in OpenGL screen coordinate system // res[0] is the face index picked by mouse
-
繪製馮氏光照模型
mShaders["BlinnPhong"]->Bind(); mShaders["BlinnPhong"]->SetMat("projection", projection); mShaders["BlinnPhong"]->SetMat("model", model); mShaders["BlinnPhong"]->SetMat("view", view); mShaders["BlinnPhong"]->SetVec("CameraPosition", glm::vec3(0, 0, -20.0f)); mShaders["BlinnPhong"]->SetVec("LightPosition", glm::vec3(-50.0f, 10.0f, -20.0f)); glBindFramebuffer(GL_FRAMEBUFFER, 0); // Bind the default framebuffer (render to screen) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glDrawArrays(GL_TRIANGLES, 0, numFaces * 3);
-
馮氏光照Shader
//BlinnPhong.vert #version 330 core layout(location = 0) in vec3 in_position; layout(location = 1) in vec3 in_normal; layout(location = 2) in float in_index; uniform mat4 model; uniform mat4 view; uniform mat4 projection; out vec3 thePosition; out vec3 theNormal; void main() { vec4 v = vec4(in_position,1.0); gl_Position = projection * view * model * v; thePosition = vec3(model * v); theNormal = normalize(vec3(model * vec4(in_normal,0))); }
#version 430 out vec4 daColor; in vec3 thePosition; in vec3 theNormal; uniform vec3 CameraPosition; uniform vec3 LightPosition; void main() { float finalBrightness = 0.0; float ambient = 0.05; //diiffuse vec3 lightDir = normalize(LightPosition - thePosition); float diff = max(dot(lightDir,theNormal),0.0); //specular vec3 viewDir = normalize(CameraPosition - thePosition); vec3 reflectDir = reflect(-lightDir,theNormal); float spec = 0.0; vec3 halfwayDir = normalize(lightDir + viewDir); spec = pow(max(dot(theNormal,halfwayDir),0.0),32.0); float specular = 0.3 * spec; finalBrightness = ambient + diff + specular; vec3 f = vec3(finalBrightness); vec3 test = min(f,vec3(1.0)); daColor = vec4(test, 1.0); }
-
-
繪製高亮三角形
-
高亮面的編號為 res[0] * numFaces
DrawCoverVertices(res[0] * numFaces, projection, model, view);
-
獲取高亮面的三個點座標
void Application::DrawCoverVertices(int index, glm::mat4& projection, glm::mat4& model, glm::mat4& view) { if (index < 0 || index >= numFaces) { return; } int a = index * 3 + 0; int b = index * 3 + 1; int c = index * 3 + 2; CoveredVertices[0] = sortedVertices[a * 7 + 0]; CoveredVertices[1] = sortedVertices[a * 7 + 1]; CoveredVertices[2] = sortedVertices[a * 7 + 2]; CoveredVertices[3] = sortedVertices[b * 7 + 0]; CoveredVertices[4] = sortedVertices[b * 7 + 1]; CoveredVertices[5] = sortedVertices[b * 7 + 2]; CoveredVertices[6] = sortedVertices[c * 7 + 0]; CoveredVertices[7] = sortedVertices[c * 7 + 1]; CoveredVertices[8] = sortedVertices[c * 7 + 2];
-
繪製高亮面 (關閉depth test 從而與之前的有光照的兔兔疊加)
glDisable(GL_DEPTH_TEST); // NOTICE: Disable depth test to cover last result glDisable(GL_CULL_FACE); glBindVertexArray(CoveredVAO); glBufferSubData(GL_ARRAY_BUFFER, 0, 9 * sizeof(float), CoveredVertices); mShaders["postprocess"]->Bind(); mShaders["postprocess"]->SetMat("projection", projection); mShaders["postprocess"]->SetMat("model", model); mShaders["postprocess"]->SetMat("view", view); glBindVertexArray(CoveredVAO); glDrawArrays(GL_TRIANGLES, 0, 9);
-
繪製高亮面Shader
//postprocess.vert #version 330 core layout(location = 0) in vec3 in_position; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { vec4 v = vec4(in_position,1.0); gl_Position = projection * view * model * v; }
//postprocess.frag #version 430 out vec4 daColor; void main() { daColor = vec4(1.0,0,0,1.0); }
-
-
-
加一個簡單延遲,鎖定在三十幀左右(也可以直接用glfw的垂直同步)
DrawCoverVertices(res[0] * numFaces, projection, model, view);
Sleep(20);
}
- 附上一個Application與Shader的宣告
class Application {
public:
void Run();
private:
void InitShader();
void Init();
void Render();
void HandleMouse();
void DrawCoverVertices(int index, glm::mat4& projection, glm::mat4& model, glm::mat4& view);
private:
using ShaderLibrary = std::unordered_map<std::string,Shader*>;
ShaderLibrary mShaders;
float* vertices;
float* sortedVertices;
int numFaces;
int numVertices;
GLuint vao;
GLuint FaceIndexTex;
GLuint FaceIndexFBO;
double MouseX, MouseY;
double LastMouseX, LastMouseY;
GLFWwindow* window;
float CoveredVertices[9];
GLuint CoveredVAO;
GLuint CoveredVBO;
glm::vec3 viewDir = glm::vec3(0.0f, 0.0f, 20.0f);
glm::vec3 viewPos = glm::vec3(0.0f, 0.0f, -3.0f);
glm::vec3 viewUp = glm::vec3(0.0f, 1.0f, 0.0f);
float CoverVertices[9];
};
class Shader {
public:
Shader(const std::string& shaderName);
GLuint GetProgramID() const { return mProgramID; }
void SetMat(const std::string& name, const glm::mat4& mat);
void SetMat(const std::string& name, const glm::mat3& mat);
void SetVec(const std::string& name, const glm::vec3& vec);
void SetVec(const std::string& name, const glm::vec4& vec);
void SetInt(const std::string& name, int val);
void SetFloat(const std::string& name, float val);
void Bind();
private:
GLuint mProgramID;
};
實現效果
碎碎念
主要難點的地方還是實現拾取,這裡用了三個drawcall顯然應該是多餘的,可優化的地方還很多
給別人講圖形學是真的非常難,概念冗雜不說,還一環套一環,必須全部搞懂才能實現一個非常簡單的東西
相關文章
- 兔兔蛋糕店2022最新版
- Best Wishes「兔」You!
- 雞兔同籠
- 極兔出海求生
- 小米8安兔兔跑分效能公佈 小米8跑分多少?
- 轉轉&安兔兔:2020年最保值二手手機是iPhoneXiPhone
- 小米平板4安兔兔跑分測試 小米平板4跑分多少
- 最新2018年6月安卓手機安兔兔效能排行榜安卓
- 安兔兔:2024年5月安卓旗艦手機效能排行榜安卓
- 雞兔同籠35個頭94只腳 問雞和兔各有幾隻?
- 榮耀Magic 2跑分曝光,榮耀Magic 2安兔兔跑分多少?
- iQOO手機跑分多少?iQOO Monster安兔兔跑分手機效能測試
- 有病信仰的新作來了,和貓貓兔兔一起打理你的神社!
- 安兔兔:60Hz屏Android機型份額佔比跌至4成Android
- osg三維場景中拾取滑鼠在模型表面的點選點模型
- 轉轉&安兔兔:iPhone 13釋出前二手市場iPhone價格普降iPhone
- 蘋果iPad Pro 2018安兔兔跑分效能測試 新iPad Pro跑分多少?蘋果iPad
- OPPO K1安兔兔跑分效能測試 OPPO K1跑分多少?
- 麒麟710對比驍龍710跑分對比 麒麟710安兔兔跑分多少
- vivo X23安兔兔跑分效能測試 vivo X23跑分多少
- 榮耀v20跑分多少?榮耀V20安兔兔跑分測試
- 2019年1月份安兔兔3000元以上手機價效比排行榜
- vivo NEX安兔兔跑分測試 搭載驍龍845的vivo NEX跑分多少
- 小米8透明探索版安兔兔跑分評測 小米8透明探索版跑分多少
- 聯發科Helio A22安兔兔跑分實測 紅米6A跑分多少?
- 新年新故事 | Nice 兔 Meet U
- 龜兔比賽 多執行緒執行緒
- 極兔財報:2023年全年極兔公司總收入為88.49億美元 同比增長約22%
- 安兔兔:2022年2月Android手機價效比榜 次旗艦不足千元Android
- 小米9安兔兔跑分效能測試:搭載驍龍855的小米9跑分多少?
- 2018年11月安兔兔價效比手機排行榜:360兩款手機入選
- 華為nova3i跑分測試 海思麒麟710安兔兔跑分實測
- 安兔兔公佈最新5月安卓手機價效比排行:這四款無敵安卓
- 紅魔Mars安兔兔跑分多少?努比亞紅魔Mars電競手機效能測試
- 【梟·編劇】老兵“鐵拳兔”的故事
- 安兔兔:2024年8月車機效能榜出爐 小米SU7排名第五
- iQOO Pro 5G手機安兔兔跑分成績曝光,擁有接近50萬的跑分成績
- 2019年1月份安兔兔1000-1999元價位手機價效比排行榜