【Three.JS零基础入门教程】第六篇:物体详解

 前期回顾:

【Three.JS零基础入门教程】第一篇:搭建开发环境

【Three.JS零基础入门教程】第二篇:起步案例

【Three.JS零基础入门教程】第三篇:开发辅助

【Three.JS零基础入门教程】第四篇:基础变换

【Three.JS零基础入门教程】第五篇:项目规划


下面将进一步详解介绍Threejs中的常用对象, 包括:

  • 物体
    • 几何
    • 材质
    • 纹理
  • 场景
  • 相机
  • 光线与阴影(付费)
  • 加载外部模型(付费)
  • 粒子效果(付费)
  • 动画与交互(付费)
  • 物理仿真(付费)

本篇内容主要介绍物体基本原理(几何、材质和纹理)和几种几何体(立方体、平面、球体)的讲解。

其他的将在后续的更新中逐一介绍。

简介

如何在虚拟的3D场景中模拟真实世界, 核心是物体

three.js中, 物体使用Mesh(网格)来描述

网格模型

在计算机图形学中, 几何体都是由三角形拼接而成, 由三角形组成的模型叫网格模型

从上面可以看到, 三角形越多, 模拟的模型越接近真实情况。

ChatGPT的解释

图形学中使用三角形作为基本的构造单元来生成复杂的物体,这是因为使用三角形可以带来多方面的好处。下面是一些主要的好处:

  1. 易于处理:三角形是一种简单的几何图形,易于计算和处理。它们有三个顶点,每个顶点有三个坐标,这使它们易于表示和存储。
  2. 适合多边形的形状:复杂的多边形形状可以通过简单的三角形组合来近似表示。这种近似往往能够满足需要,同时也可以提高计算效率。
  3. 易于进行光照和渲染:计算机图形学中的光照和渲染算法通常是针对三角形的。这是因为三角形是最简单的几何形状之一,它们的光照和渲染可以在多个工具和库中找到。
  4. 具有一致性:使用三角形作为构造单元可以带来一致性的好处。因为三角形是通用的基本几何形状,它们可以被用于不同的场景和应用中,从简单的2D图形到复杂的3D物体,这样可以使代码更加通用。
  5. 确保物体的平面:在三维空间中,三角形是唯一可以确保与另一个三角形在同一平面上的几何形状。这是因为只要三个点不共线,就可以构建出确定的平面,这保障了物体的正确表现。

综上,使用三角形作为构造单元有多种好处,使得其成为图形学中最受欢迎的几何形状之一。

Mesh主要由这些部分组成

  • 几何
  • 材质
  • 纹理

1. 几何

如何理解几何?

几何描述了物体的形状大小, 比如同样是桌子形状大小就各不相同

  • 形状上: 有方形的, 圆形的
  • 大小上: 有大的, 有小的

2. 材质

如何理解材质?

材质就是物体使用的材料, 材质不同对光线的反射效果不同

还是以桌子以例, 不同的桌面有的是木头, 有的是大理石, 有的是塑料(复合材料), 有的是金属

再比如, 门有木门铁门玻璃门

3. 纹理

如何理解纹理?

纹理就是物体表面的图案花纹

还是以桌子为例, 不同木头, 表面的花纹不一样

几何详解

threejs中给我们内置了一些几何形状, 具体内容参见文档

docs中搜索geometry几何体

这里, 我们主要介绍几个常用的几何体(立方体, 球体, 平面)

1. 立方体

立方体, 我们主要需要设置其(长宽高)属性值

  • 长(width): x轴所占据的空间
  • 宽(depth): z轴所占据的空间
  • 高(height): y轴所占据的空间
// 三. 创建物体
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2)
const cubeMaterial = new THREE.MeshNormalMaterial()
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
scene.add(cube)

为了方便观察, 我们可以考虑使用Gui工具, 调整立方体的坐标和大小

完整示例:

// 导入threejs
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import * as dat from 'dat.gui'

// 一. 创建场景
const scene = new THREE.Scene()

// 二. 创建相机
const camera = new THREE.PerspectiveCamera(
  45,
  window.innerWidth / window.innerHeight
)
camera.position.set(20, 20, 20)
camera.lookAt(0, 0, 0)

// 三. 创建物体
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2)
const cubeMaterial = new THREE.MeshNormalMaterial()
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
scene.add(cube)

// 六. 集成Gui工具
const gui = new dat.GUI()
const data = {
  x: 0,
  y: 0,
  z: 0,
  width: 2,
  height: 2,
  depth: 2,
}
gui.add(data, 'x').onChange((value) => {
  cube.position.x = value
})
gui.add(data, 'y').onChange((value) => {
  cube.position.y = value
})
gui.add(data, 'z').onChange((value) => {
  cube.position.z = value
})
gui.add(data, 'width', 2, 20, 1).onChange((value) => {
  data.width = value
  // 销毁旧的几何体体
  cube.geometry.dispose()
  cube.geometry = new THREE.BoxGeometry(data.width, data.height, data.depth)
})
gui.add(data, 'height', 2, 20, 1).onChange((value) => {
  data.height = value
  // 销毁旧的几何体体
  cube.geometry.dispose()
  cube.geometry = new THREE.BoxGeometry(data.width, data.height, data.depth)
})
gui.add(data, 'depth', 2, 20, 1).onChange((value) => {
  data.depth = value
  // 销毁旧的几何体体
  cube.geometry.dispose()
  cube.geometry = new THREE.BoxGeometry(data.width, data.height, data.depth)
})
// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
document.body.appendChild(renderer.domElement)

function animation() {
  renderer.render(scene, camera)
}

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()

  renderer.setSize(window.innerWidth, window.innerHeight)
})
// 五. 集成辅助工具
const control = new OrbitControls(camera, renderer.domElement)

const axesHepler = new THREE.AxesHelper(10)
scene.add(axesHepler)

const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)

2. 球体

球体, 我们主要修改其半径和分段

  • 半径(radius): 球体半径
  • 经线分段数(widthSegments):
  • 纬线分段数(heightSegments):
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32)
const sphereMaterial = new THREE.MeshNormalMaterial()
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)

完整示例:

import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

import * as dat from 'dat.gui'

// 一. 创建场景
const scene = new THREE.Scene()

// 二. 创建相机
const camera = new THREE.PerspectiveCamera(
  45,
  window.innerWidth / window.innerHeight
)
camera.position.set(0, 0, 5)

// 三. 创建物体
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32)
const sphereMaterial = new THREE.MeshNormalMaterial()
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
scene.add(sphere)

const data = {
  x: 0,
  y: 0,
  z: 0,
  radius: 1,
  widthSegments: 32,
  heightSegments: 32,
}
const gui = new dat.GUI()
gui.add(data, 'x').onChange((value) => {
  sphere.position.x = value
})
gui.add(data, 'y').onChange((value) => {
  sphere.position.y = value
})
gui.add(data, 'z').onChange((value) => {
  sphere.position.z = value
})
gui.add(data, 'radius', 1, 10, 1).onChange((value) => {
  data.radius = value
  sphere.geometry.dispose()
  sphere.geometry = new THREE.SphereGeometry(
    data.radius,
    data.widthSegments,
    data.heightSegments
  )
})
gui.add(data, 'widthSegments', 3, 64, 1).onChange((value) => {
  data.widthSegments = value
  sphere.geometry.dispose()
  sphere.geometry = new THREE.SphereGeometry(
    data.radius,
    data.widthSegments,
    data.heightSegments
  )
})
gui.add(data, 'heightSegments', 2, 64, 1).onChange((value) => {
  data.heightSegments = value
  sphere.geometry.dispose()
  sphere.geometry = new THREE.SphereGeometry(
    data.radius,
    data.widthSegments,
    data.heightSegments
  )
})

// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
// 将渲染的canvas添加到body元素中
document.body.appendChild(renderer.domElement)

// 五. 辅助工具
const control = new OrbitControls(camera, renderer.domElement)

const axesHelper = new THREE.AxesHelper(10)
scene.add(axesHelper)

const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)

function animation() {
  renderer.render(scene, camera)
}

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()

  renderer.setSize(window.innerWidth, window.innerHeight)
})

上面我们发现Gui部分的代码很多都是重复的。

考虑到后续其它对象也需要使用Gui工具, 我们可以考虑统一封装

创建src/gui文件夹

gui文件夹下创建index.js导出两个类

  • BaseGui
  • MeshGui
export { BaseGui } from './BaseGui'
export { MeshGui } from './MeshGui'
import * as dat from 'dat.gui'

export class BaseGui {
  constructor() {
    this.gui = new dat.GUI()
  }
  initGuiFolder(item, name, parent, config) {
    const folder = this.gui.addFolder(name)

    Object.keys(config).forEach((key) => {
      this.initGuiItem(folder, item, key, parent, config[key])
    })
  }
  initGuiItem(gui, item, key, parent, options = {}) {
    // 构造数据
    const controls = {}
    if (key === 'color') {
      controls[key] = item.color.getHex()
    } else {
      controls[key] = item[key]
    }

    // console.log(controls)

    const method = options.method || 'add'

    const min = options.min || (options.extend && options.extend[0])
    const max = options.max || (options.extend && options.extend[1])
    const step = options.step || 1
    const name = options.name

    let guiItem = gui[method](controls, key)

    if (guiItem.min && guiItem.max && guiItem.step) {
      guiItem = min !== undefined ? guiItem.min(min) : guiItem
      guiItem = max !== undefined ? guiItem.max(max) : guiItem
      guiItem = step !== undefined ? guiItem.step(step) : guiItem
    }

    guiItem = name !== undefined ? guiItem.name(name) : guiItem

    guiItem.onChange((value) => {
      if (options.handler) {
        options.handler(item, key, value, parent)
      } else {
        item[key] = value
      }
    })
  }
}

import * as THREE from 'three'

import { BaseGui } from './BaseGui'

function defaultHandler(item, key, value) {
  item[key] = value
}
const Vector3Config = {
  x: {
    handler: defaultHandler,
  },
  y: {
    handler: defaultHandler,
  },
  z: {
    handler: defaultHandler,
  },
}
function eulerHandler(item, key, value) {
  item[key] = THREE.MathUtils.degToRad(value)
}
const EulerConfig = {
  x: {
    extend: [-180, 180],
    handler: eulerHandler,
  },
  y: {
    extend: [-180, 180],
    handler: eulerHandler,
  },
  z: {
    extend: [-180, 180],
    handler: eulerHandler,
  },
}
function geometryHandler(item, key, value, parent) {
  const params = { ...parent.geometry.parameters }
  params[key] = value
  parent.geometry.dispose()
  parent.geometry = new THREE[parent.geometry.type](...Object.values(params))
}
const GeometryMapping = {
  BoxGeometry: {
    width: {
      name: 'x轴宽度',
      extend: [2, 20],
      handler: geometryHandler,
    },
    height: {
      name: 'y轴高度',
      extend: [2, 20],
      handler: geometryHandler,
    },
    depth: {
      name: 'z轴深度',
      extend: [2, 20],
      handler: geometryHandler,
    },
  },
  SphereGeometry: {
    radius: {
      name: '半径',
      min: 1,
      handler: geometryHandler,
    },
    widthSegments: {
      name: '水平分段数',
      min: 3,
      handler: geometryHandler,
    },
    heightSegments: {
      name: '垂直分段数',
      min: 2,
      handler: geometryHandler,
    },
  },
  PlaneGeometry: {
    width: {
      name: 'x轴宽度',
      min: 1,
      handler: geometryHandler,
    },
    height: {
      name: 'y轴高度',
      min: 1,
      handler: geometryHandler,
    },
  },
}

export class MeshGui extends BaseGui {
  constructor(options = {}) {
    if (!options.target.isMesh) {
      console.error('target must be an instance of Mesh')
      return
    }

    super()
    this.init(options)
  }
  init(options) {
    this.mesh = options.target
    this.geometry = this.mesh.geometry
    this.material = this.mesh.material

    this.position = this.mesh.position
    this.rotation = this.mesh.rotation
    this.scale = this.mesh.scale

    options.position !== false ? this.initPosition() : ''
    options.rotation !== false ? this.initRotation() : ''
    options.scale !== false ? this.initScale() : ''
    options.geometry !== false ? this.initGeometry() : ''
  }
  initPosition() {
    console.log(this.position)
    this.initGuiFolder(this.position, '位置', this.mesh, Vector3Config)
  }
  initRotation() {
    this.initGuiFolder(this.rotation, '旋转(度)', this.mesh, EulerConfig)
  }
  initScale() {
    this.initGuiFolder(this.scale, '缩放', this.mesh, Vector3Config)
  }
  initGeometry() {
    const geometry = this.geometry
    const type = geometry.type
    const config = GeometryMapping[type]
    this.initGuiFolder(geometry.parameters, geometry.type, this.mesh, config)
  }
}

完整示例(优化封装版)

在球体几何中引用MeshGui

// 导入threejs
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

// 导入封装的Gui工具
import { MeshGui } from '../gui'

// 一. 创建场景
const scene = new THREE.Scene()

// 二. 创建相机
const camera = new THREE.PerspectiveCamera(
  45,
  window.innerWidth / window.innerHeight
)
camera.position.set(20, 20, 20)
camera.lookAt(0, 0, 0)

// 三. 创建物体
const sphereGeometry = new THREE.SphereGeometry(2)
const sphereMaterail = new THREE.MeshNormalMaterial() // 法向
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterail)
scene.add(sphere)

// 六. 集成Gui工具
new MeshGui({
  target: sphere,
})

// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
document.body.appendChild(renderer.domElement)

function animation() {
  renderer.render(scene, camera)
}

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()

  renderer.setSize(window.innerWidth, window.innerHeight)
})
// 五. 集成辅助工具
const control = new OrbitControls(camera, renderer.domElement)

const axesHepler = new THREE.AxesHelper(10)
scene.add(axesHepler)

const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)

3. 平面

对于平面, 主要设置

  • width: x轴方向的宽度
  • height: y轴方向的高度
const geometry = new THREE.PlaneGeometry(20, 20)
const material = new THREE.MeshBasicMaterial({ color: 0x333333 })

const plane = new THREE.Mesh(geometry, material)

完整示例:

import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

import { MeshGui } from './gui'
// 一. 创建场景
const scene = new THREE.Scene()

// 二. 创建相机
const camera = new THREE.PerspectiveCamera(
  45,
  window.innerWidth / window.innerHeight
)
camera.position.set(0, 0, 5)

// 三. 创建物体
const geometry = new THREE.PlaneGeometry(20, 20)
const material = new THREE.MeshNormalMaterial()
const plane = new THREE.Mesh(geometry, material)
scene.add(plane)
new MeshGui({
  target: plane,
})

// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
// 将渲染的canvas添加到body元素中
document.body.appendChild(renderer.domElement)

// 五. 辅助工具
const control = new OrbitControls(camera, renderer.domElement)

const axesHelper = new THREE.AxesHelper(10)
scene.add(axesHelper)

const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)

function animation() {
  renderer.render(scene, camera)
}

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()

  renderer.setSize(window.innerWidth, window.innerHeight)
})

4. 缓冲区几何体

虽然three.js给我们内置了很多常用的几何体

但是对于一些没有提供的几何体如何处理呢?

比如, 像下图所示的几何体

这里, 我们可以通过基类BufferGeometry自定义几何体

缓冲区几何体

  1. 根据我们的几何常识: 连点成线, 连线成面, 连面成体
  2. 如果我们要渲染一个几何体, 只需要确定几何体的顶点坐标, 然后将这些点连接起来, 但是点太多, 为了方便管理我们使用一个缓冲区Buffer来存储, 因此自定义几何体也称缓冲区几何体

步骤

  1. 实例化BufferGeometry对象
  2. 定义顶点坐标缓冲区(数组), 数组中每3个元素为一组, 表示一点的坐标
  3. 设置几何体的position属性

示例

// 1. 创建缓冲区几何体对象
const geometry = new THREE.BufferGeometry()
// 2. 定义顶点坐标缓冲区
const vertices = new Float32Array([
  // 第一个三角形
  -1.0, -1.0, 1.0, 1.0, -1.0, 0, 1.0, 1.0, 1.0,
  // 第二个三角形
  -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 0,
])
// 3. 设置顶点坐标
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))

const material = new THREE.MeshBasicMaterial({
  color: 0xff0000,
  wireframe: true,
})

const mesh = new THREE.Mesh(geometry, material)

完整示例:

import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

// 一. 创建场景
const scene = new THREE.Scene()

// 二. 创建相机
const camera = new THREE.PerspectiveCamera(
  45,
  window.innerWidth / window.innerHeight
)
camera.position.set(0, 0, 5)

