[Ray Tracing: The Next Week] 笔记

news2024/11/16 19:49:30

前言

本篇博客参照自《Ray Tracing: The Next Week》教程,地址为:https://raytracing.github.io/books/RayTracingTheNextWeek.html

该教程在ray tracing in one weekend的基础上,增加了运动模糊、BVH树、Texture映射、柏林噪声、光照、体积渲染等内容。

渲染器的构建过程

与我的上一篇系列笔记类似,我会顺序罗列我认为重要的部分。

运动模糊

这一点和散焦模糊有些类似,也是模仿真实相机拍摄到的现象,即,当相机的快门按下时,运动的物体会发生模糊。

要实现这一点,方法是,在发射采样光线时,令光线在快门的这一段时间内随机发射,再混合采样结果。为光线增加了一个时间t属性。

于此同时,为场景种添加相应的运动物体,比如运动的球体,在1秒的时间内,球体会在某一个方向上移动。这时,因为光线包含时间属性,t时刻的光线r会打到在该时刻对应位置的物体表面上。

最后渲染时,则是将在快门这一时间段内所有的光线追踪结果混合。

以下是运动模糊的渲染结果:

在这里插入图片描述

BVH树

BVH树是一种空间划分的数据结构,可以有效提高光线与物体求交的效率。

在该教程中,BVH树的建立分为以下几步:

  1. 将所有图元包裹在一个大的包围盒中。

  2. 进行BVH树的划分。

  3. 一直分到BVH树的叶子节点只有一个图元为止。

作者在划分BVH树时,不希望留下空子树的问题,因此,在划分子树时,若当前只剩下一个图元,文中会令这个图元既属于左子树又属于右子树。另外,在划分过程中,对于[begin, end]范围内的图元,中点为mid,文中会将[begin, mid]划归左子树,[mid, end]划归右子树,相当于mid这个位置的图元既属于左子树又属于右子树。这种划分方式属于作者的喜好,有它的优势。

一个BVH树的例子如下:

在这里插入图片描述

包围盒

BVH树采用的包围盒应当便于计算相交,且紧密,AABB包围盒符合这个条件。

三维场景内的AABB包围盒为包含目标图元,且边平行于坐标轴的最小六面体。

如何计算射线与AABB包围盒的相交?拿二维场景举例,如下图所示,射线在与x的两个边界相交时,对应的相交位置为t1、t2,射线在与y的两个边界相交时,对应的相交位置为t3、t4,如果射线与这个2D AABB包围盒相交,则t1、t2与t3、t4 的范围重叠。

在这里插入图片描述

三维空间中与上述类似,当射线与x、y、z方向的对应平面相交时,如果范围有重合,则说明射线与包围盒相交。

如何求解射线与对应平面的交点?因为边界都是轴对齐的,将射线的方程带入对应边界的x、y或z值即可。

BVH树的划分

BVH树的划分主要分为三个步骤:

  1. 随机选择一个轴
  2. 对BVH树种的图元进行排序
  3. 对这些图元进行二分,构建子BVH树

文中给出的方法是每次随机选出一个轴,当然也可以轮流选择x、y、z轴。在排序时,依据的排序规则是每个图元对应轴的最小值。在C++中,可以构建box_x_compare、box_y_compare、box_z_compare,然后调用sort方法对数组进行排序。

最终,按照文中给出的BVH树构建方法,光线追踪的渲染过程有所加速。

纹理映射

在光线追踪中,纹理映射是一个反向寻找的过程,即通过射线与物体的交点,反向推算出交点在纹理中的颜色、凹凸等其他属性。这和光栅化不同,光栅化是通过纹理获得物体所有表面的颜色等属性,然后通过光栅化的过程投射给图像。

文中介绍了几种纹理,分别是纯色纹理、空间纹理、以及利用uv坐标的纹理。

纯色纹理。与uv坐标和空间坐标都无关,利用纯色纹理的材质,物体表面的所有颜色都一样。

空间纹理。采样和物体表面的空间坐标有关,和uv坐标无关,即物体表面某一点的颜色由该点的世界坐标(x, y, z)计算得出。

方格纹理就是一种空间纹理,对于任意一点,其颜色的计算是这样的:对于坐标(x, y, z),首先向下取整,得到三个整数结果(⌊x⌋,⌊y⌋,⌊z⌋),将这三个结果相加并进行模2运算,得到0或1。如果结果为0将映射到偶数颜色,如果结果为1将映射到奇数颜色。

