前言
贵州“村超”的火热现象是一个多方面因素共同作用的结果,它不仅是一场体育赛事,更是一个文化现象,反映了时代的精神和人民的情感诉求,同时也推动了乡村振兴和地区发展。足球的魅力是多方面的,它不仅仅是一项运动,更是一种全球性的文化现象。
-
简单规则下的无限变化:足球的基本规则简单,但比赛中的战术变化无穷,这为观众提供了丰富的观看体验。每个球队都有自己的战术风格,每场比赛都可能出现不同的策略和惊喜。
-
强烈的集体感和团队精神:足球是一项团队运动,强调集体合作和个人牺牲。球迷们往往会对自己的球队产生强烈的归属感,这种集体感和团队精神是足球魅力的重要组成部分。
-
国际化和多样性:足球是一项全球性的运动,世界杯等国际大赛汇集了来自世界各地的球队和球迷,展现了不同文化背景下的足球风格和热情。
-
情感和激情:足球比赛常常充满了情感和激情,无论是球员在比赛中的全力以赴,还是球迷在看台上的狂热支持,都体现了人们对足球的深厚情感。
-
不确定性和戏剧性:足球比赛的结果往往充满不确定性,直到最后一刻都有可能出现逆转。这种戏剧性让足球比赛更加吸引人,也是其魅力所在。
-
社会和文化意义:足球在许多国家和地区不仅仅是一项运动,它还具有重要的社会和文化意义。足球可以成为社会团结的象征,甚至在一定程度上影响社会和政治。
-
历史和传统:足球有着悠久的历史和丰富的传统,许多俱乐部和赛事都有着百年以上的历史,这些历史和传统增加了足球的文化深度和魅力。
-
球星和个人英雄主义:尽管足球是一项团队运动,但球星的作用和个人英雄主义也是足球魅力的一部分。球星们凭借自己的才华和努力,赢得了球迷的喜爱和尊敬。
足球的魅力在于它简单规则下的复杂性、团队精神、国际化和多样性、情感和激情、不确定性和戏剧性、社会和文化意义、历史和传统,以及球星和个人英雄主义。这些因素共同构成了足球这一全球最受欢迎的体育项目。回看中国足球国家队的问题及其复杂多样,涉及青训体系、管理体制、球员水平、文化环境等多个方面。解决这些问题需要从政策制定、体制改革、人才培养等多个角度综合施策。
谈到足球与数字孪生技术的结合,是一个将现实世界中的足球运动与虚拟世界中的数据分析和模拟相结合的现代化故事。数字孪生技术是一种通过创建物理对象的虚拟副本,来模拟和分析其性能的方法。
在足球领域,这项技术可以帮助球队提高比赛策略、球员表现和整体管理效率。足球魅力始终来源于场上的激烈对抗和随时发生的变化,而数据分析工具可以帮助管理者放大这一不确定性带来的吸引力。
数字孪生在足球训练中的应用
在足球训练中,数字孪生技术可以用于创建球员和球队的虚拟模型。通过这些模型,教练和分析师可以模拟不同的比赛场景,预测对手的行动,并据此制定战术。
球员表现分析
每个球员的数字孪生可以基于他们的统计数据,如速度、耐力、射门准确率等来创建。通过分析这些数据,教练可以更准确地评估球员的表现,并针对他们的弱点进行训练。
球队战术模拟
通过模拟球队的整体表现,教练可以测试不同的战术和阵型,了解它们在不同比赛情况下的效果,而不需要在实际比赛中承担风险。
比赛日的策略调整
在比赛日,数字孪生可以继续发挥作用。通过实时数据,如球员的位置、速度和体力消耗,教练可以做出更加精准的换人决策,并实时调整战术。
球场和设施管理
除了提高球队的表现,数字孪生技术还可以用于球场的运营和管理。通过模拟人流、交通和设施使用情况,球场管理者可以更好地规划比赛日的运营,确保观众的安全和舒适。
未来展望
随着技术的发展,数字孪生在足球领域的应用将更加广泛。它可能会与增强现实(AR)和虚拟现实(VR)技术结合,为球迷提供全新的观赛体验,让他们能够在虚拟环境中观看比赛,并实时获取球员和比赛的统计数据。
足球与数字孪生技术的结合,展现了科技如何改变传统的体育项目,使之更加科学、高效和现代化。这不仅为球队带来了竞争优势,也为球迷提供了更多了解和参与他们热爱的运动的方式。
针对足球比赛动态、连续、多维、强不确定等复杂对抗特性挑战,以现有大数据和规则知识驱动的智能体实现方式为基础,以多智能体理论、深度强化学习、模糊推理等理论方法为支撑,以实现不同层级的足球运动员在比赛、训练中的辅助作用。
像《基于博弈对抗的足球推演系统》课题立项的背后,是颗粒度更细的研发方向和内容。目前,该项目下设了五个子课题,涵盖了数据收集、运算推演、决策分析、实景应用等多个领域复杂的系统性研究。
数字孪生技术在足球训练中的应用效果是多方面的,尤其在提高训练效率和数据分析方面表现出显著的优势。以下是一些具体的应用实例和效果分析:
-
校园足球数字化技术的应用:在杭州,浙江大学光电学院竺星团队开发了一套校园足球数字化技术。这项技术使用无感式数据采集方式,通过视频采集数据,无需穿戴设备,因此具有零门槛、高普及性的特点。它能进行运动追踪、行为识别,并高效提取职业级的运动数据,形成多元分析评测模型,进行技战术分析和决策能力分析。此技术在“智圣杯”杭州市校园足球邀请赛中得到了应用,比赛结束后,参赛的小球员们都得到了一份个人数据分析报告,这些报告包含了阵容分析、比赛整体数据、球员位置分布、热图、传接网络等丰富信息,对教练和球员的训练和比赛策略提供了重要参考。
-
科技和创新在足球训练中的应用:科技和创新在足球训练中的应用不仅限于数据分析,还包括虚拟现实技术的模拟训练和智能化设备的使用,这些都有助于提高球员训练和比赛体验,以及提升场馆效率和可持续性。例如,虚拟现实技术可以用于模拟训练,提高球队和球员在比赛中的竞争力。
-
数字孪生技术赋能智慧体育:数字孪生技术在智慧体育领域的发展和应用,特别是在训练、比赛策略、伤病预防与康复、体育器材和场馆设计优化等方面,提供了精准、全面的数据支持和决策参考。这些应用不仅帮助运动员提高训练效果和竞技表现,还改善了观众体验和比赛环境,推动了体育产业的创新和发展。
数字孪生技术在足球训练中的应用展现出其提高训练效率、优化比赛策略、提升运动员表现等多方面的积极效果。随着技术的进一步发展,其在足球及其他体育领域的应用潜力将会更加显著。
数字孪生技术在优化足球训练计划方面主要通过以下几个方面实现:
-
数据驱动的决策制定:数字孪生技术可以收集和分析球员在训练和比赛中的各种数据,包括运动轨迹、速度、心率、疲劳程度等。这些数据帮助教练团队更准确地评估球员的表现和身体状况,从而制定更科学的训练计划。
-
模拟和预测:通过创建球员和球队的虚拟模型,数字孪生技术可以模拟不同的训练场景和比赛情况。教练可以使用这些模型来预测特定训练计划的效果,以及球员在不同战术配置中的表现。
-
个性化训练方案:基于对球员表现的详细分析,数字孪生技术可以帮助教练为每位球员定制个性化的训练方案。这样的方案能够针对球员的特定需求和弱点进行优化,从而提高训练效果。
-
风险评估和伤病预防:通过监测球员的训练负荷和身体反应,数字孪生技术可以帮助教练评估训练计划对球员身体的潜在风险,从而及时调整训练强度和内容,减少受伤的可能性。
-
战术演练和比赛准备:数字孪生技术可以模拟对手的战术和行为,让球队在没有实际比赛的情况下进行战术演练和策略测试。这有助于球队更好地准备即将到来的比赛,并针对特定的对手制定有效的战术。
数字孪生技术通过提供准确的数据分析和模拟预测,帮助教练团队更科学、更有效地制定和调整训练计划,从而提高球队的整体表现和竞争力。随着技术的不断发展,数字孪生在足球训练中的应用将变得更加广泛和深入。
使用数字孪生技术进行球员的个性化训练涉及多个步骤,主要包括数据收集、分析、模拟和实施。以下是一个详细的流程:
-
数据收集:
- 生物统计数据:收集球员的身高、体重、年龄等基本数据。
- 运动表现数据:通过视频分析、可穿戴设备等技术收集球员在训练和比赛中的表现数据,如速度、加速度、耐力、技术动作的准确性等。
- 生理数据:监测球员的心率、血压、氧气饱和度等生理指标,以及疲劳和恢复情况。
-
数据分析:
- 利用先进的分析工具和算法处理收集到的数据,识别球员的技术特点和体能状况。
- 对比球队标准和最佳实践,找出球员的强项和弱点。
-
创建数字孪生模型:
- 基于收集的数据,创建球员的数字孪生模型。这个模型是一个虚拟的、动态的复制品,能够模拟球员在实际环境中的行为和反应。
- 模型可以包括球员的运动学参数、生理特征和技能水平。
-
模拟和预测:
- 使用数字孪生模型模拟不同的训练场景,预测球员在不同训练负荷下的表现和恢复情况。
- 通过模拟,教练可以评估特定训练计划对球员个人发展的潜在影响。
-
个性化训练计划:
- 根据数字孪生模型的输出,教练和训练团队可以为球员设计个性化的训练计划,针对其特定需求进行优化。
- 训练计划可能包括特定的技术练习、体能训练和恢复策略。
-
实施和监控:
- 实施个性化的训练计划,并持续监控球员的表现和生理反应。
- 定期更新数字孪生模型,以反映球员的进步和任何变化。
-
调整和优化:
- 根据球员的实际表现和模型的预测结果,不断调整训练计划,确保训练内容与球员的发展目标保持一致。
通过这种方式,数字孪生技术使得足球训练更加个性化和高效,有助于最大化球员的潜力,同时减少受伤风险。随着技术的进步,数字孪生在个性化训练方面的应用将变得更加精细和智能化。
爱好踢球、喜欢足球的人都知道,在足球场上有一种能力叫做意识,意识好的人经常会出现在更合理的位置,其实只要抓住球场上的运动规律,我们每名球员,不管是业余的还是职业的,足球意识都可以得到迅速提升。
在球场上的一个客观事实是, 90分钟的比赛时间里,绝大部分的时间都是被运动员用于跑位以及传接球。
模拟点球大战
使用blender设计足球场的三维场景,再用three.js+cannon.js让它在web中运动起来,配上足球战术战略深度学习模块,像英国Sports Interactive开发的《足球经理》系列游戏,同时兼顾了“管理球员和指挥球队”的教练以及“经营球队”的经理两大身份,还可以体验球迷加球员的双重乐趣。
<!DOCTYPE html>
<html class="fullscreen" lang="zh-CN">
<head>
<title>three.js+cannon.js Web 3D</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">
<meta name="renderer" content="webkit">
<meta name="force-rendering" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=10,chrome=1">
<meta data-rh="true" name="keywords" content="three.js,JavaScript,cannon.js">
<meta data-rh="true" name="description" content="three.js+cannon.js Web 3D">
<meta data-rh="true" property="og:title" content="THREE.JS and CANNON.JS">
<meta data-rh="true" property="og:url" content="">
<meta data-rh="true" property="og:description" content="three.js+cannon.js Web 3D">
<meta data-rh="true" property="og:image" content="">
<meta data-rh="true" property="og:type" content="article">
<meta data-rh="true" property="og:site_name" content="">
<link rel="icon" href="">
<style>
.fullscreen {
margin: 0px;
padding: 0px;
width: 100vw;
height: 100vh;
overflow: hidden;
}
html, body {
overflow: hidden;
font-family: '宋体', sans-serif;
color: #2f2f2f;
}
.power,.power-box {
position: fixed;
bottom: 20px;
left: 50vw;
transform: translateX(-50%);
width: 280px;
height: 30px;
border-radius: 10px;
background-color: #fff;
}
.power {
position: fixed;
left: 50vw;
bottom: 20px;
transform: translateX(-140px);
z-index: 110;
background: linear-gradient(to right, rgb(156, 113, 108) 50px, red 150px, #d1b041);
}
.score {
position: fixed;
left: 20px;
top: 60px;
font-weight: 700;
font-size: 28px;
color: red;
}
.mask {
width: 100%;
height: 100%;
position: fixed;
left: 0;
top: 0;
z-index: 2222;
background-color: #00000088;
display: flex;
justify-content: center;
align-items: center;
}
.load {
display: inline-block;
height: auto;
font-size: 39px;
font-weight: 900;
color: #000;
}
.start {
background-color: #b6b6b6;
display: inline-block;
height: auto;
padding: 8px 20px;
font-size: 39px;
font-weight: 900;
cursor: pointer;
border-radius: 20px;
color: #fff;
}
.tip {
position: fixed;
top: 120px;
left: 20px;
}
.view {
position: fixed;
bottom: 50px;
left: 20px;
padding: 10px 20px;
border-radius: 20px;
cursor: pointer;
background-color: #252525;
color: #fff;
font-weight: 700;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.162.0/+esm",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/",
"lil-gui": "https://threejsfundamentals.org/3rdparty/dat.gui.module.js",
"@tweenjs/tween.js": "https://cdn.jsdelivr.net/npm/@tweenjs/tween.js@23.1.1/dist/tween.esm.js",
"cannonjs": "https://cdn.bootcdn.net/ajax/libs/cannon.js/0.6.2/cannon.min.js",
"gsapjs":"http://139.224.164.2/public/gsap.js"
}
}
</script>
</head>
<body class="fullscreen">
<div class="mask" id="maskdom">
<div class="load" id="loadDom"></div>
<div class="start" id="startdom">游戏开始</div>
<div class="tip" id="tipsdom"></div>
</div>
<div class="power-box" style="display: none;"></div>
<div class="score" id="scoredom">得分:0</div>
<div class="view" id='viewdom' style="{cursor: 'pointer'}">跟踪视角</div>
<div class="power" id="powerdom"></div>
<script type="module">
import * as THREE from 'three';
import * as TWEEN from '@tweenjs/tween.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { GUI } from 'lil-gui';
import 'cannonjs';
import 'gsapjs';
let container;
let scene, camera, renderer, controls;
let NEAR = 0.1, FAR = 1000;
let light, ambient, stats, info;
let rings;
let nrOfCuboids;
let SHADOW_MAP_WIDTH = 512;
let SHADOW_MAP_HEIGHT = 512;
let MARGIN = 0;
let SCREEN_WIDTH = window.innerWidth;
let SCREEN_HEIGHT = window.innerHeight - 2 * MARGIN;
let sceneHUD, cameraOrtho, hudMaterial;
let mouseX = 0, mouseY = 0;
let windowHalfX = window.innerWidth / 2;
let windowHalfY = window.innerHeight / 2;
let scoredom = document.getElementById('scoredom')
let viewdom = document.getElementById('viewdom')
// Create physics world
var world
var renderModes = ["solid","wireframe"];
const Detector = {
canvas: !! window.CanvasRenderingContext2D,
webgl: ( function () { try { return !! window.WebGLRenderingContext && !! document.createElement( 'canvas' ).getContext( 'experimental-webgl' ); } catch( e ) { return false; } } )(),
workers: !! window.Worker,
fileapi: window.File && window.FileReader && window.FileList && window.Blob,
getWebGLErrorMessage: function () {
var element = document.createElement( 'div' );
element.id = 'webgl-error-message';
element.style.fontFamily = 'monospace';
element.style.fontSize = '13px';
element.style.fontWeight = 'normal';
element.style.textAlign = 'center';
element.style.background = '#fff';
element.style.color = '#000';
element.style.padding = '1.5em';
element.style.width = '400px';
element.style.margin = '5em auto 0';
if ( ! webgl ) {
element.innerHTML = window.WebGLRenderingContext ? [
'Your graphics card does not seem to support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation" style="color:#000">WebGL</a>.<br />',
'Find out how to get it <a href="http://get.webgl.org/" style="color:#000">here</a>.'
].join( '\n' ) : [
'Your browser does not seem to support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation" style="color:#000">WebGL</a>.<br/>',
'Find out how to get it <a href="http://get.webgl.org/" style="color:#000">here</a>.'
].join( '\n' );
}
return element;
},
addGetWebGLMessage: function ( parameters ) {
var parent, id, element;
parameters = parameters || {};
parent = parameters.parent !== undefined ? parameters.parent : document.body;
id = parameters.id !== undefined ? parameters.id : 'oldie';
element = Detector.getWebGLErrorMessage();
element.id = id;
parent.appendChild( element );
}
};
// stats.js r8 - http://github.com/mrdoob/stats.js
var Stats = function() {
var h, a, n = 0, o = 0, i = Date.now(), u = i, p = i, l = 0, q = 1E3, r = 0, e, j, f, b = [[16, 16, 48], [0, 255, 255]], m = 0, s = 1E3, t = 0, d, k, g, c = [[16, 48, 16], [0, 255, 0]];
h = document.createElement("div");
h.style.cursor = "pointer";
h.style.width = "80px";
h.style.opacity = "0.9";
h.style.zIndex = "10001";
h.addEventListener("mousedown", function(a) {
a.preventDefault();
n = (n + 1) % 2;
n == 0 ? (e.style.display = "block",
d.style.display = "none") : (e.style.display = "none",
d.style.display = "block")
}, !1);
e = document.createElement("div");
e.style.textAlign = "left";
e.style.lineHeight = "1.2em";
e.style.backgroundColor = "rgb(" + Math.floor(b[0][0] / 2) + "," + Math.floor(b[0][1] / 2) + "," + Math.floor(b[0][2] / 2) + ")";
e.style.padding = "0 0 3px 3px";
h.appendChild(e);
j = document.createElement("div");
j.style.fontFamily = "Helvetica, Arial, sans-serif";
j.style.fontSize = "9px";
j.style.color = "rgb(" + b[1][0] + "," + b[1][1] + "," + b[1][2] + ")";
j.style.fontWeight = "bold";
j.innerHTML = "FPS";
e.appendChild(j);
f = document.createElement("div");
f.style.position = "relative";
f.style.width = "74px";
f.style.height = "30px";
f.style.backgroundColor = "rgb(" + b[1][0] + "," + b[1][1] + "," + b[1][2] + ")";
for (e.appendChild(f); f.children.length < 74; )
a = document.createElement("span"),
a.style.width = "1px",
a.style.height = "30px",
a.style.cssFloat = "left",
a.style.backgroundColor = "rgb(" + b[0][0] + "," + b[0][1] + "," + b[0][2] + ")",
f.appendChild(a);
d = document.createElement("div");
d.style.textAlign = "left";
d.style.lineHeight = "1.2em";
d.style.backgroundColor = "rgb(" + Math.floor(c[0][0] / 2) + "," + Math.floor(c[0][1] / 2) + "," + Math.floor(c[0][2] / 2) + ")";
d.style.padding = "0 0 3px 3px";
d.style.display = "none";
h.appendChild(d);
k = document.createElement("div");
k.style.fontFamily = "Helvetica, Arial, sans-serif";
k.style.fontSize = "9px";
k.style.color = "rgb(" + c[1][0] + "," + c[1][1] + "," + c[1][2] + ")";
k.style.fontWeight = "bold";
k.innerHTML = "MS";
d.appendChild(k);
g = document.createElement("div");
g.style.position = "relative";
g.style.width = "74px";
g.style.height = "30px";
g.style.backgroundColor = "rgb(" + c[1][0] + "," + c[1][1] + "," + c[1][2] + ")";
for (d.appendChild(g); g.children.length < 74; )
a = document.createElement("span"),
a.style.width = "1px",
a.style.height = Math.random() * 30 + "px",
a.style.cssFloat = "left",
a.style.backgroundColor = "rgb(" + c[0][0] + "," + c[0][1] + "," + c[0][2] + ")",
g.appendChild(a);
return {
domElement: h,
update: function() {
i = Date.now();
m = i - u;
s = Math.min(s, m);
t = Math.max(t, m);
k.textContent = m + " MS (" + s + "-" + t + ")";
var a = Math.min(30, 30 - m / 200 * 30);
g.appendChild(g.firstChild).style.height = a + "px";
u = i;
o++;
if (i > p + 1E3)
l = Math.round(o * 1E3 / (i - p)),
q = Math.min(q, l),
r = Math.max(r, l),
j.textContent = l + " FPS (" + q + "-" + r + ")",
a = Math.min(30, 30 - l / 100 * 30),
f.appendChild(f.firstChild).style.height = a + "px",
p = i,
o = 0
}
}
};
function setup() {
nrOfCuboids = 64;
setupScene();
setupCamera();
setupRenderer();
setupCuboids();
setupLights();
//setPlane();
setControls();
initCannon();
setupEventListeners();
animate();
}
function setupScene() {
scene = new THREE.Scene();
scene.fog = new THREE.Fog( 0x222222, 1000, FAR );
scene.background = new THREE.Color(0xf5e6d3);
}
function setupCamera() {
let res = SCREEN_WIDTH / SCREEN_HEIGHT;
camera = new THREE.PerspectiveCamera(45, res, NEAR, FAR);
//camera.up.set(0,0,1);
//camera.position.set(0,30,20);
//camera.position.z = 19;
//camera.position.y = -45;
camera.position.set(20, 10, 0)
//camera.lookAt(0, 0, 0);
//camera.lookAt(new THREE.Vector3(0, 0, 0));
}
function setupRenderer() {
renderer = new THREE.WebGLRenderer({
clearColor: 0x000000,
clearAlpha: 1,
antialias: true,// 抗锯齿
logarithmicDepthBuffer: true // 使用Three进行加载模型时,总会遇到模型相接处或某些区域出现频闪问题或内容被相邻近元素覆盖掉的情况,对数缓存开启可解决,使用对数缓存
});
renderer.setSize(window.innerWidth, window.innerHeight);
//renderer.setSize( SCREEN_WIDTH, SCREEN_HEIGHT );
renderer.setClearColor( scene.fog.color, 1 );
renderer.autoClear = false;
renderer.shadowMapEnabled = true;
renderer.shadowMapSoft = true;
//开启阴影效果 设置阴影类型
// BasicShadowMap 能够给出没有经过过滤的阴影映射 —— 速度最快,但质量最差。
// PCFShadowMap 为默认值,使用Percentage-Closer Filtering (PCF)算法来过滤阴影映射。
// PCFSoftShadowMap 和PCFShadowMap一样使用 Percentage-Closer Filtering (PCF)算法过滤阴影映射,但在使用低分辨率阴影图时具有更好的软阴影。
// VSMShadowMap 使用Variance Shadow Map (VSM)算法来过滤阴影映射。当使用VSMShadowMap时,所有阴影接收者也将会投射阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.BasicShadowMap
renderer.shadowMap.autoUpdate = false
renderer.shadowMap.needsUpdate = true
// 是否使用传统照明模式,默认为是,关闭传统照明模式即可模仿物理光照,光亮随距离可递减
renderer.useLegacyLights = false;
// 设置色调映射
renderer.toneMapping = THREE.ACESFilmicToneMapping;
// 曝光强度
renderer.toneMappingExposure = 0.8
renderer.outputEncoding = THREE.sRGBEncoding;
container = document.createElement( 'div' );
document.body.appendChild( container );
//document.body.appendChild(renderer.domElement);
//renderer.domElement.style.position = "relative";
//renderer.domElement.style.top = MARGIN + 'px';
container.appendChild( renderer.domElement );
}
function setupCuboids() {
rings = [];
addCuboidRing(25, new THREE.BoxGeometry(1, 0.1, 1));
//addCuboidRing(10, new THREE.BoxGeometry(4, 0.7, 4));
//addCuboidRing(16, new THREE.BoxGeometry(5, 1.05, 5));
//addCuboidRing(24, new THREE.BoxGeometry(6, 1.5, 6));
//addCuboidRing(33, new THREE.BoxGeometry(7, 2.2, 7));
}
function addCuboidRing(radius, geometry) {
let cuboids = [];
for (let i = 0; i < nrOfCuboids; i++) {
let angle = i / nrOfCuboids * Math.PI * 2;
let cuboid = createCuboid(i, geometry);
cuboid.position.y = Math.cos(angle) * radius;
cuboid.position.z = Math.sin(angle) * radius;
// 几何体绕着x轴旋转45度
cuboid.rotateX(Math.PI / 4);
scene.add(cuboid);
cuboids.push(cuboid);
}
rings.push(cuboids);
}
function createCuboid(i, geometry) {
let material = new THREE.MeshPhongMaterial({
color: 0x123456,
side: THREE.DoubleSide,
//map: texture,
//normalMap: texture,
normalScale: new THREE.Vector2(1.1,1.1),
//specularMap: texture,
specular: 0xffffff,
// pbr
//envMap: textureCube,
metalness: 0.9,//
roughness: 0.5,//
clearcoat: 1, //
clearcoatRoughness: 0.01, //
envMapIntensity: 2.5, //Mesh
opacity: 0.1,
transparent: true,
shininess: 10 });
return new THREE.Mesh(geometry, material);
}
function setPlane(){
let planeGeometry = new THREE.PlaneGeometry(60, 20);
let planeMaterial = new THREE.MeshBasicMaterial({color: 0x6688aa});
let plane = new THREE.Mesh(planeGeometry, planeMaterial);
// 几何体绕着x轴旋转-90度
plane.rotateX(-Math.PI/2);
// 设置平面网格为接受阴影的投影面
plane.receiveShadow = true;
scene.add(plane);
}
function setupLights() {
let ambientLight = new THREE.AmbientLight(0xffffff);
ambientLight.castShadow = true;
scene.add(ambientLight);
// LIGHTS
ambient = new THREE.AmbientLight( 0xffffff );
ambient.castShadow = true;
scene.add( ambient );
// 添加聚光灯1
addSpotlight(50,50,50);
// 添加聚光灯2
addSpotlight(-50,50,50);
// 添加聚光灯3
addSpotlight(50,50,-50);
// 添加聚光灯4
addSpotlight(-50,50,-50);
addLight();
}
function addLight(){
light = new THREE.SpotLight( 0xffffff );
light.position.set( 30, 30, 30 );
light.target.position.set( 0, 0, 0 );
light.castShadow = true;
light.shadowCameraNear = 10;
light.shadowCameraFar = 100;//camera.far;
light.shadowCameraFov = 30;
light.shadowMapBias = 0.0039;
light.shadowMapDarkness = 0.5;
light.shadowMapWidth = SHADOW_MAP_WIDTH;
light.shadowMapHeight = SHADOW_MAP_HEIGHT;
light.shadowCameraVisible = true;
scene.add( light );
}
function setupEventListeners() {
window.addEventListener("resize", onWindowResize);
}
function onDocumentMouseMove( event ) {
mouseX = ( event.clientX - windowHalfX );
mouseY = ( event.clientY - windowHalfY );
}
function onWindowResize( event ) {
SCREEN_WIDTH = window.innerWidth;
SCREEN_HEIGHT = window.innerHeight;
renderer.setSize( SCREEN_WIDTH, SCREEN_HEIGHT );
//camera.aspect = window.innerWidth / window.innerHeight;
//renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = SCREEN_WIDTH / SCREEN_HEIGHT;
camera.updateProjectionMatrix();
//controls.screen.width = SCREEN_WIDTH;
//controls.screen.height = SCREEN_HEIGHT;
camera.radius = ( SCREEN_WIDTH + SCREEN_HEIGHT ) / 4;
}
//let clock = new THREE.Clock()
function render () {
renderer.clear();
renderer.render(scene, camera);
controls.update();
//let delta = clock.getDelta()
//world.step(delta)
// 更新物理世界 step ( dt , [timeSinceLastCalled] , [maxSubSteps=10] )
// dt:固定时间戳(要使用的固定时间步长)
// [timeSinceLastCalled]:自上次调用函数以来经过的时间
// [maxSubSteps=10]:每个函数调用可执行的最大固定步骤数
// * 设置更新物理世界world的步长timestep
// * 这里选用60Hz的速度,即1.0 / 60.0
world.step(1.0 / 60.0)
//world.fixedStep()
if(ball && ballBody) {
// 2个库的 vector3 类型不完全相同,我们暂时使用 @ts-ignore 忽视掉 ts 报错
// @ts-ignore
ball.position.copy(ballBody.position)
// @ts-ignore
ball.quaternion.copy(ballBody.quaternion)
}
// Update the CannonDebugger meshes
// cannonDebugger.update()
// stats.update()
}
function animate() {
render();
requestAnimationFrame(animate);
requestAnimationFrame(draw);
}
function addTexture(url){
// 加载纹理
const textureLoader = new THREE.TextureLoader()
textureLoader.load(url, (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping
scene.background = texture
scene.environment = texture
// 背景模糊强度
scene.backgroundBlurriness = 0.01
})
}
function setControls(){
controls = new OrbitControls( camera, renderer.domElement );
controls.enableDamping = true;
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.2;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = false;
controls.dynamicDampingFactor = 0.3;
var radius = 6;
controls.minDistance = 0.0;
controls.maxDistance = radius * 10;
controls.enablePan = true
controls.dampingFactor = 0.25
controls.screenSpacePanning = false
controls.enableZoom = true
controls.zoomScale = 10
controls.minZoom = 1
controls.maxZoom = 10
controls.minPolarAngle = 1 * -Math.PI / 180
controls.maxPolarAngle = 90 * Math.PI / 180
controls.minAzimuthAngle = 90 * -Math.PI / 180
controls.maxAzimuthAngle = 90 * Math.PI / 180
//controls.keys = [ 65, 83, 68 ]; // [ rotateKey, zoomKey, panKey ]
//controls.screen.width = SCREEN_WIDTH;
//controls.screen.height = SCREEN_HEIGHT;
/*
// Trackball controls
controls = new THREE.TrackballControls( camera, renderer.domElement );
controls.rotateSpeed = 1.0;
controls.zoomSpeed = 1.2;
controls.panSpeed = 0.2;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = false;
controls.dynamicDampingFactor = 0.3;
var radius = 100;
controls.minDistance = 0.0;
controls.maxDistance = radius * 1000;
//controls.keys = [ 65, 83, 68 ]; // [ rotateKey, zoomKey, panKey ]
controls.screen.width = SCREEN_WIDTH;
controls.screen.height = SCREEN_HEIGHT;
*/
}
function addSpotlight (x,y,z){
const spotLight2 = new THREE.SpotLight(0xffffff, 1)
spotLight2.position.set(x, y, z)
spotLight2.target.position.set( 0, 0, 0 )
spotLight2.castShadow = true
spotLight2.shadow.camera.near = 0.1
spotLight2.shadow.camera.far = 30
spotLight2.shadow.camera.fov = 30
spotLight2.shadow.mapSize.width = 256
spotLight2.shadow.mapSize.height = 256
// 设置灯光 bias ,解决自阴影问题
spotLight2.shadow.bias = -0.0008
spotLight2.power = 1
scene.add(spotLight2)
// 使用辅助器对灯光和阴影进行调整 !!!!!!!!!!
//const cameraHelper = new THREE.SpotLightHelper(spotLight2)
//scene.add(cameraHelper)
}
let loadDom = document.getElementById('loadDom')
function addModel(url){
let model = null
// 纹理加载器管理
const manager = new THREE.LoadingManager()
manager.onLoad = ( ) =>{
console.log( 'Loading complete!')
complete = false
loadDom.style.display="none"
calcMeshCenter(model)
scene.add(model)
let startdom = document.querySelector('#startdom');
startdom.addEventListener('click', ()=> {
gameStart();
});
let viewdom = document.querySelector('#viewdom');
viewdom.addEventListener('click', ()=> {
viewChange();
});
}
manager.onProgress = function ( url, itemsLoaded, itemsTotal ) {
console.log( 'Loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' )
if(loadDom) {
loadDom.innerHTML = itemsLoaded / itemsTotal === 1 ? '' : `载入中,请稍等--${((itemsLoaded / itemsTotal) * 100).toFixed(0)}%`
}
}
manager.onError = function ( url ) {
console.log( 'There was an error loading ' + url )
}
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('./draco/')
const gltfLoader = new GLTFLoader(manager)
gltfLoader.setDRACOLoader(dracoLoader)
gltfLoader.load(url, (gltf) => {
model = gltf.scene
model.traverse((child) =>{
//if(child.isMesh) {
// child.castShadow = true;
// child.receiveShadow = true;
//}
// 不包括足球模型和一些外围模型 减少物理顶点计算
if(child.isMesh && child.name.search(/Solid/) == -1 && child.name != 'soccerball' && child.name != 'Scene' ) {
// if(child.isMesh && child.name.search(/Solid/) == -1 && child.name.search(/Cube019/) == -1 && child.name.search(/Cube020/) == -1 && child.name != 'Plane009' && child.name != 'door') {
//3DXY_geometry_001.001
child.castShadow = true;
child.receiveShadow = true;
// trimesh类型 不规则格点网 两个参数第一个是顶点参数, 第二个是索引
// 新的CANNON.Trimesh class可用于trimesh碰撞。目前它仅限于球面和平面碰撞。
if( ['3DXY_geometry','Plane','door1','door2',].includes(child.name)){
const trimesh = new CANNON.Trimesh(
child.geometry.attributes.position.array,
child.geometry.index.array
)
// 创建刚体
const trimeshBody = new CANNON.Body({
// 刚体的质量mass,质量为0的物体为静止的物体
mass: 0,
// 刚体形状
shape: trimesh,
material: defaultMaterial
})
// 获取世界位置和旋转给到物理世界
// Three.js获得世界坐标.getWorldPosition(target) 将值复制到参数target
// 通过.getWorldScale(target )方法可以获得一个模型的世界缩放系数
// 通过.getWorldQuaternion(THREE.Quaternion)方法可以获得一个模型的世界空间中旋转的四元数 将值复制到参数
trimeshBody.position.copy(child.getWorldPosition(new THREE.Vector3()))
trimeshBody.quaternion.copy(child.getWorldQuaternion(new THREE.Quaternion()))
// 保存足球们的刚体ID
if(child.name === 'door1') {
doorID = trimeshBody.id
child.material = doorMaterial
}
// 添加刚体到物理世界
world.addBody(trimeshBody)
}
}
// 足球模型集合
if(child.name === 'soccerball') {
child.rotateX(Math.PI)
calcMeshCenter(child)
ball = child
// 创建球体 0.2m半径球体
const ballShape = new CANNON.Sphere(0.15)
// 创建刚体 可以把Body称为碰撞体,用来模拟生活中的物体
ballBody = new CANNON.Body({
mass: 1, // 碰撞体质量1kg
// 位置 碰撞体的三维空间中位置
//position: new CANNON.Vec3(0, 15, 0),
shape: ballShape,
material: ballMaterial
})
}
//setTimeout(()=>{
// ballBody.position.set(0,15,0) // '球位置'
// ballBody.velocity.set(0,0,0) // '球速度'
// ballBody.angularVelocity.set(0,0,0)
//},3000)
})
})
}
function draw(now) {
let angle = now / 1000;
rings.forEach((cuboids, ringIndex) => {
let sign = ringIndex % 2 === 0 ? -1 : 1;
cuboids.forEach((cuboid, cuboidIndex) => {
let offsetAngle = cuboidIndex / nrOfCuboids * Math.PI;
let zAngle = cuboidIndex / nrOfCuboids * Math.PI * 4;
let zRotation = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 0, 1), zAngle);
let a = (angle + zAngle) * sign;
let wave = (Math.sign(Math.sin(a)) - 1) * Math.pow(Math.sin(a), 2) * 0.5;
let yRotation = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 1, 0), (angle + offsetAngle + wave) * sign);
zRotation.multiply(yRotation);
cuboid.rotation.setFromRotationMatrix(zRotation);
});
});
}
if (!Detector.webgl){
Detector.addGetWebGLMessage();
}
let pc = true
let tipsdom = document.getElementById('tipsdom')
// 检测是否移动端
function browserRedirect() {
const sUserAgent = navigator.userAgent.toLowerCase();
const bIsIpad = sUserAgent.indexOf('ipad') != -1
const bIsIphoneOs = sUserAgent.indexOf('iphone os') != -1
const bIsMidp = sUserAgent.indexOf('midp') != -1
const bIsUc7 = sUserAgent.indexOf('rv:1.2.3.4') != -1
const bIsUc = sUserAgent.indexOf('ucweb') != -1
const bIsAndroid = sUserAgent.indexOf('android') != -1
const bIsCE = sUserAgent.indexOf('windows ce') != -1
const bIsWM = sUserAgent.indexOf('windows mobile') != -1
if (!(bIsIpad || bIsIphoneOs || bIsMidp || bIsUc7 || bIsUc || bIsAndroid || bIsCE || bIsWM) ) {
pc = true
tipsdom.innerHTML = '<div>Tip:右击鼠标蓄力,松开鼠标射球!</div>'
} else {
pc = false
tipsdom.innerHTML = '<div>Tip:触碰屏幕蓄力,离开射球!</div> '
}
}
browserRedirect();
/**
* Sounds
*/
const hitSound = new Audio('http://139.224.164.2/public/kick.mp3')
const scoreSound = new Audio('http://139.224.164.2/public/score.wav')
const playHitSound = function (collision, score) {
if(score) {
scoreSound.currentTime = 8
scoreSound.play()
} else {
const impactStrength = collision.contact.getImpactVelocityAlongNormal()
if (impactStrength > 1.5) {
hitSound.volume = Math.random()
hitSound.currentTime = 0
hitSound.play()
}
}
}
const doorMaterial = new THREE.MeshBasicMaterial({
color: 0x000000,
opacity: 0.1,
transparent: true
})
// 力度百分比
let percentage = 0
let powerdom = document.getElementById('powerdom')
let powerID
// const powerLine = () => {
// powerID = setInterval(() => {
// if(percentage < 100) {
// percentage++
// } else {
// percentage = 0
// }
// }, 10)
// }
// 力量条数值
const powerLine = () => {
if(percentage < 100) {
percentage = percentage + 1
} else {
percentage = 0
}
powerdom.style.width = percentage * 2.8 + 'px'
powerID = requestAnimationFrame(powerLine)
}
// let stats = new Stats()
// document.body.appendChild( stats.dom )
let viewTitle = '跟踪视角'
let viewFlag = false
function viewChange () {
viewFlag = !viewFlag
//viewTitle = viewFlag ? '不跟着视角' : '跟踪视角'
if(viewFlag){
viewTitle = '不跟着视角'
}else{
viewTitle = '跟踪视角'
}
viewdom.innerHTML = viewTitle
if(!viewFlag) {
translateCamera( new THREE.Vector3(5, 3, 0), new THREE.Vector3(0, 0, 0))
}
}
// 加载进度变量
let complete = true
// 开始游戏变量
let start = false
//
let isClick = false
let maskDom = document.getElementById('maskdom')
// 开始游戏
function gameStart () {
start = true
if(start){
maskDom.style.display = 'none'
}
ballBody.position.set(0, 15, 0)
// 添加钢体到物理世界
world.addBody(ballBody)
let isScore = true
// 对足球刚体监听碰撞事件 可以监听的事件有 'collide', 'sleep' or 'wakeup' 等
ballBody.addEventListener('collide', (e) => {
playHitSound(e, false)
// 节流计分 把足球丢场外
if(isScore && e.body.id === doorID) {
gsap.to(ballBody.position, {
x: 0,
y: 200,
z: 0,
duration: 1
})
gsap.to(controls.target, {
x: 0,
y: 0,
z: 0,
duration: 1
})
ballBody.position.set(0, 15, 0)
ballBody.velocity.set(0, 0, 0)
ballBody.angularVelocity.set(0, 0, 0)
score++
scoredom.innerHTML = '得分:' + score
playHitSound(e, true)
isScore = false
// 中球特效
gsap.to(doorMaterial.color, {
r: 1,
g: 1,
b: 1,
duration: 0.8,
yoyo: true,
repeat: 3
})
}
})
let cameraViewID
// 相机镜头追踪
const cameraView = () => {
if(isScore) {
if(ball.position.x < -10) {
x.old = x.new
x.new = -5
if(x.old != x.new) {
gsap.to(x, {
value: x.new,
duration: 4
})
}
} else {
x.old = x.new
x.new = 5
if(x.old != x.new) {
gsap.to(x, {
value: x.new,
duration: 4
})
}
}
if(ball.position.z < 2) {
z.old = z.new
z.new = -1
if(z.old != z.new) {
gsap.to(z, {
value: z.new,
duration: 2
})
}
} else {
z.old = z.new
z.new = 1
if(z.old != z.new) {
gsap.to(z, {
value: z.new,
duration: 2
})
}
}
if(ball.position.y > 1) {
y.old = y.new
y.new = 1
if(y.old != y.new) {
gsap.to(y, {
value: y.new,
duration: 1
})
}
} else if(ball.position.y < -0.5) {
y.old = y.new
y.new = -4
if(y.old != y.new) {
gsap.to(y, {
value: y.new,
duration: 1
})
}
} else {
y.old = y.new
y.new = 0
if(y.old != y.new) {
gsap.to(y, {
value: y.new,
duration: 1
})
}
}
} else {
gsap.to(x, {
value: 0.5,
duration: 2
})
gsap.to(y, {
value: 3,
duration: 2
})
gsap.to(z, {
value: 0,
duration: 2
})
}
camera.position.set(ball.position.x + x, ball.position.y + y, ball.position.z + z)
cameraViewID = requestAnimationFrame(cameraView)
}
const down = () => {
if(isClick) return
percentage = Math.random() * 100
powerLine()
}
const up = () => {
if(isClick) return
cancelAnimationFrame(powerID)
clearInterval(powerID)
isClick = true
viewdom.style.cursor = 'no-drop'
console.log("Number of Triangles :", renderer.info.render.triangles)
percentage = percentage > 100 ? 100 : percentage
hitSound.volume = percentage / 100
hitSound.currentTime = 0
// 播放音效
hitSound.play()
// 相机移动追踪
if(viewFlag) {
cameraView()
gsap.to(controls.target, {
x: -10,
y: 1,
z: 0,
duration: 1
})
}
// 第一个参数力度的大小,第二个参数是力度施加的位置
ballBody.applyForce(new CANNON.Vec3(-12 * percentage - 100, 8 * percentage + 6, (Math.random() - 0.7) * percentage * 2), ballBody.position)
setTimeout(() => {
isScore = true
if(viewFlag) {
cancelAnimationFrame(cameraViewID)
translateCamera( new THREE.Vector3(4, 2, 0), new THREE.Vector3(0, 0, 0))
}
isClick = false
viewdom.style.cursor = 'pointer'
ballBody.position.set(0, 1, 0)
ballBody.velocity.set(0, 0, 0)
ballBody.angularVelocity.set(0, 0, 0)
}, 5000)
}
// pc
if(pc) {
window.addEventListener('mousedown', (e) => {
if(e.button == 2) {
down()
}
})
window.addEventListener("mouseup", (e) => {
if(e.button == 2) {
up()
}
})
} else {
window.addEventListener('touchstart', () => {
down()
})
window.addEventListener("touchend", () => {
up()
})
}
}
const x = {
old: 4,
new: 4,
value: 4
}
const y = {
old: 2,
new: 2,
value: 2
}
const z = {
old: 0,
new: 0,
value: 0
}
// 使用补间动画移动相机
const timeLine1 = gsap.timeline()
const timeLine2 = gsap.timeline()
// 定义相机移动函数
function translateCamera(position, target) {
//镜头移动动画
const cameraAct=new TWEEN.Tween(camera.position)
.to(target,3000)
// 相机移动时,焦点始终为模型的位置
.onUpdate(function(){
//camera.lookAt(snowflakePoint.position)
}).start()
/*
timeLine1.to(camera.position, {
x: position.x,
y: position.y,
z: position.z,
duration: 1,
ease: 'power2.inOut'
})
timeLine2.to(controls.target, {
x: target.x,
y: target.y,
z: target.z,
duration: 1,
ease: 'power2.inOut'
})*/
}
// 得分
let score = 0
let ball, ballBody, doorID
// 创建默认材质
let defaultMaterial = null
//创建足球材质
let ballMaterial = null
function initCannon() {
// 初始化物理世界
world = new CANNON.World()
// 设置物理世界重力加速度 单位:m/s² 重力加速度x、y、z分量值,假设y轴竖直向上,这样重力就速度就是y方向负方向。
world.gravity.set(0, -9.82, 0)
// npm install cannon-es-debugger
// 加入 cannon-es-debugger 可以展示模型的物理世界的轮廓
// scene: 场景
// 物理世界
// 第三个参数为可选参数,其中的的onInit方法返回场景中的每个刚体和对应的物理世界的轮廓的three mesh
// const cannonDebugger = CannonDebugger(scene, world)
// const cannonDebugger = CannonDebugger(scene, world, {
// onInit(body: CANNON.Body, mesh: THREE.Mesh) {
// //
// mesh.visible = true
// console.log(body);
// },
// })
// 还要在每帧更新调用中更新 Update the CannonDebugger meshes
// cannonDebugger.update()
// 创建默认材质
defaultMaterial = new CANNON.Material('default')
//创建足球材质
ballMaterial = new CANNON.Material('ball')
// 定义两种材质之间的摩擦因数和弹力系数 设置地面材质和小球材质之间的碰撞反弹恢复系数
const defaultContactMaterial = new CANNON.ContactMaterial(defaultMaterial, ballMaterial, {
friction: 5,
restitution: 0.5, //反弹恢复系数
})
// 把关联的材质添加到物理世界中
world.addContactMaterial(defaultContactMaterial)
// NaiveBroadphase Cannon 默认的算法。检测物体碰撞时,一个基础的方式是检测每个物体是否与其他每个物体发生了碰撞
// GridBroadphase 网格检测。轴对齐的均匀网格 Broadphase。将空间划分为网格,网格内进行检测。
// SAPBroadphase(Sweep-and-Prune) 扫描-剪枝算法,性能最好。
// 默认为 NaiveBroadphase,建议替换为 SAPBroadphase
// 碰撞算法
world.broadphase = new CANNON.SAPBroadphase(world)
//world.broadphase = new CANNON.NaiveBroadphase();
// 创建一个物理世界的平面
const planeShape = new CANNON.Plane()
// 创建一个刚体
const planeBody = new CANNON.Body({
mass: 0, // 设置质量为0,不受碰撞的影响
shape: planeShape,
position: new CANNON.Vec3(0, 1, 0)
})
// 改变平面默认的方向,法线默认沿着z轴,旋转到平面向上朝着y方向
// 设置刚体旋转(设置旋转X轴)旋转规律类似threejs 平面
planeBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2)
// 将刚体添加到物理世界当中
world.addBody(planeBody)
addTexture('http://139.224.164.2/public/outdoor.jpg');
addModel('http://139.224.164.2/public/playgroundball.glb');
}
const calcMeshCenter = (group)=>{
/**
* 包围盒全自动计算:模型整体居中
*/
let box3 = new THREE.Box3()
// 计算层级模型group的包围盒
// 模型group是加载一个三维模型返回的对象,包含多个网格模型
box3.expandByObject(group)
// 计算一个层级模型对应包围盒的几何体中心在世界坐标中的位置
let center = new THREE.Vector3()
box3.getCenter(center)
// console.log('查看几何体中心坐标', center);
// 重新设置模型的位置,使之居中。
group.position.x = group.position.x - center.x
group.position.y = group.position.y - center.y
group.position.z = group.position.z - center.z
}
setup();
draw(1);
</script>
</body>
</html>
演示demo:three.js+cannon.js Web 3D
最后,希望看官们都有时间参与足球运动,从基层推动中国足球不断进步和发展,也许最终能实现问鼎世界杯的梦想。
参见: