Skip to content

Instantly share code, notes, and snippets.

@uzzu
Last active May 17, 2025 09:39
Show Gist options
  • Save uzzu/a185b88ca578f9f89e14f10d04d8171f to your computer and use it in GitHub Desktop.
Save uzzu/a185b88ca578f9f89e14f10d04d8171f to your computer and use it in GitHub Desktop.

ワールド内で近くの椅子に座った人同士でボイス通話するのを実現するClusterScript群です。
改変、ご利用ご自由にどうぞ。
質問あればXでメンションしていただくか、clusterにいる時に声かけてください。

サンプル

  • https://cluster.mu/w/3b7a6eeb-9f89-43dd-8604-160f43003a3e
  • 丸く配置された椅子が3グループあります。それぞれ座った人同士のみボイス通話ができます
  • 席を立っている人同士でもボイス通話ができます
  • サブ音声は皆に聞こえます
  • XRAnimator、OSCを利用したトラッキングに対応しています

使い方

  • 01_ItemVoiceRate.js
    • 空のGameObjectにScriptableItemをアタッチし、そのソースコードとして使用します
    • PlayerScriptをアタッチする機能が含まれているので、不要であれば、 269行目の attachScriptTask(localValues); をコメントアウト(行の先頭に // を追加)してください
  • 02_PlayerXraOscVoiceRate.js
    • XRAnimatorとOSCを利用した「デスクトップでフルトラできるスクリプト」改良版 V2.0のソースに、グループボイス通話機能を足したものです
    • ワールド内にある PlayerScriptをアタッチするアイテムにPlayerScriptコンポーネントをアタッチし、そのソースコードとして使用します
      • 01_ItemVoiceRate.js を使用しているアイテムに追加でOKです
    • PlayerScriptはワールド内で1つしかつけられないので、グループボイス通話のみを導入したい方は 495〜517行目のソースコードをいい感じに組み込んでください
    • 「デスクトップでフルトラできるスクリプト」改良版 V2.0を導入済の場合は、このスクリプトを代わりに使用すればOKです
  • 03_ItemVoiceGroupChair.js
    • 椅子のアイテムのScriptableItemのソースコードとして使用します
    • 椅子のアイテムにWorldItemReferenceList を追加し、01_ItemVoiceRate.jsがアタッチされているItemを追加する必要があります
      • 追加したアイテムのIdは VoiceItem としてください (03_ItemVoiceGroupChair.jsの1行目に書いてありますので、変更したい場合はそちらを修正してください)。
    • スクリプトの2行目あたりを修正して、 const voiceGroup = "nantoka"; みたいに、ボイス通話をさせたいグループごとに名前を分けてください
      • 音の実験ワールドでは入室後左から "A", "B", "C" となっています
      • 距離減衰はなくならないので、ボイス設定自体が「全体に聞こえる」になっていないと、遠くの人同士ではボイス通話はできません

ゴーストやグループビューイングで動かないのはしょうがないということで

おまけ

  • Z1_VoiceGroupReloadButtonEventRole.js
    • イベント中、スタッフがインタラクトするとボイス設定を更新して再送信するScriptableItemのデモ
    • GameObjectには InteractableShape コンポーネントを追加してください。
    • WorldItemReferenceList を追加し、01_ItemVoiceRate.jsがアタッチされているItemをとして追加する必要があります。Idは VoiceItem としてください。
  • ZZZ_ALBassBandExtrudeVertices.shader
    • AudioLinkでBass帯に反応して頂点をビャーっとするシェーダー。色は寂しかったのでインスペクタ無視してゲーミング色になる
// voice item
/**
* voiceRateの計算頻度(ミリ秒)
*/
const voiceRateComputingInterval = 250;
/**
* voiceRateの送信頻度(ミリ秒)
* @type {number}
*/
const voiceRateSendingInterval = 100;
/**
* voiceRate送信時に、1回で送るプレイヤーの数 (sendのデータサイズ制限対策)
* 現状のデータサイズはchunkSize: 10 で 600 Bytes 程度
*/
const voiceRateSendingChunkSize = 10;
/**
* reload messageを処理する頻度(ミリ秒)
* @type {number}
*/
const reloadMessageInterval = 3000;
// 各所で使う定数
const vector3Zero = new Vector3(0.0, 0.0, 0.0);
const voiceRateOff = 0;
const voiceRateOn = 1;
// region 各タスクで使い回すローカル変数をどうにかするやつ
function initLocalValues(dt) {
return {
dt: dt,
now: Date.now(),
playersNear: null,
}
}
/**
* getPlayersNearを何度も実行したくないので1回実行したらそのフレームでは値をキャッシュする
*/
function localPlayersNear(local) {
if (local.playersNear === null) {
local.playersNear = $.getPlayersNear(vector3Zero, Infinity);
}
return local.playersNear;
}
// endregion
// region PlayerScriptを定期的にアタッチするタスク
/**
* このアイテムで実行するタスクの初期化
*/
function initTasks() {
initAttachTask();
initAllPlayersTask();
}
/**
* PlayerScriptをセットするタスクで使う諸々を初期化
*/
function initAttachTask() {
$.state.attachedPlayers = [];
$.state.attachedPlayersRefreshedAt = Date.now();
}
/**
* 入室した人にPlayerScriptをセットする
*/
function attachScriptTask(local) {
const refreshedAt = $.state.attachedPlayersRefreshedAt;
if (local.now - refreshedAt < voiceRateComputingInterval) {
return;
}
$.state.attachedPlayersRefreshedAt = local.now;
const prev = $.state.attachedPlayers;
const players = localPlayersNear(local);
const playerIds = players.map(x => x.id);
const attachedPlayerIds = [];
const attachedPlayers = [];
// いなくなっていたらアタッチ済リストに含めない (再入室時にアタッチできるように)
for (let player of prev) {
if (playerIds.includes(player.id)) {
attachedPlayerIds.push(player.id);
attachedPlayers.push(player);
}
}
for (let player of players) {
if (!attachedPlayerIds.includes(player.id)) {
$.setPlayerScript(player);
attachedPlayers.push(player);
}
}
$.state.attachedPlayers = attachedPlayers;
}
// endregion
// region Playerに対してボイス設定を送信するタスク
/**
* Playerに対してボイス設定を送信するタスクで使う諸々を初期化
*/
function initAllPlayersTask() {
const now = Date.now();
// compute
$.state.allPlayers = [];
$.state.voiceGroups = {};
$.state.shouldSendVoiceGroups = false;
$.state.voiceRateComputedAt = now;
// send
$.state.sendingQueue = {};
$.state.voiceRateSentAt = now;
// reload
$.state.reloadingProcessedAt = now;
}
/**
* voice Onの設定データを生成
*/
function argPlayerVoiceOn(player) {
return {
a: player,
b: voiceRateOn,
};
}
/**
* voice Offの設定データを生成
*/
function argPlayerVoiceOff(player) {
return {
a: player,
b: voiceRateOff,
};
}
/**
* 全プレイヤー一括voice Offの設定データを生成
*/
function argPlayersVoiceOff(players) {
const results = [];
for (let player of players) {
results.push(argPlayerVoiceOff(player));
}
return results;
}
function chunkedArray(array, size) {
const result = [];
for (let i = 0; i < array.length; i += size) {
result.push(array.slice(i, i + size));
}
return result;
}
/**
* Playerに対してボイス設定を計算するタスク
*/
function computeVoiceRateTask(local) {
const refreshedAt = $.state.voiceRateComputedAt;
if (local.now - refreshedAt < voiceRateComputingInterval) {
return;
}
$.state.voiceRateComputedAt = local.now;
const prev = $.state.allPlayers;
const allPlayers = localPlayersNear(local);
const voiceGroups = $.state.voiceGroups;
const sendingQueue = $.state.sendingQueue;
let shouldSendVoiceGroups = $.state.shouldSendVoiceGroups;
const authorizedPlayers = new Set();
// 前回とplayerに差分があったらvoiceGroupを送信する実装が各所に入ってる (// mark: symmetricDifference)
// Set#symmetricDifference実装されたらそっちを使った方が良い
const prevPlayerIdSet = new Set(prev.map(x => x.id))
const nextPlayerIdSet = new Set(allPlayers.map(x => x.id));
shouldSendVoiceGroups = shouldSendVoiceGroups || prev.length !== allPlayers.length;
for (let player of prev) {
if (!nextPlayerIdSet.has(player.id)) {
// いなくなってたらgroup未所属にする & キュー粉砕
voiceGroups[player.id] = null;
sendingQueue[player.id] = [];
// mark: symmetricDifference
shouldSendVoiceGroups = true;
}
}
for (let player of allPlayers) {
// mark: symmetricDifference
if (!prevPlayerIdSet.has(player.id)) {
shouldSendVoiceGroups = true;
}
// group未設定だったらとりあえず未所属にする
if (!voiceGroups.hasOwnProperty(player.id)) {
voiceGroups[player.id] = null;
}
// キュー未作成だったら作る
if (!sendingQueue.hasOwnProperty(player.id)) {
sendingQueue[player.id] = [];
}
authorizedPlayers.add(player.id);
}
if (!shouldSendVoiceGroups) {
$.state.allPlayers = allPlayers;
$.state.voiceGroups = voiceGroups;
$.state.shouldSendVoiceGroups = shouldSendVoiceGroups;
return;
}
for (let playerToSend of allPlayers) {
let argPlayers = [];
const voiceGroupToSend = voiceGroups[playerToSend.id];
if (authorizedPlayers.has(playerToSend.id)) {
for (let player of allPlayers) {
// 自身はスキップ
if (playerToSend.id === player.id) {
continue;
}
// nullチェックしない
// === voiceGroupがnull同士
// === 席に座っていない人同士は喋れる
if (authorizedPlayers.has(player.id)
&& voiceGroups[player.id] === voiceGroupToSend) {
argPlayers.push(argPlayerVoiceOn(player));
} else {
argPlayers.push(argPlayerVoiceOff(player));
}
}
} else {
// 認可されていないユーザは聞こえない
argPlayers = argPlayersVoiceOff(allPlayers);
}
sendingQueue[playerToSend.id] = chunkedArray(argPlayers, voiceRateSendingChunkSize);
}
$.state.allPlayers = allPlayers;
$.state.voiceGroups = voiceGroups;
$.state.sendingQueue = sendingQueue;
$.state.shouldSendVoiceGroups = false;
}
/**
* Playerに対してボイス設定を送信するタスク
*/
function sendVoiceRateTask(local) {
const refreshedAt = $.state.voiceRateSentAt;
if (local.now - refreshedAt < voiceRateSendingInterval) {
return;
}
$.state.voiceRateSentAt = local.now;
const players = localPlayersNear(local);
const sendingQueue = $.state.sendingQueue;
for (let player of players) {
const queue = sendingQueue[player.id];
if (queue === undefined || queue.length <= 0) {
continue;
}
const argPlayers = queue.shift();
player.send("voiceRate", { a: argPlayers });
}
$.state.sendingQueue = sendingQueue;
}
// endregion
$.onReceive((messageType, args, sender) => {
switch (messageType) {
case "updateVoiceGroup":
// 椅子からsendされる想定
// 椅子に座ったり離席したらvoiceGroupを更新
const playerId = args.playerId;
const voiceGroup = args.voiceGroup;
const voiceGroups = $.state.voiceGroups;
voiceGroups[playerId.id] = voiceGroup;
$.state.voiceGroups = voiceGroups;
$.state.shouldSendVoiceGroups = true;
break;
case "reload":
const refreshedAt = $.state.reloadingProcessedAt;
const now = Date.now();
if (now - refreshedAt < reloadMessageInterval) {
return;
}
$.state.reloadingProcessedAt = now;
$.state.shouldSendVoiceGroups = true;
break;
default:
break;
}
}, { item: true, player: false });
$.onUpdate((dt) => {
if ($.state.isInitialized !== true) {
initTasks();
$.state.isInitialized = true;
}
const localValues = initLocalValues(dt);
attachScriptTask(localValues);
computeVoiceRateTask(localValues);
sendVoiceRateTask(localValues);
});
// (2025-03-25)
// PlayerScript v2.0
// https://xra.fanbox.cc/posts/9578926
// Options START
// Apply a look-at rotation to VMC motion. Depending on various situations, the applied rotation can be head+chest (default), head only (when hips motion is on), or off (when VMC tracker is used). Set apply_look_rotation to false if you want to completely disable it.
let apply_look_rotation = true;
// Options END
const bone_hierarchy = {
"Hips": {
"Spine": {
"Chest": {
"UpperChest": {
"Neck": {
"Head": {
"LeftEye": {},
"RightEye": {},
"Jaw": {} // is it right position?
}
},
"LeftShoulder": {
"LeftUpperArm": {
"LeftLowerArm": {
"LeftHand": {
"LeftThumbProximal": {
"LeftThumbIntermediate": {
"LeftThumbDistal": {}
}
},
"LeftIndexProximal": {
"LeftIndexIntermediate": {
"LeftIndexDistal": {}
}
},
"LeftMiddleProximal": {
"LeftMiddleIntermediate": {
"LeftMiddleDistal": {}
}
},
"LeftRingProximal": {
"LeftRingIntermediate": {
"LeftRingDistal": {}
}
},
"LeftLittleProximal": {
"LeftLittleIntermediate": {
"LeftLittleDistal": {}
}
}
}
}
}
},
"RightShoulder": {
"RightUpperArm": {
"RightLowerArm": {
"RightHand": {
"RightThumbProximal": {
"RightThumbIntermediate": {
"RightThumbDistal": {}
}
},
"RightIndexProximal": {
"RightIndexIntermediate": {
"RightIndexDistal": {}
}
},
"RightMiddleProximal": {
"RightMiddleIntermediate": {
"RightMiddleDistal": {}
}
},
"RightRingProximal": {
"RightRingIntermediate": {
"RightRingDistal": {}
}
},
"RightLittleProximal": {
"RightLittleIntermediate": {
"RightLittleDistal": {}
}
}
}
}
}
}
}
}
},
"LeftUpperLeg": {
"LeftLowerLeg": {
"LeftFoot": {
"LeftToes": {}
}
}
},
"RightUpperLeg": {
"RightLowerLeg": {
"RightFoot": {
"RightToes": {}
}
}
}
}
};
let rotation_apply_order = [];
let bone_rotations = {};
let bone_positions = {}; // 位置情報を保持する変数
let apply_hips_position = false; // 初期状態では適用しない
// 追加:Hip位置変換用の基準(アバターのワールド座標と回転)を保持する変数
let base_avatar_position = null;
let base_avatar_rotation = null;
function init_rotation_apply_order(hierarchy) {
for (const bone of Object.keys(hierarchy)) {
rotation_apply_order.push(bone);
init_rotation_apply_order(hierarchy[bone]);
}
}
// 受信したボーンの位置・回転情報を処理する
function on_receive_vmc_ext_bone_pos(args) {
if (args.length < 8) {
_.log("arguments of /VMC/Ext/Bone/Pos is too short" +
" (8 expected, but " + args.length + ")");
return;
}
const bone_name = args[0].getAsciiString();
const pos_x = args[1].getFloat();
const pos_y = args[2].getFloat();
const pos_z = args[3].getFloat();
const q_x = args[4].getFloat();
const q_y = args[5].getFloat();
const q_z = args[6].getFloat();
const q_w = args[7].getFloat();
const rotation = new Quaternion(q_x, q_y, q_z, q_w);
bone_rotations[bone_name] = rotation;
// Hips の位置を保存(ローカル座標)
// keep leg bones' positions for calculation of hip height
if (bone_name === "Hips" || /left(upperleg|lowerleg|foot|toes)/i.test(bone_name)) {
bone_positions[bone_name] = new Vector3(pos_x, pos_y, pos_z);
}
}
// 再帰的にボーンの回転を計算する
function get_bone_rotations_from_current_bone(rotations, cur_rotation, bone_hierarchy) {
for (const key of Object.keys(bone_hierarchy)) {
let rotation = cur_rotation.clone();
if (key in bone_rotations) {
rotation = rotation.multiply(bone_rotations[key]);
if (rotation_offset[key])
rotation = rotation_offset[key].clone().multiply(rotation);
}
rotations[key] = rotation;
// 再帰呼び出しで子ボーンの回転を計算
get_bone_rotations_from_current_bone(rotations, rotation, bone_hierarchy[key]);
}
}
// アバターのルート回転から各ボーンの回転を計算する
function get_bone_rotations_from_root(avatar_rotation) {
let rotations = {};
const init_rotation = avatar_rotation; // アバターの回転を初期値として使用
get_bone_rotations_from_current_bone(rotations, init_rotation, bone_hierarchy);
return rotations;
}
function set_humanoid_pose() {
const avatar_rotation = _.getRotation();
if (avatar_rotation) {
const bone_rotations_from_root = get_bone_rotations_from_root(avatar_rotation);
for (const key of rotation_apply_order) {
if (!(key in bone_rotations)) continue;
const rotation = bone_rotations_from_root[key];
_.setHumanoidBoneRotationOnFrame(HumanoidBone[key], rotation);
}
}
}
// ローカル座標からワールド座標へ変換する処理
// ※ここでは「基準」となるアバターの位置と回転(base_avatar_position, base_avatar_rotation)を使う
function convertLocalToWorld(localPos, basePos, baseRot) {
let worldPos = localPos.clone();
// アバターの回転を適用
worldPos.applyQuaternion(baseRot);
// アバターのワールド位置を加算
worldPos.add(basePos);
return worldPos;
}
let pos_last;
const lower_body_RE = /hips|leg|foot|toes/i;
// 毎フレーム、アバターの回転および位置を更新する
function update_avatar_transform() {
update_base_avatar_transform();
const avatar_rotation = _.getRotation();
const full_body_mocap = apply_hips_position && base_avatar_position && base_avatar_rotation && "Hips" in bone_positions;
let skip_hips_mov = !full_body_mocap;
let upper_body_mocap;
if (skip_hips_mov) {
// use upper body mocap when riding on items
let flags = _.getAvatarMovementFlags();
let isRiding = (flags & 0x0004) !== 0;
upper_body_mocap = isRiding;
// skip mocap if avatar is moving (via WASD/Space/etc)
if (!upper_body_mocap) {
const pos_now = _.getPosition();
if (pos_now && pos_last && !pos_now.equals(pos_last)) {
pos_last = pos_now?.clone();
return;
}
}
}
if (avatar_rotation) {
const bone_rotations_from_root = get_bone_rotations_from_root(avatar_rotation);
for (const key of Object.keys(bone_rotations)) {
if (key in HumanoidBone && (!upper_body_mocap || !lower_body_RE.test(key))) {
const rotation = bone_rotations_from_root[key];
_.setHumanoidBoneRotationOnFrame(HumanoidBone[key], rotation);
}
}
}
// Hips の位置を適用(Y軸を -0.8 ずらす)
// ※ここで注意:毎フレーム現在のアバターの位置・回転を使うと連鎖的に変換され
// 急激に位置がずれてしまう可能性があるため、Hips位置適用をONにした時点の基準を使用する
if (!skip_hips_mov) {
let localHipsPosition = bone_positions["Hips"].clone();
adjust_hips_position(localHipsPosition);
// ローカル座標を、Hips適用開始時の基準アバター位置・回転を使ってワールド座標に変換
let worldHipsPosition = convertLocalToWorld(localHipsPosition, base_avatar_position, base_avatar_rotation);
_.setPosition(worldHipsPosition);
}
pos_last = _.getPosition()?.clone();
}
let hips_height_OSC = 0;
let hips_height_avatar = 0;
function adjust_hips_position(hips_pos) {
let update_log;
if (!hips_height_OSC) {
const length = {};
for (const bone_name of ['LeftUpperLeg', 'LeftLowerLeg', 'LeftFoot']) {
const pos = bone_positions[bone_name];
if (!pos) {
hips_pos.y -= 0.8;
return;
}
length[bone_name] = pos?.length();
}
let hips_height = length['LeftUpperLeg']*0.5 + length['LeftLowerLeg'] + length['LeftFoot']*1.2;
hips_height = Math.round(hips_height*1000) / 1000;
hips_height_OSC = hips_height;
update_log = true;
}
const bone_pos = {};
for (const bone_name of ['Hips', 'LeftUpperLeg', 'LeftLowerLeg', 'LeftFoot']) {
const pos = _.getHumanoidBonePosition(HumanoidBone[bone_name]);
if (!pos) {
hips_pos.y -= hips_height_OSC;
return;
}
bone_pos[bone_name] = pos;
}
let hips_height = bone_pos['Hips'].sub(bone_pos['LeftUpperLeg']).length()*0.5 + bone_pos['LeftUpperLeg'].sub(bone_pos['LeftLowerLeg']).length() + bone_pos['LeftLowerLeg'].sub(bone_pos['LeftFoot']).length()*1.2;
hips_height = Math.round(hips_height*1000) / 1000;
if (hips_height_avatar !== hips_height) {
hips_height_avatar = hips_height;
update_log = true;
}
const hips_scale = hips_height_avatar / hips_height_OSC;
hips_pos.multiplyScalar(hips_scale);
hips_pos.y -= hips_height_avatar;
if (update_log) {
_.log('Hips height (OSC): ' + hips_height_OSC);
_.log('Hips height (Avatar): ' + hips_height_avatar);
_.log('Hips scale: x' + hips_scale);
}
}
let OSC_enabled = false;
let OSC_initialized = false;
let OSC_active_timestamp = 0;
let VMC_camera_active_timestamp = 0;
let apply_hips_position_last = false;
let VMC_tracker_items;
let VMC_tracker_active_timestamp = 0;
function check_OSC() {
if (!_.oscHandle.isReceiveEnabled() || OSC_initialized) return;
OSC_initialized = true;
_.oscHandle.onReceive((messages) => {
for (const msg of messages) {
if (msg.address === "/VMC/Ext/Bone/Pos") {
OSC_active_timestamp = Date.now();
on_receive_vmc_ext_bone_pos(msg.values);
}
else if (msg.address === "/VMC/Ext/Cam") {
VMC_camera_active_timestamp = Date.now();
}
else if (msg.address === "/VMC/Ext/Tra/Pos") {
VMC_tracker_active_timestamp = Date.now();
VMC_tracker_items?.receive_message(msg.values);
}
}
});
_.log("OSC receiver is enabled, thus onReceive() is activated");
}
let rotation_offset = {};
// Hips適用開始時点のアバターのワールド座標と回転を基準として保持する
// _.getPosition()/_.getRotation() may return null in some cases
function update_base_avatar_transform() {
// Apply a look-at rotation to VMC motion, based on the default head rotation offset.
rotation_offset = {};
if (apply_look_rotation && !_.isVr && (VMC_tracker_active_timestamp < Date.now()-1000)) {
const q_head = _.getHumanoidBoneRotation(HumanoidBone.Head);
const q_root = _.getRotation();
if (q_head && q_root) {
let q_offset = q_head.clone().multiply(q_root.clone().invert());
const q_offset_half = q_offset.slerp(new Quaternion(0,0,0,1), 0.5);
if (apply_hips_position) {
rotation_offset.Head = rotation_offset.Neck = q_offset_half;
}
else {
rotation_offset.Head = rotation_offset.Chest = q_offset_half;
}
//_.log(rotation_offset.Head.w)
}
}
if (!apply_hips_position) return;
let updating;
if (!base_avatar_position) {
base_avatar_position = _.getPosition()?.clone();
updating = true;
}
if (!base_avatar_rotation) {
base_avatar_rotation = _.getRotation()?.clone()
updating = true;
}
if (updating && base_avatar_position && base_avatar_rotation)
_.log("Hipsの位置適用: ON");
}
// 毎フレーム、位置と回転を更新
_.onFrame((deltaTime) => {
check_OSC();
const time = Date.now();
let OSC_active = OSC_active_timestamp > time - 1000;
if (OSC_enabled != OSC_active) {
OSC_enabled = OSC_active;
// recalculate hips height whenever OSC status changes
hips_height_OSC = 0;
hips_height_avatar = 0;
if (OSC_enabled) VMC_tracker_items?.init();
_.log('OSC enabled: ' + ((OSC_enabled) ? 'ON' : 'OFF'));
}
let VMC_camera_active = VMC_camera_active_timestamp > time - 1000;
apply_hips_position = VMC_camera_active;
if (apply_hips_position_last != apply_hips_position) {
apply_hips_position_last = apply_hips_position;
if (apply_hips_position) {
_.log("Hipsの位置適用: 初期化中");
} else {
// prevent cases like when avatar may fall through the ground because the last hip y was too low
if (base_avatar_position) _.setPosition(base_avatar_position);
base_avatar_position = base_avatar_rotation = null;
_.log("Hipsの位置適用: OFF");
}
}
if (OSC_enabled)
update_avatar_transform();
VMC_tracker_items?.Item.check_active();
});
init_rotation_apply_order(bone_hierarchy);
// Math functions
// https://github.com/mrdoob/three.js/blob/master/src/math/Quaternion.js
Quaternion.prototype.slerp = function ( qb, t ) {
if ( t === 0 ) return this;
if ( t === 1 ) return this.copy( qb );
const x = this.x, y = this.y, z = this.z, w = this.w;
// http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/
let cosHalfTheta = w * qb.w + x * qb.x + y * qb.y + z * qb.z;
if ( cosHalfTheta < 0 ) {
this.w = - qb.w;
this.x = - qb.x;
this.y = - qb.y;
this.z = - qb.z;
cosHalfTheta = - cosHalfTheta;
} else {
this.copy( qb );
}
if ( cosHalfTheta >= 1.0 ) {
this.w = w;
this.x = x;
this.y = y;
this.z = z;
return this;
}
const sqrSinHalfTheta = 1.0 - cosHalfTheta * cosHalfTheta;
if ( sqrSinHalfTheta <= Number.EPSILON ) {
const s = 1 - t;
this.w = s * w + t * this.w;
this.x = s * x + t * this.x;
this.y = s * y + t * this.y;
this.z = s * z + t * this.z;
this.normalize(); // normalize calls _onChangeCallback()
return this;
}
const sinHalfTheta = Math.sqrt( sqrSinHalfTheta );
const halfTheta = Math.atan2( sinHalfTheta, cosHalfTheta );
const ratioA = Math.sin( ( 1 - t ) * halfTheta ) / sinHalfTheta,
ratioB = Math.sin( t * halfTheta ) / sinHalfTheta;
this.w = ( w * ratioA + this.w * ratioB );
this.x = ( x * ratioA + this.x * ratioB );
this.y = ( y * ratioA + this.y * ratioB );
this.z = ( z * ratioA + this.z * ratioB );
return this;
};
// 追加分
_.onReceive((messageType, args, sender) => {
switch (messageType) {
case "voiceRate":
for (let element of args.a) {
const playerId = element.a;
const voiceRate = element.b;
_.setVoiceVolumeRateOf(playerId, voiceRate);
}
break;
default:
break;
}
}, { item: true });
const voiceItem = $.worldItemReference("VoiceItem");
const voiceGroup = "A";
// const voiceGroup = "B";
// const voiceGroup = "C";
function voiceGroupArgs(player, voiceGroup) {
return {
playerId: player,
voiceGroup: voiceGroup,
}
}
$.onRide((isGetOn, player) => {
if (isGetOn) {
voiceItem.send("updateVoiceGroup", voiceGroupArgs(player, voiceGroup));
} else {
voiceItem.send("updateVoiceGroup", voiceGroupArgs(player, null));
}
});
const voiceItem = $.worldItemReference("VoiceItem");
$.onInteract((player) => {
if ($.isEvent()) {
return;
}
const eventRole = player.getEventRole();
if (eventRole !== EventRole.Staff) {
return;
}
voiceItem.send("reload", null);
});
const voiceItem = $.worldItemReference("VoiceItem");
const voiceGroup = "1";
function voiceGroupArgs(player, voiceGroup) {
return {
playerId: player,
voiceGroup: voiceGroup,
}
}
$.onRide((isGetOn, player) => {
if (isGetOn) {
voiceItem.send("updateVoiceGroup", voiceGroupArgs(player, voiceGroup));
} else {
voiceItem.send("updateVoiceGroup", voiceGroupArgs(player, null));
}
});
const voiceItem = $.worldItemReference("VoiceItem");
const voiceGroup = "2";
function voiceGroupArgs(player, voiceGroup) {
return {
playerId: player,
voiceGroup: voiceGroup,
}
}
$.onRide((isGetOn, player) => {
if (isGetOn) {
voiceItem.send("updateVoiceGroup", voiceGroupArgs(player, voiceGroup));
} else {
voiceItem.send("updateVoiceGroup", voiceGroupArgs(player, null));
}
});
Shader "Uzzu/ALBassExtrudeVertices"
{
Properties
{
_BaseColor ("Base Color", Color) = (1,1,1,1) // 使ってない
_Strength ("Jump Strength", Float) = 2.0
_FadeSpeed ("Fade Out Speed", Float) = 1.0
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
LOD 100
CGINCLUDE
#include "UnityCG.cginc"
#include "Packages/com.llealloo.audiolink/Runtime/Shaders/AudioLink.cginc"
float _Strength;
float _FadeSpeed;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float life : TEXCOORD0;
};
float hash(float2 p)
{
// 疑似乱数的な何かになれば良いので数値はでかくなりすぎない程度に適当
return frac(sin(dot(p, float2(12.9898,78.233))) * 43758.5453);
}
v2f vert_common(appdata v)
{
v2f o;
float3 localPos = v.vertex.xyz;
float instanceTime = AudioLinkDecodeDataAsSeconds(ALPASS_GENERALVU_INSTANCE_TIME);
// 破片ごとに乱数で個体差を作る
float offset = hash(v.uv * 100.0);
float age = frac(instanceTime + offset);
// Bass帯の音量
float volume = AudioLinkLerpMultiline(ALPASS_AUDIOBASS + float2(0.0, 0.0)).r;
float force = volume * _Strength * (1.0 - age);
localPos += v.normal * force;
o.pos = UnityObjectToClipPos(float4(localPos, 1.0));
o.life = saturate(1.0 - age * _FadeSpeed);
return o;
}
ENDCG
Pass
{
Name "DepthOnly"
Tags { "LightMode" = "Always" }
ZWrite On
ColorMask 0
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
v2f vert(appdata v)
{
return vert_common(v);
}
fixed4 frag(v2f i) : SV_Target
{
return 0;
}
ENDCG
}
Pass
{
Name "Forward"
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 _BaseColor;
v2f vert(appdata v)
{
return vert_common(v);
}
fixed4 frag(v2f i) : SV_Target
{
// 黒色以外の色に時間で遷移
int colorCount = 7;
float3 colors[7] = {
float3(1, 0, 0),
float3(1, 1, 0),
float3(0, 1, 0),
float3(0, 1, 1),
float3(0, 0, 1),
float3(1, 0, 1),
float3(1, 1, 1)
};
float interval = 0.5;
float cycle = interval * colorCount;
float t = fmod(_Time.y, cycle);
int index = (int) (t / interval);
int next = (index + 1) % colorCount;
float local = frac(t / interval);
float3 color = lerp(colors[index], colors[next], local);
// 透明度の高い白色がDepth of Field Postprocessingで暴走する問題への対策
// 2pass構成(Depth用 + 描画用) + 透明度が高い場合は描画しない
// これでもダメなら
// - 完全な白を抑制
// - gamma補正
// - 色の白さに応じてalphaの最大値にclamp
// - discardするalphaの閾値をあげる
float alpha = 1.0 * i.life;
if (alpha < 0.05)
{
discard;
}
return fixed4(color, alpha);
}
ENDCG
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment