LOADING

加载过慢请开启缓存 浏览器默认开启

TyranitarX

算法docker环境构建

docker 2025/6/18

1.FastAPI服务构建

首先需要在对应算法服务的虚拟环境venv或者 conda venv 中安装fastapi需要的python库,列举如下:

pip install fastapi
pip install uvicron

同样,根据算法服务需要编写一个简单的fastapi服务,实例如下:

# -*-coding: utf-8 -*-
# @Time    : 2025/4/1 17:34
# @Author  : TyranitarX
import argparse
import time

import mcubes
import numpy as np
import point_cloud_utils as pcu
import torch
import trimesh
from fastapi import FastAPI, File, UploadFile
from starlette.responses import FileResponse

from model.cuboidSegNet import CuboidSegNet

app = FastAPI()

N_vol = 2048  # volume points
N_near = 1024  # near-surface points

N_face = 2048


@app.post("/execute/", response_model=None)
async def create_item(
        surfacefile: UploadFile = File(...) //算法输入
):
    parser = argparse.ArgumentParser()
    # 模型需要的超参数
    parser.add_argument('--E_name', default='EXP_1', type=str, help='Experiment name')
   # ...

    args = parser.parse_args()
    #读取模型
    model = CuboidSegNet.load_from_checkpoint('../ckpts/newloss.ckpt', params=args, map_location='cuda')
    #输入处理
    #...
    out = model(face.cuda(), vol.cuda())
    #输出梳理
    #...
    filename = f"output_+{time.time()}.obj"
    #保存文件
    mcubes.export_obj(vertices, faces, filename)
    #返回模型结果
    return FileResponse(
        filename,
        media_type="application/octet-stream",
        filename=filename
    )

2. Dockerfile编写

有了以上内容便可以构建包含有算法服务的docker容器,核心便是编写Dockerfile进行容器构建。

  1. 在构建容器之前,我们需要把算法的虚拟环境打包应用在我们的容器中。这里以conda为例
# 安装conda-pack
conda install conda-pack
# 使用conda-pack打包当前环境
conda pack -n ENV_NAME -o FILENAME.tar.gz

执行成功后能得到打包好的环境压缩包
2) 之后便可以进行Dockerfile的编写,首先我们需要在算法环境目录下创建一个名为Dockerfile的文件,其内容如下:

# 使用 conda 官方镜像
FROM continuumio/miniconda3

# 设置工作目录
WORKDIR .

# 复制 Conda 环境文件(ENV_NAME 替换为你的环境名)
COPY ENV_NAME.tar.gz .

# 创建Conda 环境
RUN mkdir -p /opt/conda/envs/ENV_NAME && \
    tar -xzf DEPF_ENV.tar.gz -C /opt/conda/envs/ENV_NAME && \
    rm ENV_NAME.tar.gz

# 激活Conda 环境
RUN echo "source $(conda info --base)/etc/profile.d/conda.sh" >> ~/.bashrc && \
    echo "conda activate ENV_NAME" >> ~/.bashrc

ENV PATH /opt/conda/envs/ENV_NAME/bin:$PATH

# 复制应用代码(需要改成你需要的目录)
COPY ./ckpts /ckpts
COPY ./model /model
COPY ./tools /tools
COPY fastServer.py .

# 暴露 FastAPI 端口
EXPOSE 8000

# 运行 FastAPI 这里设置的启动端口是容器内的端口,在运行容器时通过-p参数选择暴露在宿主机的哪一个端口上
CMD ["uvicorn", "fastServer:app", "--host", "0.0.0.0", "--port", "8000"]

之后再当前目录执行

docker build -t IMAGE_NAME . 

# 之后会有如下上下文展示
[+] Building 65.0s (13/13) FINISHED                                                                                      docker:default
 => [internal] load build definition from Dockerfile                                                                               0.0s
 => => transferring dockerfile: 727B                                                                                               0.0s
 => [internal] load metadata for docker.io/continuumio/miniconda3:latest                                                          64.7s
 => [internal] load .dockerignore                                                                                                  0.0s
 => => transferring context: 65B                                                                                                   0.0s
 => [1/9] FROM docker.io/continuumio/miniconda3:latest@sha256:4a2425c3ca891633e5a27280120f3fb6d5960a0f509b7594632cdd5bb8cbaea8     0.0s
 => [internal] load build context                                                                                                  0.0s
 => => transferring context: 5.10kB                                                                                                0.0s
 => CACHED [2/9] COPY DEPF_ENV.tar.gz .                                                                                            0.0s
 => CACHED [3/9] RUN mkdir -p /opt/conda/envs/DEPF &&     tar -xzf DEPF_ENV.tar.gz -C /opt/conda/envs/DEPF &&     rm DEPF_ENV.tar  0.0s
 => CACHED [4/9] RUN echo "source $(conda info --base)/etc/profile.d/conda.sh" >> ~/.bashrc &&     echo "conda activate DEPF" >>   0.0s
 => CACHED [5/9] COPY ./ckpts /ckpts                                                                                               0.0s
 => [6/9] COPY ./model /model                                                                                                      0.1s
 => [7/9] COPY ./tools /tools                                                                                                      0.0s
 => [8/9] COPY fastServer.py .                                                                                                     0.0s
 => exporting to image                                                                                                             0.2s
 => => exporting layers                                                                                                            0.2s
 => => writing image sha256:b01629a61370e51017e401980671bc2d83bec408abd2292fcef714c59d5c2537                                       0.0s
 => => naming to docker.io/library/algserver                                                   

此时执行docker images即可看到生成的docker镜像image
执行命令docker run -d -p 8000:8000 algserver 启动容器
image
发现端口服务正常启动image接口正常访问

阅读全文

Superquadrics Revisited Learning 3D Shape Parsing beyond Cuboids

3DVision 2025/6/9

1、整体方法

超二次曲面参数方程如下所示:$$r(\eta,\omega)=\left[\begin{aligned}a_1cos^{\epsilon1}\eta cos^{\epsilon2}\omega\a_2cos^{\epsilon1}\eta sin^{\epsilon2}\omega\a_3sin^{\epsilon1}\eta\end{aligned}\right]\space\space\begin{aligned}-\pi/2\leq\eta\leq\pi/2\-\pi\leq\omega\leq\pi\end{aligned}$$

其中$a=[a_1,a_2,a_3]$定义了超二次曲面的大小 $\epsilon=[\epsilon_1,\epsilon_2]$定义了超二次曲面的全局形状。根据常识,方法将$\epsilon_1,\epsilon_2$的范围定义在$[0.1,1.9]$之间去避免生成非凸的形状。这样的方程定义了一个基于标准坐标系原点的超二次曲面。为了使其能够处于任意位置,同时定义了一个位移$t=[t_x,t_y,t_z]$向量定义位置,四元组向量$q=[q_0,q_1,q_2,q_3]$定义了旋转。组合在一起定义了仿射变换矩阵为$T(x)=R(\lambda)x+t(\lambda)$

文章的输入基于体素组,因此编码器解码器如下所示:
image

2、重建损失

文章通过与双向SDFloss测试后,使用标准的倒角距离作为重建损失来监督重建效果。即:
$$L_D(P,X)=L_{P->X}(P,X)+L_{X->P}(X,P)$$
其中$P->X$表示预测的基元$P$到原始点云$X$的距离,$X->P$则表示原始点云$X$到预测基元$P$的距离。文章对两组距离加以不同的权重,分别为1.2与0.8。

  • 基元到点云
    目标点云由一组3D点组成$X={x_i}^N_{i=1}$。同样,这里将m个基元的连续表面近似为一组点$Y_m={y_k^m}^K_{k=1}$通过这样的离散化表示便能计算基元到目标点云的距离。对于任意一个基元上的任意一点$y_k^m$,我们计算他到目标点云的最近一个点的最近距离,最终取所有点的平均值作为距离$L^m_{P->X}$即:$$L^m_{P->X}(P,X)=\frac{1}{K}\sum^K_{k=1}\Delta^m_k$$
    其中$$\Delta^m_k=\min_{i=1,..,N}||T_m(x_i)-y_k^m||2$$
    因为并非所有物体都能用固定的N个基元表示,这里同时预测了当前基元存在的概率$p(z_m)$即有$p(z)=\Pi_m(z_m)$。对于所有基元到点云的距离loss就可以表示为:$$L
    {P->X}(P,X)=E_{p(z)}\left[\sum_{m=1}^ML_{P->X}^m(P,X)\right]$$$$=\sum_{m=1}^M\gamma_mL_{P->X}^m(P,X)$$
  • 点云到基元
    文章定义了$$\Delta_i^m=\min_{k=1,..,K}||T_m(x_i)-y_k^m||2$$相较于之前基元到点云不同的是,这里需要对K个点根据预测的基元最小化距离。在这里同样需要取每个基元的存在概率$\Delta_i^m$。不同的是,这里需要检索出离点云X中每一个点距离最近且存在的基元$m(z_m=1)$:$$L{X->P}(X,P)=E_{p(z)}\left[\sum_{x_i∈X}\min_{m|z_m=1}\Delta_i^m\right]$$
    这里注意到上述方法在基元数量M特别大时,时间复杂度会达到$2^M$的量级,因此十分耗时。文章这里提供了一种线性时间复杂度的计算方法来替代这种复杂的计算。这里假设对于每个基元的存在性$\Delta_i^m$是升序排列的,即:$$\Delta_i^1\leq\Delta_i^2\leq…\leq\Delta_i^M$$在假设这样的序列的情况下,我们可以声明如下:如果第一个基元存在,则第一个基元就是与当前点最接近的基元。如果第一个基元不存在而第二个基元存在,则第二个基元是当前点最近的基元,余下以此类推。$$\min_{m|z_m=1}\Delta_i^m=\left{\begin{align}
    &\Delta_i^1,ifz_1=1 \
    &\Delta_i^2,ifz_1=0,z_2=1\
    &…\
    &\Delta_i^M,ifz_m=0,…,z_M=1
    \end{align}\right.$$因此可以通过上式简化上上式有:$$L_{X->P}(X,P)=\sum_{x_i∈X}\sum^M_{m=1}\Delta_i^m\gamma_m\prod^{m-1}\bar{m}(1-\gamma{\bar{m}})$$$\Delta_{\bar{m}}$为除了$m$更远的基元的存在概率。
阅读全文

Shared Latent Membership Enables Joint Shape Abstraction and Segmentation With Deformable Superquadrics

3DVision 2025/6/9

1、整体方法

A. 超椭球体的定义

文章定义了一种超二次曲面,用于表示 球,圆柱,椭球,方体,八面体等图形。用超椭球面表示,超二次曲面的表面向量可用以下函数表示。

image
$−π/2 ≤ η ≤ π/2$, $−π ≤ ω ≤ π. $a := {a1, a2, a3}$ 是大小参数. $ε := {ε1, ε2}$ 是形状参数.

可变形的超二次曲面可以 对其进行线性锥化和弯曲操作。对于沿着Z轴进行线性锥化的表面向量可记录为:其中$k:={k_x,k_y}$ 称为锥度系数。
弯曲变形通过一个弯曲方向角α和曲率参数b来控制,其表面向量$x_b$被记录为
其中
因此 一个超椭球体的参数向量可以表示为$s := {a, ε, k, b, α, t, r, δ} ∈ R^{17}$ 其中${δ ∈ [0, 1]}$表示当前超椭球体在形状中的出现概率。

B.任务介绍

文章将需要预测的参数定义如下image
首先针对无明显朝向的点云,文章将这样的任务转换为了一个聚类任务。假设我们用Kmeans对点云进行聚类。则聚类的目标函数可表示为:其中M个聚类的中心点可表示为$C:={C_m∈R^3}^M_{m=1}.$ $G∈R^{M×N}$表示聚类指示器其中$g_{mn}$仅能取0或1。在这种情况下,G描述了在分割中点和部分的强关联性。然而,这样的在欧式空间中的聚类中心点C缺少对于部分的几何与语义信息,又因为高位空间能够更好地去捕获部分的语义,因此我们将k-means聚类应用在了一个D维的潜在层中。其中$F^{pc}$为整体点云的特征,$F^{part}$为在潜在层中的部件特征。由于在高维空间中的表示仅仅是一些列部分特征的表达而缺乏几何信息。为了获取这些几何信息,这里利用了一系列可变的超椭球体作为基元去捕获更多部件级别的语义信息去重建每个部分。
当我们将分割结果与形状抽象相结合时,不论在欧式空间还是潜在空间,我们都能很容易地将部件替换为可变形的超椭球体。
为了将其组合起来,文章将参数空间中的可变形超椭球体映射到了与$F^{pc}$相同的潜在空间 因此 有
其中$F^{dsq}$为在潜在空间的可变形超椭球体。
文章提出了根据非负矩阵分解的规则,$F^{dsq}$所代表的基元特征,能够被点特征$F^{pc}$与一个权重矩阵$W∈ R^{N ×M}$所表示。
因此有
在这里,W中的每一个元素$ω_{nm}$指的是点$x_n$与基元$s_m$之间的从属关系即,$∀m,\sum_{n=1}^Nω_{nm}=1$ 因此有:

阅读全文

EditVAE Unsupervised Parts-Aware Controllable 3D Point Cloud ShapeGeneration

3DVision 2025/6/9

1. 整体方法

image
方法以超二次曲线为基元,通过co-segmentation的方法预测基元以及点的归属。

文章将基于部件的点云分割和编辑的任务分为以下三部分:
1、将无标签的点云分割为语义上有意义的部分
2、在通过类型和关系位姿区分每个部分
3、将上述分解的内容用一个隐藏层表示,允许在生成过程中人为地调整其类型和位姿

为了解决这些问题,文章对于输入点云X的m个部分生成了另一个表示部件的点云$Y_m$ ,与一个部件的超二次曲线原型$\hat{P_m}$。为了解决问题2,我们通过一个仿射变换矩阵$T_m$来获取$\hat{P_m}$和$\hat{Y_m}$的标准参考位姿,因此有:$$P_m=T_m(\hat{P_m})\space and\space Y_m=T_m(\hat{Y_m})$$从而表示点云和基元部件的原始位姿。
因此 文章这里使用了边缘似然 即贝叶斯方法去计算在整体latent z分布下 不同交集$ζ_m = { \hat{Y_m}, \hat{P_m}, T_m}$ 的union,$ζ = ⋃^M_{m=1} ζ_m$ ,因此我们定义了训练目标为对输入X针对参数$\theta$的边缘似然$$P_{\theta}(X) = \int P_{\theta}(X,z,ζ)dzdζ$$
对其取对数和Jensen不等式得到:
$$logP_{\theta}(X) = log\int{P_{\theta}(X,z,ζ)dzd}ζ$$$$=log\int\frac{Q_φ(z,\zeta|X)}{Q_φ(z,\zeta|X}P_\theta(X,z,\zeta)dzd\zeta$$
$$\geq\int{Q_φ(z,\zeta|X)log\frac{P_\theta(X,z,\zeta)}{Q_φ(z,\zeta|X)}}dzd\zeta$$

阅读全文

Unsupervised Learning for Cuboid Shape Abstraction via JointSegmentation from Point Clouds

3DVision 2025/6/5

1、整体方法

给定一个包含$N$个点的点云$P$,目标是重建一系列长方体${C_i}_{i=1,…,M}$来简单表示一个物体。每个长方体被三个向量所表示,包括位移$t∈ R^3$,旋转$q∈ R^4$和缩放$s∈ R^3$。
针对相同种类的物体,文章预测M个长方体。然而,我们很容易判断出即使是同一个类别的物体,对于不同的个体来说几何结构也有不同。比如说某些椅子有把手而某些没有。为了适应不同的结构,为每个长方体增加了一个参数$𝛿 ∈ {0, 1}$去表明当前长方体是否存在于当前物体实例内。因此,这里用一个11维向量的参数化表示方式来表示每一个长方体$p_m=[t_m;r_m;s_m,𝛿 _m]$。
文章使用了一个VAE将输入的点云编码为深层特征,然后通过解码器解码出每个长方体的参数信息。
image

A. 特征编码网络

网络首先通过一个DGCNN,之后通过一个MLP+maxpool输出一个1024维的全局特征。同标准VAE一致,本方法将特征通过2个MLP 回归预测为高斯分布的均值$𝜇 ∈ R^{512}$与方差$𝜎 ∈ R^{512}$。之后得到的latent code z 可以从标准高斯分布中samle的噪声$n∈R^{512}$得到。即$z=μ+σ⊗n$,⊗为逐元素相乘。

B. 形状抽象网络

1) 网络将特征z分出了M个分支,对于每一个分支,文章提供了一个onehot编码的位置编码向量v与其编码器$E_{cb}$。其中v长度为M,对于第i个分支,编码向量为$[0,0,…,1,0]$其中值为1的位置索引为i。之后提供了一个编码器$E_{cf}$,将上述编码向量输入到编码器中得到一个128维的长方体特征$f_{cm}$。作者认为通过这样的编码,解码器中的每个长方体特征不仅包含形状几何信息而且包含了部分相对于整体的结构信息。
2) 根据上一步骤获取到的latentcode,最终提供了一个MLP Regressor $D_{cp}$进行长方体形状的预测,即方法中提到的向量$p_m$。(其中$E_{cf},E_{cb},D_{cp}$)对于不同分支来说权重共享。

