Showcase :-D
更多作品及思维导图: github.com/boombb12138…
相机
PerspectiveCamera
透视相机
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 1, 100)
const camera = new THREE.PerspectiveCamera(垂直方向视野角度,纵横比,near,far)
比near更近或比far更远的对象将不会显示
OrthographicCamera
正交相机
OrbitControls
创建相机后再新建OrbitControls
const controls = new OrbitControls(camera,canvas)
窗口操作
使画布适合窗口
const sizes = {
width: window.innerWidth,
height: window.innerHeight
}
响应式
window.addEventListener("resize", () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
// 更新相机aspect属性
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix(); //更新矩阵
// 更新renderer
renderer.setSize(sizes.width, sizes.height);
});
处理锯齿 ?
renderer.setPixelRatio(Math.min(window.devicePixelRatio,2))
//window.devicePixelRatio 可以使用的屏幕像素比
双击全屏
window.addEventListener("dblclick", () => {
// 判断是否出于全屏
const fullscreenElement =
document.fullscreenElement || document.webkitFullscreenElement;
if (!fullscreenElement) {
// 请求使canvas元素全屏
canvas.requestFullscreen() ?? canvas.webkitRequestFullscreen;
} else {
// 离开全屏
document.exitFullscreen() ?? document.webkitExitFullscreen();
}
//webkitRequestFullscreen webkitFullscreenElement等带webkit前缀都是为了适配
});
几何体
BoxGeometry
- widthSegments 在x轴上有多少个部分
- heightSegments 在y轴上有多少个部分
- depthSegments 在z轴上有多少个部分
自定义几何体
// 1.创建空的几何体
const identityGeometry = new THREE.BufferGeometry();
// 2.创建顶点 Float32Array是数组类型 只能存储浮点数,且长度是固定的
const positionsArray = new Float32Array([0, 0, 0, 0, 1, 0, 1, 0, 0]);
// 3.将数组转为BufferAttribute
const positionsAttribute = new THREE.BufferAttribute(positionsArray, 3);
// 4.将属性添加到几何体
identityGeometry.setAttribute("position", positionsAttribute);
const identityMaterial = new THREE.MeshBasicMaterial({ color: "red" });
const identityMesh = new THREE.Mesh(identityGeometry, identityMaterial);
identityMesh.position.y = -1;
scene.add(identityMesh);
调试面板
借助lil-gui
npm install --save lil-gui
重启server
import * as dat from 'lil-gui'
const gui = new dat.GUI()
向面板添加元素
gui.add(对象,要调整的对象的属性,最小值,最大值,精度)
另一种写法:
gui.add(对象,要调整的对象的属性).min(最小值).max(最大值).step(精度)
更改标签:
gui.add(对象,要调整的对象的属性).min(最小值).max(最大值).step(精度).name('elevation')
gui.add(mesh, "visible"); //控制显示和隐藏
处理颜色
const parameters = {
color : 0xff0000
}//创建一个对象 该对象包含color属性
gui
.addColor(parameters,'color')
.onChange(() => {
material.color.set(parameters.color)
})
在颜色值改变时提醒 链式调用
触发函数eg:动画
给parameters添加spin属性
const parameters = {
color : 0xff0000,
spin: () => {
gsap.to(mesh.rotation,{duration:1, y:mesh.rotation.y + Math.PI * 2})
}
}
gui.add(parameters,'spin')
更改面板宽度
const gui = new dat.GUI({ width: 400 })
GUI API文档
github.com/dataarts/da…示例:jsfiddle.net/ikatyang/18…
纹理Texture
加载纹理
使用TextureLoader加载纹理
const textureLoader = new THREE.TextureLoader()
const doorColorTexture = textureLoader.load('/textures/door/color.jpg')
使用加载管理器和TextureLoader加载纹理
// 在加载图像时收到通知 用loadingManager
const loadingManager = new THREE.LoadingManager();
const textureLoader = new THREE.TextureLoader(loadingManager);
loadingManager.onStart = () => {
console.log("loading started");
};
// 开始加载所需的所有纹理
const colorTexture = textureLoader.load("/door.jpg");
使用纹理
const material = new THREE.MeshBasicMaterial({ map: colorTexture })
处理纹理
colorTexture.wrapS = THREE.RepeatWrapping //重复纹理
colorTexture.wrapT = THREE.RepeatWrapping
colorTexture.rotation = Math.PI * 0.25; //旋转
哪里找Texture
材料Material
MeshBasicMaterial
最基本的材料
map 在几何体的表面上应用纹理
color 在几何体的表面上应用统一的颜色
wireframe 用细线展示几何体
opacity transparent 控制透明度
const material = new THREE.MeshBasicMaterial();
material.map = doorColorTexture;
material.color = new THREE.Color("#ff0000"); //給紋理著色
material.wireframe = true; //細綫顯示
material.transparent = true;
material.opacity = 0.5; //transparent 和 opacity控制透明度
material.side = THREE.DoubleSide; //控制正反面都可見 會降低性能
MeshNormalMaterial
显示渐变彩色
const material = new THREE.MeshNormalMaterial();
// material.flatShading = true; //每个组成面都有棱有角
MeshMatcapMaterial
需要一个球体纹理
const material = new THREE.MeshMatcapMaterial()
material.matcap = matcapTexture
哪里可以找到 matcaps 纹理:
observablehq.com/@makio135/matcaps?ui=classic
对光做出反应的材料
MeshLambertMaterial
MeshPhongMaterial
const material = new THREE.MeshPhongMaterial()
material.shininess = 100//光反射强度 值越高 表面越亮
material.specular = new THREE.Color(0x1188ff)//更改反射的颜色
MeshToonMaterial
卡通风材料
const material = new THREE.MeshToonMaterial()
MeshStandardMaterial
const material = new THREE.MeshStandardMaterial();
material.metalness = 0.7;
material.roughness = 0.2;
aoMap 环境光遮蔽贴图,将在纹理较暗的地方添加阴影,增加对比度,真实性
sphere.geometry.setAttribute(
"uv2",
new THREE.BufferAttribute(sphere.geometry.attributes.uv.array, 2)
);
plane.geometry.setAttribute(
"uv2",
new THREE.BufferAttribute(sphere.geometry.attributes.uv.array, 2)
);
torus.geometry.setAttribute(
"uv2",
new THREE.BufferAttribute(sphere.geometry.attributes.uv.array, 2)
);
material.aoMap = doorAmbientOcclusionTexture; //使用纹理
material.aoMapIntensity = 1; //强度
material.displacementMap = doorHeightTexture; // 移动顶点以创建真正的浮雕
material.displacementScale = 0.05; //调顶点
环境贴图
环境贴图 需要用CubeTextureLoader加载纹理集合,
const cubeTextureLoader = ne THREE.CubeTextureLoader()
const environmentMapTexture = cubeTextureLoader.load([
'/textures/environmentMaps/0/px.jpg',
'/textures/environmentMaps/0/nx.jpg',
'/textures/environmentMaps/0/py.jpg',
'/textures/environmentMaps/0/ny.jpg',
'/textures/environmentMaps/0/pz.jpg',
'/textures/environmentMaps/0/nz.jpg'])
material.envMap = environmentMapTexture
查找纹理地图:
将HDRI转为立方体地图:
matheowis.github.io/HDRI-to-Cub…
字体
Three.js只接受特定json格式的字体
转换字体:gero3.github.io/facetype.js…
或者直接从Three.js的example中导入字体
import typefaceFont from 'three/examples/fonts/helvetiker_regular.typeface.json'
将helvetiker_regular.typeface.json文件放在static/fonts文件夹中
加载字体
从three/examples/jsm/loaders导入FontLoader
加载成功后,配置文本内容;文本样式
fontLoader.load("/fonts/helvetiker_regular.typeface.json", (font) => {
// 使用文本几何
const textGeometry = new TextGeometry("Li Naomi", {
font: font,
size: 0.5,
height: 0.2,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
bevelOffset: 0,
bevelSegments: 5,
});
// 使文本居中
textGeometry.computeBoundingBox(); //计算几何体的框边界
console.log(textGeometry.boundingBox);
textGeometry.center();
使用matcap纹理
**下载matcap纹理:
const textureLoader = new THREE.TextureLoader();
const matcapTexture = textureLoader.load("/textures/bgGreen.png");
const material = new THREE.MeshMatcapMaterial({ matcap: matcapTexture });
const text = new THREE.Mesh(textGeometry, material);
光
对光有反应的材料: MeshStandardMaterial
环境光
const ambientLight = new THREE.AmbientLight(color, intensity)
在调试面板中添加环境光强度元素
gui.add(ambientLight,'intensity').min(0).max(1).step(0.001)
定向光
const directionalLight = new THREE.DirectionalLight(0x00fffc, 0.8);
directionalLight.position.set(20, 0.25, 0); //改变环境光的方向
scene.add(directionalLight);
半球灯
const hemisphereLight = new THREE.HemisphereLight(color,groundColor,intensity);
const hemisphereLight = new THREE.HemisphereLight(0xff0000, 0x0000ff, 0.3);
scene.add(hemisphereLight);
点光源
// 点光源 就像打火机
const pointLight = new THREE.PointLight(0xffffff, 0.5);
pointLight.position.x = -1;
pointLight.position.y = -0.5;
pointLight.position.z = 1;
scene.add(pointLight);
矩形区域灯
const rectAreaLight = new THREE.RectAreaLight(color,intensity,width,height);
const rectAreaLight = new THREE.RectAreaLight(0x4e00ff, 2, 1, 1);
rectAreaLight.position.set(1.5, 0, 1);
聚光灯
const spotLight = new THREE.SpotLight(
0x78ff00,颜色
0.5,强度
10,强度下降到的距离
Math.PI * 0.1,角度
0.25,光束轮廓的扩散长度
1光线变暗的速度
);
spotLight.position.set(0, 2, 3);
scene.add(spotLight);
// 移动聚光灯
spotLight.target.position.x = -0.75;
scene.add(spotLight.target);
- 性能高 环境光 半球灯
- 性能低 聚光灯 矩形区域灯
灯光助手
实例化类并添加到场景中
const hemisphereLightHelper = new THREE.HemisphereLightHelper(
hemisphereLight,
0.2//helper的size
);
scene.add(hemisphereLightHelper);
const directionalLightHelper = new THREE.DirectionalLightHelper(
directionalLight,
0.2
);
scene.add(directionalLightHelper);
点光源助手没有参数 在移动物体后需要在下一帧更新helper
const spotLightHelper = new THREE.SpotLightHelper(spotLight)
scene.add(spotLightHelper)
window.requestAnimationFrame(() =>
{
spotLightHelper.update()
})
矩形光源助手需要单独引入
import { RectAreaLightHelper } from 'three/examples/jsm/helpers/RectAreaLightHelper.js';
const rectAreaLightHelper = new RectAreaLightHelper(rectAreaLight)
scene.add(rectAreaLightHelper)
阴影
激活阴影
renderer.shadowMap.enabled = true;
sphere.castShadow = true; //投射阴影
plane.receiveShadow = true; //接收阴影
directionalLight.castShadow = true; //激活光源上的阴影
阴影
设置阴影贴图大小 值必须是2的幂
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
设置相机的最远最近面 可以修复看不到阴影或阴影突然被裁减的错误
directionalLight.shadow.camera.near = 1
directionalLight.shadow.camera.far = 6
波幅(可视范围)控制相机每一侧可以看到的距离 值越小阴影越准确 但阴影将被裁剪
directionalLight.shadow.camera.top = 2;
directionalLight.shadow.camera.right = 2;
directionalLight.shadow.camera.bottom = -2;
directionalLight.shadow.camera.left = -2;
阴影模糊
directionalLight.shadow.radius = 10;
阴影贴图算法
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
模型
Blender制作模型
快捷键文档
数字键盘上 1 3 7 切换坐标轴
tab键切换模式
SHIFT + D复制
A 全选
导出gltf的时候 Mesh要apply modifier
在Edit Mode下,ctrl + R 可以增加切面
调整芝士片曲面的边角
在Edit Mode, 选中芝士片,右键选择Subdivide,设置Number of Cuts为10, 选中一个角
开启Proportional Editing,调整芝士片的边角 滚轮可以调整影响范围
最后在Object Mode,右键选择 Shade Smooth
导入模型
在哪里找模型?
GLTF格式
- glTF
- glTF-Binary
- glTF-Draco
- glTF-Embedded
加载模型
对于glTF,glTF-Binary,glTF-Embedded
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
gltfLoader.load(
'/models/Duck/glTF/Duck.gltf',
(gltf) =>
{
scene.add(gltf.scene.children[0])//children只有1个元素
}
)
对于children有多个元素
gltfLoader.load("/models/FlightHelmet/glTF/FlightHelmet.gltf", (gltf) => {
const children = [...gltf.scene.children];
for (const child of children) {
scene.add(child);
}
});
对于glTF-Draco
Draco版本,有利于加载大模型
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("/draco/");
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.load("/models/Duck/glTF-Draco/Duck.gltf", (gltf) => {
scene.add(gltf.scene);
});
模型动画
Fox中 gltf.scene.children是一个3D模型 所以添加到场景直接用scene.add(gltf.scene)
let mixer = null;
gltfLoader.load("/models/Fox/glTF/Fox.gltf", (gltf) => {
//...
// 处理动画
mixer = new THREE.AnimationMixer(gltf.scene);
const action = mixer.clipAction(gltf.animations[1]);
action.play();
});
const tick = () => {
//...
// 动画需要每一帧都更新
// 当mixer有值 即模型加载hou
if (mixer) {
mixer.update(deltaTime);
}
//...
}
综合案例:Portfolio
清除背景色
方法1
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: true, ////清除背景色法1 使canvas背景为透明
});
方法2
renderer.setClearAlpha(0);
之后在css文件中设置
html
{
background: #1e1a20;
}
让相机跟随页面滚动而滚动
如果用户滚动页面到下一部分,那么相机将向下移动到下一个对象
// 1.获取页面滚动位置
let scrollY = window.scrollY;
window.addEventListener("scroll", () => {
scrollY = window.scrollY;
const newSection = Math.round(scrollY / sizes.height);
console.log(newSection);
//...
});
const tick = () => {
const elapsedTime = clock.getElapsedTime();
const deltaTime = elapsedTime - previousTime; //现在这一帧和上一帧花费的时间
previousTime = elapsedTime;
//2.更新相机位置
// -scrollY因为three.js y轴往下是正
// -scrollY / sizes.height得到 n 个section
camera.position.y = (-scrollY / sizes.height) * objectsDistance;
}
综合案例:渲染真实效果
灯
-
方向光
将光源调得更真实:
renderer.physicallyCorrectLights = true处理shadow acne阴影线:
directionalLight.shadow.normalBias = 0.05;//圆表面 normalBias应用于圆表面 可以处理shadow acne阴影线 处理平面用bias
-
环境贴图
- 加载环境地图
const cubeTextureLoader = new THREE.CubeTextureLoader(); const environmentMap = cubeTextureLoader.load([ '/textures/environmentMaps/0/px.jpg', '/textures/environmentMaps/0/nx.jpg', '/textures/environmentMaps/0/py.jpg', '/textures/environmentMaps/0/ny.jpg', '/textures/environmentMaps/0/pz.jpg', '/textures/environmentMaps/0/nz.jpg' ])- 把环境贴图添加到场景
scene.background = environmentMap- 让模型反映环境
环境贴图应用于具有MeshStandardMaterial的Mesh。
const debugObject = {}//用来调envMapIntensity const updateAllMaterials = () => { // 将环境映射到所有的mesh中 具有MeshStandard的mesh scene.traverse((child) => { if ( child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial ) { child.material.envMap = environmentMap; child.material.envMapIntensity = debugObject.envMapIntensity; //5 激活Mesh上的阴影 child.castShadow = true; child.receiveShadow = true; } }); }; debugObject.envMapIntensity = 2.5; gui.add(debugObject, 'envMapIntensity').min(0).max(10).step(0.001)
加载模型
const gltfLoader = new GLTFLoader();
gltfLoader.load("/models/FlightHelmet/glTF/FlightHelmet.gltf", (gltf) => {
// 调位置
gltf.scene.scale.set(10, 10, 10);
gltf.scene.position.set(0, -4, 0);
gltf.scene.rotation.y = Math.PI * 0.5;
// 向调试面板中添加rotation
gui
.add(gltf.scene.rotation, "y")
.min(-Math.PI)
.max(Math.PI)
.step(0.001)
.name("rotation");
scene.add(gltf.scene);
updateAllMaterials();
});
调整编码
renderer.outputEncoding = THREE.sRGBEncoding; //设置输出渲染编码 让场景更真实
environmentMap.encoding = THREE.sRGBEncoding; //设置环境贴图的编码
滤镜
renderer.toneMapping = THREE.ACESFilmicToneMapping;
gui
.add(renderer, "toneMapping", {
No: THREE.NoToneMapping,
Linear: THREE.LinearToneMapping,
Reinhard: THREE.ReinhardToneMapping,
Cineon: THREE.ACESFilmicToneMapping,
ACESFilmic: THREE.ACESFilmicToneMapping,
})
.onFinishChange(() => {
renderer.toneMapping = Number(renderer.toneMapping);
});
阴影
renderer.shadowMap.enabled = true; //1 开启渲染器的阴影
renderer.shadowMap.type = THREE.PCFSoftShadowMap; //2 改阴影类型
directionalLight.castShadow = true; //3 开启方向光的投射阴影
directionalLight.shadow.camera.far = 15;
directionalLight.shadow.mapSize.set(1024, 1024); //4 增加阴影真实性和精确度
scene.traverse((child) => {
if (
child instanceof THREE.Mesh &&
child.material instanceof THREE.MeshStandardMaterial
) {
...
//5 激活Mesh上的阴影
child.castShadow = true;
child.receiveShadow = true;
}
});
着色器Shaders
着色器的概念
用GLSL编写的 会被发送到GPU。
作用
定位几何体的顶点,并为该几何体的每个可见像素(片段)着色
-
顶点着色器
作用:定位几何体的顶点 可以发送顶点位置,Mesh的变换,相机信息。GPU拿到顶点着色器的信息后,会将顶点投射到canvas上. 换句话说,顶点着色器会将 3D 顶点坐标转换为我们的 2D 画布坐标。
-
在每个顶点之间变化的数据:Attributes
-
在每个顶点之间一样的数据:Uniforms 可以在顶点着色器和片段着色器中使用。
片段着色器在顶点着色器之后执行。
// 矩阵 他们的值对几何体所有顶点都是相同的 因此用uniform uniform mat4 projectionMatrix;//将坐标转换为剪辑空间坐标 uniform mat4 viewMatrix;//处理和相机相关的所有变换 uniform mat4 modelMatrix;//处理和Mesh相关的所有变换 比如缩放、旋转、移动网格 attribute vec3 position;//检索顶点 void main() { vec4 modelPosition = modelMatrix * vec4(position,1.0); modelPosition.z += sin(modelPosition.x * 10.0) * 0.1;//将平整的面改为波形 vec4 viewPosition = viewMatrix * modelPosition; vec4 projectedPosition = projectionMatrix * viewPosition; // 要把mat4赋给一个变量 该变量的类型必须是vec4 gl_Position = projectedPosition; }
-
-
片段着色器
作用:为几何体的每个可见片段着色 可以像顶点着色器那样发送统一数据到GPU,也可以将数据从顶点着色器发送到片段着色器(这种数据称为varing)
precision mediump float;//决定float的精确度 一般用mediump void main() { gl_FragColor = vec4(0.5, 0.0, 1.0, 1.0); //vec4(r, g, b, a) 当a = 1,要设置new THREE.RawShaderMaterial({transparent: true}) }Attributes
将平整的面改为锯齿形
-
自定义属性aRandom
// 将平整的面改为锯齿形 const count = geometry.attributes.position.count; //获取几何体有多少顶点 const randoms = new Float32Array(count); //count此时是数组长度 for (let i = 0; i < count; i++) { randoms[i] = Math.random(); } geometry.setAttribute("aRandom", new THREE.BufferAttribute(randoms, 1)); //最后一个参数 1:randoms中的元素只有1个值 不是vec2,如果是就是2 -
在顶点着色器中获取此属性并使用它来移动顶点
modelPosition.z += aRandom * 0.1 ;//将平整的面改为锯齿形 -
使用aRandom属性为fragment着色. 由于不能在片段着色器中使用该属性。就得从顶点着色器发送到片段着色器
//顶点着色器 varying float vRandom; void main(){ //... // 将数据从顶点着色器发送到名为片段着色器 vRandom = aRandom; }//片段着色器 varying float vRandom; void main() { gl_FragColor = vec4(0.5, vRandom, 1.0, 1.0); //vec4(r, g, b, a) 当a = 1,设置new THREE.RawShaderMaterial({transparent: true}) }
Uniforms
Uniforms是一种将数据从js发送到着色器的方法。
自定义Uniforms
-
将uniforms添加到到material
const material = new THREE.RawShaderMaterial({ //... uniforms: { uFrequency: { value: new THREE.Vector2(10, 5) }, //为了区分uniforms类型的数据和其他数据,将uniforms类型的数据以u开头 }, }); -
在顶点着色器中检索并使用
uniform vec2 uFrequency; void main() { //... modelPosition.z += sin(modelPosition.x * uFrequency.x) * 0.1; //将平整的面改为波形 采用自定义Uniform,可以控制波 modelPosition.z += sin(modelPosition.y * uFrequency.y) * 0.1; //... } -
加动画
const material = new THREE.RawShaderMaterial({ //... uniforms: { uFrequency: { value: new THREE.Vector2(10, 5) }, //新增uTime uTime: { value: 0 }, }, }); const tick = () => { const elapsedTime = clock.getElapsedTime(); // ... // Update material material.uniforms.uTime.value = elapsedTime; // .. }; //顶点着色器 uniform float uTime; void main() { //... modelPosition.z += sin(modelPosition.x * uFrequency.x - uTime) * 0.1; ////将平整的面改为波形 加动画 modelPosition.z += sin(modelPosition.y * uFrequency.y - uTime) * 0.1; }
材质
-
加载材质
const flagTexture = textureLoader.load("/textures/bubble.jpg"); -
将材质作为uniforms发送
const material = new THREE.RawShaderMaterial({ // ... uniforms: { // ... uTexture: { value: flagTexture } } })在几何体上投射纹理的坐标 即为 uv坐标
材质应用到片段着色器是调用texture2D方法,该方法第2个参数是 uv坐标
所以需要将uv坐标传递给片段着色器 平面几何体自动生成uv属性
// console.log(geometry.attributes.uv);
所以在顶点着色器中直接检索
attribute vec2 uv; //将数据从顶点着色器发送到片段着色器 varying vec2 vUv;//片段着色器 uniform sampler2D uTexture; varying vec2 vUv; void main() { //... // 使用材质 vec4 textureColor = texture2D(uTexture,vUv); gl_FragColor = textureColor; }
-
GLSL
类似c语言 是一种静态语言
浮点数
float bar = 1.0//必须始终提供小数 即使值是整数
整数
int foo = 123
float a = 1.0;
int b = 2;
float c = a * float(b);//动态转换类型
布尔值
bool foo = true;
Vector2
vec2 foo = vec2(1.0, 2.0);
vec2 foo = vec2();//将报错
//创建Vector2
vec2 foo = vec2(0.0 );
foo.x = 1.0;
foo.y = 2.0;
Vector3
vec3 foo = vec3(1.0, 2.0, 3.0);
或者
vec2 foo = vec2(1.0, 2.0);
vec3 bar = vec3(foo, 3.0);
取一部分vec3来生成一个vec2
vec3 foo = vec3(1.0, 2.0, 3.0);
vec2 bar = foo.xy;
Functions
函数必须以返回的值的类型开头
float add(float a, float b){
return a + b;
}
关于Shader的文档
www.shaderific.com/glsl-functi…
gl_Position
为什么要给gl_Position4个值?
因为我们绘制顶点实际上最终是在剪辑空间中定位的,gl_Position的功能是在剪辑空间中定位顶点,需要4个方向
剪辑空间是有4个方向的,范围从-1到+1的空间。超出此范围的内容都会被剪辑。其中3个方向分别是x,y,z 另外一个方向是w,负责透视