方格纹理的一个例子如下:

在这里插入图片描述

利用uv坐标的纹理。需要知道如何根据物体表面的三维坐标,计算uv纹理坐标,再利用纹理坐标做颜色运算。因为场景中目前提供的模型都是球体,且目前也不涉及旋转,因此这里的uv坐标直接沿着球面计算。

球面的uv坐标用经纬度表示,其中u = Φ / 2π,v = θ / π,Φ为对应点在单位球中的经度,θ为对应点在单位球中的纬度。球面上点的三维关系与经纬度的关系如下:
y = − c o s ( θ ) x = − c o s ( ϕ ) s i n ( θ ) z = s i n ( ϕ ) s i n ( θ ) y = -cos(\theta)\\ x = -cos(\phi)sin(\theta)\\ z = sin(\phi)sin(\theta) y=cos(θ)x=cos(ϕ)sin(θ)z=sin(ϕ)sin(θ)
从而可得
ϕ = a t a n 2 ( − z , x ) + π θ = a r c c o s ( − y ) \phi = atan2(-z, x) + \pi\\ \theta = arccos(-y) ϕ=atan2(z,x)+πθ=arccos(y)
注意,这里因为atan2的数值范围为-π到π,但是为了让u映射到[0, 1]范围,需要加上一个π。

利用uv坐标的纹理效果如下:

在这里插入图片描述

柏林噪声

柏林噪声是一种自然噪声生成算法。文中主要利用柏林噪声生成空间纹理。

教程中一步步地给出3D柏林噪声的迭代改进过程。

第零次迭代,按照上述方格纹理的思路,首先生成一个大小为256的double数组ranfloat,其中的每个值为从0到1的随机。对于物体表面的每一个点,根据其三维坐标(x, y, z),得出一个指向ranfloat的下标,并根据这个下标,计算出方格的灰阶。其计算代码如下:

int i = static_cast<int>(4 * x) % 256;
int j = static_cast<int>(4 * y) % 256;
int k = static_cast<int>(4 * z) % 256;

int index = i ^ j ^ k;
double grayscale = ranfloat[index];

这一步可以得出一种类似瓦片的效果,这种纹理是重复的。

在这里插入图片描述

第一次迭代,在上述瓦片贴图的基础上,加一些随机,具体到计算,主要是对上述代码中的i,j,k进行随机。这一部分的计算代码如下:

static int* perlin_generate_perm(){
    int p = new int[256];
    for(int i = 0;i<256;i++) p[i] = i;
    
  	for(int i = 255;i>0;i--){
        int target = random_int(0, i); // 返回一个[0, i]范围的整数
        swap(p[i], p[target]);
    }
    return p;
}
// noise函数内部
{
    ...
    int* perm_x = perlin_generate_perm();
    int* perm_y = perlin_generate_perm();
    int* perm_z = perlin_generate_perm();
    i = perm_x[i];
    j = perm_y[j];
    k = perm_z[k];
    ...
}

这一步的纹理效果:

在这里插入图片描述

第二次迭代,在生成噪声时,进行线性插值。这一步是将一个点周围8个位置的值做平均加权运算,得出当前位置的灰阶。其核心计算代码如下:

static double trilinear_interp(double c[2][2][2], double u, double v, double w){
    double accum = 0.0;
    for (int i=0; i < 2; i++)
            for (int j=0; j < 2; j++)
                for (int k=0; k < 2; k++)
                    accum += (i*u + (1-i)*(1-u))*
                            (j*v + (1-j)*(1-v))*
                            (k*w + (1-k)*(1-w))*c[i][j][k];
	return accum;
}

// noise函数内部
{
    ...
    int u = x - floor(x); // floor为向下取整运算
    int v = y - floor(y);
    int w = z - floor(z);

    int i = static_cast<int>(floor(x));
    int j = static_cast<int>(floor(y));
    int k = static_cast<int>(floor(z));
    double c[2][2][2];

    for(int di=0;di < 2;di++){
        for(int dj=0;dj<2;dj++){
            for(int dk=0;dk<2;dk++){
                c[di][dj][dk] = ranfloat[
                    perm_x[(i+di) % 256] ^
                    perm_y[(j+dj) % 256] ^
                    perm_z[(k+dk) % 256]
                ];
            }
        }
    }
    
    double grayscale = trilinear_interp(c, u, v, w);
	...
}

此时得出的结果:

在这里插入图片描述

第三次迭代,采用Hermitian插值,而不是线性插值来得到uvw的值,让结果更平滑,消除上述明显的网格特征。添加代码:

u = u*u*(3-2*u);
v = v*v*(3-2*v);
w = w*w*(3-2*w);

这一步得出的结果:

在这里插入图片描述

第四次迭代,调整噪声贴图的频率,方法就是将一个比例系数scale乘以(x, y, z)坐标值,scale越大,频率就越大。我的理解是,这种噪声的生成有一个模式,每经过一定的间隔就会得出相似的噪声结果。把坐标值增大,再来采样,等效于将模式的间隔缩小,这样导致的结果便是噪声图看起来频率更高。

这一步得出的效果如下:

在这里插入图片描述

第五次迭代,上面的结果看起来仍然有些块状,或许是因为这种模式的最小值和最大值总是恰好落在(x, y, z)都为整数的坐标上。因为当(x, y, z)为整数时,u, v, w的值均为0,此时trilinear_interp的结果为c[0][0][0],等于这个点没有和周围做平均运算。

可以用随机vec3数组ranvec代替之前的随机double数组ranfloat进行采样。在加权平均时,用权重向量和周围点的随机向量做点积,累加获得当前点的采样值。这样可以避免最小和最大值总是恰好落在整数坐标上。由于乘出来的值有可能小于零,需要做0.5 * (grayscale + 1.0)的归一化操作。

这一步的核心代码如下:

static double perlin_interp(vec3 c[2][2][2], double u, double v, double w) {
	double uu = u*u*(3-2*u);
    double vv = v*v*(3-2*v);
    double ww = w*w*(3-2*w);
    double accum = 0.0;

    for (int i=0; i < 2; i++)
        for (int j=0; j < 2; j++)
            for (int k=0; k < 2; k++) {
                vec3 weight_v(u-i, v-j, w-k);
                accum += (i*uu + (1-i)*(1-uu))
                       * (j*vv + (1-j)*(1-vv))
                       * (k*ww + (1-k)*(1-ww))
                       * dot(c[i][j][k], weight_v);
            }

    return accum;
}
// noise函数内部
{
    ...
    double u = x - floor(x);
    double v = y - floor(y);
    double w = z - floor(z);
    int i = static_cast<int>(floor(x));
    int j = static_cast<int>(floor(y));
    int k = static_cast<int>(floor(z));
    vec3 c[2][2][2];
    
    for (int di=0; di < 2; di++)
            for (int dj=0; dj < 2; dj++)
                for (int dk=0; dk < 2; dk++)
                    c[di][dj][dk] = ranvec[
                        perm_x[(i+di) % 256] ^
                        perm_y[(j+dj) % 256] ^
                        perm_z[(k+dk) % 256]
                    ];
    double grayscale = perlin_interp(c, u, v, w);
    ...
}

这一步的结果如下图:

在这里插入图片描述

第六次迭代,制造湍流效果。将多个不同频率的噪声以不同的权重相加,这就是湍流。湍流部分的计算代码如下:

double turb(const point3& p, int depth=7) const {
    double accum = 0.0;
    double temp_p = p;
    double weight = 1.0;

    for (int i = 0; i < depth; i++) {
        accum += weight*noise(temp_p);
        weight *= 0.5;
        temp_p *= 2;
    }

    return fabs(accum);
}

这一步得出的效果如下:

注:这里的噪声图颜色很深,是因为这里的效果没有做归一化操作,而是直接把小于零的结果直接取绝对值了。

在这里插入图片描述

第七次,最后一次迭代,调整相位。将得出的灰度值送入正弦函数做计算,可以得到起伏的结果。利用相位迭代,可以实现类似大理石表面的纹理效果。这一步的核心代码如下:

double s = scale * p;
color = color(1,1,1) * 0.5 * (1 + sin(s.z() + 10*noise.turb(s)));

效果图如下:

在这里插入图片描述

平行四边形

除了球体之外,教程中终于添加了另外一种图元:平行四边形。

平行四边形用一个基点和两个向量表示:基点Q、两个边向量u和v。

如何实现射线与平行四边形的求交?主要分三步。

  1. 找到包含这个平行四边形的平面。

  2. 判断射线与这个平面的相交情况。

  3. 判断交点是否在这个平行四边形内。

找到四边形的平面

可以用u和v的叉乘得出平面法线,再将基点Q带入,便可得出这个平面方程。

判断射线与这个平面的相交

射线由一个基点和一个方向向量表示,判断射线与这个平面的相交,可以将射线的方程带入平面的方程。如果射线与平面平行或共面,则认为不相交,如果不平面,则求出这个交点,判断是否在射线的有效范围内。我们可以得到以下的式子。
平面方程: A x + B y + C z = D  射线方程: R ( t ) = P + t d 代入可得: n ∗ ( P + t d ) = D 解方程: n ∗ P + n ∗ t d = D n ∗ P + t ( n ∗ d ) = D t = D − n ∗ P n ∗ d 平面方程:Ax + By + Cz = D \ 射线方程:R(t) = \mathbf P + t\mathbf d\\ 代入可得:\mathbf n * (\mathbf P + t\mathbf d) = D \\ 解方程:\mathbf n *\mathbf P +\mathbf n * t\mathbf d = D\\ \mathbf n *\mathbf P + t(\mathbf n *\mathbf d) = D\\ t = \frac{D - \mathbf n * \mathbf P}{\mathbf n * \mathbf d} 平面方程:Ax+By+Cz=D 射线方程:R(t)=P+td代入可得:n(P+td)=D解方程:nP+ntd=DnP+t(nd)=Dt=ndDnP
如果t属于射线范围[t_min, t_max]之内,则认为射线与这个平面相交。

判断交点是否在这个平行四边形内

将平面的点转换成以u、v为基底的二维坐标系中,对于任意一个点P,可以表示为P = Q + αu + βv。做以下运算:
令 p = P − Q = α u + β v , p 是从 Q 到 P 的向量 将 u 、 v 向量分别与 p 叉乘: v × p = α ( v × u ) + β ( v × v ) = α ( v × u ) u × p = α ( u × u ) + β ( u × v ) = β ( u × v ) 向量的除法不能直接进行,两边点乘 n n ∗ ( v × p ) = n ∗ α ( v × u ) n ∗ ( u × p ) = n ∗ β ( u × v ) 则 α = n ∗ ( v × p ) n ∗ ( v × u ) β = n ∗ ( u × p ) n ∗ ( u × v ) 令 w = n n ∗ ( u × v ) = n n ∗ n α = w ∗ ( p × v ) β = w ∗ ( u × p ) 令 p =\mathbf P -\mathbf Q = \alpha \mathbf u + \beta \mathbf v ,p是从Q到P的向量 \\ \\将u、v向量分别与p叉乘:\\ \mathbf v \times \mathbf p = \alpha (\mathbf v \times \mathbf u) + \beta (\mathbf v \times \mathbf v) = \alpha (\mathbf v \times \mathbf u)\\ \mathbf u \times \mathbf p = \alpha (\mathbf u \times \mathbf u) + \beta (\mathbf u \times \mathbf v) = \beta (\mathbf u \times \mathbf v)\\ \\ 向量的除法不能直接进行,两边点乘n\\ \mathbf n * (\mathbf v \times \mathbf p) = \mathbf n * \alpha (\mathbf v \times \mathbf u)\\ \mathbf n * (\mathbf u \times \mathbf p) = \mathbf n * \beta (\mathbf u \times \mathbf v)\\则 \\ \alpha = \frac{\mathbf n * (\mathbf v \times \mathbf p)}{\mathbf n * (\mathbf v \times \mathbf u)}\\ \beta = \frac{\mathbf n * (\mathbf u \times \mathbf p)}{\mathbf n * (\mathbf u \times \mathbf v)}\\ \\ 令 w = \frac{\mathbf n}{\mathbf n * (\mathbf u \times \mathbf v)} = \frac{\mathbf n}{\mathbf n * \mathbf n} \\ \alpha = \mathbf w * (\mathbf p \times \mathbf v)\\ \beta = \mathbf w * (\mathbf u \times \mathbf p) p=PQ=αu+βvp是从QP的向量uv向量分别与p叉乘:v×p=α(v×u)+β(v×v)=α(v×u)u×p=α(u×u)+β(u×v)=β(u×v)向量的除法不能直接进行,两边点乘nn(v×p)=nα(v×u)n(u×p)=nβ(u×v)α=n(v×u)n(v×p)β=n(u×v)n(u×p)w=n(u×v)n=nnnα=w(p×v)β=w(u×p)
要判断点是否在平行四边形中,判断α和β在[0, 1]范围内即可。

平行四边形的绘制结果:

在这里插入图片描述

灯光

文中将能发光的特性作为一个灯光材质。

灯光材质会有一个主动发光的函数,这样在光线打到灯光材质表面时,会增加一项主动发光所产生的颜色。对于不发光的材质而言,主动发光产生的颜色为零。

同时设置背景颜色,当射线最终没有打到物体时,赋予默认背景颜色,而不是根据画布位置得出的渐变色。

得到的灯光效果如下:

在这里插入图片描述

实例化

这一部分主要引入了模型的移动和旋转。

所谓实例是已放置到场景中的几何图元的副本,它完全独立于图元的其他副本,并且可以移动或旋转。

模型移动

在这个过程的实现中,文中没有直接移动模型的坐标,而是反向变换光线的原点位置,然后与模型做求交运算。具体而言,主要分为以下三步:

  1. 将射线的原点反向移动偏移量

  2. 判断添加偏移后的射线是否与物体存在交点(如果存在,判断在何处)

  3. 给交点的位置增加偏移量

这一过程也可以从坐标变化的角度来思考:

  1. 将射线从世界坐标系转换到物体局部坐标系
  2. 判断射线在物体坐标系内是否与物体存在交点(如果存在,判断在何处)
  3. 将交点从物体局部坐标系转换到世界坐标系

文中将移动封装成了一个可击中对象,相当于构建了一个实例,这部分的代码如下:

class translate : public hittable {
  public:
    bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
        // 将射线的原点反向移动偏移量
        ray offset_r(r.origin() - offset, r.direction(), r.time());

        // 判断添加偏移后的射线是否与物体存在交点(如果存在,判断在何处)
        if (!object->hit(offset_r, ray_t, rec))
            return false;

        // 给交点的位置增加偏移量
        rec.p += offset;

        return true;
    }

  private:
    shared_ptr<hittable> object;
    vec3 offset;
};

模型旋转

模型的旋转与上述的构建过程类似,不过稍微复杂。

文中给出的旋转表示方法是欧拉角表示法。当射线变换到有旋转的物体坐标系时,不仅原点位置改变,射线的方向也会改变。这一过程如下:

  1. 将光线的原点和方向从世界坐标系变换到物体坐标系
  2. 判断光线在物体坐标系内与物体是否相交以及交点在何处
  3. 将交点的位置以及交点处的法线从物体坐标系变换到世界坐标系

旋转同样被封装成了一个可击中对象,相当于一个实例,这部分的核心代码如下:

class rotate_y : public hittable {
  public:

    bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
        // 将光线从世界坐标系转换到物体局部坐标系
        auto origin = r.origin();
        auto direction = r.direction();

        origin[0] = cos_theta*r.origin()[0] - sin_theta*r.origin()[2];
        origin[2] = sin_theta*r.origin()[0] + cos_theta*r.origin()[2];

        direction[0] = cos_theta*r.direction()[0] - sin_theta*r.direction()[2];
        direction[2] = sin_theta*r.direction()[0] + cos_theta*r.direction()[2];

        ray rotated_r(origin, direction, r.time());

        // 判断是否有交点以及交点在何处
        if (!object->hit(rotated_r, ray_t, rec))
            return false;

        // 将交点从物体局部坐标系转换到世界坐标系
        auto p = rec.p;
        p[0] =  cos_theta*rec.p[0] + sin_theta*rec.p[2];
        p[2] = -sin_theta*rec.p[0] + cos_theta*rec.p[2];

        // 将交点处的法线从物体局部坐标系转换到世界坐标系
        auto normal = rec.normal
        normal[0] =  cos_theta*rec.normal[0] + sin_theta*rec.normal[2];
        normal[2] = -sin_theta*rec.normal[0] + cos_theta*rec.normal[2];

        rec.p = p;
        rec.normal = normal;

        return true;
    }
};

运用平移和旋转,渲染出的cornell场景如下:

在这里插入图片描述

体积物体

这一部分将的是对于类似雾这种对象的光线追踪渲染,不过为了简便,文中只举例了密度恒定,且边界不变的情况。

光线在射向这类物体时,有概率直接穿透,也有概率散射开来。

在这里插入图片描述

对于这样的物体,文中主要设置了两种参数,一个是密度,另一个是边界。

在做射线求交运算时,会设置计算两个碰撞点,分别代表射入和射出的点,并根据密度参数和随机值,计算得出一个新的碰撞点,用来做计算。如果这个点不在体积内,则判定这个射线没有和物体相交,如果在体积内,则判定为相交,进行计算。

判定相交时,射入的光线会在碰撞点处随机散射。

这一步的渲染效果:在这里插入图片描述

最终效果

和上一部教程一样,作者给出了一个大场景,渲染出一个最终效果。

这里我为了能够尽快得出渲染结果,将采样次数降低,牺牲了质量,此时的渲染结果如下:

在这里插入图片描述

完整代码

这一次同样上传在网盘上:

链接:https://pan.baidu.com/s/1hspYR2VGNlxynRJYa9jHug?pwd=qyfj
提取码:qyfj
–来自百度网盘超级会员V6的分享

ps: 只分享了源码,没有什么依赖库,应该可以直接跑出ppm格式的图片。

参考

https://www.cnblogs.com/wickedpriest/p/12269564.html

https://baike.baidu.com/item/%E5%8C%85%E5%9B%B4%E7%9B%92/4562345

https://blog.csdn.net/weixin_44176696/article/details/118655688

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1339118.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前端文件在虚拟机,后端在本机,两个如何通信

前端文件在虚拟机&#xff0c;后端在本机&#xff0c;两个如何通信 如果前端的文件放在虚拟机里面&#xff0c;但是调用接口的后端在本地调试&#xff0c;如何做到在虚拟机中也能访问到本地的接口内容。 其实这个问题很简单&#xff0c;只要讲本地的IP和虚拟机中的IP结合就可…

在wps里导入Mathtype、改变字体

1 在wps里导入Mathtype 开发工具--加载项 2. 在“模板和加载项”窗口中再点击“添加” 3.找到mathtype安装路径下面的“OfficeSupprot”&#xff0c;这时会看到有“32”和“64”两个文件夹&#xff0c;分别对应WPS软件的系统&#xff08;任务管理器可以直接查看wps版本&#x…

k8s搭建(二、k8s组件安装)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

MySQL日期查询 今天、明天、本月、下月、星期、本周第一天、本周最后一天、本周七天日期

文章目录 今天日期明天日期本月第一天本月最后一天下个月第一天当前月已过几天当前月天数当前月所有日期获取星期本周第一天本周最后一天获取本周的七天日期 今天日期 select curdate()明天日期 select DATE_SUB(curdate(),INTERVAL -1 DAY) AS tomorrow本月第一天 select d…

c 语言,指针,指针的指针

c 语言&#xff0c;指针&#xff0c;指针的指针 指针就是指向变量地址的东西。 比如&#xff1a; 定义了一个 int 变量 p&#xff0c;值为 1 。定义了 int 指针 pInt 指向了变量 p&#xff0c; 它的名字前面有个 * &#xff0c;此时 pInt 就是 p 的地址&#xff0c;当前面加…

vue+element实现动态表格:根据后台返回的属性名和字段动态生成可变表格

现有一个胡萝卜厂生产不同品种的胡萝卜&#xff0c;为了便于客户了解产品&#xff0c;现需在官网展示胡萝卜信息。现有的萝卜信息&#xff1a;编号&#xff08;id&#xff09;、名称&#xff08;name&#xff09;、保质期&#xff08;age&#xff09;、特点&#xff08;remark&…

Python编程:计算当前日期是一年中的第几天

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 在Python中&#xff0c;计算当前日期是一年中的第几天可以通过内置的datetime模块来实现。这个任务涉及到日期处理和一些基本的数学计算。在本文中&#xff0c;将详细介绍如何使用Python编写代码来完成这个任务&…

视频压缩不影响画质简单方法,一分钟搞定!

很多朋友在处理视频的时候都会遇到视频过大的问题&#xff0c;想要压缩视频的同时不影响画质&#xff0c;简单的方法有两种。一种是用专业的压缩软件&#xff0c;在压缩的时候设置一个合适的压缩比例&#xff0c;压缩大小的同时保持清晰度&#xff0c;也能提高压缩率&#xff0…

机器人创新实验室任务三参考文档

一、JAVA环境配置 需要在Linux里面下载并且安装java。 sudo apt-get install openjdk-17-jre-headless 打开终端并且运行指令&#xff0c;用apt下载安装java。官方用的好像是java11&#xff0c;我安装的是java17。 如果无法定位软件安装包&#xff0c;可以试试更新一下 sudo …

ubuntu 在线安装 python3 pip

ubuntu 在线安装 python3 pip 安装 python3 pip sudo apt -y install python3 python3-pip升级 pip python3 -m pip install --upgrade pip

【零成本实现接口自动化测试】Java+TestNG 测试Restful service

接口自动化测试 – JavaTestNG 测试 Restful Web Service 关键词&#xff1a;基于Rest的Web服务&#xff0c;接口自动化测试&#xff0c;数据驱动测试&#xff0c;测试Restful Web Service&#xff0c; 数据分离&#xff0c;JavaMavenTestNG 本文主要介绍如何用Java针对Restf…

成都爱尔樊映川院长解析视网膜脱离之紧急72小时

熬夜工作、熬夜游戏…… 眼睛模糊到看不清? 突然眼前发生大量漂浮物? 眼前有块遮挡黑影? 眼前有闪光感? 这些情况虽然并非确诊视网膜发生脱离&#xff0c;但需要赶紧到院就诊&#xff0c;做到及时发现及时治疗。 视网膜脱离 视网膜脱离(简称“网脱”)是视网膜神经上皮层与色…

【并发设计模式】聊聊 基于Copy-on-Write模式下的CopyOnWriteArrayList

在并发编程领域&#xff0c;其实除了使用上一篇中的属性不可变。还有一种方式那就是针对读多写少的场景下。我们可以读不加锁&#xff0c;只针对于写操作进行加锁。本质上就是读写复制。读的直接读取&#xff0c;写的使用写一份数据的拷贝数据&#xff0c;然后进行写入。在将新…

基于Java (spring-boot)的超市管理系统

一、项目介绍 超市管理系统对商品日常维护、售卖、商品类别、存储记录、库存维护等功能的统计及整理。系统设计了系统管理员&#xff0c;销售人员&#xff0c;仓库管理员多个角色。通过多角色、多功能的使用&#xff0c;可以方便地查询商品的库存值、是否临期、是否库存紧张&am…

ubuntu20 安装缺失的字体

在/usr/share/fonts创建文件夹winfonts sudo mkdir winfonts 下载缺失的字体后&#xff0c;复制命令到对应的文件夹。 刷新字体库 sudo mkfontscale sudo mkfontdir sudo fc-cache

可视化远程监控EasyCVR及AI识别技术在种植养殖基地/果园场景中的应用建设

一、背景与需求 种植园、果园一般场地面积较大&#xff0c;看守人员较少&#xff0c;财产安全成为了关注的重点。传统靠人力监管的方式效率低下&#xff0c;管理也较为片面且人力投入大&#xff0c;那么如何实现少人化、智能化监管&#xff0c;助力果园安全监控、守护园区安全…

怎么用Facebook找客户?Facebook开发客户攻略分享

跨境人最了解的电商平台之一就是Facebook了&#xff0c;说到Facebook&#xff0c;它拥有超过20亿的活跃用户&#xff0c;所以你可以在这个平台上面找到更多的潜在客户。今天的文章就主要分享用Facebook开发客户的方法&#xff0c;全是干货&#xff0c;建议收藏&#xff01; 一、…

IPv4 VS IPv6代理全解,跨境电商代理IP选哪种?

IP&#xff08;互联网协议&#xff09;被网站用来收集用户位置信息。当今最流行的两种互联网协议是 IPv4 和 IPv6。 在我们选择IP代理的过程中可以看到IPv4、IPv6两种类型&#xff0c;他们有何区别&#xff1f;如何选择&#xff1f;下面为大家讲解&#xff01; 在这篇文章中&a…

全面理解Stable Diffusion采样器

全面理解Stable Diffusion采样器 原文&#xff1a;Stable Diffusion Samplers: A Comprehensive Guide 在 AUTOMATIC1111 的 SD webui 中&#xff0c;有许多采样器&#xff08;sampler&#xff09;&#xff0c;如 Euler a&#xff0c;Heun&#xff0c;DDIM&#xff0c;… 什么是…

.NET CORE 无法调试 当前不会命中断点

多个项目直接可以设置项目的属性->生成->输出的配置文件输出地址 然后路径统一输入该项目的bib/debug/.netcorex.x就可以了