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

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

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

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

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

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

后续持续更新

GIS开发资料分享icon-default.png?t=N7T8https://docs.qq.com/doc/DRmxxaVhpbGJXSGho?u=5295a88d71d8480d971da4e3334ee913

下面将进一步详解介绍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)
})

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值