【Threejs】完成3D汽车动态换肤的案列

Threejs完成3D汽车动态换肤的案列

课程目标

  1. 基于案列实现对three核心理论剖析
  2. 实战为王、理论为纲。跟着实战一起快速进入3D世界
  3. 一天时间就可以搞定threejs的入门学习

课程内容

一、环境的搭建
(1)搭建项目

在前端的世界中3D是必不可少的一部分,尤其是现在产品多元化后,很多应用中都会涉及3D相关的技术开发。接下来我们的任务认识3D技术,开始借助threejs来帮助我们完成3D开发。

Three.js是基于原生WebGL封装运行的三维引擎,在所有WebGL引擎中,Three.js是国内文资料最多、使用最广泛的三维引擎。

你如果你要理解Three.js和WebGL的关系,那就相当于jQuery和原生js的关系。

threejs的每个版本都有一些差异,在api和threejs项目文件夹下面,本案列使用的版本

npm i three@0.153.0

项目的目录结构如下:

03-fulldemo
└───css
    │───main.css
    │
└───draco
    │───gltf——存放Google Draco解码器插件
    │
└───models——存放模型
    │───ferrari.glb——模型文件,可以是glb也可以是gltf格式
		│───ferrari_ao.png——模型贴图,这个图片是阴影效果
    │
└───textures——纹理材质
    │───venice_sunset_1k.hdr——将其用作场景的环境映射或者用来创建基于物理的材质
    │
(2)代码基础结构搭建

创建对应的html文件并引入相应的环境

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js webgl - materials - car</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="./css/main.css">
		<style>
			body {
     
				color: #bbbbbb;
				background: #333333;
			}
			a {
     
				color: #08f;
			}
			.colorPicker {
     
				display: inline-block;
				margin: 0 10px
			}
		</style>
	</head>

	<body>
    <!--设置三个按钮,用于切换车身、轮毂、玻璃的颜色-->
		<div id="info">
			<span class="colorPicker"><input id="body-color" type="color" value="#ff0000"></input><br/>Body</span>
			<span class="colorPicker"><input id="details-color" type="color" value="#ffffff"></input><br/>Details</span>
			<span class="colorPicker"><input id="glass-color" type="color" value="#ffffff"></input><br/>Glass</span>
		</div>
		<!--要渲染3D的容器-->
		<div id="container"></div>
		<script type="importmap">
			{
     
				"imports": {
     
					"three": "./node_modules/three/build/three.module.js",
					"three/addons/": "./node_modules/three/examples/jsm/"
				}
			}
		</script>
		<script type="module">
			import * as THREE from 'three';
      //用于显示屏幕渲染帧率的面板
			import Stats from 'three/addons/libs/stats.module.js';
      //相机控件OrbitControls实现旋转缩放预览效果。
			import {
      OrbitControls } from 'three/addons/controls/OrbitControls.js';
      //加载GLTF文件格式的加载器,用于加载外部为gltf的文件
			import {
      GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
      //Draco是一个用于压缩和解压缩 3D 网格和点云的开源库
			import {
      DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
      //RGBELoader可以将HDR图像加载到Three.js应用程序中
			import {
      RGBELoader } from 'three/addons/loaders/RGBELoader.js';    
      
      //下面的代码就是JS渲染逻辑代码
		</script>
	</body>
</html>

在css/main.css文件中我们的代码如下

body {
   
	margin: 0;
	background-color: #000;
	color: #fff;
	font-family: Monospace;
	font-size: 13px;
	line-height: 24px;
	overscroll-behavior: none;
}

a {
   
	color: #ff0;
	text-decoration: none;
}

a:hover {
   
	text-decoration: underline;
}

button {
   
	cursor: pointer;
	text-transform: uppercase;
}

#info {
   
	position: absolute;
	top: 0px;
	width: 100%;
	padding: 10px;
	box-sizing: border-box;
	text-align: center;
	-moz-user-select: none;
	-webkit-user-select: none;
	-ms-user-select: none;
	user-select: none;
	pointer-events: none;
	z-index: 1; /* TODO Solve this in HTML */
}

a, button, input, select {
   
	pointer-events: auto;
}

.lil-gui {
   
	z-index: 2 !important; /* TODO Solve this in HTML */
}

@media all and ( max-width: 640px ) {
   
	.lil-gui.root {
    
		right: auto;
		top: auto;
		max-height: 50%;
		max-width: 80%;
		bottom: 0;
		left: 0;
	}
}

#overlay {
   
	position: absolute;
	font-size: 16px;
	z-index: 2;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	display: flex;
	align-items: center;
	justify-content: center;
	flex-direction: column;
	background: rgba(0,0,0,0.7);
}

#overlay button {
   
  background: transparent;
  border: 0;
  border: 1px solid rgb(255, 255, 255);
  border-radius: 4px;
  color: #ffffff;
  padding: 12px 18px;
  text-transform: uppercase;
  cursor: pointer;
}

#notSupported {
   
	width: 50%;
	margin: auto;
	background-color: #f00;
	margin-top: 20px;
	padding: 10px;
}

效果如下图:

image-20230627175428242

二、进行3D场景的渲染
(1)进行初始化函数设计

在项目中我们添加一个carInit函数进行动画的初始化

...省略之前代码
//下面的代码就是JS渲染逻辑代码
let scene, renderer, grid, camera;
function initCar(){
   
  //里面就开始进行3D场景的搭建
}

//执行初始化函数
initCar()

上面的函数设计用于执行我们所有3d业务代码。

(2)创建场景
/**
 * (1)获取要渲染的容器
 */
const container = document.getElementById('container');

/**
 * (2)创建场景对象Scene
 */
//创建一个场景对象,用来模拟3d世界
scene = new THREE.Scene();
//设置一个场景的背景颜色
scene.background = new THREE.Color(0x333333);
//这个类中的参数定义了线性雾。也就是说,雾的密度是随着距离线性增大的
scene.fog = new THREE.Fog("red", 10, 15);

background:这个属性用于设置我们场景的背景颜色,0x333333默认采用深灰来作为我们初始颜色

fog:定义了线性雾,类似于在背景指定位置设置雾化的效果,让背景看起来更加模糊,凸显空旷效果。

(3)坐标格辅助对象
/**
* (3)坐标格辅助对象. 坐标格实际上是2维线数组.
*/
//创建网格对象,参数1:大小,参数2:网格细分次数,参数3:网格中线颜色,参数4:网格线条颜色
grid = new THREE.GridHelper(40, 40, 0xffffff, 0xffffff);
//网格透明度
grid.material.opacity = 1;
grid.material.depthWrite = false;
grid.material.transparent = true;
scene.add(grid);

坐标格辅助对象GridHelper可以在3D场景中定义坐标格出现。后续我们会在坐标格上面放我们的模型进行展示

代码编写完毕后,最终渲染出来的坐标格效果如下:

image-20230628155733478

(4) 创建相机对象
/**
* (4)创建透视相机
* 参数一:摄像机视锥体垂直视野角度
* 参数二:摄像机视锥体长宽比
* 参数三:摄像机视锥体近端面
* 参数四:摄像机视锥体远端面
*/
camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.3, 100);
camera.position.set(0, 1.4, - 4.5);

任何一个3D渲染效果都需要相机来成像

这一投影模式被用来模拟人眼所看到的景象,它是3D场景的渲染中使用得最普遍的投影模式

透视相机最大的特点就是满足近大远小的效果。

(5)创建一个渲染器
/**
 *  (5)创建一个渲染器
 */
renderer = new THREE.WebGLRenderer({
    antialias: true });
renderer = new THREE.WebGLRenderer({
    antialias: true });
//设置设备像素比。通常用于避免HiDPI设备上绘图模糊
renderer.setPixelRatio(window.devicePixelRatio);
//设置渲染出来的画布范围
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
renderer.render(scene, camera);

有了场景、相机、坐标格辅助,我们想要让画面能够呈现出来,那就得有渲染器。

相当于你拍照需要将画面呈现到交卷上面。

其中renderer.render(scene, camera); 这段代码就是在进行渲染器的渲染。

如果render在指定频率内不断被调用,那就意味着可以不断拍照,不断渲染。可以实现动态切换效果

(6)效果渲染

当执行完上面的代码后,你需要确保调用了carInit这个函数,页面就可以渲染出对应的效果了

image-20230628161312404

说明:

  1. 场景的背景色为0x333333效果为深灰色。
  2. 我们设置的fog线性雾颜色为红色,所以你会发现在背景和网格之间会有一个过渡颜色。
  3. 网格的颜色采用的是0xffffff效果为灰色。

对应的各种参数,当你在学习的时候都都可以进行调整。一遍调整就能看懂参数和最终渲染的效果差异。

当你把fog的颜色调整为跟背景一样的时候,你会发现画面上就类似产生了迷雾效果,让3D背景更加立体

scene.fog = new THREE.Fog(0x333333, 10, 15);

效果如下:

image-20230628161707934

你也可以继续设置网格线条的透明度,让网格线不那么抢眼

grid.material.opacity = 0.3;

效果如下:

image-20230628161827094

是不是整个画面看起来3D立体效果会更强一些,背景看起来更深邃一些。

三、加载外部模型进行渲染
(1)添加轨道控制器

threejs官方给我们提供了一个类,OrbitControls(轨道控制器)可以使得相机围绕目标进行轨道运动。

换句话说,引入了OrbitControls后,我们可以操作鼠标来控制页面上动态效果。

比如:鼠标滚动、鼠标点击、鼠标左右滑动效果。

代码如下:

...省略了 (5)创建一个渲染器】
/**
 *  (6)开启OrbitControls控件,可以支持鼠标操作图像
 */
controls = new OrbitControls(camera, container);
//你能够将相机向外移动多少(仅适用于PerspectiveCamera),其默认值为Infinity
controls.maxDistance = 9;
//你能够垂直旋转的角度的上限,范围是0到Math.PI,其默认值为Math.PI
controls.maxPolarAngle = THREE.MathUtils.degToRad(90);
controls.target.set(0, 0.5, 0);
controls.update();

加入上面代码后,我们还要继续优化代码

在carInit函数后面在添加一个render函数,用于执行渲染

function initCar(){
   
  
  /**
 *  (5)创建一个渲染器
 */
  renderer = new THREE.WebGLRenderer({
    antialias: true });
  renderer = new THREE.WebGLRenderer({
    antialias: true });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  container.appendChild(renderer.domElement);
  //注释掉这句话
  //renderer.render(scene, camera);
  //调用一次render函数进行渲染
  render()
}
function render(){
   
  renderer.render(scene, camera);
  requestAnimationFrame(render)
}

效果实现如下:

(2)加载汽车模型

既然要加载外部模型,那我们肯定需要通过模型软件来设计对应的模型。本案列不讲解如何设计模型,我使用threejs官方提供的模型来进行展示。

我们常用的模型格式如下:

  1. OBJ (Wavefront OBJ):

    OBJ 是一种常见的纯文本模型格式,支持存储模型的几何信息(顶点、面)和材质信息(纹理坐标、法线等)。可以通过OBJLoader来加载和解析OBJ格式的模型。

  2. FBX (Autodesk FBX):

    FBX 是由Autodesk开发的一种常用的二进制模型格式,支持存储模型的几何信息、材质、动画等。可以通过FBXLoader来加载和解析FBX格式的模型。

  3. GLTF (GL Transmission Format):

    GLTF 是一种基于JSON的开放标准,用于存储和传输三维模型和场景。GLTF格式支持几何信息、材质、骨骼动画、节点层次结构等,并且通常具有较小的文件大小。可以通过GLTFLoader来加载和解析GLTF格式的模型。

  4. STL (Stereolithography):

    STL 是一种常用的三维打印文件格式,用于存储模型的几何信息。STL 文件通常包含三角形面片的列表,用于定义模型的外观。可以通过STLLoader来加载和解析STL格式的模型。

  5. GLB:

    GLB是GL Transmission Format(gltf)的二进制版本,GLB格式将模型的几何信息、材质、骨骼动画、节点层次结构等存储在单个二进制文件中,通常具有较小的文件大小和更高的加载性能.

本案列采用glb格式来加载外部模型。

因为案列中使用glb模型数据采用了Draco来进行压缩,所以我们需要引入DRACOLoader来解析我们的模型

(1)引入DRACOLoader加载模型

/**
*  (7)汽车模型相关的内容
*  DRACOLoader 主要用于解析使用 Draco 压缩的 GLB 模型,而不是所有的 GLB 模型都使用了 Draco 压缩
*/
const dracoLoader = new DRACOLoader();
//配置加载器的位置,这个需要提前下载到项目中
dracoLoader.setDecoderPath('./draco/gltf/');
const loader = new GLTFLoader();
//设置GLTFLoader加载器使用DRACO来解析我们的模型数据
loader.setDRACOLoader(dracoLoader);

并不是所有的模型都需要Draco来进行加载,取决于你的模型在设计导出的时候是否用了Draco来进行压缩。

./draco/gltf/目录下面的文件如下:代码可以从gitee上面下载

image-20230629142933880

(3)加载glb模型数据

当你已经创建了`const loader = new GLTFLoader();这个类实例后,我们就可以加载模型了

/**
 * (8)加载glb模型
*/
loader.load('./models/gltf/ferrari.glb', function (gltf) {
   
  //获取到模型的数据
  const carModel = gltf.scene.children[0];
  //将模型添加到3D场景中
  scene.add(carModel);
});
render()

加载的效果如下:

image-20230629143514305

模型已经加载成功了,但是你会发现他在整个背景中是黑色的。当然模型本身是有材质贴图的,车身默认是红色的。

之所以产生这个效果那是因为我们现在缺少一个非常重要的元素,那就是光照。

你试想一下,一个物体在没有任何光源的情况下,呈现出来的就是黑色的效果。如果你的场景背景也是黑色,那根本看不到效果。

(4)加载光影效果

我们设置光源的时候主要有两个部分

  1. 环境光:相当于天空的颜色,物体表面可以反射出对应的颜色。
  2. 点光源:相当于开启手电筒,照射到模型表面反射出来的颜色。

设置环境光

/**
 * (9)添加光影效果
 */
       
//创建环境光
var ambient = new THREE.AmbientLight("blue");
scene.add(ambient);

环境光的颜色为blue,效果如下:

image-20230629145635074

环境光为blue的情况下,模型表面反射出来的颜色就是蓝色,一般金属材质和玻璃材质反射的效果更佳明显。所以轮毂和车辆挡风玻璃效果会更强烈一些。

设置点光源

/**
 * (9)添加光影效果
*/

//创建环境光
var ambient = new THREE.AmbientLight("blue");
scene.add(ambient);

//创建点光源
var point = new THREE.PointLight("#fff");
//设置点光源位置
point.position.set(0, 300, 0); 
//点光源添加到场景中
scene.add(point); 

效果如下:

image-20230629145913328

此刻我们基本上完成了模型的渲染,环境光蓝色默认替换为黑色,这样车辆立体感会更强一些

//环境光
var ambient = new THREE.AmbientLight("#000");

效果如下:

image-20230629150241875

(5)加载hdr文件设置环境渲染

HDR(High Dynamic Range)文件是一种存储图像高动态范围信息的文件格式。

HDR可以理解成一张真实世界的图片或者设计者想要的灯光效果。

他的作用主要如下:

  1. HDR文件经常被用作环境贴图,用于模拟反射和光照环境。环境贴图是将场景的背景、反射和光照信息包装成一个纹理,然后将其应用到物体表面上。通过使用HDR文件作为环境贴图,可以更真实地模拟光线在场景中的反射和折射,增强渲染效果。
  2. HDR文件还可以用于模拟全局照明效果。全局照明是一种渲染技术,它考虑了场景中所有光源的组合对物体的影响,以获得更真实的照明效果。通过使用HDR文件提供的高动态范围和丰富的光照信息,可以在Three.js中实现更逼真的全局照明效果

也就说在本案列中如果我们想要获取更加真实的照明效果,我们可以使用设计师导出的hdr文件。将这个文件作为3D场景(Scene)的环境贴图

/**
 * (2)创建场景对象Scene
 */
scene = new THREE.Scene();
scene.background = new THREE.Color(0x333333);
//通过RGBELoader加载hdr文件,它是一种图像格式,将其用作场景的环境映射或者用来创建基于物理的材质
scene.environment = new 
RGBELoader().load('textures/equirectangular/venice_sunset_1k.hdr');
scene.environment.mapping = THREE.EquirectangularReflectionMapping;
scene.fog = new THREE.Fog(0x333333, 10, 15);

删除我们(9)添加光影效果中我们自己的光影效果

/**
 * (9)添加光影效果
*/

//创建环境光
//var ambient = new THREE.AmbientLight("blue");
//scene.add(ambient);

//创建点光源
//var point = new THREE.PointLight("#fff");
//设置点光源位置
//point.position.set(0, 300, 0); 
//点光源添加到场景中
//scene.add(point); 

这样渲染下来我们物体在场景中显示的会更加自然

image-20230629152457227

不管你用hdr文件来作为环境贴图,还是采用光源设置来设计,我们都可以让模型在3D场景中更方便的显示出来。

四、汽车材质贴图

目前我们已经将模型渲染出来了,但是你会发现不管是车身、轮毂、还是玻璃材质跟我们想要的真实车辆材质是有区别的。比如你希望玻璃透明的、反光的。车身的漆面是可以反光的。模型在设计的时候使用默认材质。我们想要进行材质的替换。

(1)在步骤8中继续优化代码
/**
 * (8)加载glb模型
 * 并设置不同部位的材质。
*/
//物理网格材质(MeshPhysicalMaterial)
//车漆,碳纤,被水打湿的表面的材质需要在面上再增加一个透明的
const bodyMaterial = new THREE.MeshPhysicalMaterial({
   
  color: 0xff0000, metalness: 1.0, roughness: 0.5, clearcoat: 1.0, clearcoatRoughness: 0.03
});

//汽车轮毂的材质,采用了标准网格材质,threejs解析gltf模型,会用两种材质PBR材质去解析
const detailsMaterial = new THREE.MeshStandardMaterial({
   
  color: 0xffffff, metalness: 1.0, roughness: 0.5
});

//汽车玻璃的材质
const glassMaterial = new THREE.MeshPhysicalMaterial({
   
  color: 0xffffff, metalness: 0.25, roughness: 0, transmission: 1.0
});

loader.load('./models/gltf/ferrari.glb', function (gltf) {
   
  //获取到模型的数据
  const carModel = gltf.scene.children[0];
  //将模型添加到3D场景中
  scene.add(carModel);
});

材质创建了过后,接下来我们就可以将材质加载了到模型中了。

loader.load('./models/gltf/ferrari.glb', function (gltf) {
   
  //获取到模型的数据
  const carModel = gltf.scene.children[0];

  //获取模型中指定的模块,将默认材质替换为我们自定义材质
  carModel.getObjectByName('body').material = bodyMaterial;
  //轮毂的材质替换
  carModel.getObjectByName('rim_fl').material = detailsMaterial;
  carModel.getObjectByName('rim_fr').material = detailsMaterial;
  carModel.getObjectByName('rim_rr').material = detailsMaterial;
  carModel.getObjectByName('rim_rl').material = detailsMaterial;
  座椅的材质
  carModel.getObjectByName('trim').material = detailsMaterial;
  //玻璃的材质替换
  carModel.getObjectByName('glass').material = glassMaterial;

  scene.add(carModel);
});

上面的代码分别是获取模型中车身区域(body),获取轮毂区域(rim_fl、rim_fr、rim_rr、rim_rl)、座椅区域(trim)、玻璃区域(glass)

将我们自己创建的材质拿去替换默认材质实现加载渲染。

效果如下:

image-20230629164136907

替换过后的模型,更有金属质感和玻璃质感。材质对应的颜色你们都可以自己进行替换。

(2)给车底盘添加阴影效果

车底盘是没有阴影效果的,我们可以使用图片来进行模型贴图,让底盘有阴影效果会更加立体。

贴图的图片为png,图片由设计师出的

效果如下:

ferrari_ao

创建一个材质对象,并使用这张图片作为贴图

loader.load('./models/gltf/ferrari.glb', function (gltf) {
   
  //获取到模型的数据
  const carModel = gltf.scene.children[0];

  //获取模型中指定的模块,将默认材质替换为我们自定义材质
  carModel.getObjectByName('body').material = bodyMaterial;
  //轮毂的材质替换
  carModel.getObjectByName('rim_fl').material = detailsMaterial;
  carModel.getObjectByName('rim_fr').material = detailsMaterial;
  carModel.getObjectByName('rim_rr').material = detailsMaterial;
  carModel.getObjectByName('rim_rl').material = detailsMaterial;
  //座椅的材质
  carModel.getObjectByName('trim').material = detailsMaterial;
  //玻璃的材质替换
  carModel.getObjectByName('glass').material = glassMaterial;
  
	// shadow阴影效果图片
  const shadow = new THREE.TextureLoader().load( './models/gltf/ferrari_ao.png' );
  // 创建一个材质模型
  const mesh = new THREE.Mesh(
    new THREE.PlaneGeometry(0.655 * 4, 1.3 * 4),
    new THREE.MeshBasicMaterial({
   
      map: shadow, blending: THREE.MultiplyBlending, toneMapped: false, transparent: true
    })
  );
  mesh.rotation.x = - Math.PI / 2;
  mesh.renderOrder = 2;
  carModel.add(mesh);

  scene.add(carModel);
});

效果如下:

image-20230629175934463

通过效果图能看出,车辆底部是有阴影效果的,让整个3D效果渲染更加立体。

五、设置动画效果
(1)获取轮毂的材质对象

轮毂和网格地板我们都要动画加载

网格需要进行平移,按照z的反方向进行移动。

轮毂需要按照x轴的方向进行旋转

代码如下:

let wheels = []
function initCar(){
   
  loader.load('./models/gltf/ferrari.glb', function (gltf) {
   
    //获取到模型的数据
    const carModel = gltf.scene.children[0];

    ...省略代码
    //将车轮的模块保存到数组中,后面可以设置动画效果
    wheels.push(
      carModel.getObjectByName('wheel_fl'),
      carModel.getObjectByName('wheel_fr'),
      carModel.getObjectByName('wheel_rl'),
      carModel.getObjectByName('wheel_rr')
    );
		
    scene.add(carModel);
  });
}

上面的代码将轮毂模块获取到过后,放入到wheels数组中。

(2)设置轮毂的动画效果

接下来在render函数中进行动画控制

function render() {
   
  controls.update();
  //performance.now()是一个用于测量代码执行时间的方法。它返回一个高精度的时间戳,表示自页面加载以来的毫秒数
  const time = - performance.now() / 1000;
  //控制车轮的动画效果
  for (let i = 0; i < wheels.length; i++) {
   
    wheels[i].rotation.x = time * Math.PI * 2;
  }
  //控制网格的z轴移动
  grid.position.z = - (time) % 1;

  renderer.render(scene, camera);
  requestAnimationFrame(render)
}

通过上面的代码我们已经能够实现轮毂和网格的动画效果了

六、切换颜色

实现颜色切换就必须绑定js的事件。

三个按钮,我们都绑定点击事件,并获取对应的颜色

function initCar(){
   
  ...省略代码
  /**
 		* (10)切换车身颜色
		* 获取到指定的按钮,得到你选中的颜色,并将颜色设置给我们自己的模型对象
		*/
  const bodyColorInput = document.getElementById('body-color');
  bodyColorInput.addEventListener('input', function () {
   
    bodyMaterial.color.set(this.value);
  });

  const detailsColorInput = document.getElementById('details-color');
  detailsColorInput.addEventListener('input', function () {
   
    detailsMaterial.color.set(this.value);
  });

  const glassColorInput = document.getElementById('glass-color');
  glassColorInput.addEventListener('input', function () {
   
    glassMaterial.color.set(this.value);
  });
}

当我们将上面的代码实现后,切换颜色就完成分了。

只要修改bodyMaterial材质对象的颜色,页面刷新的时候就可以应用成功。

课程小结

threejs是WebGL的框架,可以快速帮助我们在项目开发过程中进行3D渲染

对于外部3D模型的加载,需要我们针对不同模型进行不同的解析,这个需要和建模师沟通协调好

相关推荐

  1. VUE3 /根据主题动态切换图片

    2024-01-09 11:00:02       20 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-01-09 11:00:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-09 11:00:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-09 11:00:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-09 11:00:02       20 阅读

热门阅读

  1. C++ STL中vector的模拟实现

    2024-01-09 11:00:02       36 阅读
  2. 安卓adb

    2024-01-09 11:00:02       33 阅读
  3. 逐步递进地手写一个Promise

    2024-01-09 11:00:02       31 阅读
  4. 探索 GitHub:高效使用技巧与实例分享

    2024-01-09 11:00:02       40 阅读
  5. git常用指令及应用案例

    2024-01-09 11:00:02       43 阅读
  6. 程序员必备的面试技巧

    2024-01-09 11:00:02       36 阅读
  7. Django创建RSS订阅

    2024-01-09 11:00:02       36 阅读
  8. 网络协议到底是什么?

    2024-01-09 11:00:02       38 阅读