C. 长方体聚集分割网络

1) 分割分支的目的是将输入的N个点分配到生成的M个长方体中。实际上完成了对输入点云的M类别的分割,且每个类别属于一个长方体。
2) 作者利用两个MLP对点云特征以及长方体特征进行降维至64维,生成特征向量$g^p$和$g^c$。这里将N个点于M个长方体的关联矩阵定义为为了计算概率,对每行进行softmax后得到每个点属于长方体在(0,1)之间的概率。即:通过此公式即可得到输入点云的分割结果。

D. 损失函数

1)重建损失$L_{recons}$
重建损失在这里不仅要最小化几何距离,并且要鼓励高度一致的部件分配。首先定义了距离$d(p_n,C_m)$为点n到预测长方体m的距离,同时乘以分割网络中得到的概率$W_{m,n}$作为形状重建损失:
由于单纯的点到长方体的距离作为loss可能使模型退化。文章利用了法向量信息作为重建损失来避免退化。文章通过沿着点p法向量的方向,根据高斯分布$N(0,σ^2_s)$随机采样的距离获取一个新的点$p^s_n$,并且找到选择的长方体表面上离该点最近的点$q^c_m$从而定义$p_n$到$C_m$的距离为:其中σ默认设置为0.05
image
2) 长方体紧凑损失
显然,更多数量的长方体能够更加精细地表示整个物体,但是整个物体的表达更倾向于更加简洁明了。因此在分割任务中更倾向于使用更少的长方体,因此在分割网络中每个立方体存在概率$w_m=\frac{1}{N}\sum^N_{n=1}W_{m,n}$我们希望$w_m$尽可能地小,因此这里提出了使用$L_0.5$损失来优化整个图形。(由于l1损失在当$w_m$之和为1的时候不再更新)即:
其中小量$𝜖_{sps}=0.01$ 的加入防止当$w_m$为0,长方体不存在时导致的梯度爆炸。image
根据图片可以明显发现,在优化过程中使用$L_2$损失时会导致$w_2$和$w_1$均趋向0.5,当$w_1+w_2=1$时使用$L_1$损失会导致梯度消失。而当使用$L_{0.5}$时则会使$w_1$或者$w_2$趋于0,另一个则趋于1,符合我们的期望。
3) 长方体存在性损失
在谈及长方体参数时,文章提供了一个存在概率𝛿。对于分割网络概率结果来说,当每个长方体中分配的点的概率>0.05时 我们认为该长方体是存在的即$𝛿^{gt}_m=1$否则$𝛿^{gt}_m=0$。对于这个0~1概率问题,这里使用的二进制交叉熵作为损失函数。即:

$$L_{exist}=-\frac{1}{M}\sum^M_{m=1}|𝛿^{gt}_mlog𝛿_m+(1-𝛿^{gt}_m)log(1-𝛿_m)|$$

4) 深层掩码KL-散度损失
模型前期对物体原始点云通过vanilla VAE 的形式进行编码获取到了一个符合标准高斯分布的512维的深层编码z。和原文同样使用KL散度对均值𝜇和方差𝜎进行约束。即:
因此,整合的网络loss为

其中$𝜆_1 = 0.1, 𝜆_2 = 0.01, 𝜆_3 = 6𝑒 − 6$

阅读全文

Parameterize Structure with Differentiable Templatefor 3D Shape Generation

2025/6/4

1、整体方法

利用参数可微模版,与众不同地定义了长方体。将其定义为有8个参数的“棍子”

image

用三视图方法代替点云描述三维形状

image

2、实现细节

A. 通过算法获取SDF
Cuboids 𝑩, Three-view details 𝑫, Resolution R, in(p, d): whether point p is inside polygon of detail d, dis(p, d): the distance from point p to polygon of detail d.

SDF 𝑽

𝑽 ← the volumes of SDF with resolution R

for volume v in 𝑽 do

    p ← v.coordinate

    i ← the index such that p is inside 𝑩⁢[i]

    if i is not exist then

         v.value ← 1

    else

         p ← MB⁢[i]−1⁢p

         px,py,pz ← projection p to yz, xz, xy plane

         flag ← in(px, 𝑫⁢[i]x) and in(py, 𝑫⁢[i]y) and in(pz, 𝑫⁢[i]z)

         dis ← min(dis(px, 𝑫⁢[i]x), dis(py, 𝑫⁢[i]y), dis(pz, 𝑫⁢[i]z))

         v.value ← −dis if flag else dis

    end if

end for
  • 需求:首先定义了表示整个物体的长方体集合B,三视图细节集合D,分辨率R:其中in(p,d)表示当前点是否在多边形细节d内,dis(p,d)则代表点p与多边形细节d之间的欧氏距离。
  • 计算:定义SDF 值为V V代表对应分辨率的体素RSDF
  • 遍历对应分辨率体素V中的体素点v
  • pv的坐标
  • i 为某个 当PB[i]中的索引
  • i 不存在 则 对应点sdf值为1
  • i存在
  • p 代表第i个长方体的逆变换矩阵乘p 将点坐标系转换为三视图坐标系
  • 将三个维度的分量px,py,pz投影到 yz,xz,xy平面
  • flag判断 点p的三个分量 是否均在三个平面内部
  • dis 为三个分量到三视图平面的最短距离
  • flagtrue 则在part内,sdf值为-dis,否则点在partsdf值为正 即可求得点在此部分中的sdf

    B. 形状参数化

    1) 可微分的计算图模版
    针对第$i$个长方体的参数化表示$b_i$如下所示:

    $b_i = c_i + r_i \textcolor{red}{a_{i1}} + s_{i1} K_{j1k1} e_1 + s_{i2} \left( \textcolor{red}{a_{i2}} K_{j2k2} + (1 - \textcolor{red}{a_{i2}}) K_{j3k3} \right)e_2$

其中每一部分记为1⃝2⃝3⃝4⃝
其中$\textcolor{red}{K_{jk}}$ 指的是第$j$个长方体的第$k$个关键点(其中定义了26个关键点,包括6个面的中心,8个顶点和12条边的中心点)其中$a_{i1}, a_{i2} \in [0,1]$, $c_i, r_i \in \mathbb{R}$, $s_{i1}, s_{i2} \in \{-1,0,1\}$, $j_1, j_2, j_3 \leq i$, $K_{j_k}$ $e1,e2$表示x,y,z三轴上的单位向量$[1,0,0]^T,[0,1,0]^T,[0,0,1]^T$

1⃝代表一个固定的偏移量
2⃝代表被一个参数控制的偏移量
3⃝代表$b_i$与第$j$个长方体有关
4⃝代表$b_i$与两个关键点之间的连线有关
仅有$a_{i1},a_{i2}$是参数,其他变量都定义在模版配置文件中。某一类物体的参数是固定的。当$b_i==1$时,整个方程是可微的

若要计算$K_{jk}$ 遵循以下的流程:
1)计算第$J$个长方体的变换矩阵M
2)找到单位长方体上的第k个关键点坐标$p_k$
3)应用变换矩阵得到$K_{jk}=M_jp_k$
配置与表达式参数关系如下所示

Code ci ri si1 j1 k1 e1 si2 j2 k2 j3 k3 e2 Npara
const: 0.5 0.5 0 0 0 0 0 0 0 0 0 0 0 0
range: [-1,0,1] -1 2 0 0 0 0 0 0 0 0 0 0 1
relate: -C.k4.x 0 0 -1 #C 4 x 0 0 0 0 0 0 0
range: [-1,0,1] const: 0.5 -0.5 2 0 0 0 0 0 0 0 0 0 0 1
range: [-1,0,1] relate: B.k9.y -1 2 1 #B 9 y 0 0 0 0 0 0 1
line: p1:D.k2,p2:E.k6 relate:line.z 0 0 0 0 0 0 1 #D 2 #E 6 z 1

2) 模版配置:image
由如上图Fig.3模版语言表示 包括两个长方体之间的各种联系

C. 模版自动配置流程

1) 将长方体由长方体参数转换为前文所讲的”stick”参数
1) 找到长方体最长的轴
2) 找到上述长方体的两个控制点
2) 检查每一个长方体是否轴对称
1) 将长方体标准化到坐标轴中心
2) 对YZ XZ XY平面做翻转
3) 计算翻转后的长方体与原始长方体的距离
4) 若此距离低于某个设定好的阈值,则此长方体是对称的
5) 用图3中的关系3来表示这种关系
3) 检查每个长方体是否与其他长方体轴对称,与自轴对称检查方法类似
4) 寻找每个长方体之间的交点
1) 取长方体的一个控制点
2) 计算控制点和其他所有长方体关键点的距离
3) 找到距离最小的关键点
4) 如果两点之间距离小于一定的阈值,则认为两个长方体之间有连接关系
5) 用图3中的关系1来表示这种关系
每个长方体会根据PartNet数据集中语义层级关系进行标注。

D. 点云重建细节

编码器使用PointNet++ 解码器使用简单MLP结构 输出1024维的特征
解码器层特征数量分别为1024 512 256 以及$N_a(参数数量)$通过MSEloss计算预测的参数与真实值之间的差距
1) 根据输入点云重建结构
1) 利用重建网络预测长方体参数
2) 通过可微模版生成表示结构的长方体
2) 重建立方体内部细节
1) 根据每个立方体的位置分割原始点云
2) 将分割好的点云标准化到单位立方体中
3) 将点云投影到三视图平面
4) 对2D投影做Alpha Shape算法 计算其广义凸包 即形状边界
5) 讲这些边界存储当做立方体的细节
3) 利用最开始提到的算法A来重建mesh网格

E. 生成细节

1) 结构生成:
1) 文章使用VAE作为backbone,利用简单的MLP作为生成手段。编码器输入为原始的形状参数,输出则为预测的有32个通道的高斯分布均值µ以及方差σ 。编码器MLP每层的通道数为$N_{para}$,512,256,128,128,128,32。解码器则为32,128.256,256,256,$N_{para}$.其中$N_{para}$为待预测的参数长度。
2) 和标准VAE思路相同,进行生成任务时,我们从标准高斯分布中sample出$C_{norm}$并且通过公式$C_{real}= σ*C_{norm} + µ$作为判别器的输入,判别器用于评估输入真实性的概率。loss也和标准VAE的思路相同包括:
1) 参数重建的MSE loss
2) 隐含特征与标准高斯分布的KL散度loss
2) 细节生成:
1) 文章对每个cuboid的边界生成了分辨率为128×128的二进制图像,在细节内部为1 细节外部为0。
2) 同样使用了VAE对边界特征图做生成工作,其中编码器每层通道数为128 × 128, 1024,1024,512,256,256,128 解码器则为128,256,256,512,1024,1024,128 × 128,重建损失与结构生成中VAE类似
3) 整体生成完整流程为:
1) 生成一个长方体的参数
2) 根据参数生成对应的长方体
3) 生成长方体的细节
1) 为长方体生成三视图
2) 找到生成的图片的边界并将其作为三视图绘制细节
3) 如果该长方体与某个长方体对称,则不需要生成,将对应长方体三视图翻转即可
4) 用最开始提到的算法A来重建mesh网络

我的总结 limitations
  1. 文章对于一个物体表示需要依赖的参数过多
  2. 同1的结论,训练依赖大量的标注数据。和作者在自己limitation中写到的一样,可以有更少数据的无监督方案
  3. 由于参数非常细致,对于结构依赖过于苛刻,甚至将单独的椅子分类都再次分出了5种
  4. 需要训练多个网络完成整个流程
阅读全文

着色器Shaders

OpenGL 2024/10/11

GSGL


着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。

着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。

一个典型的着色器有下面的结构:

#version version_number 
in type in_variable_name; 
in type in_variable_name; 
out type out_variable_name; 
uniform type uniform_name; 
void main() 
{ 
    // 处理输入并进行一些图形操作 
    ... 
    // 输出处理过的结果到输出变量 
    out_variable_name = weird_stuff_we_processed; 
}

数据类型


GSGL中有两种容器类型向量Vector和矩阵Matrix,这里先讨论向量类型

向量

GLSL中的向量是一个可以包含有2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(n代表分量的数量):

类型 含义
vecn 包含n个float分量的默认向量
bvecn 包含n个bool分量的向量
ivecn 包含n个int分量的向量
uvecn 包含n个unsigned int分量的向量
dvecn 包含n个double分量的向量

输入与输出


GSGL 通过定义inout关键字实现每个着色器的输入与输出。只要输出变量与下一个着色器的输入变量相匹配,流水线就会传递下去。

其中,顶点着色器接受的是顶点数据的输入而非任意着色器的输出。我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。layout (location = 0)顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。

同样,片段着色器的输出需要一个vec4的颜色输出变量。

顶点着色器

#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0

out vec4 vertexColor; // 为片段着色器指定一个颜色输出

void main()
{
    gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}

片段着色器

#version 330 core
out vec4 FragColor;

in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)

void main()
{
    FragColor = vertexColor;
}

Uniform


Uniform类型类似于我们常用的全局变量,但是他的作用范围并不仅仅局限于一个代码文件中。他在整个着色器程序(Program)中是独一无二的,同时可以被着色器程序任意阶段所访问。

要使用Uniform 首先需要在GLSL程序中定义它(==任意着色器均可==)
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量

这里我们希望在我们的C程序中实时修改它,因此需要在主循环中获取它的索引并更新它。

float timeValue = glfwGetTime(); 
float greenValue = (sin(timeValue) / 2.0f) + 0.5f; 
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUseProgram(shaderProgram); 
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

注意到glUniform这个方法 由于opengl是C编写的 因此没有重载。入参变化后只能通过另一个函数实现
因为OpenGL在其核心是一个C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数;glUniform是一个典型例子。这个函数有一个特定的后缀,标识设定的uniform的类型。可能的后缀有:

后缀 含义
f 函数需要一个float作为它的值
i 函数需要一个int作为它的值
ui 函数需要一个unsigned int作为它的值
3f 函数需要3个float作为它的值
fv 函数需要一个float向量/数组作为它的值

更多属性!


之前我们的vertices数组仅有顶点属性,在这里我们可以把颜色属性也添加进去

float vertices[] = { 
    // 位置              // 颜色 
    0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f, // 右下 
    -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下 
    0.0f, 0.5f, 0.0f,   0.0f, 0.0f, 1.0f // 顶部 
};

调整了顶点数组后,同时我们要调整顶点着色器的入参,能够接收这样的顶点属性输入

#version 330 core 
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
out vec3 ourColor; // 向片段着色器输出一个颜色 
void main() 
{ 
    gl_Position = vec4(aPos, 1.0); 
    ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色 
}
阅读全文

Camera Code

OpenGL 2024/10/11

以下是完整的可长按鼠标右键控制旋转的常见FPS相机完整代码
Main.cpp

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include <iostream>
#include <fstream>

#include <thread>
#include <chrono>

#include "shader.h"
#include "texture.h"
#include "camera.h"

using std::cout;
using std::endl;

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window, int rateLocation, float rate, Camera *camera);
void mouseCallBack(GLFWwindow* window, double xpos, double ypos);
void scrollCallBack(GLFWwindow* window, double xoffset, double yoffset);

//硬编码的顶点着色器 包括位置旋转等操作
const char* vertexShaderSource = "./shaders/VertexShader.glsl";
//硬编码的片段着色器 主要是渲染颜色
const char* fragmentShaderSource = "./shaders/FragmentShader.glsl";


// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;


float deltaTime = 0.0f; // 当前帧与上一帧的时间差
float lastFrame = 0.0f; // 上一帧的时间


int main()
{

    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // ready for render

    float vertices[] = {
    -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
     0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
     0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
     0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
    -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

    -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
     0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
     0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
    -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
    -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

    -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
    -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
    -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
    -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

     0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
     0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
     0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
     0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
     0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

    -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
     0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
     0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
     0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
    -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

    -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
     0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
     0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
    -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
    -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };

    glm::vec3 cubePositions[] = {
        glm::vec3(0.0f,  0.0f,  0.0f),
        glm::vec3(2.0f,  5.0f, -15.0f),
        glm::vec3(-1.5f, -2.2f, -2.5f),
        glm::vec3(-3.8f, -2.0f, -12.3f),
        glm::vec3(2.4f, -0.4f, -3.5f),
        glm::vec3(-1.7f,  3.0f, -7.5f),
        glm::vec3(1.3f, -2.0f, -2.5f),
        glm::vec3(1.5f,  2.0f, -2.5f),
        glm::vec3(1.5f,  0.2f, -1.5f),
        glm::vec3(-1.3f,  1.0f, -1.5f)
    };
    //定义并生成一个VAO 存储VBO的链表结构
    unsigned int VAO;
    glGenVertexArrays(1, &VAO);

    //绑定VAO
    glBindVertexArray(VAO);
    // 定义并生成一个顶点缓冲对象 通过无符号整数引用
    unsigned int VBO;
    glGenBuffers(1, &VBO);
    // 绑定新生成的顶点缓冲对象
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // 顶点数据复制到缓冲中
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // 编辑顶点属性
    // 1. 设置顶点属性指针
    //链接顶点属性 (向顶点着色器指定输入
    // 1、顶点着色器中定义的location
    // 2、顶点缓冲的长度
    // 3、顶点数据的类型
    // 4、数据是否标准化(即映射到标准化设备坐标中)
    // 5、连续顶点属性组之间的间隔
    // 6、初始顶点在缓冲中距离地址最开始的偏移量
    // 0. 复制顶点数组到缓冲中供OpenGL使用
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3* sizeof(float)));
    glEnableVertexAttribArray(1);


    Shader shader = Shader(vertexShaderSource, fragmentShaderSource);
    Texture texture1 = Texture("123.png", GL_RGB);
    Texture texture2 = Texture("awesomeface.png",GL_RGBA);

    shader.use();
    shader.setInt("ourTexture", 0);
    shader.setInt("secondTexture", 1);

    // 定义第二个图片的初始透明度
    int rateLocation = glGetUniformLocation(shader.ID, "rate");
    glUniform1f(rateLocation, 0.2f);

    // 定义初始的位移矩阵为单位矩阵
    glm::mat4 trans = glm::mat4(1.0f);
    int transLocation = glGetUniformLocation(shader.ID, "transform");
    glUniformMatrix4fv(transLocation, 1, GL_FALSE, glm::value_ptr(trans));

    //相机作为观察矩阵 不每次初始化
    Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        //计算设备每一帧渲染的时间,乘以速度获取更流畅的动画
        float currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        //Z缓冲 保证透视正常
        glEnable(GL_DEPTH_TEST);
        // input
        // -----        

        float ratevalue;
        glGetUniformfv(shader.ID, rateLocation, &ratevalue);

        float transMatrix[16];
        glGetUniformfv(shader.ID, transLocation, transMatrix);
        processInput(window, rateLocation, ratevalue, &camera);
        glfwSetWindowUserPointer(window, &camera);
        glfwSetCursorPosCallback(window, mouseCallBack);
        glfwSetScrollCallback(window, scrollCallBack);
        shader.setMat4("view", camera.GetViewMatrix());
        glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
        shader.setMat4("projection", projection);

        // render a verticle
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture1.ID);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, texture2.ID);

        glBindVertexArray(VAO);

        for (unsigned int i = 0; i < 10; i++)
        {
            glm::mat4 model = glm::mat4(1.0f);
            model = glm::translate(model, cubePositions[i]);
            float angle = 20.0f * (i + 1);
            model = glm::rotate(model, (float)glfwGetTime()*glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
            shader.setMat4("model", model);

            glDrawArrays(GL_TRIANGLES, 0, 36);
        }


        // opengl绘制图元方式
        /*glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);*/
        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow* window,int rateLocation, float rate, Camera *camera)
{
    float cameraSpeed = 1.0f * deltaTime;
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
    else if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
        rate = rate + 0.1 > 1.0 ? 1.0 : rate + 0.1;
        camera->ProcessKeyboard(FORWARD, deltaTime);
        glUniform1f(rateLocation, rate);
    }
    else if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) {
        camera->ProcessKeyboard(BACKWARD, deltaTime);
        glUniform1f(rateLocation, rate - 0.1);
    }
    else if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) {
        camera->ProcessKeyboard(LEFT, deltaTime);
        rate = rate - 0.1 < 0.0 ? 0.0 : rate - 0.1;
        glUniform1f(rateLocation, rate - 0.1);
    }
    else if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) {
        camera->ProcessKeyboard(RIGHT, deltaTime);
        rate = rate - 0.1 < 0.0 ? 0.0 : rate - 0.1;
        glUniform1f(rateLocation, rate - 0.1);
    }
}

bool firstMouse = true;
bool rightMouseDown = false;
float lastx = 400;
float lasty = 300;

void mouseCallBack(GLFWwindow* window, double xpos, double ypos)
{
    Camera* camera = (Camera*)glfwGetWindowUserPointer(window);
    if (firstMouse)
    {
        lastx = xpos;
        lasty = ypos;
        firstMouse = false;
    }
    if (rightMouseDown) {
        float xoffset = xpos - lastx;
        float yoffset = lasty - ypos;
        lastx = xpos;
        lasty = ypos;

        float sensitivity = 0.05f;
        xoffset *= sensitivity;
        yoffset *= sensitivity;
        camera->ProcessMouseMovement(xoffset, yoffset);
    }

    if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS) {
        rightMouseDown = true;
        lastx = xpos;
        lasty = ypos;
    }

    if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_RELEASE) {
        rightMouseDown = false;
    }
}


