当前位置: 首页 > article >正文

GAMES101:现代计算机图形学入门-作业五

作业五

这次作业给了许多脚本,我们现在可以把每个脚本的代码逐行细细分析一下。

main.cpp

#include "Scene.hpp"
#include "Sphere.hpp"
#include "Triangle.hpp"
#include "Light.hpp"
#include "Renderer.hpp"

// In the main function of the program, we create the scene (create objects and lights)
// as well as set the options for the render (image width and height, maximum recursion
// depth, field-of-view, etc.). We then call the render function().
int main()
{
    Scene scene(1280, 960);//设置场景屏幕尺寸

    auto sph1 = std::make_unique<Sphere>(Vector3f(-1, 0, -12), 2);//生成小球1//(Vector3f(-1, 0, -12), 2)中分别代表球心位置与半径
    sph1->materialType = DIFFUSE_AND_GLOSSY;//材质选择漫反射——Glossy
    sph1->diffuseColor = Vector3f(0.6, 0.7, 0.8);//设置color的RGB值

    auto sph2 = std::make_unique<Sphere>(Vector3f(0.5, -0.5, -8), 1.5);//生成小球2
    sph2->ior = 1.5;//ior代表材质的折射率
    sph2->materialType = REFLECTION_AND_REFRACTION;//材质选择反射_折射型

    scene.Add(std::move(sph1));
    scene.Add(std::move(sph2));//将小球添加到场景中

    Vector3f verts[4] = {{-5,-3,-6}, {5,-3,-6}, {5,-3,-16}, {-5,-3,-16}};//给了四个顶点
    uint32_t vertIndex[6] = {0, 1, 3, 1, 2, 3};//给了六个序号
    Vector2f st[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};//给了四个纹理坐标
    auto mesh = std::make_unique<MeshTriangle>(verts, vertIndex, 2, st);//生成了一个MeshTriangle类型的对象,接受参数:verts:顶点坐标数组,vertIndex:顶点索引数组,2:两个三角形,st:纹理坐标数组
    mesh->materialType = DIFFUSE_AND_GLOSSY;//同样定义材质

    scene.Add(std::move(mesh));//场景中添加这个网格
    scene.Add(std::make_unique<Light>(Vector3f(-20, 70, 20), 0.5));
    scene.Add(std::make_unique<Light>(Vector3f(30, 50, -12), 0.5));   //添加了两个点光源 

    Renderer r;//实例化渲染器
    r.Render(scene);//渲染器对场景进行渲染

    return 0;
}

每句代码的意义如注释所示,我们生成了两个材质不同的小球,一个是漫反射材质一个是反射材质;接着给定一系列顶点坐标与纹理坐标后,我们定义一个网格并添加到场景中,接着向场景中添加两个点光源,最后调用渲染器进行渲染。

这个脚本比较有意思的点在于std命名空间里的make_unique与move:

make_unique的作用在于创建一个unique_ptr指针,这种指针可以独享一个对象的权限,且智能地在超出作用域后释放多余的内存。

move的作用在于将对象转换成右值引用,这样可以将一个对象的所有权全部转移到另一个指针上而省去了拷贝的麻烦。

有关move与右值引用的概念相对来说比较复杂,详细地可以看这里:C++右值引用和std::move 阅读笔记 - 知乎 (zhihu.com)

 Renderer.cpp

这个脚本比较长,我们分开慢慢说:

#include <fstream>
#include "Vector.hpp"
#include "Renderer.hpp"
#include "Scene.hpp"
#include <optional>

inline float deg2rad(const float &deg)//将度(degree)转换为弧度制(radians)
{ return deg * M_PI/180.0; }

// Compute reflection direction
Vector3f reflect(const Vector3f &I, const Vector3f &N)//为我们写好了反射函数
{
    return I - 2 * dotProduct(I, N) * N;
}

// [comment]
// Compute refraction direction using Snell's law
//
// We need to handle with care the two possible situations:
//
//    - When the ray is inside the object
//
//    - When the ray is outside.
//
// If the ray is outside, you need to make cosi positive cosi = -N.I
//
// If the ray is inside, you need to invert the refractive indices and negate the normal N
// [/comment]
Vector3f refract(const Vector3f &I, const Vector3f &N, const float &ior)//折射
{
    float cosi = clamp(-1, 1, dotProduct(I, N));
    float etai = 1, etat = ior;
    Vector3f n = N;
    if (cosi < 0) { cosi = -cosi; } else { std::swap(etai, etat); n= -N; }
    float eta = etai / etat;
    float k = 1 - eta * eta * (1 - cosi * cosi);
    return k < 0 ? 0 : eta * I + (eta * cosi - sqrtf(k)) * n;
}

// [comment]
// Compute Fresnel equation
//
// \param I is the incident view direction
//
// \param N is the normal at the intersection point
//
// \param ior is the material refractive index
// [/comment]
float fresnel(const Vector3f &I, const Vector3f &N, const float &ior)//菲涅尔项
{
    float cosi = clamp(-1, 1, dotProduct(I, N));
    float etai = 1, etat = ior;
    if (cosi > 0) {  std::swap(etai, etat); }
    // Compute sini using Snell's law
    float sint = etai / etat * sqrtf(std::max(0.f, 1 - cosi * cosi));
    // Total internal reflection
    if (sint >= 1) {
        return 1;
    }
    else {
        float cost = sqrtf(std::max(0.f, 1 - sint * sint));
        cosi = fabsf(cosi);
        float Rs = ((etat * cosi) - (etai * cost)) / ((etat * cosi) + (etai * cost));
        float Rp = ((etai * cosi) - (etat * cost)) / ((etai * cosi) + (etat * cost));
        return (Rs * Rs + Rp * Rp) / 2;
    }
    // As a consequence of the conservation of energy, transmittance is given by:
    // kt = 1 - kr;

首先是一个内联的函数,用于将平时数学上讨论的度(degree)转换成弧度制(radians),有关内联函数(inline),大体上我们可以理解为我们稍微牺牲一点存储空间将一个可能会高度调用的小体量函数内嵌至调用点,这样可以提高这个函数调用的效率。主要用于针对哪些可能调用函数花费的时间大于执行函数的函数使用inline关键字。

接着是一个反射函数,本质上他的逻辑就是反射定理:入射角等于出射角,但是怎么得到这个代码的算式的呢?

I代表入射角,N代表法线方向

大体上就是如此~

接着是折射函数的部分,同样参数是入射角和法线,还多了一个折射率。

首先计算入射角和法线的夹角的余弦值且利用clamp函数将这个余弦值限制在[-1,1]之间。接着我们需要对这个余弦值进行一个判断:如果这个值小于0,证明光线是从外部射入内部(入射角大于180度证明入射角是与法线方向相悖,也就是从外部射向内部);所以如果我们法线这个余弦值大于0的话,我们需要转换法线的方向。

接着是对是否发生全反射的判断:我们需要计算两种介质的折射率之比,并利用折射定理:

这也就是上述代码的由来。

接着是菲涅尔项的推导:

综上所述,其实我们只用把入射介质和反射介质的n1,n2就可以得到菲涅尔项。

// [comment]
// Returns true if the ray intersects an object, false otherwise.
//
// \param orig is the ray origin
// \param dir is the ray direction
// \param objects is the list of objects the scene contains
// \param[out] tNear contains the distance to the cloesest intersected object.
// \param[out] index stores the index of the intersect triangle if the interesected object is a mesh.
// \param[out] uv stores the u and v barycentric coordinates of the intersected point
// \param[out] *hitObject stores the pointer to the intersected object (used to retrieve material information, etc.)
// \param isShadowRay is it a shadow ray. We can return from the function sooner as soon as we have found a hit.
// [/comment]
std::optional<hit_payload> trace(
        const Vector3f &orig, const Vector3f &dir,
        const std::vector<std::unique_ptr<Object> > &objects)//光线追踪函数//返回的类型std::optional<hit_payload>代表可能返回一个 hit_payload 对象,也可能返回 std::nullopt(表示没有交点)
{                                                            //接受的参数包括光源的起点,光的方向,以及场景内物体
    float tNear = kInfinity;
    std::optional<hit_payload> payload;//optional类型满足一个函数返回值或者不返回值,避免了裸指针的情况
    for (const auto & object : objects)
    {
        float tNearK = kInfinity;//初始化当前对象的最近交点距离为无穷大
        uint32_t indexK;//初始化当前对象的交点索引
        Vector2f uvK;//初始化当前对象的交点纹理坐标
        if (object->intersect(orig, dir, tNearK, indexK, uvK) && tNearK < tNear)//判断是否有交点,接受的参数包括光源坐标,光线方向,交点距离,交点索引,纹理坐标,判断目前的交点距离是否小于无穷大
        {
            payload.emplace();//填充payload
            payload->hit_obj = object.get();//完善payload的object
            payload->tNear = tNearK;//完善payload的交点距离
            payload->index = indexK;//完善payload的交点序号
            payload->uv = uvK;//完善payload的纹理坐标
            tNear = tNearK;//重新修正交点距离至无穷远
        }
    }

    return payload;
}

接着是有关追踪部分的代码:std::optional代表的是该类型的返回值可以有也可以是没有,从而避免裸指针的情况。

这段代码的大意我都写在了注释上,这段代码的意义就是判断有没有交点,有的话读取诸如交点距离等数据给到我们的payload并返回。但是我这里补充一下几个里面的定义:

struct hit_payload 
{
    float tNear;
    uint32_t index;
    Vector2f uv;
    Object* hit_obj;
};

class Renderer//渲染器类定义
{
public:
    void Render(const Scene& scene);

private:
};

hit_payload是一个结构体,包含的成员变量有:交点距离,交点序号,纹理坐标以及产生碰撞的物体的指针。如果我们接着展开Object的定义的话:

#pragma once

#include "Vector.hpp"
#include "global.hpp"

class Object//物体类定义
{
public:
    Object()//构造函数
        : materialType(DIFFUSE_AND_GLOSSY)//材质类型:漫反射与
        , ior(1.3)//折射率
        , Kd(0.8)//漫反射系数
        , Ks(0.2)//镜面反射系数
        , diffuseColor(0.2)//漫反射颜色
        , specularExponent(25)//高光指数
    {}

    virtual ~Object() = default;//虚析构函数=default

    virtual bool intersect(const Vector3f&, const Vector3f&, float&, uint32_t&, Vector2f&) const = 0;//定义纯虚函数便于派生类重写(Override)//这里的const与= 0是分开写的,const用来修饰函数,= 0 是纯虚函数的一部分

    virtual void getSurfaceProperties(const Vector3f&, const Vector3f&, const uint32_t&, const Vector2f&, Vector3f&,
                                      Vector2f&) const = 0;

    virtual Vector3f evalDiffuseColor(const Vector2f&) const
    {
        return diffuseColor;
    }

    // material properties
    MaterialType materialType;//材质类型
    float ior;//折射率
    float Kd, Ks;//漫反射系数和镜面反射系数
    Vector3f diffuseColor;//漫反射颜色
    float specularExponent;//高光指数
};

除了最基础的成员变量以及无参构造函数,虚析构函数以外,Object类中还定义了一个判断是否有交点的函数intersect与一个获取物体表面材质属性的函数getSurfaceProperties与一个返回漫反射颜色的函数。前两个是纯虚函数(代表性的=0),而最后一个函数则是已经提供了默认的实现(返回漫反射颜色)。

// [comment]
// Implementation of the Whitted-style light transport algorithm (E [S*] (D|G) L)
//
// This function is the function that compute the color at the intersection point(计算在交点的颜色)
// of a ray defined by a position and a direction. Note that thus function is recursive (it calls itself)(递归的).
//
// If the material of the intersected object is either reflective or reflective and refractive,
// then we compute the reflection/refraction direction and cast two new rays into the scene
// by calling the castRay() function recursively. When the surface is transparent, we mix
// the reflection and refraction color using the result of the fresnel equations (it computes
// the amount of reflection and refraction depending on the surface normal, incident view direction
// and surface refractive index).
//
// If the surface is diffuse/glossy we use the Phong illumation model to compute the color
// at the intersection point.
// [/comment]
Vector3f castRay(
        const Vector3f &orig, const Vector3f &dir, const Scene& scene,
        int depth)//接受的参数包括光源坐标,光线方向,场景以及递归深度(防止无限递归)
{
    if (depth > scene.maxDepth) {
        return Vector3f(0.0,0.0,0.0);
    }//如果递归深度超过了最大深度则返回黑色表示已经递归超过了最大深度

    Vector3f hitColor = scene.backgroundColor;//获取场景的背景色
    if (auto payload = trace(orig, dir, scene.get_objects()); payload)//调用trace函数,如果光线与物体产生交点则会返回payload结构体
    {
        Vector3f hitPoint = orig + dir * payload->tNear;//获取交点坐标,计算方式为光源点加上方向乘以payload的交点距离与方向的乘积
        Vector3f N; // normal法线
        Vector2f st; // st coordinates纹理坐标
        payload->hit_obj->getSurfaceProperties(hitPoint, dir, payload->index, payload->uv, N, st);//调用
        switch (payload->hit_obj->materialType) {//根据产生交点的物体的材质进行分类讨论:
            case REFLECTION_AND_REFRACTION://反射_折射材质
            {
                Vector3f reflectionDirection = normalize(reflect(dir, N));//获取反射方向单位向量(normalize)
                Vector3f refractionDirection = normalize(refract(dir, N, payload->hit_obj->ior));//获取折射反向单位向量
                Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?//通过判断反射方向与法线向量的夹角余弦值来判断出射光是从表面的内部还是外部发射出去
                                             hitPoint - N * scene.epsilon :
                                             hitPoint + N * scene.epsilon;//进行一定程度的起点移动防止出现自交问题
                Vector3f refractionRayOrig = (dotProduct(refractionDirection, N) < 0) ?
                                             hitPoint - N * scene.epsilon :
                                             hitPoint + N * scene.epsilon;
                Vector3f reflectionColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1);
                Vector3f refractionColor = castRay(refractionRayOrig, refractionDirection, scene, depth + 1);//进行递归且递归深度加一
                float kr = fresnel(dir, N, payload->hit_obj->ior);//通过计算菲涅尔项得到反射与折射的比值
                hitColor = reflectionColor * kr + refractionColor * (1 - kr);//得到颜色
                break;
            }
            case REFLECTION://反射材质
            {
                float kr = fresnel(dir, N, payload->hit_obj->ior);
                Vector3f reflectionDirection = reflect(dir, N);
                Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
                                             hitPoint + N * scene.epsilon :
                                             hitPoint - N * scene.epsilon;
                hitColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1) * kr;
                break;
            }
            default://默认即Phong模型,由漫反射与高光组成
            {
                // [comment]
                // We use the Phong illumation model int the default case. The phong model
                // is composed of a diffuse and a specular reflection component.
                // [/comment]
                Vector3f lightAmt = 0, specularColor = 0;//定义光照强度和高光颜色
                Vector3f shadowPointOrig = (dotProduct(dir, N) < 0) ?//计算影子的位置,对于夹角余弦的判断同前文一样
                                           hitPoint + N * scene.epsilon :
                                           hitPoint - N * scene.epsilon;
                // [comment]
                // Loop over all lights in the scene and sum their contribution up
                // We also apply the lambert cosine law
                // [/comment]
                for (auto& light : scene.get_lights()) {
                    Vector3f lightDir = light->position - hitPoint;//通过向量相减的性质,用光源位置减去交点得到光源射向交点的光线向量
                    // square of the distance between hitPoint and the light
                    float lightDistance2 = dotProduct(lightDir, lightDir);//计算光线向量的模长平方
                    lightDir = normalize(lightDir);//归一化
                    float LdotN = std::max(0.f, dotProduct(lightDir, N));//判断入射光是否从表面外部射入(是否在影子里)
                    // is the point in shadow, and is the nearest occluding object closer to the object than the light itself?
                    auto shadow_res = trace(shadowPointOrig, lightDir, scene.get_objects());
                    bool inShadow = shadow_res && (shadow_res->tNear * shadow_res->tNear < lightDistance2);

                    lightAmt += inShadow ? 0 : light->intensity * LdotN;//如果该光线不在影子里,我们将光的强度(intensity)加给lightAmt
                    Vector3f reflectionDirection = reflect(-lightDir, N);//反射角等于入射角

                    specularColor += powf(std::max(0.f, -dotProduct(reflectionDirection, dir)),//powf用于计算幂//这里用来计算高光颜色
                        payload->hit_obj->specularExponent) * light->intensity;
                }

                hitColor = lightAmt * payload->hit_obj->evalDiffuseColor(st) * payload->hit_obj->Kd + specularColor * payload->hit_obj->Ks;
                break;
            }
        }
    }

    return hitColor;
}

我们的castRay函数可谓是重中之重了,他的内容就是ws风格的光线追踪的内容。

(作业代码1处)

// [comment]
// The main render function. This where we iterate over all pixels in the image, generate
// primary rays and cast these rays into the scene. The content of the framebuffer is
// saved to a file.
// [/comment]
void Renderer::Render(const Scene& scene)//继承自Render的Renderer
{
    std::vector<Vector3f> framebuffer(scene.width * scene.height);//帧缓存,容器大小初始为屏幕宽乘以高

    float scale = std::tan(deg2rad(scene.fov * 0.5f));//定义尺度
    float imageAspectRatio = scene.width / (float)scene.height;//定义宽高比

    // Use this variable as the eye position to start your rays.
    Vector3f eye_pos(0);//相机观察的坐标
    int m = 0;
    for (int j = 0; j < scene.height; ++j)
    {
        for (int i = 0; i < scene.width; ++i)
        {
            // generate primary ray direction
            // TODO: Find the x and y positions of the current pixel to get the direction
            // vector that passes through it.
            // Also, don't forget to multiply both of them with the variable *scale*, and
            // x (horizontal) variable with the *imageAspectRatio*            
            float x = (2.0f * (float(i) + 0.5f) / scene.width - 1.0f) * scale * imageAspectRatio;
            float y = (1.0f - 2.0f * (float(j) + 0.5f) / scene.height) * scale;
            Vector3f dir = Vector3f(x, y, -1); // Don't forget to normalize this direction!
            framebuffer[m++] = castRay(eye_pos, dir, scene, 0);
        }
        UpdateProgress(j / (float)scene.height);
    }

    // save framebuffer to file
    FILE* fp = fopen("binary.ppm", "wb");
    (void)fprintf(fp, "P6\n%d %d\n255\n", scene.width, scene.height);
    for (auto i = 0; i < scene.height * scene.width; ++i) {
        static unsigned char color[3];
        color[0] = (char)(255 * clamp(0, 1, framebuffer[i].x));
        color[1] = (char)(255 * clamp(0, 1, framebuffer[i].y));
        color[2] = (char)(255 * clamp(0, 1, framebuffer[i].z));
        fwrite(color, 1, 3, fp);
    }
    fclose(fp);    
}

Triangle.hpp

(作业代码2处)

#pragma once

#include "Object.hpp"

#include <cstring>

bool rayTriangleIntersect(const Vector3f& v0, const Vector3f& v1, const Vector3f& v2, const Vector3f& orig,
                          const Vector3f& dir, float& tnear, float& u, float& v)//判断光线与三角形是否有交点
{   //vo,v1,v2代表三个顶点,orig代表光源坐标,dir代表光线方向,tnear代表交点距离,u,v代表重心坐标系中其二的系数
    // TODO: Implement this function that tests whether the triangle
    // that's specified bt v0, v1 and v2 intersects with the ray (whose
    // origin is *orig* and direction is *dir*)
    // Also don't forget to update tnear, u and v.
    Vector3f E1 = v1 - v0;
    Vector3f E2 = v2 - v0;
    Vector3f S = orig - v0;
    Vector3f S1 = crossProduct(dir, E2);//叉乘
    Vector3f S2 = crossProduct(S, E1);
    float n = 1.0f / dotProduct(S1, E1);
    Vector3f res(dotProduct(S2, E2), dotProduct(S1, S), dotProduct(S2, dir));
    res = n * res;
    tnear = res.x;
    u = res.y;
    v = res.z;
    if (tnear > 0.f && 1 - u - v >= 0.f && u >= 0.f && v >= 0.f)//判断重心坐标系的三个系数为非负
        return true;
    else
        return false;

}

class MeshTriangle : public Object//继承自Object
{
public:
    MeshTriangle(const Vector3f* verts, const uint32_t* vertsIndex, const uint32_t& numTris, const Vector2f* st)
    {
        uint32_t maxIndex = 0;
        for (uint32_t i = 0; i < numTris * 3; ++i)
            if (vertsIndex[i] > maxIndex)
                maxIndex = vertsIndex[i];
        maxIndex += 1;
        vertices = std::unique_ptr<Vector3f[]>(new Vector3f[maxIndex]);
        memcpy(vertices.get(), verts, sizeof(Vector3f) * maxIndex);
        vertexIndex = std::unique_ptr<uint32_t[]>(new uint32_t[numTris * 3]);
        memcpy(vertexIndex.get(), vertsIndex, sizeof(uint32_t) * numTris * 3);
        numTriangles = numTris;
        stCoordinates = std::unique_ptr<Vector2f[]>(new Vector2f[maxIndex]);
        memcpy(stCoordinates.get(), st, sizeof(Vector2f) * maxIndex);
    }

    bool intersect(const Vector3f& orig, const Vector3f& dir, float& tnear, uint32_t& index,
                   Vector2f& uv) const override
    {
        bool intersect = false;
        for (uint32_t k = 0; k < numTriangles; ++k)
        {
            const Vector3f& v0 = vertices[vertexIndex[k * 3]];
            const Vector3f& v1 = vertices[vertexIndex[k * 3 + 1]];
            const Vector3f& v2 = vertices[vertexIndex[k * 3 + 2]];
            float t, u, v;
            if (rayTriangleIntersect(v0, v1, v2, orig, dir, t, u, v) && t < tnear)
            {
                tnear = t;
                uv.x = u;
                uv.y = v;
                index = k;
                intersect |= true;
            }
        }

        return intersect;
    }

    void getSurfaceProperties(const Vector3f&, const Vector3f&, const uint32_t& index, const Vector2f& uv, Vector3f& N,
                              Vector2f& st) const override
    {
        const Vector3f& v0 = vertices[vertexIndex[index * 3]];
        const Vector3f& v1 = vertices[vertexIndex[index * 3 + 1]];
        const Vector3f& v2 = vertices[vertexIndex[index * 3 + 2]];
        Vector3f e0 = normalize(v1 - v0);
        Vector3f e1 = normalize(v2 - v1);
        N = normalize(crossProduct(e0, e1));
        const Vector2f& st0 = stCoordinates[vertexIndex[index * 3]];
        const Vector2f& st1 = stCoordinates[vertexIndex[index * 3 + 1]];
        const Vector2f& st2 = stCoordinates[vertexIndex[index * 3 + 2]];
        st = st0 * (1 - uv.x - uv.y) + st1 * uv.x + st2 * uv.y;
    }

    Vector3f evalDiffuseColor(const Vector2f& st) const override
    {
        float scale = 5;
        float pattern = (fmodf(st.x * scale, 1) > 0.5) ^ (fmodf(st.y * scale, 1) > 0.5);
        return lerp(Vector3f(0.815, 0.235, 0.031), Vector3f(0.937, 0.937, 0.231), pattern);
    }

    std::unique_ptr<Vector3f[]> vertices;
    uint32_t numTriangles;
    std::unique_ptr<uint32_t[]> vertexIndex;
    std::unique_ptr<Vector2f[]> stCoordinates;
};

继承自Object类的三角形类Triangle重写了三个虚函数,不过我们的重点在光线与三角形求交的部分:

由之前在笔记六中的算法可以得知这里的做法。

结果:


http://www.kler.cn/a/459206.html

相关文章:

  • 基于 LangChain 实现数据库问答机器人
  • git 中 工作目录 和 暂存区 的区别理解
  • python爬虫--小白篇【selenium自动爬取文件】
  • STM32 SPI读取SD卡
  • archlinux使用
  • Android着色器SweepGradient渐变圆环,Kotlin
  • 基于neurokit2的呼吸仿真数据生成实例解析
  • 解决海康相机SDK导致 `libusb_set_option` 问题的经验总结
  • 论文解读 | 《我国桑黄产业发展现状、问题及展望:桑黄产业发展千岛湖宣言》
  • Springboot:后端接收数组形式参数
  • 【漏洞复现】NetMizer 日志管理系统 hostdelay.php 前台RCE漏洞复现
  • Mono里运行C#脚本9—do_mono_image_open
  • STM32-笔记20-测量按键按下时间
  • CGAL windows 安装教程
  • ABAQUS随机多面体骨料再生混凝土细观力学分析
  • 12月30日网络编程
  • MySQL数据库误删恢复_mysql 数据 误删
  • 计算机网络•自顶向下方法:网络应用原理
  • FPGA中EMIO接口的模块引出
  • ZooKeeper注册中心实现
  • 使用 ASP.NET Core wwwroot 上传和存储文件
  • MySQL内存分析常用语句
  • 基本算法——聚类
  • 基于eBPF的微服务网络安全(Cilium 1)
  • spring-boot 日志配置的几种方式
  • 【每日学点鸿蒙知识】Shape描述、全局loading组件、checkbox样式、H5监听键盘收起、弹窗不关闭