文章目录
1.背景
在之前的【CodeSys创建自定义的html5控件】中,我们介绍了如何创建html5控件并加到CodeSys的控件库中。
目前在做一个机械手项目,想要显示一下机械手的姿态。很自然地想到,利用html5控件来显示。
经过搜索,恰好有个叫three.js的能在html5中使用的3d库满足需求。于是,就打算将其集成进来。
实际在集成的过程中,碰到了很多问题,但基本都一一解决了。现在把我实现的过程记录下来。
2.效果
先看看效果
控件参数界面:
运行界面:
3.出现的问题及解决方案
3.1.can not read property of undefined…
假如按照官方的例子【Project Logo HTML5-API-Examples 】来写,有可能会报can not read property of undefined。如下图所示
这个原因官方貌似也有说明。
【API that HTML5 controls can call to interact with IEC code】
主要是
CDSWebVisuAccess
这个对象要在iFrame加载完成后才能使用。因此,我们所有的函数,最好是在load信号发出后再执行。类似这样:
this.init = function(){
// 自己的js代码
}
window.addEventListener("load", this.init);
3.2.three.js脚本文件的下载及使用
用来渲染3d界面的three.js库,假如直接从github【/mrdoob/three.js/】下载,得到的几个js文件貌似不能直接用(可能是给npm用的?)。因此,我参考了这里【html three.js 引入.stl模型示例】,直接从cdn下载算了。
src="https://cdn.jsdelivr.net/npm/three@0.137/build/three.min.js"
src="https://cdn.jsdelivr.net/npm/three@0.137/examples/js/controls/OrbitControls.js"
src="https://cdn.jsdelivr.net/npm/three@0.137/examples/js/loaders/STLLoader.js"
从github官方直接下载的貌似是module,导入的用法(写法)貌似不一样。要类似这样子用。(还没测试,先挖个坑)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Three.js Module Example</title>
</head>
<body>
<script type="module">
import * as THREE from './path/to/three.module.min.js';
// 使用THREE创建场景、相机、渲染器等
</script>
</body>
</html>
或者
<script type="module" src="path/to/three.module.min.js"></script>
关于如何实现three.js加载机械臂模型及通过角度值控制机械臂的运动,比较复杂(相对来说),到时有时间再另外写一篇文章来说明了。
3.3.文件没有上传至PLC
更改版本号。
有时候你明明在【HTML5控件编辑器】中添加了某些图片、脚本或者其他文件,但是安装、上传后,却无法在PLC对应的位置找到。
这可能是codesys的检测文件功能问题。最好最彻底的应对办法是,每做一次修改(代码修改、文件增减等),都设置一个新的版本号。这样就会触发全量更新。这样做就应该没问题了。
3.4.文件路径问题
上传到PLC的文件,并不会保持原有的名字。而是出于避免文件重名冲突的考虑,自动把文件重命名了。假如想知道重命名后的实际文件名,就需要使用
CDSWebVisuAccess.getAdditionalFile
这个函数了。
window.CDSWebVisuAccess.getAdditionalFile("Codesys_Logo.svg").then( (newFilePath) => {
var image = new Image();
image.src = newFilePath;
document.body.appendChild(image);
}
);
3.5.Content-Security-Policy的问题
Content-Security-Policy:由于违反了下列指令:“default-src ‘nonce-ItNLPfWapdlvNbrIxlV4CA==’ ‘unsafe-inline’”,此页面位于 blob:null/6f717d2c-d87e-4cdd-a2b7-9019f897e440 的资源(connect-src)无法加载
这个问题好像是three.js的问题?利用CodeSys的自带函数(CDSWebVisuAccess中的函数)对文件读取是没啥问题的、用Image加载图片也是没问题的。就是这个THREE.STLLoader.load()
才出问题
var tmpLoader = new THREE.STLLoader();
tmpLoader.load(filePath);
必须要绕开他才行。
直接读取blob,然后blob转 ArrayBuffer,直接使用 STLLoader的parse函数。
function loadModel(modelFile) {
// 加载 STL 文件
window.CDSWebVisuAccess.getBinaryFile(modelFile).then((fileBlob) => {
// 创建FileReader实例
const reader = new FileReader();
// 当FileReader完成读取Blob时,这个函数会被调用
reader.onload = function (event) {
// event.target.result 将是一个ArrayBuffer
const arrayBuffer = event.target.result;
var loader = new THREE.STLLoader();
var geometry = loader.parse(arrayBuffer)
}
// 以ArrayBuffer的形式读取Blob
reader.readAsArrayBuffer(fileBlob);
});
}
经过测试,凡是使用了URL.createObjectURL
,fetch
等方式访问资源的话,都会触犯它的天规。因此都必须避开。好在‘data:xxx’的base64连接没问题。
狗日的,连data类型的也拒绝
3.6.在某些工程中无法看到安装的html5工具箱的问题
这是由于codesys默认不支持html5工具,需要开启支持才行。
比如说codesys自带的例程Robotics_Jogging,直接去界面工具箱是看不到html5相关工具的
必须要在Visulization Mananger处右键点击添加WebVisu,并且在Visulization Mananger处勾选【支持客户端动化和覆盖本地元素】
然后就可以看到了(假如还是看不到,就重启工程)
3.7.模型贴图问题
假如直接使用THREE.TextureLoader,也是会触犯CodeSys的安全策略,导致无法顺利加载的。
但我们可以利用CDSWebVisuAccess.getBinaryFile
加载得到贴图的原始数据,然后再做处理。
搞了贴图的效果:
3.7.1.使用图片解码库解码 (不建议)
调用pngjs来对数据进行解码,然后把解码后的数据给到THREE.DataTexture从而得到贴图,这个时候才能贴到材质上。
你搜pngjs的话,会找到好几个,我是到这里[foliojs/png.js]下载的。将png.js和zlib.js加进来就行了。
window.CDSWebVisuAccess.getBinaryFile("myImage.png").then((fileBlob) => {
// 创建FileReader实例
const reader = new FileReader();
// 当FileReader完成读取Blob时,这个函数会被调用
reader.onload = function (event) {
// event.target.result 将是一个ArrayBuffer
const arrayBuffer = event.target.result;
// 创建一个 Uint8Array 来引用 ArrayBuffer
const data = new Uint8Array(arrayBuffer);
const png = new PNG(data);
let pixelBytes = png.pixelBitlength / 8
console.log("png size", png.width, png.height, pixelBytes)
let imgFormat = THREE.RGBFormat; // 默认设置为 RGB 格式
switch(pixelBytes) {
case 1: imgFormat = THREE.LuminanceFormat; break; // 灰度图像
case 2: imgFormat = THREE.LuminanceAlphaFormat; break; // 灰度图像带 Alpha 通道
case 3: imgFormat = THREE.RGBFormat; break; // RGB 图像
case 4: imgFormat = THREE.RGBAFormat; break; // RGBA 图像
default: imgFormat = THREE.RGBFormat; break; // 默认设置为 RGB 格式
}
const width = png.width; // 宽度
const height = png.height; // 高度
// 创建DataTexture
var texture = new THREE.DataTexture(
png.decodePixels(), // 包含像素数据的Uint8Array
width, // 纹理宽度
height, // 纹理高度
imgFormat, // THREE.RGBAFormat, // 数据格式
THREE.UnsignedByteType, // THREE.UnsignedByteType, // 数据类型
THREE.UVMapping, // UV映射模式
THREE.ClampToEdgeWrapping, // wrapping模式
THREE.ClampToEdgeWrapping, // wrapping模式
THREE.LinearFilter, // minFilter
THREE.LinearFilter // magFilter
);
texture.flipY = false;
texture.encoding = THREE.LinearEncoding; //THREE.LinearEncoding THREE.RGBEEncoding THREE.sRGBEncoding
texture.generateMipmaps = true;
texture.needsUpdate = true; // 重要:通知 three.js 更新纹理
// 直接设置不知道为何不行,在html是可以的
// joint4.material.map = texture
var material = new THREE.MeshStandardMaterial(joint4.material);
material.map = texture
joint4.material = material
// var material = new THREE.MeshStandardMaterial({ map: texture });
// joint4.material = material
console.log(joint4, joint4.material)
}
// 以ArrayBuffer的形式读取Blob
reader.readAsArrayBuffer(fileBlob);
});
3.7.2.使用Image、canvas来进行解码 (建议)
除了使用解码库来解码,更加推荐的是使用canvas来将图片绘制出来然后再获取canvas的rgba数据,这样就不用找解码库了。
window.CDSWebVisuAccess.getBinaryFile("myImage.png").then((fileBlob) => {
let sourceURI = URL.createObjectURL(fileBlob);
// 创建 Image 对象
const image = new Image();
// 设置 Image 对象的 src 属性
image.src = sourceURI;
// 监听 onload 事件
image.onload = () => {
// 创建 Canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 设置 Canvas 的大小与图像一致
canvas.width = image.width;
canvas.height = image.height;
// 将图像绘制到 Canvas
ctx.drawImage(image, 0, 0, image.width, image.height);
// 获取解码后的 RGBA 数据
const imageData = ctx.getImageData(0, 0, image.width, image.height);
const rgbaData = imageData.data;
// 创建DataTexture
var texture = new THREE.DataTexture(
rgbaData, // 包含像素数据的Uint8Array
image.width, // 纹理宽度
image.height, // 纹理高度
THREE.RGBAFormat, // THREE.RGBAFormat, // 数据格式
THREE.UnsignedByteType, // THREE.UnsignedByteType, // 数据类型
THREE.UVMapping, // UV映射模式
THREE.ClampToEdgeWrapping, // wrapping模式
THREE.ClampToEdgeWrapping, // wrapping模式
THREE.LinearFilter, // minFilter
THREE.LinearFilter // magFilter
);
texture.flipY = false;
texture.encoding = THREE.LinearEncoding; //THREE.LinearEncoding THREE.RGBEEncoding THREE.sRGBEncoding
texture.generateMipmaps = true;
texture.needsUpdate = true; // 重要:通知 three.js 更新纹理
// 直接设置不知道为何不行,在html是可以的
// joint4.material.map = texture
var material = new THREE.MeshStandardMaterial(joint4.material);
material.map = texture
joint4.material = material
// var material = new THREE.MeshStandardMaterial({ map: texture });
// joint4.material = material
console.log(joint4, joint4.material)
}
});
3.8.加载gltf/glb模型
因为gltf/glb模型文件会在一个文件里面包含了物体网格、材质、贴图、动画等等的资源。比较适合我的应用场景。
加载的话,是对GLTFLoader.js进行魔改实现的。
这个原始文件的下载地址为
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
主要是把原本调用fileloader、textureloader的地方进行了修改,改成CDSWebVisuAccess.getBinaryFile
,以及使用pngjs进行解码(最新的改成利用Image+canvas来进行解码,更好)。
修改了两个地方,
第一个,在load( url, onLoad, onProgress, onError )
函数处:
将原来的注释掉,然后加入修改后的。
修改完的函数为:
load( url, onLoad, onProgress, onError ) {
console.log('---begine load1', url)
const scope = this;
let resourcePath;
if ( this.resourcePath !== '' ) {
resourcePath = this.resourcePath;
} else if ( this.path !== '' ) {
resourcePath = this.path;
} else {
resourcePath = THREE.LoaderUtils.extractUrlBase( url );
} // Tells the LoadingManager to track an extra item, which resolves after
// the model is fully loaded. This means the count of items loaded will
// be incorrect, but ensures manager.onLoad() does not fire early.
console.log('---begine load2', resourcePath)
this.manager.itemStart( url );
const _onError = function ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
scope.manager.itemEnd( url );
};
// const loader = new THREE.FileLoader( this.manager );
// loader.setPath( this.path );
// loader.setResponseType( 'arraybuffer' );
// loader.setRequestHeader( this.requestHeader );
// loader.setWithCredentials( this.withCredentials );
// console.log("loader begin load", loader)
// loader.load( url, function ( data ) {
// try {
// scope.parse( data, resourcePath, function ( gltf ) {
// onLoad( gltf );
// scope.manager.itemEnd( url );
// }, _onError );
// } catch ( e ) {
// _onError( e );
// }
// }, onProgress, _onError );
window.CDSWebVisuAccess.getBinaryFile(url).then((fileBlob) => {
console.log("file blob", fileBlob)
// 创建FileReader实例
const reader = new FileReader();
// 当FileReader完成读取Blob时,这个函数会被调用
reader.onload = function (event) {
// event.target.result 将是一个ArrayBuffer
const arrayBuffer = event.target.result;
console.log("arrayBuffer", arrayBuffer)
try {
scope.parse(arrayBuffer, resourcePath, function (gltf) {
console.log("gltf", gltf)
onLoad(gltf);
scope.manager.itemEnd(url);
}, _onError);
} catch (e) {
_onError(e);
}
}
// 以ArrayBuffer的形式读取Blob
reader.readAsArrayBuffer(fileBlob);
});
}
第二个,在loadTextureImage(textureIndex, source, loader)
函数处:
将原来的注释掉,然后加入修改后的。
修改完的函数为(这是旧版的,只支持png格式的贴图;新版的更好,理论支持大部分图片格式。请继续往前看):
loadTextureImage(textureIndex, source, loader) {
const parser = this;
const json = this.json;
const options = this.options;
const textureDef = json.textures[textureIndex];
const URL = self.URL || self.webkitURL;
let sourceURI = source.uri;
let isObjectURL = false;
let hasAlpha = true;
if (source.mimeType === 'image/jpeg') hasAlpha = false;
// if (source.bufferView !== undefined) {
// // Load binary image data from bufferView, if provided.
// sourceURI = parser.getDependency('bufferView', source.bufferView).then(function (bufferView) {
// if (source.mimeType === 'image/png') {
// // Inspect the PNG 'IHDR' chunk to determine whether the image could have an
// // alpha channel. This check is conservative — the image could have an alpha
// // channel with all values == 1, and the indexed type (colorType == 3) only
// // sometimes contains alpha.
// //
// // https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header
// const colorType = new DataView(bufferView, 25, 1).getUint8(0, false);
// hasAlpha = colorType === 6 || colorType === 4 || colorType === 3;
// }
// isObjectURL = true;
// const blob = new Blob([bufferView], {
// type: source.mimeType
// });
// sourceURI = URL.createObjectURL(blob);
// console.log("--the created blob", sourceURI)
// return sourceURI;
// });
// } else if (source.uri === undefined) {
// throw new Error('THREE.GLTFLoader: Image ' + textureIndex + ' is missing URI and bufferView');
// }
// return Promise.resolve(sourceURI).then(function (sourceURI) {
// return new Promise(function (resolve, reject) {
// let onLoad = resolve;
// if (loader.isImageBitmapLoader === true) {
// onLoad = function (imageBitmap) {
// resolve(new THREE.CanvasTexture(imageBitmap));
// };
// }
// loader.load(resolveURL(sourceURI, options.path), onLoad, undefined, reject);
// // 就在这里取代 textureloader 的操作
// console.log("-------replace loader:",
// source.uri)
// });
// }).then(function (texture) {
// // Clean up resources and configure Texture.
// if (isObjectURL === true) {
// URL.revokeObjectURL(sourceURI);
// }
// texture.flipY = false;
// if (textureDef.name) texture.name = textureDef.name; // When there is definitely no alpha channel in the texture, set THREE.RGBFormat to save space.
// if (!hasAlpha) texture.format = THREE.RGBFormat;
// const samplers = json.samplers || {};
// const sampler = samplers[textureDef.sampler] || {};
// texture.magFilter = WEBGL_FILTERS[sampler.magFilter] || THREE.LinearFilter;
// texture.minFilter = WEBGL_FILTERS[sampler.minFilter] || THREE.LinearMipmapLinearFilter;
// texture.wrapS = WEBGL_WRAPPINGS[sampler.wrapS] || THREE.RepeatWrapping;
// texture.wrapT = WEBGL_WRAPPINGS[sampler.wrapT] || THREE.RepeatWrapping;
// parser.associations.set(texture, {
// type: 'textures',
// index: textureIndex
// });
// return texture;
// });
// 取得图片的原生blob数据
if (source.bufferView !== undefined) {
sourceURI = parser.getDependency('bufferView', source.bufferView).then(function (bufferView) {
// 这个bufferView就是 ArrayBuffer 类型
console.log("bufferview", bufferView)
return new Promise((resolve, reject) => {
// 创建一个 Uint8Array 来引用 ArrayBuffer
const data = new Uint8Array(bufferView);
const png = new PNG(data);
let pixelBytes = png.pixelBitlength / 8
console.log("png size", png.width, png.height, pixelBytes)
let imgFormat = THREE.RGBFormat; // 默认设置为 RGB 格式
switch (pixelBytes) {
case 1: imgFormat = THREE.LuminanceFormat; break; // 灰度图像
case 2: imgFormat = THREE.LuminanceAlphaFormat; break; // 灰度图像带 Alpha 通道
case 3: imgFormat = THREE.RGBFormat; break; // RGB 图像
case 4: imgFormat = THREE.RGBAFormat; break; // RGBA 图像
default: imgFormat = THREE.RGBFormat; break; // 默认设置为 RGB 格式
}
const width = png.width; // 宽度
const height = png.height; // 高度
// 创建DataTexture
var texture = new THREE.DataTexture(
png.decodePixels(), // 包含像素数据的Uint8Array
width, // 纹理宽度
height, // 纹理高度
imgFormat, // THREE.RGBAFormat, // 数据格式
THREE.UnsignedByteType, // THREE.UnsignedByteType, // 数据类型
THREE.UVMapping, // UV映射模式
THREE.ClampToEdgeWrapping, // wrapping模式
THREE.ClampToEdgeWrapping, // wrapping模式
THREE.LinearFilter, // minFilter
THREE.LinearFilter // magFilter
);
texture.flipY = false;
texture.encoding = THREE.LinearEncoding; //THREE.LinearEncoding THREE.RGBEEncoding THREE.sRGBEncoding
texture.generateMipmaps = true;
texture.needsUpdate = true; // 重要:通知 three.js 更新纹理
console.log("---", texture)
resolve(texture)
});
});
}
return Promise.resolve(sourceURI).then(function (texture) {
parser.associations.set(texture, {
type: 'textures',
index: textureIndex
});
console.log("the texture", texture)
return texture;
})
}
上面的使用了pngjs,有点麻烦,而且还不支持其他格式的图片(比如jpeg)。经过一通测试,发现可以用canvas绘制再获取的方式避开cors的限制。请把loadTextureImage(textureIndex, source, loader)
更改成下面这样:
loadTextureImage(textureIndex, source, loader) {
const parser = this;
const json = this.json;
const options = this.options;
const textureDef = json.textures[textureIndex];
const URL = self.URL || self.webkitURL;
let sourceURI = source.uri;
let isObjectURL = false;
let hasAlpha = true;
if (source.mimeType === 'image/jpeg') hasAlpha = false;
if (source.bufferView !== undefined) {
// Load binary image data from bufferView, if provided.
sourceURI = parser.getDependency('bufferView', source.bufferView).then(function (bufferView) {
if (source.mimeType === 'image/png') {
// Inspect the PNG 'IHDR' chunk to determine whether the image could have an
// alpha channel. This check is conservative — the image could have an alpha
// channel with all values == 1, and the indexed type (colorType == 3) only
// sometimes contains alpha.
//
// https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header
const colorType = new DataView(bufferView, 25, 1).getUint8(0, false);
hasAlpha = colorType === 6 || colorType === 4 || colorType === 3;
}
isObjectURL = true;
const blob = new Blob([bufferView], {
type: source.mimeType
});
sourceURI = URL.createObjectURL(blob);
return sourceURI;
});
} else if (source.uri === undefined) {
throw new Error('THREE.GLTFLoader: Image ' + textureIndex + ' is missing URI and bufferView');
}
return Promise.resolve(sourceURI).then(function (sourceURI) {
// return new Promise(function (resolve, reject) {
// let onLoad = resolve;
// if (loader.isImageBitmapLoader === true) {
// onLoad = function (imageBitmap) {
// resolve(new THREE.CanvasTexture(imageBitmap));
// };
// }
// loader.load(resolveURL(sourceURI, options.path), onLoad, undefined, reject);
// // 就在这里取代 textureloader 的操作
// console.log("-------replace loader:",
// source.uri)
// });
return new Promise(function (resolve, reject) {
// 创建 Image 对象
const image = new Image();
// 设置 Image 对象的 src 属性
image.src = resolveURL(sourceURI, options.path);
console.log("blob url", sourceURI)
// 创建 Canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 监听 onload 事件
image.onload = () => {
console.log("image loaded", image)
// 设置 Canvas 的大小与图像一致
canvas.width = image.width;
canvas.height = image.height;
// 将图像绘制到 Canvas
ctx.drawImage(image, 0, 0, image.width, image.height);
// 获取解码后的 RGBA 数据
const imageData = ctx.getImageData(0, 0, image.width, image.height);
const rgbaData = imageData.data;
// 因为这样得到的数据一定是rgba格式的,所有要将hasAlpha设置为true
hasAlpha = true
// 输出解码后的 RGBA 数据
console.log('Decoded RGBA data:', rgbaData);
// 创建DataTexture
var texture = new THREE.DataTexture(
rgbaData, // 包含像素数据的Uint8Array
image.width, // 纹理宽度
image.height, // 纹理高度
THREE.RGBAFormat, // THREE.RGBAFormat, // 数据格式
THREE.UnsignedByteType, // THREE.UnsignedByteType, // 数据类型
THREE.UVMapping, // UV映射模式
THREE.ClampToEdgeWrapping, // wrapping模式
THREE.ClampToEdgeWrapping, // wrapping模式
THREE.LinearFilter, // minFilter
THREE.LinearFilter // magFilter
);
texture.flipY = false;
texture.encoding = THREE.LinearEncoding; //THREE.LinearEncoding THREE.RGBEEncoding THREE.sRGBEncoding
texture.generateMipmaps = true;
texture.needsUpdate = true; // 重要:通知 three.js 更新纹理
console.log("---texture", texture)
resolve(texture)
};
});
}).then(function (texture) {
// Clean up resources and configure Texture.
if (isObjectURL === true) {
URL.revokeObjectURL(sourceURI);
}
texture.flipY = false;
if (textureDef.name) texture.name = textureDef.name; // When there is definitely no alpha channel in the texture, set THREE.RGBFormat to save space.
if (!hasAlpha) texture.format = THREE.RGBFormat;
const samplers = json.samplers || {};
const sampler = samplers[textureDef.sampler] || {};
texture.magFilter = WEBGL_FILTERS[sampler.magFilter] || THREE.LinearFilter;
texture.minFilter = WEBGL_FILTERS[sampler.minFilter] || THREE.LinearMipmapLinearFilter;
texture.wrapS = WEBGL_WRAPPINGS[sampler.wrapS] || THREE.RepeatWrapping;
texture.wrapT = WEBGL_WRAPPINGS[sampler.wrapT] || THREE.RepeatWrapping;
parser.associations.set(texture, {
type: 'textures',
index: textureIndex
});
console.log("---final texture:", texture)
return texture;
});
}
4.总结
有很多坑及限制,但是确实能用。
参考
【html three.js 引入.stl模型示例】
【Three.js的3D显示】
【API that HTML5 controls can call to interact with IEC code】