void scrollCallBack(GLFWwindow* window, double xoffset, double yoffset)
{
    Camera* camera = (Camera*)glfwGetWindowUserPointer(window);
    camera->ProcessMouseScroll(static_cast<float>(yoffset));
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

Camera.h

#ifndef CAMERA_H
#define CAMERA_H

#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

enum Camera_Movement {
    FORWARD,
    BACKWARD,
    LEFT,
    RIGHT
};

const float YAW = -90.0f;
const float PITCH = 0.0f;
const float SPEED = 2.5f;
const float SENSITIVITY = 0.5f;
const float ZOOM = 45.0f;


class Camera
{
    public:
    // camera Attributes
    glm::vec3 Position;
    glm::vec3 Front;
    glm::vec3 Up;
    glm::vec3 Right;
    glm::vec3 WorldUp;
    // euler Angles
    float Yaw;
    float Pitch;
    // camera options
    float MovementSpeed;
    float MouseSensitivity;
    float Zoom;

    // constructor with vectors
    Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), float yaw = YAW, float pitch = PITCH) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM)
    {
        Position = position;
        WorldUp = up;
        Yaw = yaw;
        Pitch = pitch;
        updateCameraVectors();
    }
    // constructor with scalar values
    Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM)
    {
        Position = glm::vec3(posX, posY, posZ);
        WorldUp = glm::vec3(upX, upY, upZ);
        Yaw = yaw;
        Pitch = pitch;
        updateCameraVectors();
    }

    // returns the view matrix calculated using Euler Angles and the LookAt Matrix
    glm::mat4 GetViewMatrix()
    {
        return glm::lookAt(Position, Position + Front, Up);
    }

    // processes input received from any keyboard-like input system. Accepts input parameter in the form of camera defined ENUM (to abstract it from windowing systems)
    void ProcessKeyboard(Camera_Movement direction, float deltaTime)
    {
        float velocity = MovementSpeed * deltaTime;
        if (direction == FORWARD)
            Position += Front * velocity;
        if (direction == BACKWARD)
            Position -= Front * velocity;
        if (direction == LEFT)
            Position -= Right * velocity;
        if (direction == RIGHT)
            Position += Right * velocity;
    }

    // processes input received from a mouse input system. Expects the offset value in both the x and y direction.
    void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = false)
    {
        xoffset *= MouseSensitivity;
        yoffset *= MouseSensitivity;

        Yaw += xoffset;
        Pitch += yoffset;

        // make sure that when pitch is out of bounds, screen doesn't get flipped
        if (constrainPitch)
        {
            if (Pitch > 89.0f)
                Pitch = 89.0f;
            if (Pitch < -89.0f)
                Pitch = -89.0f;
        }

        // update Front, Right and Up Vectors using the updated Euler angles
        updateCameraVectors();
    }

    // processes input received from a mouse scroll-wheel event. Only requires input on the vertical wheel-axis
    void ProcessMouseScroll(float yoffset)
    {
        Zoom -= (float)yoffset;
        if (Zoom < 1.0f)
            Zoom = 1.0f;
        if (Zoom > 45.0f)
            Zoom = 45.0f;
    }

private:
    // calculates the front vector from the Camera's (updated) Euler Angles
    void updateCameraVectors()
    {
        // calculate the new Front vector
        glm::vec3 front;
        front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));
        front.y = sin(glm::radians(Pitch));
        front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));
        Front = glm::normalize(front);
        // also re-calculate the Right and Up vector
        Right = glm::normalize(glm::cross(Front, WorldUp));  // normalize the vectors, because their length gets closer to 0 the more you look up or down which results in slower movement.
        Up = glm::normalize(glm::cross(Right, Front));
    }
};
#endif

VertexShader.glsl

#version 460 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 texCoord;

uniform mat4 transform;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = transform * projection * view * model * vec4(aPos, 1.0);
    texCoord = aTexCoord;
}
阅读全文

Shader Code

OpenGL 2024/10/11

#ifndef SHADER_H
#define SHADER_H

#include <glad/glad.h>
#include <string>
#include <iostream>
#include <fstream>
#include <sstream>

using std::string;
using std::ifstream;
using std::stringstream;
using std::cout;
using std::endl;

class Shader
{
    public:
        unsigned int ID;

        Shader(const char* vertexPath, const char* fragmentPath)
        {
            string vertexCode;
            string fragmentCode;
            ifstream vShaderFile;
            ifstream fShaderFile;

            vShaderFile.exceptions(ifstream::failbit | ifstream::badbit);
            fShaderFile.exceptions(ifstream::failbit | ifstream::badbit);

            try
            {
                // 打开文件
                vShaderFile.open(vertexPath);
                fShaderFile.open(fragmentPath);
                stringstream vShaderStream, fShaderStream;
                // 读取文件的缓冲内容到数据流中
                vShaderStream << vShaderFile.rdbuf();
                fShaderStream << fShaderFile.rdbuf();
                // 关闭文件处理器
                vShaderFile.close();
                fShaderFile.close();
                // 转换数据流到string
                vertexCode = vShaderStream.str();
                fragmentCode = fShaderStream.str();
            }
            catch (ifstream::failure e)
            {
                cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << endl;
            }
            const char* vShaderCode = vertexCode.c_str();
            const char* fShaderCode = fragmentCode.c_str();
            unsigned int vertex, fragment;
            int success;
            char infoLog[512];

            //顶点着色器
            vertex = glCreateShader(GL_VERTEX_SHADER);
            glShaderSource(vertex, 1, &vShaderCode, NULL);
            glCompileShader(vertex);

            checkCompileErrors(vertex, "SHADER");

            //片段着色器
            fragment = glCreateShader(GL_FRAGMENT_SHADER);
            glShaderSource(fragment, 1, &fShaderCode, NULL);
            glCompileShader(fragment);

            checkCompileErrors(fragment, "SHADER");

            ID = glCreateProgram();
            glAttachShader(ID, vertex);
            glAttachShader(ID, fragment);
            glLinkProgram(ID);

            checkCompileErrors(ID, "PROGRAM");

            glDeleteShader(vertex);
            glDeleteShader(fragment);
        }
        void use() {
            glUseProgram(ID);
        }

        void setBool(const string& name, bool value)
        {
            glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
        }

        void setInt(const std::string& name, int value) const
        {
            glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
        }

        void setFloat(const std::string& name, float value) const
        {
            glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
        }

    private:
        // errors
        void checkCompileErrors(unsigned int shader, std::string type)
        {
            int success;
            char infoLog[1024];
            if (type != "PROGRAM")
            {
                glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
                if (!success)
                {
                    glGetShaderInfoLog(shader, 1024, NULL, infoLog);
                    std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
                }
            }
            else
            {
                glGetProgramiv(shader, GL_LINK_STATUS, &success);
                if (!success)
                {
                    glGetProgramInfoLog(shader, 1024, NULL, infoLog);
                    std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
                }
            }
        }
};
#endif
阅读全文

纹理 Textures

OpenGL 2024/10/11

我们已经了解到,我们可以为每个顶点添加颜色来增加图形的细节,从而创建出有趣的图像。但是,如果想让图形看起来更真实,我们就必须有足够多的顶点,从而指定足够多的颜色。这将会产生很多额外开销,因为每个模型都会需求更多的顶点,每个顶点又需求一个颜色属性。

艺术家和程序员更喜欢使用纹理(Texture)。纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D的房子上,这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。

纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终止于(1, 1),即纹理图片的右上角。下面的图片展示了我们是如何把纹理坐标映射到三角形上的。

image

我们为三角形指定了3个纹理坐标点。如上图所示,我们希望三角形的左下角对应纹理的左下角,因此我们把三角形左下角顶点的纹理坐标设置为(0, 0);同理右下方的顶点设置为(1, 0);三角形的上顶点对应于图片的上中位置所以我们把它的纹理坐标设置为(0.5, 1.0)。我们只要给顶点着色器传递这三个纹理坐标就行了,接下来它们会被传到片段着色器中,它会为每个片段进行纹理坐标的插值。

just like this:

float texCoords[] = 
{ 
	0.0f, 0.0f, // 左下角 
	1.0f, 0.0f, // 右下角 
	0.5f, 1.0f // 上中 
};

纹理环绕方式


纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们基本上忽略浮点纹理坐标的整数部分),但OpenGL提供了更多的选择:

环绕方式 描述
GL_REPEAT 对纹理的默认行为。重复纹理图像。
GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。
GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色。
当纹理坐标超出默认范围时,每个选项都有不同的视觉效果输出。我们来看看这些纹理图像的例子:
image
前面提到的每个选项都可以使用glTexParameter*函数对单独的一个坐标轴设置(st(如果是使用3D纹理那么还有一个r)它们和xyz是等价的):
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);

第一个参数指定了纹理目标;我们使用的是2D纹理,因此纹理目标是GL_TEXTURE_2D。第二个参数需要我们指定设置的选项与应用的纹理轴。我们打算配置的是WRAP选项,并且指定ST轴。最后一个参数需要我们传递一个环绕方式(Wrapping),在这个例子中OpenGL会给当前激活的纹理设定纹理环绕方式为GL_MIRRORED_REPEAT。

如果我们选择GL_CLAMP_TO_BORDER选项,我们还需要指定一个边缘的颜色。这需要使用glTexParameter函数的fv后缀形式,用GL_TEXTURE_BORDER_COLOR作为它的选项,并且传递一个float数组作为边缘的颜色值:

float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

纹理过滤


个人理解为是一个采样方法,当纹理分辨率远小于物体大小时。通过某种方案补全像素的色彩。OpenGL提供了两种方案。

  • GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
  • image
  • GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
  • image
    以下是不同过滤方案的效果对比
    image

多级渐远纹理


想象一下,假设我们有一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。

OpenGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。让我们看一下多级渐远纹理是什么样子的:

image

手工为每个纹理图像创建一系列多级渐远纹理很麻烦,幸好OpenGL有一个glGenerateMipmap函数,在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作了。后面的教程中你会看到该如何使用它。

在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用NEAREST和LINEAR过滤。为了指定不同多级渐远纹理级别之间的过滤方式,你可以使用下面四个选项中的一个代替原有的过滤方式:

过滤方式 描述
GL_NEAREST_MIPMAP_NEAREST 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样
GL_LINEAR_MIPMAP_NEAREST 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
GL_NEAREST_MIPMAP_LINEAR 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
GL_LINEAR_MIPMAP_LINEAR 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样

就像纹理过滤一样,我们可以使用glTexParameteri将过滤方式设置为前面四种提到的方法之一:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

一个常见的错误是,将放大过滤的选项设置为多级渐远纹理过滤选项之一。这样没有任何效果,因为多级渐远纹理主要是使用在纹理被缩小的情况下的:纹理放大不会使用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。

生成纹理


有了上面的基础,生成纹理并应用在着色器的代码如下:

class Texture
{
	public:
		unsigned int ID;
		int width, height, nrChannels;

		Texture(const char* filePath)
		{			
			cout << filePath << endl;
			glGenTextures(1, &ID);
			glBindTexture(GL_TEXTURE_2D, ID);
			// 为当前绑定的纹理对象设置环绕方式
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
			// 纹理过滤方式
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
			// 加载并生成纹理
			stbi_set_flip_vertically_on_load(true);
			unsigned char* data = stbi_load(filePath, &width, &height, &nrChannels, 0);
			if (data)
			{
				glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
				glGenerateMipmap(GL_TEXTURE_2D);
			}
			else
			{
				cout << "Failed to load texture" << endl;
			}
			stbi_image_free(data);
		}
};	

以上内容定义了一个texture类,通过输入文件路径获得纹理对象,其实有很多是可以扩充的,比如读取图像的图像类型即可当做参数传入,仅仅GL_RGB无法读取有透明图层的图片。

	    float vertices[] = {
        //     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -
             0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   // 右上
             0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   // 右下
            -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // 左下
            -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f    // 左上
    };

有了纹理之后,我们必须告诉OpenGL如何采样纹理。因此需要在顶点缓冲中定义纹理坐标。
image
绑定方式类似,代码如下

glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2);

同样,我们的顶点着色器需要接收这个属性

#version 460 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 triColor;
out vec2 texCoord;
void main()
{
    gl_Position = vec4(aPos.x ,aPos.y,aPos.z, 1.0);
    triColor = aColor;
    texCoord = aTexCoord;
}

片段着色器中则通过OpenGL内建的sampler对象接收纹理。并通过glBindTexture(GL_TEXTURE_2D, texture);将纹理传递给这个uniform对象
之后会有如下效果:
image
我们也可以将顶点颜色和纹理相融合得到其他的效果FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);

纹理单元


作为一个uniform类型的变量,我们不需要使用glUniform给纹理赋值。我们可以调用glUniform1i为纹理采样器指定一个位置值(同一个片段着色器最多使用16个纹理单元)
因此当有多个纹理采样器时,绑定纹理之前需要激活纹理单元,指定我的纹理绑定哪个采样器。

glActiveTexture(GL_TEXTURE0); 
glBindTexture(GL_TEXTURE_2D, texture);

我们仍然需要编辑片段着色器来接收另一个采样器。这应该相对来说非常直接了:

#version 330 core
...

uniform sampler2D texture1;
uniform sampler2D texture2;

void main()
{
    FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
阅读全文
avatar
TyranitarX

事已至此,先睡觉吧