// 三. 创建物体
// 根据顶点构建三角形
// 1. 创建缓冲区几何体对象
const geometry = new THREE.BufferGeometry()
// 2. 定义顶点坐标缓冲区
const vertices = new Float32Array([
  // 第一个三角形
  -1.0, -1.0, 1.0, 1.0, -1.0, 0, 1.0, 1.0, 1.0,
  // 第二个三角形
  -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 0,
])
// 3. 设置顶点坐标
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))

const material = new THREE.MeshBasicMaterial({
  color: 0xff0000,
  // wireframe: true,
})

const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)

// 四. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(animation)
// 将渲染的canvas添加到body元素中
document.body.appendChild(renderer.domElement)

// 五. 辅助工具
const control = new OrbitControls(camera, renderer.domElement)

const axesHelper = new THREE.AxesHelper(10)
scene.add(axesHelper)

const gridHelper = new THREE.GridHelper(20, 20, 0xffffff, 0xffffff)
gridHelper.material.transparent = true
gridHelper.material.opacity = 0.5
scene.add(gridHelper)

function animation() {
  renderer.render(scene, camera)
}

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()

  renderer.setSize(window.innerWidth, window.innerHeight)
})

需要视频版教程戳↓↓↓

GIS资料免费领icon-default.png?t=N7T8https://www.wjx.cn/vm/Qm8Ful2.aspx

  • 25
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 如果你想开始学习three.js,可以从教程源代码入手。教程源码提供了一些示例和实例,可以启发你的创作灵感,也可以让你更快地理解three.js的基础知识。 在查看源码之前,你需要先安装three.js和web开发环境。安装完成后,你就可以开始查看源码了。 教程源码一般会有一些说明和注释,这些注释可以帮助你更好地理解代码的功能和实现方法。你可以先阅读这些注释,然后尝试运行代码,看代码是如何运作的。 当你理解了一些基础的代码后,你可以尝试自己写一些小程序,练习three.js的使用。这些小程序可能简单,但是可以帮助你加深对three.js的理解,并为今后更复杂的项目做好准备。 最重要的是,遇到问题不要担心,可以参考three.js社区和文档,也可以在网上寻找相关的教程和视频。通过不断摸索和学习,你一定能掌握three.js的使用技巧,创作出自己喜欢的作品。 ### 回答2: three.js是一个能够在网页上实现3D图形和动画效果的JavaScript库,可以用于创建各种有趣的3D场景和游戏。针对初学者,有很多的入门教程和示例代码可供学习和练习。 在网上搜索“three.js入门教程源代码”可以找到很多优秀的教程和示例代码,如three.js官方文档、CodePen、Github等。此外,同类的OpenGL和WebGL等技术也有很多优秀的资源可供参考。 对于初学者来说,建议先了解three.js的基本知识,如坐标系、场景、相机、灯光、材质、几何体等等。然后可以通过实践来加深理解,例如创建一个简单的立方体、平面、球体等。同时也可以尝试调整相机、灯光和材质等参数,看看会发生什么变化。 在学习过程中,遇到问题可以查阅文档或者在相关社区寻求帮助。同时也要多注重实践,通过参考示例代码和自己实践来提升技能。 ### 回答3: three.js是一个非常流行的JavaScript 3D图形库,很多人想要学习它,但是对于初学者来说,这可能会让他们感到非常困难。因此,一些网站和博客提供了一些入门教程以帮助初学者了解并开始使用three.js,其中一些包括源代码。 这些源代码提供一些基础教学,帮助用户通过编写一些简单的three.js代码并观察结果来学习three.js的基础知识。 用户可以下载源代码,并使用它们在自己的计算机上运行三维场景。 这些源代码通常提供了许多有关three.js的基础知识,包括: 1. 创建场景,灯光和相机。 2. 从文件中加载3D模型和纹理。 3. 使用材质和纹理来渲染对象。 4. 添加动画和控制器。 5. 处理鼠标和键盘输入。 6. 在网页上嵌入three.js场景。 这些源代码通常以注释的形式提供,说明每个代码块的功能。此外,这些源代码通常遵循良好的编程实践,使读者易于理解和跟踪整个过程。 总的来说,three.js零基础入门教程源代码非常适合那些想要学习three.js的初学者,他们可以通过这些源代码快速入门,并了解更多three.js知识。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值