Python+OpenGL绘制3D模型(六)材质文件载入和贴图映射
系列文章
一、逆向工程
Sketchup 逆向工程(一)破解.skp文件数据结构
Sketchup 逆向工程(二)分析三维模型数据结构
Sketchup 逆向工程(三)软件逆向工程从何处入手
Sketchup 逆向工程(四)破解的乐趣 钩子 外挂 代码注入
二、OpenGL渲染模型
Python+OpenGL绘制3D模型(一)Python 和 PyQt环境搭建
Python+OpenGL绘制3D模型(二)程序框架PyQt5
Python+OpenGL绘制3D模型(三)程序框架PyQt6
Python+OpenGL绘制3D模型(四)绘制线段
Python+OpenGL绘制3D模型(五)绘制三角型
Python+OpenGL绘制3D模型(六)材质文件载入和贴图映射
Python+OpenGL绘制3D模型(七)制作3dsmax导出插件
Python+OpenGL绘制3D模型(八)绘制插件导出的插件
Python+OpenGL绘制3D模型(九)完善插件功能: 矩阵,材质,法线
Python+OpenGL 杂谈(一)
三、成果
疫情期间关在家里实在没事干,破解了Sketchup,成功做出可以读取并显示.skp文件的程序SuViewer
前言
Sketchup作为目前设计院最为流行的设计软件(非工程制图软件),深受设计师的喜爱,软件小巧,而功能强大,有不少为之开发的插件应运而生,不过呢,关于底层数据结构和工作原理相关的文章少之又少,本文意在填补一下这方面的空缺,通过逆向软件分析,展示软件内部奥秘。本文用到的工具:IDA Pro,Immunity Debugger,Visual Studio (逆向工程三件套)数据结构属于知识产权的核心机密:
Python+OpenGL绘制3D模型(六)材质文件载入和贴图映射
运行效果:
文章目录
一、从文件读取贴图
记得以前用c++写的时候,要编译链接图像库,用于对应图片格式的加载,每个用到的格式都要单独搞一遍,在网上找开源的图片库,下载,编译,测试,一套下来搞的人很累,现在,因为有Qt的加持,载入贴图变得非常简单,全部交给Qt来做,只需要几行代码
def load_texture_from_file( filepath ):
with open(filepath, 'rb') as hf:
data = hf.read()
image = QImage()
valid = image.loadFromData(data)
if not valid:
return False
gl_tex_obj = QOpenGLTexture(image.mirrored())
return gl_tex_obj
图片载入到Qt后,还需要调用OpenGL的库加载贴图数据,另外Qt的图片坐标Y轴是向下增加的,符合显示屏幕坐标的习惯,而OpenGL中的Y轴坐标是正常向上的,所以Y轴需要mirror处理一下
is_built = False
gl_tex = None
def draw(gl):
global is_built, gl_tex
if not is_built:
gl_tex = load_texture_from_file("c:/temp/cg.jpg")
is_built = True
为了代码保持简洁,简化接口,不要把简单的事情复杂化:
1、载入贴图后,对象保存到全局对象中,
2、载入模型的初始工作也内嵌到draw函数中执行,这种思维在项目越做越大也仍然实用
3、贴图路径使用了硬编码方式的绝对路径,在没有形成一个模型框架的时候,暂且用这种方式对测试减少不少工作量
演示用到的贴图文件下载:
二、glBindTexture
在绘制模型前,设置一下OpenGL的状态机,调用glBindTexture指定当前纹理单元的数据,然后调用glEnable(GL_TEXTURE)激活纹理单元,因为有贴图作为像素的输入颜色,所以颜色设为纯白
gl.glDisable(gl.GL_TEXTURE_2D)
gl.glColor3f(0.9, 0.83, 0.6)
# 绘制上下2个面
draw_single_face(p1, p2, p3, p4, uv1, uv2, uv3, uv4, gl)
draw_single_face(p5, p6, p7, p8, uv1, uv2, uv3, uv4, gl)
gl_tex.bind()
gl.glEnable(gl.GL_TEXTURE_2D)
gl.glColor3f(1, 1, 1)
# 绘制其他4个面,前,后,左,有
draw_single_face(p1, p2, p6, p5, uv1, uv2, uv3, uv4, gl)
draw_single_face(p3, p4, p8, p7, uv1, uv2, uv3, uv4, gl)
draw_single_face(p2, p3, p7, p6, uv1, uv2, uv3, uv4, gl)
draw_single_face(p4, p1, p5, p8, uv1, uv2, uv3, uv4, gl)
在本例中,前后左右的面映射了贴图,上下2个面仍然用了原来的模型颜色(淡黄色)
三、指定贴图坐标
在调用glVertex3f之前,需要先指定顶点的贴图坐标属性,调用glTexCoord2f
gl.glBegin(gl.GL_TRIANGLES)
# 第一个顶点
gl.glTexCoord2f( ... )
gl.glVertex3f( ... )
# 第二个顶点
gl.glTexCoord2f( ... )
gl.glVertex3f( ... )
# 第三个顶点
gl.glTexCoord2f( ... )
gl.glVertex3f( ... )
# 完成
gl.glEnd()
四、运行图效果
五、2个问题和原因
第一、模型的位置有点高:
这是应为camera坐标系的中心为(0,0,0), 而模型高度是从 0.0 到 2.0,要调整一下中心坐标,可以通过设置概念上的world matrix沿Z轴往下移动1.0个单位,模型就居中了,目前程序框架里控制视角的平移,也可以通过这个方法来解决,不过我还是打算在之后引入Camera对象的计算,计算的逻辑思维更清晰,不容易出错
# Camera
self.gl.glMatrixMode(self.gl.GL_MODELVIEW)
self.gl.glLoadIdentity()
self.gl.glTranslatef(0.0,0.0,-self.zoom)
self.gl.glRotatef(self.rotX-90,1.0,0.0,0.0)
self.gl.glRotatef(self.rotZ,0.0,0.0,1.0)
# World
self.gl.glTranslatef(0.0,0.0,-1.0) # 在这里加上对world matrix的改变
这里的矩阵叠加,是按照相反的顺序乘积的,比较容易搞错,OpenGL中有个机制 PushMatrix PopMatrix,也就是说后面加入的矩阵,可以通过PopMatrix来恢复到之前的矩阵乘积的状态,越靠后面加进来的矩阵,代表子物体的矩阵,在运算中最先乘这个矩阵
第二、当模型放大的时候,近的地方会被切掉
调整下几行代码,
# Projection
self.gl.glMatrixMode(self.gl.GL_PROJECTION)
pm = QMatrix4x4()
aspectRatio = w/h
fov = 45 / aspectRatio if w < h else 45
pm.perspective(fov, w/h, 2, 5000)
self.gl.glLoadMatrixf(pm.data())
这里是设置透视投影矩阵的代码,pm.perspective(fov, w/h, 2, 5000),这个函数调用的最后2个参数,分别代表了近裁剪平面(z-buff=0.0),和远裁剪平面(z-buff=1.0),这2个值明显不太匹配当前场景的大小,每次应该根据当前场景大小来适当选择取值范围
pm.perspective(fov, w/h, 0.1, 100)
改成 0.1 到 100 的范围,就能够比较适配当前的测试模型
到此我们已经能够绘制一个完整的模型,不过模型数据来源还没解决,不能显示复杂的模型,下节我们要讲一个模型数据来源的通用方法,通过编写一个3dsmax插件导出模型,因为3dsmax也支持python,所以下节我们用很少的python代码来完成一个复杂的模型导出插件
六、源代码
1、Draw1.py
from PyQt5.QtGui import QVector3D, QVector2D, QImage, QOpenGLTexture
################################
# FILE DESCRIPTION
# 文件描述:load_texture_and_bind()
# 对应文章:Python+OpenGL绘制3D模型(六) 载入贴图及映射到模型
# 作者:李航 Lihang
#
################################
is_built = False
gl_tex = None
def draw(gl):
global is_built, gl_tex
if not is_built:
gl_tex = load_texture_from_file("c:/temp/cg.jpg")
is_built = True
# 设置z-buff偏移
gl.glEnable(gl.GL_POLYGON_OFFSET_FILL)
gl.glPolygonOffset(1, 1)
# 绘制填充面
draw_box_faces(gl_tex, gl)
# 关闭z-buff偏移
gl.glDisable(gl.GL_POLYGON_OFFSET_FILL)
# 绘制线框
gl.glColor3f(0.0, 0.0, 0.0)
draw_box_lines(gl)
def load_texture_from_file( filepath ):
with open(filepath, 'rb') as hf:
data = hf.read()
image = QImage()
valid = image.loadFromData(data)
if not valid:
return False
gl_tex_obj = QOpenGLTexture(image.mirrored())
return gl_tex_obj
############
# draw_box_faces
# 画面 - 中间的填充部分
############
def draw_box_faces(gl_tex, gl):
p1 = QVector3D(-1, -1, 0 )
p2 = QVector3D(+1, -1, 0 )
p3 = QVector3D(+1, +1, 0 )
p4 = QVector3D(-1, +1, 0 )
p5 = QVector3D(-1, -1, 2 )
p6 = QVector3D(+1, -1, 2 )
p7 = QVector3D(+1, +1, 2 )
p8 = QVector3D(-1, +1, 2 )
uv1 = QVector2D(0, 0)
uv2 = QVector2D(1, 0)
uv3 = QVector2D(1, 1)
uv4 = QVector2D(0, 1)
gl.glDisable(gl.GL_TEXTURE_2D)
gl.glColor3f(0.9, 0.83, 0.6)
draw_single_face(p1, p2, p3, p4, uv1, uv2, uv3, uv4, gl)
draw_single_face(p5, p6, p7, p8, uv1, uv2, uv3, uv4, gl)
gl_tex.bind()
gl.glEnable(gl.GL_TEXTURE_2D)
gl.glColor3f(1, 1, 1)
draw_single_face(p1, p2, p6, p5, uv1, uv2, uv3, uv4, gl)
draw_single_face(p3, p4, p8, p7, uv1, uv2, uv3, uv4, gl)
draw_single_face(p2, p3, p7, p6, uv1, uv2, uv3, uv4, gl)
draw_single_face(p4, p1, p5, p8, uv1, uv2, uv3, uv4, gl)
def draw_single_face(p1, p2, p3, p4, uv1, uv2, uv3, uv4, gl):
gl.glBegin(gl.GL_TRIANGLES)
gl.glTexCoord2f(uv1.x(), uv1.y())
gl.glVertex3f(p1.x(), p1.y(), p1.z())
gl.glTexCoord2f(uv2.x(), uv2.y())
gl.glVertex3f(p2.x(), p2.y(), p2.z())
gl.glTexCoord2f(uv3.x(), uv3.y())
gl.glVertex3f(p3.x(), p3.y(), p3.z())
gl.glEnd()
gl.glBegin(gl.GL_TRIANGLES)
gl.glTexCoord2f(uv3.x(), uv3.y())
gl.glVertex3f(p3.x(), p3.y(), p3.z())
gl.glTexCoord2f(uv4.x(), uv4.y())
gl.glVertex3f(p4.x(), p4.y(), p4.z())
gl.glTexCoord2f(uv1.x(), uv1.y())
gl.glVertex3f(p1.x(), p1.y(), p1.z())
gl.glEnd()
############
# draw_box_lines
# 画面 - 外侧的线
############
def draw_box_lines(gl):
p1 = QVector3D(-1, -1, 0 )
p2 = QVector3D(+1, -1, 0 )
p3 = QVector3D(+1, +1, 0 )
p4 = QVector3D(-1, +1, 0 )
p5 = QVector3D(-1, -1, 2 )
p6 = QVector3D(+1, -1, 2 )
p7 = QVector3D(+1, +1, 2 )
p8 = QVector3D(-1, +1, 2 )
# 一个立方体有12条边
draw_single_line( p1, p2, gl )
draw_single_line( p2, p3, gl )
draw_single_line( p3, p4, gl )
draw_single_line( p4, p1, gl )
draw_single_line( p5, p6, gl )
draw_single_line( p6, p7, gl )
draw_single_line( p7, p8, gl )
draw_single_line( p8, p5, gl )
draw_single_line( p1, p5, gl )
draw_single_line( p2, p6, gl )
draw_single_line( p3, p7, gl )
draw_single_line( p4, p8, gl )
def draw_single_line(p1, p2, gl):
gl.glBegin(gl.GL_LINES)
gl.glVertex3d(p1.x(), p1.y(), p1.z())
gl.glVertex3d(p2.x(), p2.y(), p2.z())
gl.glEnd()
2、tOpenGLqt5.py
import sys
from PyQt5.QtCore import (QPoint)
from PyQt5.QtGui import (QMatrix4x4, QVector3D, QOpenGLVersionProfile)
from PyQt5.QtWidgets import QApplication, QOpenGLWidget
from Draw1 import draw
############
# GLWidget
# OpenGL 窗口通用程序框架
# 1、创建OpenGL环境
# 2、设置矩阵
# 3、控制窗口视角
# 4、调用 draw 绘图主函数
############
class GLWidget(QOpenGLWidget):
def __init__(self, parent):
super(GLWidget, self).__init__( parent)
self.dragPressPos = QPoint()
self.rotX=45
self.rotZ=0
self.ps_button = 0
self.ps_rotX = 0
self.ps_rotZ = 0
self.zoom=10
############
# 创建OpenGL环境
# Qt6 和 Qt5的主要区别在这里
############
def initializeGL(self):
version_profile = QOpenGLVersionProfile()
version_profile.setVersion(2, 0)
self.gl = self.context().versionFunctions(version_profile)
self.gl.initializeOpenGLFunctions()
############
# paintEvent
############
def paintEvent(self, event):
# Step 0
self.makeCurrent()
# Step 1
self.gl.glClearColor(0.85, 0.85, 0.85, 1.0)
self.gl.glClear(self.gl.GL_COLOR_BUFFER_BIT | self.gl.GL_DEPTH_BUFFER_BIT)
self.gl.glEnable(self.gl.GL_DEPTH_TEST)
# Step 2
self.SetupMatrix()
# Step 3
draw(self.gl)
#self.drawTarget(self.gl)
############
# 绘图
# 这里是个绘图的简单测试代码
############
def drawTarget(self, gl):
p = QVector3D(0, 0, 0)
gl.glColor3f(1.0, 0.0, 0.0);
gl.glBegin(gl.GL_LINES)
gl.glVertex3d(p.x()-1, p.y(), p.z() )
gl.glVertex3d(p.x()+1, p.y(), p.z() )
gl.glEnd()
gl.glColor3f(0.0, 1.0, 0.0);
gl.glBegin(gl.GL_LINES)
gl.glVertex3d(p.x(), p.y()-1, p.z() )
gl.glVertex3d(p.x(), p.y()+1, p.z() )
gl.glEnd()
gl.glColor3f(0.0, 0.0, 1.0);
gl.glBegin(gl.GL_LINES)
gl.glVertex3d(p.x(), p.y(), p.z() )
gl.glVertex3d(p.x(), p.y(), p.z() +4 )
gl.glEnd()
############
# 设置矩阵
# 透视矩阵和Camera矩阵
############
def SetupMatrix(self):
# ViewPort
w = self.width()
h = self.height()
self.gl.glViewport(0, 0, w, h)
# Projection
self.gl.glMatrixMode(self.gl.GL_PROJECTION)
pm = QMatrix4x4()
aspectRatio = w/h
fov = 45 / aspectRatio if w < h else 45
pm.perspective(fov, w/h, 0.1, 100)
self.gl.glLoadMatrixf(pm.data())
# Camera
self.gl.glMatrixMode(self.gl.GL_MODELVIEW)
self.gl.glLoadIdentity()
self.gl.glTranslatef(0.0,0.0,-self.zoom)
self.gl.glRotatef(self.rotX-90,1.0,0.0,0.0)
self.gl.glRotatef(self.rotZ,0.0,0.0,1.0)
# World
self.gl.glTranslatef(0.0,0.0,-1) # 在这里加上对world matrix的改变
############
# 视角控制
# 1、左键旋转
# 2、中间缩放
# 3、平移 **TODO**
############
def mousePressEvent(self,event):
self.dragPressPos = event.pos()
self.ps_button = event.button()
self.ps_rotX = self.rotX
self.ps_rotZ = self.rotZ
def mouseMoveEvent(self, event):
diff = event.pos() - self.dragPressPos
if self.ps_button == 1:
self.rotX = self.ps_rotX + diff.y()*0.5
if self.rotX > 90:
self.rotX = 90
if self.rotX < -90:
self.rotX = -90;
# rotZ
self.rotZ = self.ps_rotZ + diff.x()*0.5
self.repaint()
def wheelEvent(self, event):
delta = event.angleDelta().y()
if delta < 0 :
self.zoom += self.zoom * 0.2
else:
self.zoom -= self.zoom * 0.2
self.repaint()
############
# App
# 创建主窗口应用程序
# 并且进入消息循环
############
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = GLWidget(None)
widget.resize(640, 480)
widget.show()
sys.exit(app.exec())
系列文章预告
目标是一个完善的Viewer,能够显示Sketchup的.skp文件中的3D模型
Corona渲染器照片级渲染效果